From cdcba54aa4d63a69bdd7661e546b6038796aaa37 Mon Sep 17 00:00:00 2001 From: Zeeshaun Date: Tue, 17 Mar 2026 08:54:34 -0500 Subject: [PATCH] Adding --- .../.dockerignore | 17 + .../.gitignore | 334 ++ .../Api/Dockerfile | 51 + .../ReserveManagementSystemApi.Sql/Dealer.sql | 9 + .../ReserveManagementSystemApi.Sql/Import.sql | 17 + .../ReserveManagementSystemApi.Sql/Loan.sql | 41 + .../LoanAudit.sql | 13 + .../LoanType.sql | 11 + .../MinorAccountType.sql | 11 + ...ReserveManagementSystemApi.Sql.refactorlog | 38 + .../ReserveManagementSystemApi.Sql.sqlproj | 81 + ...pt.PostDeploymentPopulateLoanTypeTable.sql | 20 + .../Script.PreDeploymentTruncateLoanTypes.sql | 6 + .../Vehicle.sql | 12 + .../ReserveManagementSystemApi.Test.csproj | 28 + .../Service/ExportServiceTest.cs | 780 +++ .../Features/ChargebackCalculation.feature | 631 +++ .../Features/DealerService.feature | 155 + .../Features/DealerServiceSteps.cs | 95 + .../Features/ExportService.feature | 131 + .../Features/ExportServiceSteps.cs | 209 + .../ImportReadService Closed from DNA.feature | 392 ++ .../Features/ImportReadService Closed.feature | 194 + .../ImportReadService New from DNA.feature | 1753 ++++++ .../Features/ImportReadService New.feature | 234 + .../Features/ImportReadServiceSteps.cs | 434 ++ .../Features/ImportService.feature | 92 + .../Features/ImportServiceSteps.cs | 149 + .../Features/LoanCalculationServiceSteps.cs | 104 + ...Service - Filtering and Pagination.feature | 475 ++ ...oanService - Get All and Get By Id.feature | 183 + .../Features/LoanService - Saving.feature | 77 + .../Features/LoanServiceSteps.cs | 436 ++ .../Features/ReserveCalculation.feature | 230 + .../ReserveManagementSystemApi.Tests.csproj | 129 + .../xunit.runner.json | 3 + .../Configurations/ApiConfiguration.cs | 11 + .../Configurations/FeatureFlags.cs | 9 + .../ReceiveEndPointConfigurations.cs | 9 + .../SqlConnectionConfiguration.cs | 9 + .../Consumers/RmsActiveImportConsumer.cs | 40 + .../Controllers/AuditHistoryController.cs | 34 + .../Controllers/DealerController.cs | 51 + .../Controllers/ImportController.cs | 183 + .../Controllers/LoanController.cs | 146 + .../Controllers/StatusController.cs | 33 + .../Enums/ActivityType.cs | 10 + .../Enums/LoanStatus.cs | 10 + .../Helpers/BatchHelper.cs | 27 + .../Helpers/BoatLoanCalculationHelper.cs | 56 + .../Helpers/ImportHelper.cs | 210 + .../Helpers/LoanCalculationHelper.cs | 120 + .../HostedService/HostedImportExportActive.cs | 72 + .../HostedService/HostedImportExportClosed.cs | 72 + .../Mappers/ExportMapper.cs | 180 + .../Models/ImportBoatLoan.cs | 43 + .../Models/ImportNewLoan.cs | 113 + .../Api/ReserveManagementSystemApi/Program.cs | 249 + .../ReserveManagementSystemApi.csproj | 45 + .../ReserveManagementSystemApi.sln | 51 + .../ServiceClients/CoreApiServiceClient.cs | 120 + .../ServiceClients/ICoreApiServiceClient.cs | 19 + .../Services/AuditHistoryService.cs | 146 + .../Services/DealerService.cs | 100 + .../Services/ExportService.cs | 275 + .../Services/IAuditHistoryService.cs | 25 + .../Services/IDealerService.cs | 17 + .../Services/IExportService.cs | 18 + .../Services/IImportReadService.cs | 28 + .../Services/IImportService.cs | 20 + .../Services/ILoanCalculationService.cs | 13 + .../Services/ILoanService.cs | 31 + .../Services/IRmsActiveLoanService.cs | 9 + .../Services/IScheduleService.cs | 8 + .../Services/IStatusService.cs | 7 + .../Services/IVehicleService.cs | 10 + .../Services/ImportReadService.cs | 402 ++ .../Services/ImportService.cs | 89 + .../Services/LoanCalculationService.cs | 314 + .../Services/LoanService.cs | 448 ++ .../IQueueRmsActiveLoanService.cs | 9 + .../QueueRmsActiveLoanService.cs | 36 + .../Services/RmsActiveLoanService.cs | 31 + .../Services/ScheduleService.cs | 42 + .../Services/StatusService.cs | 28 + .../Services/VehicleService.cs | 29 + .../Api/ReserveManagementSystemApi/Using.cs | 8 + .../appsettings.json | 59 + .../Configuration/CoreApiConfiguration.cs | 14 + .../Configuration/RmsApiConfiguration.cs | 10 + .../Enums/ImportType.cs | 8 + .../Helpers/StringExtensions.cs | 17 + .../Helpers/ValidationHelper.cs | 34 + .../Models/AuditHistory.cs | 18 + .../Models/CalculationConfig.cs | 33 + .../Models/Dealer.cs | 32 + .../Models/Import.cs | 22 + .../ReserveManagementSystemLib/Models/Loan.cs | 204 + .../Models/LoanChargeback.cs | 11 + .../Models/LoanChargebackInput.cs | 22 + .../Models/LoanFilter.cs | 89 + .../Models/LoanReserve.cs | 12 + .../Models/LoanReserveInput.cs | 22 + .../Models/LoanType.cs | 22 + .../Models/MinorAccountType.cs | 13 + .../Models/PaginatedListResponse.cs | 78 + .../Models/RmsActiveImportRequest.cs | 8 + .../Models/Vehicle.cs | 38 + .../Persistence/AppDbContext.cs | 42 + .../Repositories/AuditHistoryRepository.cs | 41 + .../Repositories/BaseRepository.cs | 14 + .../Repositories/DealerRepository.cs | 47 + .../Repositories/IAuditHistoryRepository.cs | 15 + .../Repositories/IDealerRepository.cs | 17 + .../Repositories/IImportRepository.cs | 18 + .../Repositories/ILoanRepository.cs | 17 + .../Repositories/ILoanTypeRepository.cs | 11 + .../IMinorAccountTypeRepository.cs | 9 + .../Repositories/IVehicleRepository.cs | 10 + .../Repositories/ImportRepository.cs | 49 + .../Repositories/LoanRepository.cs | 40 + .../Repositories/LoanTypeRepository.cs | 20 + .../MinorAccountTypeRepository.cs | 20 + .../Repositories/VehicleRepository.cs | 16 + .../ReserveManagementSystemLib.csproj | 53 + .../Api/ReserveManagementSystemLib/Using.cs | 9 + .../azdo_pipelines/azure-k8s-pipeline.yaml | 74 + .../azure-pipelines-feature-branches.yaml | 10 + .../azdo_pipelines/azure-pipelines-prd.yaml | 9 + .../azdo_pipelines/azure-pipelines-qa.yaml | 9 + .../azdo_pipelines/azure-pipelines-uat.yaml | 9 + .../config-files/prd-pvars.yaml | 12 + .../azdo_pipelines/config-files/qa-pvars.yaml | 12 + .../config-files/uat-pvars.yaml | 12 + .../Api/kustomize/base/Deployment.yaml | 54 + .../Api/kustomize/base/Service.yaml | 11 + .../Api/kustomize/base/kustomization.yaml | 9 + .../base/secrets-provider-class.yaml | 11 + .../Api/kustomize/base/virtual-service.yaml | 19 + .../keyvaults/normal/kustomization.yaml | 10 + .../normal/set_secrets_deployment.yaml | 49 + .../normal/set_secrets_provider.yaml | 24 + .../normal/appsettings/appsettings.json | 1 + .../workloads/normal/kustomization.yaml | 14 + .../normal/set_appsettings_deployment.yaml | 12 + .../local/appsettings/appsettings.json | 22 + .../overlays/local/common/kustomization.yaml | 12 + .../common/set_appsettings_deployment.yaml | 12 + .../common/set_secrets_provider_class.yaml | 9 + .../overlays/local/kustomization.yaml | 14 + .../overlays/local/normal/kustomization.yaml | 6 + .../local/normal/set_virtual_service.yaml | 8 + .../local/set_configmap_deployment.yaml | 12 + .../overlays/prd/appsettings/appsettings.json | 25 + .../overlays/prd/component/kustomization.yaml | 7 + .../component/set_secrets_provider_class.yaml | 9 + .../kustomize/overlays/prd/kustomization.yaml | 14 + .../overlays/prd/normal/kustomization.yaml | 12 + .../prd/normal/set_virtual_service.yaml | 8 + .../prd/set_configmap_deployment.yaml | 12 + .../overlays/qa/appsettings/appsettings.json | 27 + .../overlays/qa/component/kustomization.yaml | 7 + .../component/set_secrets_provider_class.yaml | 9 + .../kustomize/overlays/qa/kustomization.yaml | 14 + .../overlays/qa/normal/kustomization.yaml | 12 + .../qa/normal/set_virtual_service.yaml | 8 + .../overlays/qa/set_configmap_deployment.yaml | 12 + .../overlays/uat/appsettings/appsettings.json | 25 + .../overlays/uat/component/kustomization.yaml | 7 + .../component/set_secrets_provider_class.yaml | 9 + .../kustomize/overlays/uat/kustomization.yaml | 14 + .../overlays/uat/normal/kustomization.yaml | 12 + .../uat/normal/set_virtual_service.yaml | 8 + .../uat/set_configmap_deployment.yaml | 12 + .../README.md | 39 + .../WebApp/ReserveManagementSystemUI.sln | 25 + .../ReserveManagementSystemUI/App.razor | 19 + .../ReserveManagementSystemUI/Dockerfile | 40 + .../Helpers/GridHelpers.cs | 69 + .../Helpers/IEHelpers.cs | 24 + .../Helpers/ServiceHelpers.cs | 46 + .../Model/Configuration/AppConfiguration.cs | 9 + .../Model/Configuration/RmsUIConfiguration.cs | 19 + .../Model/TestModel.cs | 16 + .../Model/Validators/TestModelValidator.cs | 18 + .../Pages/Error.razor | 16 + .../Pages/History.razor | 47 + .../Pages/History.razor.cs | 83 + .../Pages/Index.razor | 9 + .../Pages/Index.razor.cs | 8 + .../Pages/LoanView.razor | 255 + .../Pages/LoanView.razor.cs | 93 + .../Pages/Loans.razor | 228 + .../Pages/Loans.razor.cs | 266 + .../Pages/Statements.razor | 14 + .../Pages/Statements.razor.cs | 31 + .../Pages/Test.razor | 21 + .../Pages/Test.razor.cs | 32 + .../Pages/_Host.cshtml | 55 + .../ReserveManagementSystemUI/Program.cs | 29 + .../ReserveManagementSystemUI.csproj | 38 + .../Services/NotificationService.cs | 64 + .../Services/ReserveService.cs | 366 ++ .../Shared/Components/ImportBanner.razor | 6 + .../Shared/Components/ImportBanner.razor.cs | 93 + .../Shared/Components/ImportList.razor | 56 + .../Shared/Components/ImportList.razor.cs | 154 + .../Shared/Components/LoadingLighthouse.razor | 5 + .../Shared/Components/LoadingSpinner.razor | 9 + .../Shared/Components/Table/TableData.razor | 20 + .../Shared/Components/Table/TableHeader.razor | 29 + .../Shared/LoginDisplay.razor | 5 + .../Shared/MainLayout.razor | 23 + .../Shared/MainLayout.razor.cs | 33 + .../Shared/NavMenu.razor | 50 + .../ReserveManagementSystemUI/Startup.cs | 102 + .../ReserveManagementSystemUI/_Imports.razor | 18 + .../appsettings.json | 19 + .../ReserveManagementSystemUI/libman.json | 10 + .../wwwroot/css/bootstrap/bootstrap.min.css | 7 + .../css/bootstrap/bootstrap.min.css.map | 1 + .../wwwroot/css/font-awesome/css/all.css | 4619 +++++++++++++++ .../wwwroot/css/font-awesome/css/all.min.css | 5 + .../wwwroot/css/font-awesome/css/brands.css | 15 + .../css/font-awesome/css/brands.min.css | 5 + .../css/font-awesome/css/fontawesome.css | 4585 +++++++++++++++ .../css/font-awesome/css/fontawesome.min.css | 5 + .../wwwroot/css/font-awesome/css/regular.css | 15 + .../css/font-awesome/css/regular.min.css | 5 + .../wwwroot/css/font-awesome/css/solid.css | 16 + .../css/font-awesome/css/solid.min.css | 5 + .../css/font-awesome/css/svg-with-js.css | 371 ++ .../css/font-awesome/css/svg-with-js.min.css | 5 + .../wwwroot/css/font-awesome/css/v4-shims.css | 2172 +++++++ .../css/font-awesome/css/v4-shims.min.css | 5 + .../wwwroot/css/font-awesome/js/all.js | 4467 +++++++++++++++ .../wwwroot/css/font-awesome/js/all.min.js | 5 + .../wwwroot/css/font-awesome/js/brands.js | 586 ++ .../wwwroot/css/font-awesome/js/brands.min.js | 5 + .../css/font-awesome/js/conflict-detection.js | 998 ++++ .../font-awesome/js/conflict-detection.min.js | 5 + .../css/font-awesome/js/fontawesome.js | 2483 ++++++++ .../css/font-awesome/js/fontawesome.min.js | 5 + .../wwwroot/css/font-awesome/js/regular.js | 280 + .../css/font-awesome/js/regular.min.js | 5 + .../wwwroot/css/font-awesome/js/solid.js | 1130 ++++ .../wwwroot/css/font-awesome/js/solid.min.js | 5 + .../wwwroot/css/font-awesome/js/v4-shims.js | 68 + .../css/font-awesome/js/v4-shims.min.js | 5 + .../css/font-awesome/sprites/brands.svg | 1381 +++++ .../css/font-awesome/sprites/regular.svg | 463 ++ .../css/font-awesome/sprites/solid.svg | 3013 ++++++++++ .../font-awesome/webfonts/fa-brands-400.eot | Bin 0 -> 134346 bytes .../font-awesome/webfonts/fa-brands-400.svg | 3717 ++++++++++++ .../font-awesome/webfonts/fa-brands-400.ttf | Bin 0 -> 134040 bytes .../font-awesome/webfonts/fa-brands-400.woff | Bin 0 -> 90060 bytes .../font-awesome/webfonts/fa-brands-400.woff2 | Bin 0 -> 76764 bytes .../font-awesome/webfonts/fa-regular-400.eot | Bin 0 -> 34034 bytes .../font-awesome/webfonts/fa-regular-400.svg | 801 +++ .../font-awesome/webfonts/fa-regular-400.ttf | Bin 0 -> 33736 bytes .../font-awesome/webfonts/fa-regular-400.woff | Bin 0 -> 16276 bytes .../webfonts/fa-regular-400.woff2 | Bin 0 -> 13276 bytes .../font-awesome/webfonts/fa-solid-900.eot | Bin 0 -> 203030 bytes .../font-awesome/webfonts/fa-solid-900.svg | 5034 +++++++++++++++++ .../font-awesome/webfonts/fa-solid-900.ttf | Bin 0 -> 202744 bytes .../font-awesome/webfonts/fa-solid-900.woff | Bin 0 -> 101652 bytes .../font-awesome/webfonts/fa-solid-900.woff2 | Bin 0 -> 78196 bytes .../wwwroot/css/open-iconic/FONT-LICENSE | 86 + .../wwwroot/css/open-iconic/ICON-LICENSE | 21 + .../wwwroot/css/open-iconic/README.md | 114 + .../font/css/open-iconic-bootstrap.min.css | 1 + .../open-iconic/font/fonts/open-iconic.eot | Bin 0 -> 28196 bytes .../open-iconic/font/fonts/open-iconic.otf | Bin 0 -> 20996 bytes .../open-iconic/font/fonts/open-iconic.svg | 543 ++ .../open-iconic/font/fonts/open-iconic.ttf | Bin 0 -> 28028 bytes .../open-iconic/font/fonts/open-iconic.woff | Bin 0 -> 14984 bytes .../wwwroot/css/site.css | 300 + .../wwwroot/favicon.ico | Bin 0 -> 15406 bytes .../wwwroot/images/LMCU-Blue.png | Bin 0 -> 24751 bytes .../wwwroot/images/LMCU-White-Logo-Only.png | Bin 0 -> 7637 bytes .../wwwroot/images/lighthouse.gif | Bin 0 -> 100768 bytes .../wwwroot/images/rotating.gif | Bin 0 -> 10689 bytes .../wwwroot/images/smiling.gif | Bin 0 -> 1578 bytes .../wwwroot/js/blazor.polyfill.min.js | 11 + .../wwwroot/js/loans.js | 13 + .../azdo_pipelines/azure-k8s-pipeline.yaml | 50 + .../azure-pipelines-feature-branches.yaml | 10 + .../azdo_pipelines/azure-pipelines-prd.yaml | 9 + .../azdo_pipelines/azure-pipelines-qa.yaml | 9 + .../azdo_pipelines/azure-pipelines-uat.yaml | 9 + .../config-files/prd-pvars.yaml | 6 + .../azdo_pipelines/config-files/qa-pvars.yaml | 6 + .../config-files/uat-pvars.yaml | 6 + .../WebApp/kustomize/base/Deployment.yaml | 50 + .../WebApp/kustomize/base/Service.yaml | 11 + .../WebApp/kustomize/base/kustomization.yaml | 9 + .../base/secrets-provider-class.yaml | 11 + .../kustomize/base/virtual-service.yaml | 19 + .../keyvaults/normal/kustomization.yaml | 10 + .../normal/set_secrets_deployment.yaml | 18 + .../normal/set_secrets_provider.yaml | 8 + .../workloads/normal/kustomization.yaml | 7 + .../normal/set_appsettings_deployment.yaml | 12 + .../local/appsettings/appsettings.json | 17 + .../overlays/local/common/kustomization.yaml | 10 + .../common/set_appsettings_deployment.yaml | 12 + .../common/set_secrets_provider_class.yaml | 9 + .../overlays/local/kustomization.yaml | 9 + .../overlays/local/normal/kustomization.yaml | 6 + .../local/normal/set_virtual_service.yaml | 8 + .../overlays/prd/appsettings/appsettings.json | 17 + .../overlays/prd/component/kustomization.yaml | 7 + .../component/set_secrets_provider_class.yaml | 9 + .../kustomize/overlays/prd/kustomization.yaml | 9 + .../overlays/prd/normal/kustomization.yaml | 16 + .../normal/set_environment_deployment.yaml | 12 + .../prd/normal/set_virtual_service.yaml | 8 + .../overlays/qa/appsettings/appsettings.json | 17 + .../overlays/qa/component/kustomization.yaml | 7 + .../component/set_secrets_provider_class.yaml | 9 + .../kustomize/overlays/qa/kustomization.yaml | 9 + .../overlays/qa/normal/kustomization.yaml | 12 + .../qa/normal/set_virtual_service.yaml | 8 + .../overlays/uat/appsettings/appsettings.json | 17 + .../overlays/uat/component/kustomization.yaml | 7 + .../component/set_secrets_provider_class.yaml | 9 + .../kustomize/overlays/uat/kustomization.yaml | 9 + .../overlays/uat/normal/kustomization.yaml | 12 + .../uat/normal/set_virtual_service.yaml | 8 + .../cleanbuildrun.sh | 12 + 330 files changed, 54110 insertions(+) create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/.dockerignore create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/.gitignore create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/Dockerfile create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Dealer.sql create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Import.sql create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Loan.sql create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/LoanAudit.sql create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/LoanType.sql create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/MinorAccountType.sql create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/ReserveManagementSystemApi.Sql.refactorlog create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/ReserveManagementSystemApi.Sql.sqlproj create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Script.PostDeploymentPopulateLoanTypeTable.sql create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Script.PreDeploymentTruncateLoanTypes.sql create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Vehicle.sql create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Test/ReserveManagementSystemApi.Test.csproj create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Test/Service/ExportServiceTest.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ChargebackCalculation.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/DealerService.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/DealerServiceSteps.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ExportService.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ExportServiceSteps.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService Closed from DNA.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService Closed.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService New from DNA.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService New.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadServiceSteps.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportService.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportServiceSteps.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanCalculationServiceSteps.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Filtering and Pagination.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Get All and Get By Id.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Saving.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanServiceSteps.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ReserveCalculation.feature create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/ReserveManagementSystemApi.Tests.csproj create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/xunit.runner.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/ApiConfiguration.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/FeatureFlags.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/ReceiveEndPointConfigurations.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/SqlConnectionConfiguration.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Consumers/RmsActiveImportConsumer.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/AuditHistoryController.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/DealerController.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/ImportController.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/LoanController.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/StatusController.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Enums/ActivityType.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Enums/LoanStatus.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/BatchHelper.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/BoatLoanCalculationHelper.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/ImportHelper.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/LoanCalculationHelper.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/HostedService/HostedImportExportActive.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/HostedService/HostedImportExportClosed.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Mappers/ExportMapper.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Models/ImportBoatLoan.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Models/ImportNewLoan.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Program.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ReserveManagementSystemApi.csproj create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ReserveManagementSystemApi.sln create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ServiceClients/CoreApiServiceClient.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ServiceClients/ICoreApiServiceClient.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/AuditHistoryService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/DealerService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ExportService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IAuditHistoryService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IDealerService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IExportService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IImportReadService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IImportService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ILoanCalculationService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ILoanService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IRmsActiveLoanService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IScheduleService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IStatusService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IVehicleService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ImportReadService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ImportService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/LoanCalculationService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/LoanService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/QueueServices/IQueueRmsActiveLoanService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/QueueServices/QueueRmsActiveLoanService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/RmsActiveLoanService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ScheduleService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/StatusService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/VehicleService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Using.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Configuration/CoreApiConfiguration.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Configuration/RmsApiConfiguration.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Enums/ImportType.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Helpers/StringExtensions.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Helpers/ValidationHelper.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/AuditHistory.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/CalculationConfig.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Dealer.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Import.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Loan.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanChargeback.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanChargebackInput.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanFilter.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanReserve.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanReserveInput.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanType.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/MinorAccountType.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/PaginatedListResponse.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/RmsActiveImportRequest.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Vehicle.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Persistence/AppDbContext.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/AuditHistoryRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/BaseRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/DealerRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IAuditHistoryRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IDealerRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IImportRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ILoanRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ILoanTypeRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IMinorAccountTypeRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IVehicleRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ImportRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/LoanRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/LoanTypeRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/MinorAccountTypeRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/VehicleRepository.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/ReserveManagementSystemLib.csproj create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Using.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-k8s-pipeline.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-feature-branches.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-prd.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-qa.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-uat.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/prd-pvars.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/qa-pvars.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/uat-pvars.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/Deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/Service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/secrets-provider-class.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/virtual-service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/set_secrets_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/set_secrets_provider.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/appsettings/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/set_appsettings_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/appsettings/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/set_appsettings_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/set_secrets_provider_class.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/normal/set_virtual_service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/set_configmap_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/appsettings/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/component/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/component/set_secrets_provider_class.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/normal/set_virtual_service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/set_configmap_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/appsettings/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/component/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/component/set_secrets_provider_class.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/normal/set_virtual_service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/set_configmap_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/appsettings/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/component/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/component/set_secrets_provider_class.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/normal/set_virtual_service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/set_configmap_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/README.md create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI.sln create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/App.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Dockerfile create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/GridHelpers.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/IEHelpers.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/ServiceHelpers.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Configuration/AppConfiguration.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Configuration/RmsUIConfiguration.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/TestModel.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Validators/TestModelValidator.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Error.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/History.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/History.razor.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Index.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Index.razor.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/LoanView.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/LoanView.razor.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Loans.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Loans.razor.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Statements.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Statements.razor.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Test.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Test.razor.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/_Host.cshtml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Program.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/ReserveManagementSystemUI.csproj create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Services/NotificationService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Services/ReserveService.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportBanner.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportBanner.razor.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportList.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportList.razor.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/LoadingLighthouse.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/LoadingSpinner.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/Table/TableData.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/Table/TableHeader.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/LoginDisplay.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/MainLayout.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/MainLayout.razor.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/NavMenu.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Startup.cs create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/_Imports.razor create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/libman.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/bootstrap/bootstrap.min.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/bootstrap/bootstrap.min.css.map create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/all.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/all.min.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/brands.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/brands.min.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/fontawesome.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/fontawesome.min.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/regular.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/regular.min.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/solid.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/solid.min.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/svg-with-js.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/svg-with-js.min.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/v4-shims.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/css/v4-shims.min.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/all.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/all.min.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/brands.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/brands.min.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/conflict-detection.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/conflict-detection.min.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/fontawesome.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/fontawesome.min.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/regular.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/regular.min.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/solid.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/solid.min.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/v4-shims.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/js/v4-shims.min.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/sprites/brands.svg create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/sprites/regular.svg create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/sprites/solid.svg create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-brands-400.eot create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-brands-400.svg create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-brands-400.ttf create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-brands-400.woff create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-brands-400.woff2 create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-regular-400.eot create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-regular-400.svg create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-regular-400.ttf create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-regular-400.woff create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-regular-400.woff2 create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-solid-900.eot create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-solid-900.svg create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-solid-900.ttf create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-solid-900.woff create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/font-awesome/webfonts/fa-solid-900.woff2 create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/open-iconic/FONT-LICENSE create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/open-iconic/ICON-LICENSE create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/open-iconic/README.md create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/open-iconic/font/fonts/open-iconic.eot create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/open-iconic/font/fonts/open-iconic.otf create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/open-iconic/font/fonts/open-iconic.svg create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/open-iconic/font/fonts/open-iconic.woff create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/site.css create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/favicon.ico create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/images/LMCU-Blue.png create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/images/LMCU-White-Logo-Only.png create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/images/lighthouse.gif create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/images/rotating.gif create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/images/smiling.gif create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/js/blazor.polyfill.min.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/js/loans.js create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/azdo_pipelines/azure-k8s-pipeline.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/azdo_pipelines/azure-pipelines-feature-branches.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/azdo_pipelines/azure-pipelines-prd.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/azdo_pipelines/azure-pipelines-qa.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/azdo_pipelines/azure-pipelines-uat.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/azdo_pipelines/config-files/prd-pvars.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/azdo_pipelines/config-files/qa-pvars.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/azdo_pipelines/config-files/uat-pvars.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/base/Deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/base/Service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/base/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/base/secrets-provider-class.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/base/virtual-service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/components/keyvaults/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/components/keyvaults/normal/set_secrets_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/components/keyvaults/normal/set_secrets_provider.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/components/workloads/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/components/workloads/normal/set_appsettings_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/local/appsettings/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/local/common/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/local/common/set_appsettings_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/local/common/set_secrets_provider_class.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/local/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/local/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/local/normal/set_virtual_service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/prd/appsettings/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/prd/component/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/prd/component/set_secrets_provider_class.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/prd/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/prd/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/prd/normal/set_environment_deployment.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/prd/normal/set_virtual_service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/qa/appsettings/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/qa/component/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/qa/component/set_secrets_provider_class.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/qa/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/qa/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/qa/normal/set_virtual_service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/uat/appsettings/appsettings.json create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/uat/component/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/uat/component/set_secrets_provider_class.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/uat/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/uat/normal/kustomization.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/kustomize/overlays/uat/normal/set_virtual_service.yaml create mode 100644 LCU.WebApps.Indirect.ReserveManagementSystem/cleanbuildrun.sh diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/.dockerignore b/LCU.WebApps.Indirect.ReserveManagementSystem/.dockerignore new file mode 100644 index 0000000..8b63706 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/.dockerignore @@ -0,0 +1,17 @@ +**/*.bat +**/*.log +**/*.md +**/*.yml +**/.dockerignore +**/.editorconfig +**/.git +**/.gitattributes +**/.gitignore +**/.vs +**/.vscode +**/bin +**/dist +**/dockerfile +**/node_modules +**/obj +**/package-lock.json \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/.gitignore b/LCU.WebApps.Indirect.ReserveManagementSystem/.gitignore new file mode 100644 index 0000000..fbd6d92 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/.gitignore @@ -0,0 +1,334 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# SpecFlow pickled files +Specs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc +*.feature.cs + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/Dockerfile b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/Dockerfile new file mode 100644 index 0000000..53a6b83 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/Dockerfile @@ -0,0 +1,51 @@ +# .NET Core SDK +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS aspnetcore-build + +# Copy Projects (context is Api/) +COPY ReserveManagementSystemApi/ReserveManagementSystemApi.csproj ./source/ReserveManagementSystemApi/ +COPY ReserveManagementSystemLib/ReserveManagementSystemLib.csproj ./source/ReserveManagementSystemLib/ + +# Install Credential Provider and set env variables to enable Nuget restore with auth +ARG PAT=bh2lq3jibktw67b2wdbqyfln7cd3je6lk52kijntpj43v6kimpgq +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && \ + update-ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Download the NuGet Credential Provider directly (Docker's ADD uses trusted TLS) +ADD https://github.com/microsoft/artifacts-credprovider/releases/latest/download/Microsoft.Net6.NuGet.CredentialProvider.tar.gz /tmp/ + +# Extract and install Credential Provider +RUN mkdir -p /root/.nuget/plugins/netcore && \ + tar --strip-components=3 -xzf /tmp/Microsoft.Net6.NuGet.CredentialProvider.tar.gz -C /root/.nuget/plugins/netcore && \ + rm /tmp/Microsoft.Net6.NuGet.CredentialProvider.tar.gz + +ENV NUGET_PLUGIN_PATHS=/root/.nuget/plugins/netcore/CredentialProvider.Microsoft.dll +ENV NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED=true +ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{\"endpointCredentials\": [{\"endpoint\":\"https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json\", \"password\":\"${PAT}\"}]}" + +# .NET Core Restore, use auth'd feed +RUN dotnet restore -s "https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json" -s "https://api.nuget.org/v3/index.json" ./source/ReserveManagementSystemApi/ReserveManagementSystemApi.csproj + +# Copy All Files +COPY . ./source/ + +FROM aspnetcore-build AS test +LABEL test=true +COPY ReserveManagementSystemApi.Test/ReserveManagementSystemApi.Test.csproj ./source/ReserveManagementSystemApi.Test/ +RUN dotnet restore ./source/ReserveManagementSystemApi.Test/ReserveManagementSystemApi.Test.csproj +RUN dotnet test -c Release --results-directory /testresults --logger "trx;LogFileName=test_results.trx" ./source/ReserveManagementSystemApi.Test/ReserveManagementSystemApi.Test.csproj + +# .NET Core Publish +FROM aspnetcore-build AS publish +RUN dotnet publish ./source/ReserveManagementSystemApi/ReserveManagementSystemApi.csproj -c Release -o /dist + +# ASP.NET Core +FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine3.14 +RUN apk add --no-cache icu-libs && apk add --no-cache tzdata; +ENV TZ=America/Chicago +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +ENV ASPNETCORE_ENVIRONMENT=Development + +WORKDIR /app +COPY --from=publish /dist . +ENTRYPOINT ["dotnet", "ReserveManagementSystemApi.dll"] diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Dealer.sql b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Dealer.sql new file mode 100644 index 0000000..0842aca --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Dealer.sql @@ -0,0 +1,9 @@ +CREATE TABLE [dbo].[Dealer] +( + [DealerCode] VARCHAR(4) NULL, + [Name] VARCHAR(40) NULL, + [DealerAccountNumber] VARCHAR(20) NULL, + [DealerOrganizationNumber] VARCHAR(20) NOT NULL, + [DealerGroupCode] VARCHAR(10) NULL, + CONSTRAINT [PK_Dealer] PRIMARY KEY (DealerOrganizationNumber) +) diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Import.sql b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Import.sql new file mode 100644 index 0000000..3f4d16c --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Import.sql @@ -0,0 +1,17 @@ +/* +Do not change the database path or name variables. +Any sqlcmd variables will be properly substituted during +build and deployment. +*/ +CREATE TABLE [dbo].[Import]( + [ImportId] int NOT NULL IDENTITY, + [StartDate] DATETIME2 NOT NULL, + [EndDate] DATETIME2 NULL, + [RecordCount] [int] NOT NULL DEFAULT 0, + [RecordsSaved] [int] NOT NULL DEFAULT 0, + [Success] [bit] NOT NULL, + [FileName] [varchar](50) NULL, + [ImportType] [varchar](10) NULL + CONSTRAINT [PK_Import] PRIMARY KEY (ImportId) +) + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Loan.sql b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Loan.sql new file mode 100644 index 0000000..e1f587f --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Loan.sql @@ -0,0 +1,41 @@ +CREATE TABLE [dbo].[Loan]( + [LoanId] int NOT NULL IDENTITY, + [ImportId] [int] NOT NULL, + [MemberNumber] [varchar](13) NULL, + [LoanNumber] [varchar](20) NOT NULL, + [MemberLastName] [varchar](60) NULL, + [MemberFirstName] [varchar](20) NULL, + [MemberInitial] [char](1) NULL, + [DealerOrganizationNumber] [varchar](20) NOT NULL, + [LoanDate] [smalldatetime] NOT NULL, + [FirstPmtdate] [smalldatetime] NOT NULL, + [FinalPmtDate] [smalldatetime] NOT NULL, + [PostedDate] [smalldatetime] NOT NULL, + [LoanAmount] [money] NOT NULL, + [BuyRate] [decimal](5, 3) NOT NULL, + [ContractRate] [decimal](5, 3) NOT NULL, + [MonthlyPmt] [decimal](20, 4) NOT NULL, + [LoanTypeId] [char](2) NULL, + [Term] [int] NOT NULL DEFAULT 0, + [ReserveAmount] [money] NOT NULL DEFAULT 0, + [FlatFeePaid] [bit] NOT NULL, + [Chargeback] [money] NULL, + [PaidLoanDate] [smalldatetime] NULL, + [ReserveMessage] [varchar](100) NOT NULL, + [ChargebackMessage] [varchar](100) NULL, + [ActionRequired] [bit] NOT NULL DEFAULT 0, + [StatementDate] [smalldatetime] NULL, + [Adjustment] [money] NULL, + [LoanStatus] [varchar](25) NULL, + [ExportedReserveDate] [datetime2] NULL, + [ExportedChargebackDate] [datetime2] NULL, + [ApplicationId] VARCHAR(20) NULL, + [Warranty] BIT NOT NULL DEFAULT 0, + [GAP] BIT NOT NULL DEFAULT 0, + [CreditLife] BIT NOT NULL DEFAULT 0, + [CreditDisability] BIT NOT NULL DEFAULT 0, + [MBP] BIT NOT NULL DEFAULT 0, + [RemainingMonthsAtClose] TINYINT NULL, + [AccountType] VARCHAR(4) NULL, + CONSTRAINT [PK_Loan] PRIMARY KEY (LoanId) +) diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/LoanAudit.sql b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/LoanAudit.sql new file mode 100644 index 0000000..d4feabd --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/LoanAudit.sql @@ -0,0 +1,13 @@ +CREATE TABLE [dbo].[LoanAudit]( + [AuditId] int NOT NULL IDENTITY, + [LoanId] int NULL, + [MemberNumber] VARCHAR(13) NULL, + [LoanNumber] VARCHAR(20) NULL, + [ModifiedBy] [varchar](50) NOT NULL, + [ModifiedDate] [smalldatetime] NOT NULL, + [OldValue] [varchar](MAX) NULL, + [NewValue] [varchar](MAX) NULL, + [ActivityType][varchar](20) NULL, + [AuditDescription] [varchar] (255) NULL, + CONSTRAINT [PK_LoanAudit] PRIMARY KEY (AuditId), +) diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/LoanType.sql b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/LoanType.sql new file mode 100644 index 0000000..6aaa857 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/LoanType.sql @@ -0,0 +1,11 @@ +CREATE TABLE [dbo].[LoanType]( + [LoanTypeId] [char](2) NOT NULL, + [LoanComment] [varchar](70) NOT NULL, + [ChargebackMethod] [char](1) NOT NULL, + [NoChargebackDays] [smallint] NOT NULL, + [NoChargebackPayments] [smallint] NOT NULL, + [ReserveDiscount] [decimal](4,2) NOT NULL, + [FlatFeePercent] [decimal](4,2) NOT NULL, + [NoReserveThreshold] [decimal](4,2) NOT NULL, + CONSTRAINT [PK_LoanType] PRIMARY KEY ([LoanTypeId]) +) \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/MinorAccountType.sql b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/MinorAccountType.sql new file mode 100644 index 0000000..374f69a --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/MinorAccountType.sql @@ -0,0 +1,11 @@ +/* +Do not change the database path or name variables. +Any sqlcmd variables will be properly substituted during +build and deployment. +*/ +CREATE TABLE [dbo].[MinorAccountType]( + [MinorAccountTypeId] int NOT NULL IDENTITY, + [AccountType] [varchar](4) NULL, + [GeneralLedgerAccountNumber] [varchar](20) NULL + CONSTRAINT [PK_MinorAccountType] PRIMARY KEY (MinorAccountTypeId) +) \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/ReserveManagementSystemApi.Sql.refactorlog b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/ReserveManagementSystemApi.Sql.refactorlog new file mode 100644 index 0000000..b1b17e4 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/ReserveManagementSystemApi.Sql.refactorlog @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/ReserveManagementSystemApi.Sql.sqlproj b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/ReserveManagementSystemApi.Sql.sqlproj new file mode 100644 index 0000000..43a1da1 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/ReserveManagementSystemApi.Sql.sqlproj @@ -0,0 +1,81 @@ + + + + Debug + AnyCPU + ReserveManagementSystemApi.Sql + 2.0 + 4.1 + {5a41c8dc-6f4d-4fd2-b03d-d0b2d8b363df} + Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider + Database + + + ReserveManagementSystemApi.Sql + ReserveManagementSystemApi.Sql + 1033, CI + BySchemaAndSchemaType + True + v4.5 + CS + Properties + False + True + True + + + bin\Release\ + $(MSBuildProjectName).sql + False + pdbonly + true + false + true + prompt + 4 + + + bin\Debug\ + $(MSBuildProjectName).sql + false + true + full + false + true + true + prompt + 4 + + + 11.0 + + True + 11.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Script.PostDeploymentPopulateLoanTypeTable.sql b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Script.PostDeploymentPopulateLoanTypeTable.sql new file mode 100644 index 0000000..9861059 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Script.PostDeploymentPopulateLoanTypeTable.sql @@ -0,0 +1,20 @@ +/* +Post-Deployment Script Template +-------------------------------------------------------------------------------------- + This file contains SQL statements that will be appended to the build script. + Use SQLCMD syntax to include a file in the post-deployment script. + Example: :r .\myfile.sql + Use SQLCMD syntax to reference a variable in the post-deployment script. + Example: :setvar TableName MyTable + SELECT * FROM [$(TableName)] +-------------------------------------------------------------------------------------- +*/ +USE [ReserveManagementSystem] +GO + +INSERT INTO [dbo].[LoanType] (LoanTypeId, LoanComment, ChargebackMethod, NoChargebackDays, NoChargebackPayments, ReserveDiscount, FlatFeePercent, NoReserveThreshold) VALUES ('BB', 'Standard Loan', 'S', 0, 0, 0.00, 1.00, 0.25) +INSERT INTO [dbo].[LoanType] (LoanTypeId, LoanComment, ChargebackMethod, NoChargebackDays, NoChargebackPayments, ReserveDiscount, FlatFeePercent, NoReserveThreshold) VALUES ('NC', '75% Reserve Paid - No charge back- after 90 Days', 'N', 90, 3, 25.00, 1.00, 0.25) +INSERT INTO [dbo].[LoanType] (LoanTypeId, LoanComment, ChargebackMethod, NoChargebackDays, NoChargebackPayments, ReserveDiscount, FlatFeePercent, NoReserveThreshold) VALUES ('NR', 'No reserve to be paid', 'N', 0, 0, 0.00, 0.00, 0.25) + + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Script.PreDeploymentTruncateLoanTypes.sql b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Script.PreDeploymentTruncateLoanTypes.sql new file mode 100644 index 0000000..2ac7baf --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Script.PreDeploymentTruncateLoanTypes.sql @@ -0,0 +1,6 @@ +USE [ReserveManagementSystem] +GO +IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'LoanType') +BEGIN + TRUNCATE TABLE [dbo].[LoanType]; +END \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Vehicle.sql b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Vehicle.sql new file mode 100644 index 0000000..8e59982 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Sql/Vehicle.sql @@ -0,0 +1,12 @@ +CREATE TABLE [dbo].[Vehicle] +( + [VehicleId] INT NOT NULL IDENTITY, + [LoanId] INT NULL, + [VINNumber] VARCHAR(30) NULL, + [Make] VARCHAR(30) NULL, + [Model] VARCHAR(30) NULL, + [Year] VARCHAR(4) NULL, + [Mileage] FLOAT NULL, + [Color] VARCHAR(30) NULL, + CONSTRAINT [PK_Vehicle] PRIMARY KEY (VehicleId) +) diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Test/ReserveManagementSystemApi.Test.csproj b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Test/ReserveManagementSystemApi.Test.csproj new file mode 100644 index 0000000..c2c959d --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Test/ReserveManagementSystemApi.Test.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + + false + + enable + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Test/Service/ExportServiceTest.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Test/Service/ExportServiceTest.cs new file mode 100644 index 0000000..e3634bd --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Test/Service/ExportServiceTest.cs @@ -0,0 +1,780 @@ +using Moq; +using Microsoft.EntityFrameworkCore; +using Xunit; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction; +using ReserveManagementSystemApi.Helpers; +using ReserveManagementSystemApi.Mappers; +using ReserveManagementSystemApi.ServiceClients; +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemLib.Configuration; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Repositories; +using ReserveManagementSystemLib.Persistence; + +namespace ReserveManagementSystemApi.Test.Service +{ + public class ExportServiceTest + { + private ExportService _exportService; + private Mock _mockCoreApiServiceClient; + private Mock _mockMinorAccountRepository; + private Mock _mockConfiguration; + private AppDbContext _appDbContext; + private ILoanRepository _loanRepository; + + public ExportServiceTest() + { + _mockCoreApiServiceClient = new Mock(); + _mockMinorAccountRepository = new Mock(); + _mockConfiguration = new Mock(); + + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + _appDbContext = new AppDbContext(options); + + _loanRepository = new LoanRepository(_appDbContext); + _mockConfiguration.Object.DefaultBatchSize = 100; + _mockConfiguration.Object.DefaultImportStartDate = "2021-07-01 00:00:00"; + _mockConfiguration.Object.DefaultConvertedLoanPostDate = "2021-10-31"; + + _exportService = new ExportService(_loanRepository, _mockCoreApiServiceClient.Object, _mockMinorAccountRepository.Object, _mockConfiguration.Object); + } + + [Fact] + public async Task SendExportChargebacksAsyncTest_OK() + { + var loan = new Loan(); + + loan.ExportedChargebackDate = null; + loan.PaidLoanDate = Convert.ToDateTime("2021-11-01"); + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ChargeBack = (decimal)22.00; + + var account = new MinorAccountType() + { + MinorAccountTypeId = 1, + GeneralLedgerAccountNumber = "11111111", + AccountType = "DL01" + }; + + _loanRepository.Add(loan); + _mockMinorAccountRepository.Setup(repo => repo.GetMinorAccountType("DL01")).Returns(account); + + var result = ExportMapper.MapLoanToIndirectLoanForChargeback(loan, account); + + var groupedList = new List { result }; + + var response = new MonetaryTransactionResponse() + { + WasSuccessful = true + }; + + var listResponse = new List { response }; + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + _mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(batchList)).Returns(Task.FromResult(listResponse)).Verifiable(); + + var numberOfLoansUpdatedSuccessfully = await _exportService.SendBatchListMonetaryTransactionRequests(groupedList); + var success = numberOfLoansUpdatedSuccessfully[0].WasSuccessful.ToString(); + + Assert.True(true, success); + } + + #region "Unit tests for Chargeback rounding" + + [Fact] + public async Task MappedExportRoundedUpChargebackAsyncTest_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + var account = new MinorAccountType() + { + MinorAccountTypeId = 1, + GeneralLedgerAccountNumber = "11111111", + AccountType = "DL01" + }; + + loan.Adjustment = 0; + loan.ChargeBack = (decimal)502.525; + + var result = ExportMapper.MapLoanToIndirectLoanForChargeback(loan, account); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + Assert.Equal(-502.53, batchList[0].MonetaryTransactionRequests[1].TransactionAmount); + Assert.Equal("ChrgBk-1111100000", batchList[0].MonetaryTransactionRequests[1].Description); + } + + [Fact] + public async Task MappedExportRoundedDownChargebackAsyncTest_OK() + { + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "2411000009999"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + var account = new MinorAccountType() + { + MinorAccountTypeId = 1, + GeneralLedgerAccountNumber = "11111111", + AccountType = "DL01" + }; + + loan.Adjustment = 0; + loan.ChargeBack = (decimal)38.171850; + + var result = ExportMapper.MapLoanToIndirectLoanForChargeback(loan, account); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + Assert.Equal(-38.17, batchList[0].MonetaryTransactionRequests[1].TransactionAmount); + Assert.Equal("ChrgBk-2411000009999", batchList[0].MonetaryTransactionRequests[1].Description); + } + } + + #endregion + + #region "Unit tests for Reserve rounding" + + [Fact] + public void MappedExportRoundedUpReserveAsyncTest1_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)502.525; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-502.53, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public void MappedExportRoundedUpReserveAsyncTest2_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)1237.715; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-1237.72, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedUpReserveAsyncTest3_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)1439.675; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-1439.68, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedDownReserveAsyncTest1_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)1982.1045; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-1982.10, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedDownReserveAsyncTest2_OK() + + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)1803.52449; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-1803.52, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedDownReserveAsyncTest3_OK() + + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)38.171850; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-38.17, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedDownTenThousandthReserveAsyncTest1_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)4240.9217; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-4240.92, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedDownTenThousandthReserveAsyncTest2_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)397.2345; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-397.23, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedDownTenThousandthReserveAsyncTest3_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)434.9949; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-434.99, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedDownTenThousandthReserveAsyncTest4_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)1513.1451; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-1513.15, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedDownTenThousandthReserveAsyncTest5_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)4240.9247; + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-4240.92, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedUpTenThousandthReserveAsyncTest1_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + loan.Adjustment = 0; + loan.ReserveAmount = (decimal)1513.154999; //value that would be written to DB is 1513.155 + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-1513.16, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + #endregion + + #region "Unit tests for reserve rounding with adjustments" + + [Fact] + public async Task MappedExportRoundedDownReserveAdjustmentAsyncTest_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + var adjustmentCalculation = (decimal)(1513.154999 - 1803.4656); //1513.16 - 1803.47 + + loan.Adjustment = (decimal)adjustmentCalculation; + loan.ReserveAmount = (decimal)1513.155; //as saved in DB + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(290.31, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedDownTenThousandthReserveAdjustmentAsyncTest_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + var adjustmentCalculation = (decimal)(1513.1489 - 1803.4734); //-290.3245 + + loan.Adjustment = (decimal)adjustmentCalculation; + loan.ReserveAmount = (decimal)1513.1489; //as saved in DB + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(290.32, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedUpHundredThousandthReserveAdjustmentAsyncTest_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + var adjustmentCalculation = (decimal)(1513.145 - 1803.52449); //-290.37949 //Reserve written to DB 1803.5245 + + loan.Adjustment = (decimal)adjustmentCalculation; + loan.ReserveAmount = (decimal)1513.145; //as saved in DB + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(290.38, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedUpMillionthReserveNegativeOneAdjustmentAsyncTest_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + var adjustmentCalculation = (decimal)(39.171450 - 38.171550); //0.999899999999997 + + loan.Adjustment = (decimal)adjustmentCalculation; + loan.ReserveAmount = (decimal)38.1715; //as saved in DB + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(-1, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedUpMillionthReservePositiveOneAdjustmentAsyncTest_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + var adjustmentCalculation = (decimal)(38.171550 - 39.171450); //-0.999899999999997 + + loan.Adjustment = (decimal)adjustmentCalculation; + loan.ReserveAmount = (decimal)38.1715; //as saved in DB + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(1, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + [Fact] + public async Task MappedExportRoundedUpMillionthReserveNoAdjustmentAsyncTest_OK() + { + var loan = new Loan(); + loan.ExportedReserveDate = null; + loan.ExportedChargebackDate = null; + loan.LoanNumber = "1111100000"; + loan.LoanId = 1; + loan.DealerOrganizationNumber = "15500"; + loan.AccountType = "DL01"; + + loan.Dealer = new Dealer() + { + DealerAccountNumber = "7788888", + DealerOrganizationNumber = "15500" + }; + + var adjustmentCalculation = (decimal)(38.171450 - 38.171850); //-0.000399999999999068 + + loan.Adjustment = (decimal)adjustmentCalculation; + loan.ReserveAmount = (decimal)38.1719; //as saved in DB + + var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan); + + var groupedList = new List { result }; + + var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize); + + List batchList = new List(resultBatch.FirstOrDefault()); + + //in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly. + Assert.Equal(0, batchList[0].MonetaryTransactionRequests[0].TransactionAmount); + } + + #endregion + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ChargebackCalculation.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ChargebackCalculation.feature new file mode 100644 index 0000000..df7b380 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ChargebackCalculation.feature @@ -0,0 +1,631 @@ +Feature: Chargeback Calculation + Calculate chargeback for a paid loan + +Scenario: NC - Calculate Chargeback for Paid Date is within 15 days of Loan date, 30 days to first payment + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 2/10/2021 | 3/12/2021 | 2/12/2028 | NC | 1206.8106 | 2/19/2021 | 84 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 1206.8106 | Paid On or Before Chargeback Date | 84 | + + #May 11, 2021 vs. May 12, 2021 First Payment Date wins by one day +Scenario: NC - Calculate Chargeback for Paid Date is within 15 days of Loan date, 30 days to first payment, Compare Loan Date vs. First Pmt Date, First Pmt Date Wins by 1 day + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 2/10/2021 | 3/12/2021 | 2/12/2028 | NC | 1206.8106 | 2/19/2021 | 84 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 1206.8106 | Paid On or Before Chargeback Date | 84 | + + Scenario: NC - Calculate Chargeback for Paid Date is after 15 days of Loan date, Compare Loan Date vs. First Pmt Date, Loan Date Wins by 1 day + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 2/1/2021 | 2/15/2021 | 2/15/2028 | NC | 1206.8106 | 2/19/2021 | 85 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 1178.7452372093023255813953488 | Paid On or Before Chargeback Date | 84 | + +Scenario: NC - Calculate Chargeback for Paid Date is after 15 days of Loan date, in a different year + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 12/1/2020 | 12/15/2020 | 12/15/2027 | NC | 1206.8106 | 1/19/2021 | 85 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 1151.0100551573187414500683995 | Paid On or Before Chargeback Date | 83 | + + Scenario: NC - Calculate Chargeback for Paid Date is within 15 days of Loan date, 30 days to first payment, Compare Loan Date vs. First Pmt Date, Loan Date Wins by 1 day + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 2/10/2021 | 3/10/2021 | 2/10/2028 | NC | 1206.8106 | 2/19/2021 | 84 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 1206.8106 | Paid On or Before Chargeback Date | 84 | + + Scenario: NC - Calculate Chargeback for Paid Date is after 15 days of Loan date, Compare Loan Date vs. First Pmt Date, Loan Date Wins by 1 day, Leslie's scenario + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 1/15/2019 | 2/14/2019 | 4/14/2025 | NC | 2141.8721 | 4/15/2019 | 75 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 1975.0315364210526315789473684 | Paid On or Before Chargeback Date | 72 | + +Scenario: Calculate Chargeback for Paid Date is within 15 days of Loan date, 30 days to first payment - Missing Loan Type Id + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 2/10/2021 | 3/12/2021 | 2/12/2028 | | 1206.8106 | 2/19/2021 | 84 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 1206.8106 | Paid On or Before Chargeback Date | 84 | + +Scenario: NC - Calculate Chargeback for Paid Date is within 15 days of Loan date, 45 days to first payment + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/30/2017 | 12/14/2017 | 11/14/2023 | NC | 927.0989 | 11/6/2017 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 927.0989 | Paid On or Before Chargeback Date | 72 | + +Scenario: NC - Calculate Chargeback for Paid Date is within 15 days of Loan date, 90 days to first payment + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/7/2020 | 1/5/2021 | 12/5/2025 | NC | 229.0637 | 10/21/2020 | 48 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 238.60802083333333333333333333 | Paid On or Before Chargeback Date | 49 | + +Scenario: NC - Calculate Chargeback for Paid Date is exactly 15 days from Loan date, 45 days to first payment + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 12/7/2020 | 1/21/2021 | 12/21/2025 | NC | 878.8602 | 12/22/2020 | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 878.8602 | Paid On or Before Chargeback Date | 60 | + +Scenario: NC - Calculate Chargeback for Paid Date more than 15 days past Loan date but before or on the first payment date, 32 days until first payment date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 1/28/2021 | 3/1/2021 | 5/1/2027 | NC | 855.6368 | 2/25/2021 | 75 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 833.1200421052631578947368421 | Paid On or Before Chargeback Date | 74 | + +Scenario: NC - Calculate Chargeback for Paid Date more than 15 days past Loan date but before or on the first payment date, 45 days until first payment date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/15/2019 | 11/29/2019 | 10/29/2025 | NC | 1320.8907 | 10/31/2019 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 1320.8907 | Paid On or Before Chargeback Date | 72 | + +Scenario: NC - Calculate Chargeback for Paid Date more than 15 days past Loan date but before or on the first payment date, 90 days until first payment date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 12/22/2020 | 3/22/2021 | 2/22/2027 | NC | 1689.8999 | 1/14/2021 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 1736.8415638888888888888888889 | Paid On or Before Chargeback Date | 73 | + +Scenario: NC - Calculate Chargeback for Paid Date more than 15 days past Loan date but before or on the first payment date, 91 days until first payment date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/26/2018 | 1/25/2019 | 12/25/2024 | NC | 203.7054 | 11/15/2018 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 209.36388333333333333333333333 | Paid On or Before Chargeback Date | 73 | + +Scenario: NC - Calculate Chargeback for Paid Date more than 15 days past Loan date after the first payment date, over Feb month example to trigger "not a date" condition. + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 3/11/2021 | 4/25/2021 | 3/25/2027 | NC | 309.4481 | 4/30/2021 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 300.9700698630136986301369863 | Paid On or Before Chargeback Date | 71 | + +Scenario: NC - Calculate Chargeback for Paid Date past the chargeback date - this is day after + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/26/2018 | 1/25/2019 | 12/25/2024 | NC | 203.7054 | 3/26/2019 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Paid After Chargeback Date | 69 | + +Scenario: NC - Calculate Chargeback for Paid Date past the final payment date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/26/2018 | 1/25/2019 | 12/25/2024 | NC | 203.7054 | 3/26/2025 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Paid After Chargeback Date | 0 | + +Scenario: NR - Calculate Chargeback for NR loan - paid past chargeback date (month before first payment date) + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/15/2019 | 11/29/2019 | 10/29/2025 | NR | 0 | 10/31/2019 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | No Reserve - No Chargeback | 72 | + +Scenario: NR - Calculate Chargeback for NR loan - paid before chargeback date, 0 chargeback because reserve is 0 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/15/2019 | 11/29/2019 | 10/29/2025 | NR | 0 | 10/20/2019 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | No Reserve - No Chargeback | 72 | + +Scenario: NR - Calculate Chargeback for NR loan - paid after final payment date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/15/2019 | 11/29/2019 | 10/29/2025 | NR | 0 | 12/1/2025 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | No Reserve - No Chargeback | 0 | + +Scenario: BB - Calculate Chargeback for BB loan - 0 chargeback because reserve is 0 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/15/2019 | 11/29/2019 | 10/29/2025 | BB | 0 | 10/20/2019 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | No Reserve - No Chargeback | 72 | + +Scenario: BB - Calculate Chargeback for BB loan - 0 chargeback because it is paid after final payment date. + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 8/24/2017 | 9/23/2017 | 8/23/2020 | BB | 127.24 | 5/18/2021 | 36 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Paid After Chargeback Date | 0 | + +Scenario: BB - Calculate Chargeback for BB loan - 0 chargeback because it is paid on the final payment date. + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 8/24/2017 | 9/23/2017 | 5/18/2021 | BB | 127.24 | 5/18/2021 | 36 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Paid After Chargeback Date | 0 | + +Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, past chargeback date, NoChargebackPayments + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 9/11/2018 | 10/24/2018 | 12/24/2024 | BB | 287.3639 | 4/30/2021 | 75 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 99.82114421052631578947368421 | Paid On or Before Chargeback Date | 44 | + +Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, paid before first payment date, NoChargebackPayments + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/15/2019 | 11/29/2019 | 10/29/2025 | BB | 500 | 10/20/2019 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 500 | Paid On or Before Chargeback Date | 72 | + +Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, paid more than a year after first payment. + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 1/18/2018 | 3/4/2018 | 2/4/2023 | BB | 790.4206 | 4/30/2021 | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 99.77440360655737704918032787 | Paid On or Before Chargeback Date | 21 | + +Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, almost to final payment date. + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 5/2/2015 | 6/10/2015 | 5/10/2021 | BB | 26938.75 | 4/30/2021 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Paid After Chargeback Date | 0 | + +Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, before final payment date. + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 3/30/2017 | 5/14/2017 | 4/14/2022 | BB | 115.00 | 4/30/2021 | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 4.1475409836065573770491803279 | Paid On or Before Chargeback Date | 11 | + +Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, paid within a year of loan date. + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 12/22/2020 | 2/5/2021 | 4/5/2027 | BB | 265.9569 | 4/26/2021 | 75 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 238.52134610526315789473684211 | Paid On or Before Chargeback Date | 71 | + +Scenario: BB - Calculate Chargeback for BB loan - paid after final pmt date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 12/22/2020 | 2/5/2021 | 4/5/2027 | BB | 265.9569 | 4/26/2027 | 75 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Paid After Chargeback Date | 0 | + +Scenario: BB - Calculate Chargeback for Buydown reserve - checking Buydown message buydown reserve below threshold + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | ReserveMessage | + | 8/4/2017 | 9/3/2017 | 8/3/2022 | NC | 0 | 10/20/2019 | 60 | BuyDown - Reserve - Below Threshold | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve Paid - No charge back- after 90 Days | N | 90 | 3 | 25 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Buy Down Reserve - No Chargeback | 33 | + +Scenario: NC - Calculate Chargeback for Buydown reserve - checking Buydown message buydown reserve and negative value + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | ReserveMessage | + | 9/30/2019 | 11/14/2019 | 10/14/2025 | NC | -3471.1087 | 3/20/2021 | 72 | BuyDown - Reserve | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve Paid - No charge back- after 90 Days | N | 90 | 3 | 25 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Buy Down Reserve - No Chargeback | 55 | + +Scenario: NC - Calculate Boat Chargeback for Buydown reserve - checking Buydown message buydown reserve and negative value + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | ReserveMessage | AccountType | + | 9/30/2019 | 11/14/2019 | 10/14/2025 | NC | -3471.1087 | 3/20/2021 | 60 | BuyDown - Reserve | DL10 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve Paid - No charge back- after 90 Days | N | 90 | 3 | 25 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Buy Down Reserve - No Chargeback | 43 | + +Scenario: NC - Calculate Boat Chargeback for paid before chargeback date - Term 63 or less + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | AccountType | + | 7/4/2019 | 8/3/2019 | 10/3/2024 | NC | 460.04 | 11/21/2019 | 63 | DL12 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 390.44069444444444444444444444 | Paid On or Before Chargeback Date | 58 | + +Scenario: NC - Calculate Boat Chargeback for paid before chargeback date - Term greater than 63 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | AccountType | + | 7/3/2019 | 8/2/2019 | 7/2/2029 | NC | 1163.05 | 5/6/2020 | 120 | DL11 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 978.0193181818181818181818182 | Paid On or Before Chargeback Date | 110 | + +Scenario: NC - Calculate Boat Chargeback for after chargeback date - Term 63 or less + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | AccountType | + | 07/04/2019 | 8/3/2019 | 10/3/2024 | NC | 460.04 | 12/21/2020 | 63 | DL11 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Paid After Chargeback Date | 45 | + +Scenario: NC - Calculate Boat Chargeback for after chargeback date - Term greater than 63 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | AccountType | + | 7/3/2019 | 8/2/2019 | 7/2/2029 | NC | 1163.05 | 5/6/2021 | 120 | DL09 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 0 | Paid After Chargeback Date | 98 | + +Scenario Outline: NC - Calculate Chargeback for paid date between first payment date and chargeback date examples 1 and 2 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 12/26/2020 | 2/9/2021 | 4/9/2027 | NC | 1815.0503 | | 75 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | | | | +Examples: + | PaidLoanDate | Chargeback | ChargebackMessage | RemainingMonthsAtClose | + | 2/26/2021 | 1720.1581965964912280701754386 | Paid On or Before Chargeback Date | 73 | + | 2/25/2021 | 1720.1581965964912280701754386 | Paid On or Before Chargeback Date | 73 | + +Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 3 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 12/29/2020 | 2/12/2021 | 1/12/2027 | NC | 1835.6378 | 2/26/2021 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 1785.3463534246575342465753425 | Paid On or Before Chargeback Date | 71 | + +Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 4 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 11/23/2020 | 1/7/2021 |12/7/2025 | NC | 1085.4654 | 2/25/2021 | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 980.477762950819672131147541 | Paid On or Before Chargeback Date | 57 | + +Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 5 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 12/14/2020 | 1/28/2021 | 12/28/2024 | NC | 58.5443 | 2/19/2021 | 48 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 53.814956037414965986394557823 | Paid On or Before Chargeback Date | 46 | + +Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 6 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 12/22/2020 | 2/5/2021 | 1/5/2027 | NC | 853.1331 | 2/19/2021 | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 829.7595904109589041095890411 | Paid On or Before Chargeback Date | 71 | + +Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 7 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 12/4/2020 | 1/18/2021 | 12/18/2025 | NC | 584.2477 | 2/18/2021 | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 546.25563644808743169398907104 | Paid On or Before Chargeback Date | 58 | + +Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 8 - paid day after first payment date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 1/2/2021 | 2/16/2021 | 11/16/2024 | NC | 116.0986 | 2/17/2021 | 46 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 111.15823404255319148936170213 | Paid On or Before Chargeback Date | 45 | + +Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 9 - first payment in a month with 30 days + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 10/14/2020 | 11/28/2020 | 10/28/2023 | NC | 110.7846 | 1/25/2021 | 36 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 93.31855945945945945945945946 | Paid On or Before Chargeback Date | 33 | + +Scenario: NC - Calculate Chargeback without using Arbitrary Date (paid loan date 29th, not leap year) for Paid Date after First Payment Date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 1/16/2020 | 4/15/2020 | 3/15/2025 | NC | 169.0256 | 4/29/2020 | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 163.48377704918032786885245902 | Paid On or Before Chargeback Date | 59 | + +Scenario: NC - Calculate Chargeback without using Arbitrary Date (paid loan date 30th, not leap year) for Paid Date after First Payment Date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 3/17/2017 | 4/16/2017 | 3/16/2019 | NC | 86.4577 | 5/30/2017 | 24 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 72.912660333333333333333333333 | Paid On or Before Chargeback Date | 22 | + +Scenario: NC - Calculate Chargeback without using Arbitrary Date (paid loan date 31st, not leap year) for Paid Date after First Payment Date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 3/17/2017 | 4/16/2017 | 3/16/2019 | NC | 86.4577 | 5/31/2017 | 24 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 72.912660333333333333333333333 | Paid On or Before Chargeback Date | 22 | + +Scenario: NC - Calculate Chargeback using Arbitrary Date (paid loan date 29th, leap year) for Paid Date after First Payment Date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 1/16/2019 | 4/15/2019 | 3/15/2024 | NC | 169.0256 | 4/29/2019 | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 163.48377704918032786885245902 | Paid On or Before Chargeback Date | 59 | + +Scenario: NC - Calculate Chargeback using Arbitrary Date (paid loan date 30th, leap year) for Paid Date after First Payment Date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 3/17/2018 | 4/16/2018 | 3/16/2020 | NC | 86.4577 | 5/30/2018 | 24 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 72.912660333333333333333333333 | Paid On or Before Chargeback Date | 22 | + +Scenario: NC - Calculate Chargeback without using Arbitrary Date (paid loan date 31st, leap year) for Paid Date after First Payment Date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 3/17/2018 | 4/16/2018 | 3/16/2020 | NC | 86.4577 | 5/31/2018 | 24 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 72.912660333333333333333333333 | Paid On or Before Chargeback Date | 22 | + +Scenario: NC - Calculate Chargeback using Arbitrary Date with Final pmt after Arbitrary Date (paid loan date 31st, not leap year) for Paid Date after First Payment Date + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | + | 11/30/2018 | 1/1/2019 | 3/1/2025 | NC | 2456.5546 | 1/31/2019 | 75 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the chargeback is calculated + Then the chargeback returned is + | ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose | + | 2328.1242016140350877192982456 | Paid On or Before Chargeback Date | 73 | \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/DealerService.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/DealerService.feature new file mode 100644 index 0000000..46924d7 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/DealerService.feature @@ -0,0 +1,155 @@ +Feature: Dealer Service + Get a list of dealers when searching code or name. Update dealers + +Scenario: Search for Dealer - Code - find multiple dealers + Given the dealer table looks like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 | + | S11 | ANTHONY AUTO GROUP | 5678 | 7535434333 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 | + When I search for dealers with search string 'S1' + Then dealers returned should be + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 | + | S11 | ANTHONY AUTO GROUP | 5678 | 7535434333 | + +Scenario: Search for Dealer - Code - no dealers returned + Given the dealer table looks like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 | + | S11 | ANTHONY AUTO GROUP | 5678 | 7535434333 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 | + When I search for dealers with search string 'A1' + Then dealers returned should be + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + +Scenario: Search for Dealer - Name - find multiple dealers + Given the dealer table looks like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 | + | S11 | ANTHONY AUTO GROUP OF WEST BEND | 5678 | 7535434333 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 | + When I search for dealers with search string 'WEST' + Then dealers returned should be + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S11 | ANTHONY AUTO GROUP OF WEST BEND | 5678 | 7535434333 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 | + +Scenario: Search for Dealer - Name - no dealers returned + Given the dealer table looks like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 | + | S11 | ANTHONY AUTO GROUP | 5678 | 7535434333 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 | + When I search for dealers with search string 'A1' + Then dealers returned should be + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + +Scenario: Search for Dealer - Code and name - find multiple dealers + Given the dealer table looks like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 | + | A1 | ANTHONY AUTO GROUP OF WEST BEND S1 | 9234 | 3535434333 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 | + When I search for dealers with search string 'S1' + Then dealers returned should be + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 | + | A1 | ANTHONY AUTO GROUP OF WEST BEND S1 | 9234 | 3535434333 | + +Scenario: Update dealers from loans - add dealer that doesn't exist + Given a list of loans with dealers that look like + | LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1 | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434333 | + And the dealer table looks like + |Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + When I update the dealers from the loans + Then the dealer table now looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434333 | + +Scenario: Update dealers from loans - add dealer with blank org number + Given a list of loans with dealers that look like + | LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1 | | | | | + And the dealer table looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434333 | + When I update the dealers from the loans + Then the dealer table now looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434333 | + | | | | | + +Scenario: Update dealers from loans - update name and account number + Given a list of loans with dealers that look like + | LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1 | 1234 | S1 | SUBURBAN MOTORS HD | 4535434334 | + And the dealer table looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + When I update the dealers from the loans + Then the dealer table now looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HD | 4535434334 | + +Scenario: Update dealers from loans - DO update dealer code + Given a list of loans with dealers that look like + | LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1 | 1234 | S11 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + And the dealer table looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + When I update the dealers from the loans + Then the dealer table now looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S11 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + +Scenario: Update dealers from loans - only take updates from the first loan with that dealer + Given a list of loans with dealers that look like + | LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1 | 1234 | S1 | SUBURBAN MOTORS HD | 4535434334 | + | 1 | 1234 | S1 | BEST SUBURBAN MOTORS HD | 4535434335 | + And the dealer table looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + When I update the dealers from the loans + Then the dealer table now looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HD | 4535434334 | + +Scenario: Update dealers from loans - a null dealer code doesn't get pulled in + Given a list of loans with dealers that look like + | LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1 | 1234 | | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + And the dealer table looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + When I update the dealers from the loans + Then the dealer table now looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + +Scenario: Update dealers from loans - a null dealer name doesn't get pulled in + Given a list of loans with dealers that look like + | LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1 | 1234 | S1 | | 4535434334 | + And the dealer table looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + When I update the dealers from the loans + Then the dealer table now looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + +Scenario: Update dealers from loans - a null dealer account number doesn't get pulled in + Given a list of loans with dealers that look like + | LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1 | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | | + And the dealer table looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | + When I update the dealers from the loans + Then the dealer table now looks like + | Dealer Organization Number | Dealer Code | Name | Dealer Account Number | + | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 | \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/DealerServiceSteps.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/DealerServiceSteps.cs new file mode 100644 index 0000000..2751f6e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/DealerServiceSteps.cs @@ -0,0 +1,95 @@ + +using Microsoft.EntityFrameworkCore; +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; +using ReserveManagementSystemLib.Repositories; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using TechTalk.SpecFlow; +using TechTalk.SpecFlow.Assist; +using TechTalk.SpecFlow.Assist.ValueRetrievers; + +namespace ReserveManagementSystemApi.Tests.Features +{ + [Binding] + public sealed class DealerServiceSteps + { + // For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef + + private readonly ScenarioContext _scenarioContext; + private DealerService _dealerService; + private AppDbContext _appDbContext; + private IEnumerable _dealersResult; + private List _loans; + + public DealerServiceSteps(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + Service.Instance.ValueRetrievers.Register(new NullValueRetriever("")); + } + + [BeforeScenario] + public void CreateDatabase() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + _appDbContext = new AppDbContext(options); + _dealerService = new DealerService(new DealerRepository(_appDbContext)); + } + + [AfterScenario] + public void Dispose() + { + _appDbContext.Database.EnsureDeleted(); + _appDbContext.Dispose(); + } + + [Given(@"the dealer table looks like")] + public void GivenTheDealerTableLooksLike(Table table) + { + var dealers = table.CreateSet().ToList(); + _appDbContext.Dealers.AddRange(dealers); + _appDbContext.SaveChanges(); + } + + [Given(@"a list of loans with dealers that look like")] + public void GivenAListOfLoansWithDealersThatLookLike(Table table) + { + _loans = table.CreateSet().ToList(); + var dealers = table.CreateSet().ToList(); + for (var i=0; i < _loans.Count; i++) + { + _loans.Skip(i).First().Dealer = dealers.Skip(i).First(); + } + } + + [When(@"I search for dealers with search string '(.*)'")] + public async void WhenISearchForDealersWithSearchString(string dealerSearchText) + { + _dealersResult = await _dealerService.Search(dealerSearchText); + } + + [When(@"I update the dealers from the loans")] + public async void WhenIUpdateTheDealersFromTheLoans() + { + await _dealerService.UpdateDealersAsync(_loans); + } + + [Then(@"dealers returned should be")] + public void ThenDealersReturnedShouldBe(Table table) + { + table.CompareToSet(_dealersResult); + } + + [Then(@"the dealer table now looks like")] + public void ThenTheDealerTableNowLooksLike(Table table) + { + var dealers = _appDbContext.Dealers.ToList(); + table.CompareToSet(dealers); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ExportService.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ExportService.feature new file mode 100644 index 0000000..1c01167 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ExportService.feature @@ -0,0 +1,131 @@ +Feature: Export Service + Export loans to DNA that have updated reserves or chargebacks + +Scenario: Export loan reserves and mark as exported + Given the current loans that look like this + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + And the dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 | + | A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 | + And the Core Api is successful in saving reserves + When I export the loan reserves on the date '6/1/2021 3:00 PM' + Then the Core API is called with 2 loan reserves + And the loan list will be updated to look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 6/1/2021 3:00 PM | | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 6/1/2021 3:00 PM | | + And the result of saving the reserves will be 2 + +Scenario: Export loan reserves - core api service returns false + Given the current loans that look like this + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + And the dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 | + | A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 | + And the Core Api returns false when saving reserves + When I export the loan reserves on the date '6/1/2021 3:00 PM' + Then the Core API is called with 2 loan reserves + And the loan list will be updated to look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + And the result of saving the reserves will be 0 + +Scenario: Export loan reserves - error in service client + Given the current loans that look like this + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + And the dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 | + | A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 | + And the Core Api throws an error in saving reserves + When I export the loan reserves on the date '6/1/2021 3:00 PM' + Then the Core API is called with 2 loan reserves + And the loan list will be updated to look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + And the export service throws an Exception + +Scenario: Export loan chargebacks and mark as exported + Given the current loans that look like this + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | AccountType | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | | DL01 | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | | DL01 | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | DL01 | + And the dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 | + | A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 | + And the current minor account that look like this + | MinorAccountTypeId | GeneralLedgerAccountNumber | AccountType | + | 1 | 731 | DL01 | + And the Core Api is successful in saving chargebacks + When I export the loan chargebacks on the date '5/2/2021 2:00 PM' + Then the Core API is called with 1 loan chargebacks + And the loan list will be updated to look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | AccountType | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | 5/2/2021 2:00 PM | DL01 | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | | DL01 | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | DL01 | + And the result of saving the chargebacks will be 1 + +Scenario: Export loan chargebacks - core api returns false + Given the current loans that look like this + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + And the dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 | + | A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 | + And the Core Api returns false when saving chargebacks + When I export the loan chargebacks on the date '5/2/2021 2:00 PM' + Then the Core API is called with 1 loan chargebacks + And the loan list will be updated to look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + And the result of saving the chargebacks will be 0 + +Scenario: Export loan chargebacks - error in service client + Given the current loans that look like this + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + And the dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 | + | A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 | + And the Core Api throws an error in saving chargebacks + When I export the loan chargebacks on the date '5/2/2021 2:00 PM' + Then the Core API is called with 1 loan chargebacks + And the loan list will be updated to look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | + | 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | | + | 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | | + | 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | + And the export service throws an Exception \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ExportServiceSteps.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ExportServiceSteps.cs new file mode 100644 index 0000000..8cbc3d4 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ExportServiceSteps.cs @@ -0,0 +1,209 @@ +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests; +using Microsoft.EntityFrameworkCore; +using Moq; +using ReserveManagementSystemApi.ServiceClients; +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; +using ReserveManagementSystemLib.Repositories; +using TechTalk.SpecFlow; +using TechTalk.SpecFlow.Assist; +using Xunit; +using ReserveManagementSystemLib.Configuration; + +namespace ReserveManagementSystemApi.Tests.Features +{ + [Binding] + public sealed class ExportServiceSteps + { + // For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef + + private readonly ScenarioContext _scenarioContext; + + private ExportService _exportService; + private AppDbContext _appDbContext; + private Mock _mockCoreApiServiceClient = new Mock(); + private int _exportResult; + private Mock _mockConfiguration = new Mock(); + private Exception _exportException; + + public ExportServiceSteps(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + } + + [BeforeScenario] + public void CreateDatabase() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + _appDbContext = new AppDbContext(options); + _mockConfiguration.Object.DefaultBatchSize = 100; + _exportService = new ExportService(new LoanRepository(_appDbContext), _mockCoreApiServiceClient.Object, new MinorAccountTypeRepository(_appDbContext), _mockConfiguration.Object); + } + + [AfterScenario] + public void Dispose() + { + _appDbContext.Database.EnsureDeleted(); + _appDbContext.Dispose(); + } + + [Given(@"the current loans that look like this")] + public void GivenTheCurrentLoansThatLookLikeThis(Table table) + { + var loans = table.CreateSet(); + _appDbContext.Loans.AddRange(loans); + _appDbContext.SaveChanges(); + } + + [Given(@"the current minor account that look like this")] + public void GivenTheCurrentMinorAccountsThatLookLikeThis(Table table) + { + var accounts = table.CreateSet(); + _appDbContext.MinorAccountTypes.AddRange(accounts); + _appDbContext.SaveChanges(); + } + + [Given(@"the Core Api is successful in saving reserves")] + public void GivenTheCoreApiIsSuccessfulInSavingReserves() + { + var transactionResponse = new MonetaryTransactionResponse(); + transactionResponse.AccountNumber = 6; + transactionResponse.WasSuccessful = true; + + var transactionResponse2 = new MonetaryTransactionResponse(); + transactionResponse2.AccountNumber = 73; + transactionResponse2.WasSuccessful = true; + + var transactionResponseList = new List { transactionResponse, transactionResponse2 }; + + _mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny>())).ReturnsAsync(transactionResponseList); + } + + [Given(@"the Core Api returns false when saving reserves")] + public void GivenTheCoreApiReturnsFalseWhenSavingReserves() + { + var transactionResponse = new MonetaryTransactionResponse(); + transactionResponse.AccountNumber = 6; + transactionResponse.WasSuccessful = false; + + var transactionResponse2 = new MonetaryTransactionResponse(); + transactionResponse.AccountNumber = 73; + transactionResponse.WasSuccessful = false; + + var transactionResponseList = new List { transactionResponse, transactionResponse2 }; + + _mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny>())).ReturnsAsync(transactionResponseList); + } + + [Given(@"the Core Api returns false when saving chargebacks")] + public void GivenTheCoreApiReturnsFalseWhenSavingChargebacks() + { + var transactionResponse = new MonetaryTransactionResponse(); + transactionResponse.AccountNumber = 731; + transactionResponse.WasSuccessful = false; + + var transactionResponseList = new List { transactionResponse }; + + _mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny>())).ReturnsAsync(transactionResponseList); + } + + [Given(@"the Core Api throws an error in saving reserves")] + public void GivenTheCoreApiThrowsAnErrorInSavingReserves() + { + _mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny>())).Throws(new Exception()); + } + + [Given(@"the Core Api is successful in saving chargebacks")] + public void GivenTheCoreApiIsSuccessfulInSavingChargebacks() + { + var transactionResponse = new MonetaryTransactionResponse(); + transactionResponse.AccountNumber = 731; + transactionResponse.WasSuccessful = true; + + var transactionResponseList = new List { transactionResponse }; + + _mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny>())).ReturnsAsync(transactionResponseList); + } + + [Given(@"the Core Api throws an error in saving chargebacks")] + public void GivenTheCoreApiThrowsAnErrorInSavingChargebacks() + { + _mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny>())).Throws(new Exception()); + } + + [When(@"I export the loan reserves on the date '(.*)'")] + public async void WhenIExportTheLoanReservesOnTheDate(DateTime currentDate) + { + try + { + _exportResult = await _exportService.ExportReservesAsync(currentDate); + } + catch(Exception ex) + { + _exportException = ex; + } + } + + [When(@"I export the loan chargebacks on the date '(.*)'")] + public async void WhenIExportTheLoanChargebacksOnTheDate(DateTime currentDate) + { + try + { + _exportResult = await _exportService.ExportChargebacksAsync(currentDate); + } + catch (Exception ex) + { + _exportException = ex; + } + } + + [Then(@"the Core API is called with (.*) loan reserves")] + public void ThenTheCoreAPIIsCalledWithLoanReserves(int numberOfReserves) + { + _mockCoreApiServiceClient.Verify(x => x.PostMonetaryTransactions(It.Is>(x => x.Count == numberOfReserves))); + } + + [Then(@"the Core API is called with (.*) loan chargebacks")] + public void ThenTheCoreAPIIsCalledWithLoanChargebacks(int numberOfChargebacks) + { + _mockCoreApiServiceClient.Verify(x => x.PostMonetaryTransactions(It.Is>(x => x.Count == numberOfChargebacks))); + } + + [Then(@"the loan list will be updated to look like")] + public void ThenTheLoanListWillBeUpdatedToLookLike(Table table) + { + var loansResult = _appDbContext.Loans; + table.CompareToSet(loansResult); + } + + [Then(@"the result of saving the reserves will be (.*)")] + public void ThenTheResultOfSavingTheReservesWillBe(int exportNumber) + { + Assert.Equal(exportNumber, _exportResult); + } + + [Then(@"the result of saving the chargebacks will be (.*)")] + public void ThenTheResultOfSavingTheChargebacksWillBe(int exportNumber) + { + Assert.Equal(exportNumber, _exportResult); + } + + [Then(@"the export service throws an Exception")] + public void ThenTheExportServiceThrowsAnException() + { + Assert.IsType(_exportException); + } + + [Given(@"the dealers that look like")] + public void GivenTheDealersThatLookLike(Table table) + { + var dealers = table.CreateSet().ToList(); + _appDbContext.Dealers.AddRange(dealers); + _appDbContext.SaveChanges(); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService Closed from DNA.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService Closed from DNA.feature new file mode 100644 index 0000000..9f1df6c --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService Closed from DNA.feature @@ -0,0 +1,392 @@ +Feature: Import Read Service - Closed Loans from DNA + Gets closed loans from DNA by calling a CoreApiWrapper endpoint, reads loans into memory, calculates chargebacks, saves loans, saves import information + +Background: + Given a loan type table consisting of + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + | BB | Standard | S | 0 | 0 | 0 | 1.00 | 0.25 | + | NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 | + +Scenario: AddImport for Closed Loans from DNA - all loans are saved + Given that I get these closed loans back from DNA + | LoanNumber | PaidLoanDate | + | 147 | 02/25/2021 | + | 143 | 02/12/2021 | + | 144 | 02/12/2021 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | RemainingMonthsAtClose | + | 1 | 1 | 5841 | 147 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | | + | 2 | 2 | 10094 | 143 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | Updated | | + | 3 | 2 | 100947 | 144 | JOHNSON | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | New | | + When I process the closed loan import from DNA at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 5841 | 147 | 210.0375 | 2/25/2021 | Closed | 63 | + | 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 | + | 3 | 100947 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 | + And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed" + And AddHistoriesAsync is called with 1 history/ies having OldValue "Updated" and NewValue "Closed" + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + +Scenario: AddImport for Closed Loans from DNA - saves two loans that are in the loan table + Given that I get these closed loans back from DNA + | LoanNumber | PaidLoanDate | + | 141 | 04/25/2021 | + | 143 | 02/12/2021 | + | 144 | 02/12/2021 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | + | 1 | 1 | 12 | 3 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | + | 2 | 2 | 10094 | 143 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | + | 3 | 2 | 10621 | 144 | JOHNSON | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | + When I process the closed loan import with missing loan "141" with date from DNA at "3/5/2021 5:00:00 AM" and select from "2/25/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Closed | 1 | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 12 | 3 | | | New | | + | 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 | + | 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 | + And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed" + And AddHistoriesAsync is called with 2 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | Term | + | 3250 | 141 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 5.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 4/25/2021 | 63 | + When I process the missing active loans with detail from DNA at "3/5/2021 5:00:00 AM" and export + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Closed | 2 | + And the import has an end date that is greater or equal to the start date + And the loans with details now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 3250 | 141 | 720.6656 | 04/25/2021 | Closed | 60 | + +Scenario: AddImport for Closed Boat Loans from DNA - saves two boat loans that are in the loan table + Given that I get these closed loans back from DNA + | LoanNumber | PaidLoanDate | + | 147 | 02/25/2021 | + | 143 | 04/12/2021 | + | 144 | 04/12/2021 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | AccountType | + | 1 | 1 | 12 | 3 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | DL09 | + | 2 | 2 | 10094 | 143 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | DL10 | + | 3 | 2 | 10621 | 144 | JOHNSON | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | DL11 | + When I process the closed loan import with missing loan "147" with date from DNA at "3/5/2021 5:00:00 AM" and select from "2/25/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Closed | 1 | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 12 | 3 | | | New | | + | 2 | 10094 | 143 | 1402.7417 | 4/12/2021 | Closed | 59 | + | 3 | 10621 | 144 | 1402.7417 | 4/12/2021 | Closed | 59 | + And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed" + And AddHistoriesAsync is called with 2 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | AccountType | + | 3250 | 147 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 2/25/2021 | 1918 | DL09 | + When I process the missing active loans with detail from DNA at "3/5/2021 5:00:00 AM" and export + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Closed | 2 | + And the import has an end date that is greater or equal to the start date + And the loans with details now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 3250 | 147 | 769.1038 | 02/25/2021 | Closed | 62 | + +Scenario: AddImport for Closed Loans from DNA - Set Action Required = false + Given that I get these closed loans back from DNA + | LoanNumber | PaidLoanDate | + | 148 | 05/25/2021 | + | 143 | 02/12/2021 | + | 144 | 02/12/2021 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | ActionRequired | + | 1 | 1 | 12 | 3 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | false | + | 2 | 2 | 10094 | 143 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | true | + | 3 | 2 | 10621 | 144 | JOHNSON | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | true | + When I process the closed loan import with missing loan "148" with date from DNA at "3/5/2021 5:00:00 AM" and select from "2/25/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Closed | 1 | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | ActionRequired | + | 1 | 12 | 3 | | | New | | false | + | 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 | false | + | 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 | false | + And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed" + And AddHistoriesAsync is called with 2 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | Term | + | 3250 | 148 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 5/25/2021 | 63 | + When I process the missing active loans with detail from DNA at "3/5/2021 5:00:00 AM" and export + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Closed | 2 | + And the import has an end date that is greater or equal to the start date + And the loans with details now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 3250 | 147 | 0 | 05/25/2021 | Closed | 59 | + +Scenario: AddImport for Closed Loans from DNA - no loans are returned + Given that I get these closed loans back from DNA + | LoanNumber | PaidLoanDate | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | RemainingMonthsAtClose | + | 1 | 1 | 5841 | 147 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | | + | 2 | 2 | 10094 | 143 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | Updated | | + | 3 | 2 | 100947 | 144 | JOHNSON | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | New | | + When I process the closed loan import from DNA at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 0 | 0 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And ExportChargebacksAsync is called + +Scenario: Import Closed Loans from DNA with loan details, first adding them, starting at a certain closed date + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 | + | 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 | + | 2233 | 2233143 | JONES | FRED | | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate |Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | RemainingMonthsAtClose | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | + | 1 | 3250 | 3250144 | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | + | 2 | 12021 | 12021143 | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | + | 3 | 2233 | 2233143 | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | | + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + And ExportReservesAsync is called + +Scenario: Import Closed Loans from DNA with loan details, skipping the exports +Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 | + | 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 | + | 2233 | 2233143 | JONES | FRED | | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate |Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | RemainingMonthsAtClose | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" and skip the export + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | + | 1 | 3250 | 3250144 | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | + | 2 | 12021 | 12021143 | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | + | 3 | 2233 | 2233143 | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | | + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And MarkReservesAndChargebacksAsExportedAsync is called + And ExportChargebacksAsync is not called + And ExportReservesAsync is not called + + +Scenario: Import Closed Loans from DNA with loan details, two new loans and one updated (changed member initial) + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 | + | 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 | + | 2233 | 2233143 | JONES | FRED | X | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | + | 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | + | 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | + | 3 | 2233 | 2233143 | JONES | FRED | X | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | | + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + And ExportReservesAsync is called + +Scenario: Import Closed Loans from DNA with loan details, two new loans and one updated (to change contract rate) + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 | + | 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 | + | 2233 | 2233143 | JONES | FRED | Y | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5.5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | + | 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | + | 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | + | 3 | 2233 | 2233143 | JONES | FRED | Y | 37 | false | 524.41373886682351065543659741 | 0 | 7/3/2021 | Closed | 33 | | + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + And ExportReservesAsync is called + +Scenario: Import Closed Loans from DNA with loan details, with skip the export + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 2233 | 2233143 | JONES | FRED | Y | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5.5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" and skip the export + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 1 | 1 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | + | 1 | 2233 | 2233143 | JONES | FRED | Y | 37 | false | 524.41373886682351065543659741 | 0 | 7/3/2021 | Closed | 33 | | + And AddHistoriesAsync is called with 1 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And MarkReservesAndChargebacksAsExportedAsync is called + And ExportChargebacksAsync is not called + And ExportReservesAsync is not called + +Scenario: Import Closed Loans from DNA with loan details, two new loans and one existing which is already closed with same paid loan date - should ignore that + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 | + | 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 | + | 2233 | 2233143 | JONES | FRED | Y | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | + | 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | + | 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | + | 3 | 2233 | 2233143 | JONES | FRED | Y | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | | + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + And ExportReservesAsync is called + +Scenario: Import Closed Loans from DNA with loan details, two new loans and one existing which is already closed but had a different paid loan date + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 | + | 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 | + | 2233 | 2233143 | JONES | FRED | Y | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | + | 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | + | 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | + | 3 | 2233 | 2233143 | JONES | FRED | Y | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | | + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + And ExportReservesAsync is called + +Scenario: Import Closed Loans from DNA with loan details, Set Action Required = false + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 | + | 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 | + | 2233 | 2233143 | JONES | FRED | Y | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | ActionRequired | + | 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | false | + | 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | false | + | 3 | 2233 | 2233143 | JONES | FRED | Y | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | | false | + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + And ExportReservesAsync is called + +Scenario: Import Closed Loans from DNA with loan details, all dealers are added + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | DealerName | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | SUBURBAN MOTORS HARLEY DAVIDSON | 2123444 | | | | | | | 6/30/2021 | 1918 | + | 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | HEISER CHEVROLET CADILLAC OF WEST BEND | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 | + | 2233 | 2233143 | JONES | FRED | X | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | SUBURBAN MOTORS HARLEY DAVIDSON | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 | + And dealers that look like + | Dealer Code | Name | Address1 | Address2 | City | State | Zip | DealerOrganizationNumber | DealerAccountNumber | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | + | 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | + | 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | + | 3 | 2233 | 2233143 | JONES | FRED | X | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | | + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + +Scenario: Import Closed Loans from DNA with loan details, all dealers are added, but only dealer information from first loan is added + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | DealerName | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | | | 2123444 | | | | | | | 6/30/2021 | 1918 | + | 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | HEISER CHEVROLET CADILLAC OF WEST BEND | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 | + | 2233 | 2233143 | JONES | FRED | X | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | SUBURBAN MOTORS HARLEY DAVIDSON | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1120 | + And dealers that look like + | Dealer Code | Name | Address1 | Address2 | City | State | Zip | DealerOrganizationNumber | DealerAccountNumber | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | + | 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | + | 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | + | 3 | 2233 | 2233143 | JONES | FRED | X | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | | + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | | 3434 | | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + +Scenario: Import Closed Loans from DNA with loan details, dealers are updated + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | DealerName | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | + | 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | SUBURBAN MOTORS HARLEY DAVIDSON | 2123444 | | | | | | | 6/30/2021 | 1918 | + | 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | HEISER CHEVROLET CADILLAC OF WEST BEND | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 | + | 2233 | 2233143 | JONES | FRED | X | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | SUBURBAN MOTORS HARLEY DAVIDSON | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1120 | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HD | 3434 | 324324324324324 | + When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | + | 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | + | 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | + | 3 | 2233 | 2233143 | JONES | FRED | X | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | | + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService Closed.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService Closed.feature new file mode 100644 index 0000000..6787ce2 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService Closed.feature @@ -0,0 +1,194 @@ +Feature: Import Read Service - Closed Loans + Gets closed loans from a stream of csv data, reads loans into memory, calculates chargebacks, saves loans, saves import information + +Scenario: AddImport for Closed Loans - all loans are saved + Given an import file named "ClosedLoans 2021-03-01.txt" that contains + """ + LoanNumber,PaidLoanDate + 147,02/25/2021 + 143,02/12/2021 + 144,02/12/2021 + """ + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | RemainingMonthsAtClose | + | 1 | 1 | 5841 | 147 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | | + | 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | Updated | | + | 3 | 2 | 100947 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | New | | + When I read this closed loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 3 | 3 | true | ClosedLoans 2021-03-01.txt | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 5841 | 147 | 210.0375 | 2/25/2021 | Closed | 63 | + | 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 | + | 3 | 100947 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 | + And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed" + And AddHistoriesAsync is called with 1 history/ies having OldValue "Updated" and NewValue "Closed" + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + +Scenario: AddImport for Closed Loans - trim fields and header + Given an import file named "ClosedLoans 2021-03-01.txt" that contains + """ + LoanNumber,PaidLoanDate + 147, 02/25/2021 + 143, 02/12/2021 + 144, 02/12/2021 + """ + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | + | 1 | 1 | 5841 | 147 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | + | 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | + | 3 | 2 | 10621 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | + When I read this closed loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 3 | 3 | true | ClosedLoans 2021-03-01.txt | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 5841 | 147 | 210.0375 | 2/25/2021 | Closed | 63 | + | 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 | + | 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 | + And AddHistoriesAsync is called with 3 history/ies having OldValue "New" and NewValue "Closed" + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + +Scenario: AddImport for Closed Loans - Set Action Required to False + Given an import file named "ClosedLoans 2021-03-01.txt" that contains + """ + LoanNumber,PaidLoanDate + 147, 02/25/2021 + 143, 02/12/2021 + 144, 02/12/2021 + """ + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | ActionRequired | + | 1 | 1 | 5841 | 147 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | true | + | 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | false | + | 3 | 2 | 10621 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | true | + When I read this closed loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 3 | 3 | true | ClosedLoans 2021-03-01.txt | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | ActionRequired | + | 1 | 5841 | 147 | 210.0375 | 2/25/2021 | Closed | 63 | false | + | 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 | false | + | 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 | false | + And AddHistoriesAsync is called with 3 history/ies having OldValue "New" and NewValue "Closed" + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + +Scenario: AddImport for Closed Loans - header case insensitive + Given an import file named "ClosedLoans 2021-03-01.txt" that contains + """ + loannumber,PaidLoanDate + 147, 02/25/2021 + 143, 02/12/2021 + 144, 02/12/2021 + """ + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | + | 1 | 1 | 5841 | 147 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | + | 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | + | 3 | 2 | 10621 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | + When I read this closed loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 3 | 3 | true | ClosedLoans 2021-03-01.txt | Closed | + And the loans now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 5841 | 147 | 210.0375 | 2/25/2021 | Closed | 63 | + | 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 | + | 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 | + And the import has an end date that is greater or equal to the start date + And AddHistoriesAsync is called with 3 history/ies having OldValue "New" and NewValue "Closed" + And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And ExportChargebacksAsync is called + +Scenario: AddImport for Closed Loans - saves two loans that are in the loan table + Given an import file named "ClosedLoans 2021-03-01.txt" that contains + """ + LoanNumber,PaidLoanDate + 149,02/25/2021 + 143,02/12/2021 + 144,02/12/2021 + """ + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | + | 1 | 1 | 12 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | + | 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | + | 3 | 2 | 10621 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | + When I read this closed loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 3 | 2 | true | ClosedLoans 2021-03-01.txt | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 12 | 3 | | | New | | + | 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 | + | 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 | + And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed" + And AddHistoriesAsync is called with 2 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And AddHistoriesAsync is called with 1 history/ies having ActivityType "Error" and AuditDescription "Loan in closed import not found" + And ExportChargebacksAsync is called + +Scenario: AddImport for Closed Boat Loans - saves two loans that are in the loan table + Given an import file named "ClosedLoans 2021-03-01.txt" that contains + """ + LoanNumber,PaidLoanDate,AccountType + 151,02/25/2021,DL10 + 143,04/12/2021,DL11 + 144,04/12/2021,DL12 + """ + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | AccountType | + | 1 | 1 | 12 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | DL10 | + | 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | New | DL11 | + | 3 | 2 | 10621 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 1/10/2027 | 21274.24 | 2.490 | 4.470 | NC | 70 | 1063.7120 | 500 | false | | | Message | New | DL12 | + When I read this closed loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 3 | 2 | true | ClosedLoans 2021-03-01.txt | Closed | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | + | 1 | 12 | 3 | | | New | | + | 2 | 10094 | 143 | 1402.7417 | 4/12/2021 | Closed | 59 | + | 3 | 10621 | 144 | 1033.7483 | 4/12/2021 | Closed | 69 | + And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed" + And AddHistoriesAsync is called with 2 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status" + And AddHistoriesAsync is called with 1 history/ies having ActivityType "Error" and AuditDescription "Loan in closed import not found" + And ExportChargebacksAsync is called + +Scenario: AddImport - file has a header with one column named incorrectly + Given an import file named "ClosedLoans 2021-03-01.txt" that contains + """ + LoanNumber,PaidDate + 147,02/25/2021 + 143,02/12/2021 + 144,02/12/2021 + """ + When I read this closed loan import file at "3/5/2021 5:00:00 AM" + And I get the record from the ImportTable + Then the import looks like + | StartDate | EndDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | | 0 | 0 | false | ClosedLoans 2021-03-01.txt | Closed | + +Scenario: AddImport - file is missing the header + Given an import file named "ClosedLoans 2021-03-01.txt" that contains + """ + 147,02/25/2021 + 143,02/12/2021 + 144,02/12/2021 + """ + When I read this closed loan import file at "3/5/2021 5:00:00 AM" + And I get the record from the ImportTable + Then the import looks like + | StartDate | EndDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | | 0 | 0 | false | ClosedLoans 2021-03-01.txt | Closed | diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService New from DNA.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService New from DNA.feature new file mode 100644 index 0000000..8087436 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService New from DNA.feature @@ -0,0 +1,1753 @@ +Feature: Import Read Service - New and Updated Loans from DNA + Gets loans from the CoreApiWrapper, calculates reserve, saves loans, saves import information. Updates loans that already exist + Note that the export service is a mock, so the step for "loans now look like" will reflect the loans before the export + +Background: + Given a loan type table consisting of + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + | BB | Standard | S | 0 | 0 | 0 | 1.00 | 0.25 | + | NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 | + +Scenario: Add From DNA - Add all fields, vehicles added, missing VIN action required, VIN length longer than 17 characters works + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerCode | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1918 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 2294 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | LoanTypeId | Dealer Code | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + And the loans now look like + #before export + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | true | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3488 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | New | 1132.5564086943452341656244555 | False | True | False | True | False | 232323455 | | | false | + And the vehicle for LoanId 1 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1 | | 1 | | | 0 | 0 | | + And the vehicle for LoanId 2 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 2 | M233333389Y853128921 | 2 | Honda | Civic | 2020 | 1900 | Black | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - import but skip export + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerCode | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 2294 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | LoanTypeId | Dealer Code | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" and skip the export + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + And the loans now look like + #before export + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | true | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3488 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | New | 1132.5564086943452341656244555 | False | True | False | True | False | 232323455 | false | + And the vehicle for LoanId 1 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1 | | 1 | | | 0 | 0 | | + And the vehicle for LoanId 2 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 2 | M233333389Y853128921 | 2 | Honda | Civic | 2020 | 1900 | Black | + And no audit history record is added + And ExportReservesAsync is not called + And MarkReservesAsExportedAsync is called + +Scenario: Add From DNA - Dealer Name is updated + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | Dealer Name | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | True | False | True | 3434 | 324324324324324 | SUBURBAN MOTORS HD | 2123444 | | | | | | | 1918 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | H21 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | False | True | False | True | False | 3488 | 9888447543454544 | HEISER CHEVROLET CADILLAC OF WEST BEND | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HD | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + And the loans now look like + #before export + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | True | False | True | 2123444 | | | true | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3488 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 819.2332826134477373747357048 | False | True | False | True | False | 232323455 | | | false | + And the vehicle for LoanId 1 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1 | | 1 | | | 0 | 0 | | + And the vehicle for LoanId 2 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 2 | M233333389Y853128921 | 2 | Honda | Civic | 2020 | 1900 | Black | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - Dealer code with different casing won't matter + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | Dealer Name | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | s7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | SUBURBAN MOTORS HD | 2123444 | | | | | | | 1918 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | H21 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | False | True | True | True | False | 3488 | 9888447543454544 | HEISER CHEVROLET CADILLAC OF WEST BEND | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | LoanTypeId | Dealer Code | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HD | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | true | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3488 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 819.2332826134477373747357048 | False | True | True | True | False | 232323455 | | | false | + And the vehicle for LoanId 1 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1 | | 1 | | | 0 | 0 | | + And the vehicle for LoanId 2 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 2 | M233333389Y853128921 | 2 | Honda | Civic | 2020 | 1900 | Black | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - We only take the dealer information from the first loan with that dealer + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | Dealer Name | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | SUBURBAN MOTORS HD | 2123444 | | | | | | | 1919 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | S7 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | False | True | True | True | False | 3434 | 9888447543454544 | HEISER CHEVROLET CADILLAC OF WEST BEND | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1234 | 124324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HD | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1234 | 124324324324324 | + And the loans now look like + #before export + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | true | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3434 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 819.2332826134477373747357048 | False | True | True | True | False | 232323455 | | | false | + And the vehicle for LoanId 1 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1 | | 1 | | | 0 | 0 | | + And the vehicle for LoanId 2 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 2 | M233333389Y853128921 | 2 | Honda | Civic | 2020 | 1900 | Black | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - We only take the dealer information from the first loan with that dealer with multiple scenarios + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | Dealer Name | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | SUBURBAN MOTORS HD | 2123444 | | | | | | | 1919 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | S7 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | False | True | True | True | False | 3434 | 9888447543454544 | HEISER CHEVROLET CADILLAC OF WEST BEND | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 1447 | + | 3250 | 145 | BRABANT | STEVEN | C | A1 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 1434 | 4324324324324 | ACURA OF BROOKFIELD | 3123444 | | | | | | | 1919 | + | 12021 | 146 | DRYDEN | ALEX | W | A1 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 2000 | 4.39 | 6.39 | False | True | True | True | False | 1434 | 4324324324324 | ACURA OF BROOKFIELD | 432323455 | P233333389Y853128921 | Honda | Pilot | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | Term | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1234 | 124324324324324 | + | A1 | ACURA OF BROOKFIELD | 1434 | 4324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 4 | 4 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HD | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1234 | 124324324324324 | + | A1 | ACURA OF BROOKFIELD | 1434 | 4324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | true | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3434 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 819.2332826134477373747357048 | False | True | True | True | False | 232323455 | | | false | + | 1 | 3 | 3250 | 145 | BRABANT | STEVEN | C | 1434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 3123444 | | | true | + | 1 | 4 | 12021 | 146 | DRYDEN | ALEX | W | 1434 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 2000 | 4.39 | 6.39 | New | 65.538662609075818989978856425 | False | True | True | True | False | 432323455 | | | false | + And the vehicle for LoanId 1 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1 | | 1 | | | 0 | 0 | | + And the vehicle for LoanId 2 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 2 | M233333389Y853128921 | 2 | Honda | Civic | 2020 | 1900 | Black | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - Action Required on Dealer acount number missing + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | Dealer Name | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | SUBURBAN MOTORS HD | 2123444 | R233333389Y853128921 | | | | | | 1919 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | S7 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | False | True | True | True | False | 3434 | 9888447543454544 | SUBURBAN MOTORS HD | 232323455 | V233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 1447 | + | 3250 | 145 | BRABANT | STEVEN | C | A1 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 1434 | 4324324324324 | ACURA OF BROOKFIELD | 3123444 | M233333389Y853128921 | | | | | | 1919 | + | 12021 | 146 | DRYDEN | ALEX | W | H21 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 2000 | 4.39 | 6.39 | False | True | True | True | False | 1234 | | HEISER CHEVROLET CADILLAC OF WEST BEND | 432323455 | P233333389Y853128921 | Honda | Pilot | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | LoanTypeId | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1234 | | + | A1 | ACURA OF BROOKFIELD | 1434 | 4324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 4 | 4 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HD | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1234 | | + | A1 | ACURA OF BROOKFIELD | 1434 | 4324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | false | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3434 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 819.2332826134477373747357048 | False | True | True | True | False | 232323455 | | | false | + | 1 | 3 | 3250 | 145 | BRABANT | STEVEN | C | 1434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 3123444 | | | false | + | 1 | 4 | 12021 | 146 | DRYDEN | ALEX | W | 1234 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 2000 | 4.39 | 6.39 | New | 65.538662609075818989978856425 | False | True | True | True | False | 432323455 | | | true | + And the vehicle for LoanId 1 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1 | R233333389Y853128921 | 1 | | | 0 | 0 | | + And the vehicle for LoanId 2 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 2 | V233333389Y853128921 | 2 | Honda | Civic | 2020 | 1900 | Black | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - Action Required on Dealer org number missing + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | Dealer Name | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | SUBURBAN MOTORS HD | 2123444 | R233333389Y853128921 | | | | | | 1919 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | S7 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | False | True | True | True | False | 3434 | 9888447543454544 | SUBURBAN MOTORS HD | 232323455 | V233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 1447 | + | 3250 | 145 | BRABANT | STEVEN | C | A1 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | | 4324324324324 | ACURA OF BROOKFIELD | 3123444 | M233333389Y853128921 | | | | | | 1919 | + | 12021 | 146 | DRYDEN | ALEX | W | H21 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 2000 | 4.39 | 6.39 | False | True | True | True | False | 1234 | 124324324324324 | HEISER CHEVROLET CADILLAC OF WEST BEND | 432323455 | P233333389Y853128921 | Honda | Pilot | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | LoanTypeId | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1234 | 124324324324324 | + | A1 | ACURA OF BROOKFIELD | | 4324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 4 | 4 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HD | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1234 | 124324324324324 | + | A1 | ACURA OF BROOKFIELD | | 4324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | false | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3434 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 819.2332826134477373747357048 | False | True | True | True | False | 232323455 | | | false | + | 1 | 3 | 3250 | 145 | BRABANT | STEVEN | C | | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 3123444 | | | true | + | 1 | 4 | 12021 | 146 | DRYDEN | ALEX | W | 1234 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 2000 | 4.39 | 6.39 | New | 65.538662609075818989978856425 | False | True | True | True | False | 432323455 | | | false | + And the vehicle for LoanId 1 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1 | R233333389Y853128921 | 1 | | | 0 | 0 | | + And the vehicle for LoanId 2 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 2 | V233333389Y853128921 | 2 | Honda | Civic | 2020 | 1900 | Black | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - can handle 4 characters in dealer code + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | LoanType | Dealer Code | DealerName | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | NC | S777 | SUBURBAN MOTORS HARLEY DAVIDSON | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S777 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 2525 | 924324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S777 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 2525 | 924324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | true | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - can insert new dealer if doesn't already exist + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Dealer Name | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S777 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Dealer Name | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1434 | 424324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S777 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1434 | 424324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | true | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - if dealer code is null, add the loan, Action Required for dealer code is True + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | | HEISER CHEVROLET CADILLAC OF WEST BEND | NC | 01/23/2021 | 01/30/2021 | 02/15/2021 | 01/15/2025 | 25000 | 4.39 | 6.39 | False | True | False | True | False | 4545 | 924324324324324 | 232323455 | M233333389Y85312 | Honda | Civic | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Dealer Name | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | | + | | HEISER CHEVROLET CADILLAC OF WEST BEND | 4545 | 924324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | True | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 4545 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 819.2332826134477373747357048 | False | True | False | True | False | 232323455 | | | True | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - if dealer code is more than 4 chars, add the loan and the dealer with an empty dealer code, Action Required for dealer code is True + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | S7777 | BARTZ AUTO | NC | 01/23/2021 | 01/30/2021 | 02/15/2021 | 01/15/2025 | 25000 | 4.39 | 6.39 | False | True | False | True | False | 5656 | 43453453455565 | 232323455 | M233333389Y85312 | Honda | Civic | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Dealer Name | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | | BARTZ AUTO | 5656 | 43453453455565 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | | BARTZ AUTO | 5656 | 43453453455565 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | True | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 5656 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 819.2332826134477373747357048 | False | True | False | True | False | 232323455 | | | True | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add From DNA - if member number is null, add the loan + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + | | 143 | HAYDEN | ALEXANDER | W | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | NC | 01/23/2021 | 01/30/2021 | 02/15/2021 | 01/15/2025 | 25000 | 4.39 | 6.39 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y85312 | Honda | Civic | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | True | + | 1 | 2 | | 143 | HAYDEN | ALEXANDER | W | 3488 | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 819.2332826134477373747357048 | False | True | False | True | False | 232323455 | | | False | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Update From DNA - only amount on loan is updated and saved, Reserve is recalculated, ReserveExportedDate becomes NULL, vehicle is updated +#Gap, Warranty, Mbp insurance on an updated loan will come in false on conversion loans + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 20889.51 | 2.03 | 3.39 | False | False | False | False | True | 3434 | 324324324324324 | 2123444 | 1DODGE1234DAKOTA5 | DODGE | DAKOTA | 2009 | 97997 | | 1553 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Term | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | 793.9133223793564 | | New | True | False | True | False | True | 2123444 | 3/4/2021 | | 51 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 20889.51 | 2.03 | 3.39 | Updated | 478.99635611847757906025386875 | -314.91 | True | False | True | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1DODGE1234DAKOTA5 | 1 | DODGE | DAKOTA | 2009 | 97997 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - only first payment date on loan is updated and saved, Reserve is recalculated, ReserveExportedDate becomes NULL, vehicle is updated, updated loan after conversion +#Gap, Warranty, Mbp insurance on an updated loan will come in false on conversion loans + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/10/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | True | False | True | 3434 | 324324324324324 | 2123444 | 1DODGE1234DAKOTA5 | DODGE | DAKOTA | 2009 | 97997 | | 1553 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Term | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | 793.9133223793564 | | New | True | False | True | False | True | 2123444 | 3/4/2021 | | 51 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/10/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 639.50631986723679138365336850 | -154.40 | True | False | True | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1DODGE1234DAKOTA5 | 1 | DODGE | DAKOTA | 2009 | 97997 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - only term in days on loan is updated and saved, Reserve is recalculated, ReserveExportedDate becomes NULL, vehicle is updated, updated after conversion +#Gap, Warranty, Mbp insurance on an updated loan will come in false on conversion loans + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | False | False | True | False | True | 3434 | 324324324324324 | 2123444 | 1DODGE1234DAKOTA5 | DODGE | DAKOTA | 2009 | 97997 | | 1653 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Term | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | 793.9133223793564 | | New | False | False | True | False | True | 2123444 | 3/4/2021 | | 51 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Term | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 54 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 677.86846015764668312435076825 | -116.04 | False | False | True | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1DODGE1234DAKOTA5 | 1 | DODGE | DAKOTA | 2009 | 97997 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - buy rate and contract rate on loan is updated, Reserve is calculated at Zero, Adjustment is saved as negative, ReserveExportedDate becomes NULL, vehicle is updated +#Gap, Warranty, Mbp insurance on an updated loan will come in false on conversion loans + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.00 | 1.75 | False | False | False | False | True | 3434 | 324324324324324 | 2123444 | 1DODGE1234DAKOTA5 | DODGE | DAKOTA | 2009 | 97997 | | 1553 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Term | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | 793.9133223793564 | | New | True | False | True | False | True | 2123444 | 3/4/2021 | | 51 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.00 | 1.75 | Updated | 0 | -793.91 | True | False | True | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1DODGE1234DAKOTA5 | 1 | DODGE | DAKOTA | 2009 | 97997 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - Amount on loan is updated, reserve amount changed positive Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/06/2026 | 7494.61 | 3.75 | 4.39 | False | False | False | True | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 6494.61 | 3.75 | 4.39 | New | 88.93436782692176 | | True | True | False | False | True | 2123444 | 03/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | VehicleId | + | | 1 | | | 0 | 0 | | 1 | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/06/2026 | 7494.61 | 3.75 | 4.39 | Updated | 102.62793338773629744790336800 | 13.70 | True | True | False | True | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - Amount on loan is updated, skip export, Gap, Warranty, Mbp insurance on an updated loan will come in false on conversion loans + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/06/2026 | 7494.61 | 3.75 | 4.39 | False | False | False | True | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 6494.61 | 3.75 | 4.39 | New | 88.93436782692176 | | True | True | False | False | True | 2123444 | 03/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | VehicleId | + | | 1 | | | 0 | 0 | | 1 | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" and skip the export + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/06/2026 | 7494.61 | 3.75 | 4.39 | Updated | 102.62793338773629744790336800 | 13.70 | True | True | False | True | True | 2123444 | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is not called + And MarkReservesAsExportedAsync is called + +Scenario: Update From DNA - buy rate and contract rate on loan are the same, reserve amount did not change, Adjustment is zero and not saved, ReserveExportedDate does not change + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/06/2026 | 56494.61 | 4.8 | 6.8 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 56494.61 | 4.8 | 6.8 | New | 2483.9795673591888888559725690 | | True | False | False | True | True | 2123444 | 03/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/06/2026 | 56494.61 | 4.8 | 6.8 | Updated | 2483.9795673591888888559725690 | | True | False | False | False | True | 2123444 | 03/4/2021 | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate on loan is updated, a positive Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 4.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | 793.9136070432343 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 4.39 | Updated | 1388.9291450596866411776963565 | 595.02 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - Loan Type on loan is updated, a positive Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | BB | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 56494.61 | 2.03 | 4.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 56494.61 | 2.03 | 4.39 | 2813.495481554908 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | BB | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 56494.61 | 2.03 | 4.39 | Updated | 3751.327308739877395505521437 | 937.83 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - Loan Date on loan is updated, the difference in reserve is nothing after rounding, no adjustment is saved, ReserveExportedDate does not change. + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 02/28/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 56494.61 | 2.03 | 4.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 56494.61 | 2.03 | 4.39 | 2813.495481554908 | | New | True | False | False | False | True | 2123444 | 03/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/28/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 56494.61 | 2.03 | 4.39 | Updated | 2813.4954815549080466291410778 | | True | False | False | False | True | 2123444 | 03/4/2021 | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - Loan Date on loan is updated, difference in reserve after rounding, adjustment is saved, ReserveExportedDate is called. + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 02/28/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 56494.61 | 2.03 | 4.40 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 56494.61 | 2.03 | 4.39 | 2813.495481554908 | | New | True | False | False | False | True | 2123444 | 03/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/28/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 56494.61 | 2.03 | 4.40 | Updated | 2825.6457912806237971046561640 | 12.15 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - First Payment Date and Final payment is updated on loan, one loan has ten thousandth decimal place difference in buy rate, and same with contract rate, round up + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 02/10/2021 | 02/03/2021 | 03/13/2021 | 05/05/2025 | 6494.61 | 3.7546 | 4.3746 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1553 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 6494.61 | 3.754 | 4.374 | 88.93436782692176 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/10/2021 | 02/03/2021 | 03/13/2021 | 05/05/2025 | 6494.61 | 3.755 | 4.375 | Updated | 69.213652690701353974372119183 | -19.72 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - First Payment Date and Final payment is updated on loan, one loan has ten thousandth decimal place difference in buy rate, and same with contract rate, round down, no change in rates + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 02/10/2021 | 02/03/2021 | 03/13/2021 | 05/05/2025 | 6494.61 | 3.7543 | 4.3743 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1553 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 6494.61 | 3.754 | 4.374 | 88.93436782692176 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/10/2021 | 02/03/2021 | 03/13/2021 | 05/05/2025 | 6494.61 | 3.754 | 4.374 | Updated | 69.212747142003089864206789735 | -19.72 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - First Payment Date and Final payment is updated on loan, a negative Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 02/10/2021 | 02/03/2021 | 03/13/2021 | 05/05/2025 | 6494.61 | 3.75 | 4.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1553 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 6494.61 | 3.75 | 4.39 | 88.93436782692176 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 02/10/2021 | 02/03/2021 | 03/13/2021 | 05/05/2025 | 6494.61 | 3.75 | 4.39 | Updated | 71.451064016423407827940133096 | -17.48 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate and buy rate Buy Down on loan is updated, a negative Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 5.03 | 2.03 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 4.03 | 2.39 | -1286.924928920238171593013688 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 5.03 | 2.03 | Updated | -2366.304727958569622108164422 | -1079.38 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate and buy rate from Buy Down to Markup on loan is updated, a positive Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 6.03 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 5.03 | 2.03 | -2366.304727958569622108164422 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 6.03 | Updated | 2385.3099543374864451605083395 | 4751.61 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate on loan is updated, a negative Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 4.03 | 2.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | 793.9136070432343 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 4.03 | 2.39 | Updated | -1286.924928920238171593013688 | -2080.83 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - larger buy down to smaller buy down, a negative Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 4.03 | 2.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 5.03 | 3.39 | -1307.753251900924 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 4.03 | 2.39 | Updated | -1286.924928920238171593013688 | 20.83 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - buy down smaller buy down to larger buy down, a negative Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 5.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 4.03 | 2.39 | -1286.924928920238171593013688 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 5.03 | 3.39 | Updated | -1307.753251900923882113670130 | -20.83 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - buy down reserve to standard reserve, a positive Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 4.03 | 3.39 | -506.2845343943843 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | 1300.19 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - standard reserve to buy down reserve, a negative Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 4.03 | 2.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | 793.9136070432343 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 4.03 | 2.39 | Updated | -1286.924928920238171593013688 | -2080.83 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - standard reserve increase, a positive Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 3.03 | 5.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 4.03 | 5.39 | 819.8207658670196 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 3.03 | 5.39 | Updated | 1411.4070557133671182781753948 | 591.59 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - flat fee reserve decrease, a negative Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 800.51 | 5.00 | 5.05 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 889.51 | 5.00 | 5.02 | 6.671325 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 800.51 | 5.00 | 5.05 | Updated | 6.003825 | -0.67 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - no reserve to standard reserve, a positive Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 889.51 | 5.00 | 5.02 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | BB | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 889.51 | 5.00 | 4.99 | 0 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 889.51 | 5.00 | 5.02 | Updated | 6.671325 | 6.67 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - standard reserve to no reserve, a negative Adjustment is saved, ReserveExportedDate becomes NULL + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | BB | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 889.51 | 5.00 | 4.99 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 889.51 | 5.00 | 5.05 | 6.003825 | | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 889.51 | 5.00 | 4.99 | Updated | 0 | -6.00 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - nothing on loan actually changed, vehicle has 17 characters, no update to dealer - no audit history added, loan status and ReserveExportedDate are unchanged + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | M88333783983AS111 | | | | | | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Reserve Amount | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | 793.9136070432343049958707190 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M88333783983AS111 | 1 | | | 0 | 0 | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 0 | 0 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M88333783983AS111 | 1 | | | 0 | 0 | | + And no audit history record is added + And ExportReservesAsync is called + + +Scenario: Update From DNA - nothing on loan actually changed except middle initial, the action required used to be true + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | + | 3250 | 144 | MUELLER | STEVEN | R | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | M88333783983AS111 | | | | | | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Reserve Amount | Action Required | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | 793.9136070432343049958707190 | true | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M88333783983AS111 | 1 | | | 0 | 0 | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Action Required | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | R | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | 3/4/2021 | | false | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M88333783983AS111 | 1 | | | 0 | 0 | | + And ExportReservesAsync is called + +Scenario: Add and Update From DNA - one new loan, one with nothing on loan actually changed, vin has 17 characters, no update to dealer - no audit history added, loan status and ReserveExportedDate are unchanged + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerCode | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | M88333783983AS111 | | | | | | 1919 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | False | True | False | True | False | 3434 | 324324324324324 | 232323455 | M233333389Y85312 | Honda | Civic | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Reserve Amount | Term | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | 793.9136070432343049958707190 | 63 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M88333783983AS111 | 1 | | | 0 | 0 | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | 3/4/2021 | | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3434 | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 819.2332826134477373747357048 | False | True | False | True | False | 232323455 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M88333783983AS111 | 1 | | | 0 | 0 | | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Add and Update From DNA - one new boat loan, and an updated vehicle loan + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerCode | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | AccountType | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | M88333783983AS111 | | | | | | 1919 | DL08 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | False | True | False | True | False | 3434 | 324324324324324 | 232323455 | M233333389Y85312 | Honda | Civic | 2020 | 1900 | Black | 1447 | DL10 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Reserve Amount | Term | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | 793.9136070432343049958707190 | 63 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M88333783983AS111 | 1 | | | 0 | 0 | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | 3/4/2021 | | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3434 | 1/23/2021 | 1/30/2021 | 2/15/2021 | 1/15/2025 | 25000 | 4.39 | 6.39 | New | 1500.00 | False | True | False | True | False | 232323455 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M88333783983AS111 | 1 | | | 0 | 0 | | + And no audit history record is added + And ExportReservesAsync is called + +Scenario: Update From DNA - only non-reserve fields on loan changed, no vehicle update, no update to dealer - audit history is added, ReserveExportedDate is unchanged + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | LoanType | DealerCode | DealerName | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | + | 3250 | 144 | SMITH | STEVEN | C | NC | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Reserve Amount | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | 793.9136070432343049958707190 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | SMITH | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - only non-reserve fields on loan changed, adjustment from a previous change is still there + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | + | 3250 | 144 | SMITH | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Reserve Amount | Adjustment | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | 793.9136070432343049958707190 | 133.55 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Adjustment | + | 1 | 1 | 3250 | 144 | SMITH | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | 3/4/2021 | | 133.55 | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate on loan is updated, no vehicle update, update to dealer is not ignored + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 2.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate on Boat loan is updated, no vehicle update, update to dealer is not ignored + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | AccountType | + | 3250 | 144 | MUELLER | STEVEN | C | S47 | SKIPPER BUDS SEQUOIT HARBOR | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 6662 | 19617804574 | 2123444 | | | | | | | 1919 | DL11 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Adjustment | ReserveAmount | AccountType | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 6662 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 2.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | | 278.8951 | DL11 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S47 | SKIPPER BUDS SEQUOIT HARBOR | 6662 | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S47 | SKIPPER BUDS SEQUOIT HARBOR | 6662 | 19617804574 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 6662 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 1115.5804 | 836.68 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - only amount on Boat loan is updated and saved, Reserve is recalculated, ReserveExportedDate becomes NULL, vehicle is updated +#Gap, Warranty, Mbp insurance on an updated loan will come in false on conversion loans + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | AccountType | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 20889.51 | 2.03 | 3.39 | False | False | False | False | True | 3434 | 324324324324324 | 2123444 | 1DODGE1234DAKOTA5 | DODGE | DAKOTA | 2009 | 97997 | | 1553 | DL11 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Reserve Amount | Adjustment | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Term | AccountType | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | 793.9133223793564 | | New | True | False | True | False | True | 2123444 | 3/4/2021 | | 51 | DL11 | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Adjustment | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 20889.51 | 2.03 | 3.39 | Updated | 835.5804 | 41.67 | True | False | True | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1DODGE1234DAKOTA5 | 1 | DODGE | DAKOTA | 2009 | 97997 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate on loan is updated, no vehicle update, dealer with null org number is added as a blank + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | | | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | | | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 2.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | | | | | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate on loan is updated, no vehicle update, update to dealer is not ignored, Action Required on Dealer Org + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | | 324324324324324 | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 2.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Action Required | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | true | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate on loan is updated, no vehicle update, update to dealer is not ignored, Action Required on Dealer Account Number + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | | 2123444 | | | | | | | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 2.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | Action Required | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | true | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate on loan is updated, vehicle update, no dealer update, VIN less than 17 characters + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | M233333389Y85312 | Honda | Civic | 2020 | 1900 | Black | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 2.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M233333389Y85312 | 1 | Honda | Civic | 2020 | 1900 | Black | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate on loan is updated, vehicle update, dealer does not exist, write dealer record, VIN less than 17 characters + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Dealer Name | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S777 | SHEBOYGAN HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | M233333389Y85312 | Honda | Civic | 2020 | 1900 | Black | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Dealer Name | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 1434 | SHEBOYGAN HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 2.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1434 | 924324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1434 | 924324324324324 | + | S777 | SHEBOYGAN HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M233333389Y85312 | 1 | Honda | Civic | 2020 | 1900 | Black | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Update From DNA - contract rate on loan is updated, vehicle update, update dealer record, VIN less than 17 characters + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S777 | SHEBOYGAN HARLEY DAVIDSON LLC | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | M233333389Y85312 | Honda | Civic | 2020 | 1900 | Black | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Dealer Name | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | SHEBOYGAN HARLEY DAVIDSON | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 2.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1434 | 924324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1434 | 924324324324324 | + | S777 | SHEBOYGAN HARLEY DAVIDSON LLC | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | Updated | 793.9136070432343049958707190 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M233333389Y85312 | 1 | Honda | Civic | 2020 | 1900 | Black | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: Add From DNA - Add all fields except Buy Rate and PostedDate are missing, vehicles added, missing VIN action required for one vehicle, VIN length over 17 chars OK. + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerCode | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | | 03/05/2021 | 05/05/2026 | 27889.51 | 0 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 1919 | + | 12021 | 143 | HAYDEN | ALEXANDER | W | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | NC | 1/23/2021 | | 2/15/2021 | 1/15/2025 | 25000 | 0 | 6.39 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 1447 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | LoanTypeId | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 12:00:00 AM | 2/10/2021 12:00:00 AM | 03/05/2021 12:00:00 AM | 05/05/2026 12:00:00 AM | 27889.51 | 0 | 3.39 | New | 1946.0007321277132119237549908 | True | False | False | False | True | 2123444 | | | true | + | 1 | 2 | 12021 | 143 | HAYDEN | ALEXANDER | W | 3488 | NC | 1/23/2021 12:00:00 AM | 1/23/2021 12:00:00 AM | 2/15/2021 12:00:00 AM | 1/15/2025 12:00:00 AM | 25000 | 0 | 6.39 | New | 2547.8270112271161397638921532 | False | True | False | True | False | 232323455 | | | false | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And the vehicle for LoanId 2 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M233333389Y853128921 | 2 | Honda | Civic | 2020 | 1900 | Black | + And no audit history record is added + And ExportReservesAsync is called + + Scenario: Update From DNA - where PostedDate is null and Buy Rate is 0 loan is for now updated with that, vehicle update, no dealer update + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | DealerName | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | OriginalTermInDays | + | 3250 | 144 | MUELLER | STEVEN | C | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | NC | 2/10/2021 | | 03/05/2021 | 05/05/2026 | 27889.51 | 0 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | M233333389Y85312 | Honda | Civic | 2020 | 1900 | Black | 1919 | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | LoanStatus | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 2.39 | New | True | False | False | False | True | 2123444 | 3/4/2021 | | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the dealers now look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | + | 1 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 2/10/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 0 | 3.39 | Updated | 1946.0007321277132119237549908 | True | False | False | False | True | 2123444 | | | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M233333389Y85312 | 1 | Honda | Civic | 2020 | 1900 | Black | + And there is a record added to the Audit History table + And ExportReservesAsync is called + +Scenario: No loans from DNA - no exception + Given an import from DNA that contains + | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | + And the current vehicle for loanId 1 is + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | 0 | 0 | | + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 4545 | | + When I process this DNA new/updated loan import at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 0 | 0 | true | | Active | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | LoanTypeId | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Loan Status | Reserve Amount | Gap | Warranty | Mbp | CreditLife | CreditDisability | ApplicationId | ExportedReserveDate | ExportedChargebackDate | ActionRequired | + And no audit history record is added + And ExportReservesAsync is called \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService New.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService New.feature new file mode 100644 index 0000000..33c1f92 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadService New.feature @@ -0,0 +1,234 @@ +Feature: Import Read Service - New Loans + Gets loans from a stream of csv data, reads loans into memory, calculates reserve, saves loans, saves import information + +Scenario: AddImport - happy path + Given an import file named "NewLoans 2021-03-01.txt" that contains + """ + 3434,3250,144,MUELLER, STEVEN C,02/10/2021,02/12/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N,1462 + 4545,12021,143,HAYDEN, ALEXANDER W,01/23/2021,02/02/2021,03/09/2021,2/9/2027,14975.57,4.39,6.39,251.61,NC,XX,N,2189 + """ + When I read this new loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | NewLoans 2021-03-01.txt | Active | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | ExportedReserveDate | Term | + | 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 2.03 | 3.39 | New | | 48 | + | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 4545 | 1/23/2021 | 4.39 | 6.39 | New | | 72 | + And ExportReservesAsync is called + +Scenario: AddImport with Boat Loan Account Type + Given an import file named "NewLoans 2021-03-01.txt" that contains + """ + 3434,71229741,143,WILLIAMS,SAM R,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,4.49,472.71,NC,XX,N,1918,M233333389Y85312,Honda,Civic,2020,1900,Black,1245988,false,false,false,false,false,DL10 + """ + When I read this new loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | NewLoans 2021-03-01.txt | Active | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | ExportedReserveDate | ReserveAmount | ReserveMessage | Term | + | 1 | 71229741 | 143 | WILLIAMS | SAM | R | 3434 | 1/06/2021 | 2.49 | 4.49 | New | | 1673.3706 | Standard Reserve | 63 | + And ExportReservesAsync is called + +Scenario: AddImport - file is wrong format (missing column, etc.) - note that this doesn't return the import that was saved but it is in the table + Given an import file named "NewLoans 2021-03-01.txt" that contains + """ + C9,3250,144,MUELLER, STEVEN C,02/10/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N + R23,12021,143,HAYDEN, ALEXANDER W,01/23/2021,03/09/2021,2/9/2027,14975.57,4.39,6.39,251.61,NC,XX,N + """ + When I read this new loan import file at "3/5/2021 5:00:00 AM" + And I get the record from the ImportTable + Then the import looks like + | StartDate | EndDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | | 0 | 0 | false | NewLoans 2021-03-01.txt | Active | + And ExportReservesAsync is not called + +Scenario: AddImport - file name has more than 50 characters + Given an import file named "NewLoans 2021-03-011234567890123456789012345678901234567890.txt" that contains + """ + 1234,3250,144,MUELLER, STEVEN C,02/10/2021,02/12/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N,1462 + 5678,12021,143,HAYDEN, ALEXANDER W,01/23/2021,02/02/2021,03/09/2021,2/9/2027,14975.57,4.39,6.39,251.61,NC,XX,N,2189 + """ + When I read this new loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | + | 3/5/2021 5:00:00 AM | 2 | 2 | true | NewLoans 2021-03-011234567890123456789012345678901 | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | ImportId | Member Number | Loan Number | Member LastName | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | ExportedReserveDate | Term | + | 1 | 3250 | 144 | MUELLER | STEVEN | C | 1234 | 2/10/2021 | 2.03 | 3.39 | New | | 48 | + | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 5678 | 1/23/2021 | 4.39 | 6.39 | New | | 72 | + And ExportReservesAsync is called + +Scenario: AddImport for Update Loans - updates three new loans that are in the loan table, saves one brand new loan + Given an import file named "NewLoans 2021-03-01.txt" that contains + """ + 3434,13,3,SMITH, JOE,02/25/2021,02/28/2021,04/11/2021,6/11/2026,12615.87,4.03,5.790,579.76,NC,XX,N,1930 + 4545,3250,144,MUELLER, STEVEN C,02/10/2021,02/12/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N,1462 + 1212,12021,143,HAYDEN, ALEXANDER W,01/23/2021,02/02/2021,03/09/2021,2/9/2027,14975.57,4.39,6.39,251.61,NC,XX,N,2199 + 7878,13991,145,RUTHENBERG, RICHARD J,01/22/2021,02/02/2021,03/08/2021,5/8/2027,32891.50,2.43,3.19,484.92,NC,XX,N,2287 + """ + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term | + | 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 | 4.030 | 4.790 | New | 5/2/2021 | 63 | + | 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 | 2.03 | 3.39 | New | 5/2/2021 | 48 | + | 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 | 4.39 | 6.39 | New | 5/2/2021 | 72 | + When I read this new loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | + | 3/5/2021 5:00:00 AM | 4 | 4 | true | NewLoans 2021-03-01.txt | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term | + | 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 12:00:00 AM | 4.03 | 5.790 | Updated | | 63 | + | 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 12:00:00 AM | 2.03 | 3.39 | Updated | | 48 | + | 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 12:00:00 AM | 4.39 | 6.39 | Updated | | 72 | + | 4 | 1 | 13991 | 145 | RUTHENBERG | RICHARD | J | 7878 | 1/22/2021 12:00:00 AM | 2.43 | 3.19 | New | | 75 | + And AddHistoriesAsync is called with 3 history/ies having ActivityType "NewLoanUpdate" and AuditDescription "Loan found in table, record updated" + And AddHistoriesAsync is called with 1 history/ies having LoanNumber "3" and MemberNumber "13" + And ExportReservesAsync is called + +Scenario: AddImport for Update Loans - updates three new loans that are in the loan table, one loan has ten thousandth decimal place difference in buy rate, and same with contract rate, round down + Given an import file named "NewLoans 2021-03-01.txt" that contains + """ + 3434,13,3,SMITH, JOE,02/25/2021,02/28/2021,04/11/2021,6/11/2026,12615.87,4.03124,5.790,579.76,NC,XX,N,1930 + 4545,3250,144,MUELLER, STEVEN C,02/10/2021,02/12/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N,1462 + 1212,12021,143,HAYDEN, ALEXANDER W,01/23/2021,02/02/2021,03/09/2021,2/9/2027,14975.57,4.39,6.39,251.61,NC,XX,N,2199 + 7878,13991,145,RUTHENBERG, RICHARD J,01/22/2021,02/02/2021,03/08/2021,5/8/2027,32891.50,2.43,3.19,484.92,NC,XX,N,2287 + """ + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term | + | 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 | 4.031 | 4.790 | New | 5/2/2021 | 63 | + | 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 | 2.03 | 3.39 | New | 5/2/2021 | 48 | + | 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 | 4.39 | 6.39 | New | 5/2/2021 | 72 | + When I read this new loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | + | 3/5/2021 5:00:00 AM | 4 | 4 | true | NewLoans 2021-03-01.txt | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term | + | 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 12:00:00 AM | 4.031 | 5.790 | Updated | | 63 | + | 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 12:00:00 AM | 2.03 | 3.39 | Updated | | 48 | + | 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 12:00:00 AM | 4.39 | 6.39 | Updated | | 72 | + | 4 | 1 | 13991 | 145 | RUTHENBERG | RICHARD | J | 7878 | 1/22/2021 12:00:00 AM | 2.43 | 3.19 | New | | 75 | + And AddHistoriesAsync is called with 3 history/ies having ActivityType "NewLoanUpdate" and AuditDescription "Loan found in table, record updated" + And AddHistoriesAsync is called with 1 history/ies having LoanNumber "3" and MemberNumber "13" + And ExportReservesAsync is called + +Scenario: AddImport for Update Loans - updates three new loans that are in the loan table, one loan has ten thousandth decimal place difference in buy rate, and same with contract rate, round up + Given an import file named "NewLoans 2021-03-01.txt" that contains + """ + 3434,13,3,SMITH, JOE,02/25/2021,02/28/2021,04/11/2021,6/11/2026,12615.87,4.03155,5.790,579.76,NC,XX,N,1930 + 4545,3250,144,MUELLER, STEVEN C,02/10/2021,02/12/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N,1462 + 1212,12021,143,HAYDEN, ALEXANDER W,01/23/2021,02/02/2021,03/09/2021,2/9/2027,14975.57,4.39,6.2465,251.61,NC,XX,N,2199 + 7878,13991,145,RUTHENBERG, RICHARD J,01/22/2021,02/02/2021,03/08/2021,5/8/2027,32891.50,2.43,3.19,484.92,NC,XX,N,2287 + """ + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term | + | 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 | 4.031 | 5.790 | New | 5/2/2021 | 63 | + | 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 | 2.03 | 3.39 | New | 5/2/2021 | 48 | + | 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 | 4.39 | 6.246 | New | 5/2/2021 | 72 | + When I read this new loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | + | 3/5/2021 5:00:00 AM | 4 | 4 | true | NewLoans 2021-03-01.txt | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term | + | 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 12:00:00 AM | 4.032 | 5.790 | Updated | | 63 | + | 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 12:00:00 AM | 2.03 | 3.39 | Updated | | 48 | + | 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 12:00:00 AM | 4.39 | 6.247 | Updated | | 72 | + | 4 | 1 | 13991 | 145 | RUTHENBERG | RICHARD | J | 7878 | 1/22/2021 12:00:00 AM | 2.43 | 3.19 | New | | 75 | + And AddHistoriesAsync is called with 3 history/ies having ActivityType "NewLoanUpdate" and AuditDescription "Loan found in table, record updated" + And AddHistoriesAsync is called with 1 history/ies having LoanNumber "3" and MemberNumber "13" + And ExportReservesAsync is called + +Scenario: AddImport for Update Loans - updates one loan that is in the loan table, 1 records to audit table for updates + Given an import file named "NewLoans 2021-03-01.txt" that contains + """ + 3434,12,3,SMITH, JOE,02/25/2021,02/28/2021,04/11/2021,6/11/2026,12615.87,4.03,5.790,579.76,NC,XX,N,1930 + """ + And a loan table consisting of + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term | + | 1 | 1 | 12 | 3 | SMITH | JOE | | 3434 | 2/25/2021 | 4.030 | 4.790 | New | 5/2/2021 | 63 | + When I read this new loan import file at "3/5/2021 5:00:00 AM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 3/5/2021 5:00:00 AM | 1 | 1 | true | NewLoans 2021-03-01.txt | Active | + And the import has an end date that is greater or equal to the start date + And the loans now look like + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term | + | 1 | 1 | 12 | 3 | SMITH | JOE | | 3434 | 2/25/2021 12:00:00 AM | 4.03 | 5.790 | Updated | | 63 | + And AddHistoriesAsync is called with 1 history/ies having ActivityType "NewLoanUpdate" and AuditDescription "Loan found in table, record updated" + And AddHistoriesAsync is called with 1 history/ies having LoanNumber "3" and MemberNumber "12" + And ExportReservesAsync is called + +Scenario: AddImport - Vehicle Info - One record populated with VIN, one record optional, Not checking for invalid dealer code any more, Two Invalid Missing VIN action required True + Given an import file named "NewLoans 2021-05-19.txt" that contains + """ + 3434,71229741,143,WILLIAMS,SAM R,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918,M233333389Y85312,Honda,Civic,2020,1900,Black + 4545,7488740,144,STERLING, JIM C,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918 + 4545,8488740,144,STERLING, KIM C,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918,,Honda,Civic,2020,1900,Black + """ + And dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3333 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 4545 | + When I read this new loan import file at "5/19/2021 1:00:00 PM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 5/19/2021 1:00:00 PM | 3 | 3 | true | NewLoans 2021-05-19.txt | Active | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | Action Required | ExportedReserveDate | Term | + | 1 | 1 | 71229741 | 143 | WILLIAMS | SAM | R | 3434 | 1/06/2021 | 2.49 | 2.49 | New | False | | 63 | + | 1 | 2 | 7488740 | 144 | STERLING | JIM | C | 4545 | 1/06/2021 | 2.49 | 2.49 | New | True | | 63 | + | 1 | 3 | 8488740 | 144 | STERLING | KIM | C | 4545 | 1/06/2021 | 2.49 | 2.49 | New | True | | 63 | + And ExportReservesAsync is called + +Scenario: AddImport - Vehicle Info - One record populated with VIN, one record optional + Given an import file named "NewLoans 2021-05-19.txt" that contains + """ + 3434,71229741,143,WILLIAMS,SAM R,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918 + 4545,7488740,144,STERLING, JIM C,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918,M233333389Y85312,Honda,Civic,2020,1900,Black + """ + When I read this new loan import file at "5/19/2021 1:00:00 PM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 5/19/2021 1:00:00 PM | 2 | 2 | true | NewLoans 2021-05-19.txt | Active | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | ExportedReserveDate | Term | + | 1 | 1 | 71229741 | 143 | WILLIAMS | SAM | R | 3434 | 1/06/2021 | 2.49 | 2.49 | New | | 63 | + | 1 | 2 | 7488740 | 144 | STERLING | JIM | C | 4545 | 1/06/2021 | 2.49 | 2.49 | New | | 63 | + And the vehicle for LoanId 2 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | M233333389Y85312 | 2 | Honda | Civic | 2020 | 1900 | Black | + And the vehicle for LoanId 1 looks like this + | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | | 1 | | | | 0 | | + And ExportReservesAsync is called + +Scenario: AddImport - Vehicle Info - Both records populated with VIN, can handle VIN longer than 17 characters + Given an import file named "NewLoans 2021-05-19-2.txt" that contains + """ + 3434,7488740,144,SWEENEY, JIM C,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918,M733P33389Y85216,Chevrolet,Cruze,2013,10K,White + 4545,71229741,143,BARNES,BILL R,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918,M233333389Y853128921,Buick,LaCrosse,2017,56000,Burgundy + """ + When I read this new loan import file at "5/19/2021 1:00:00 PM" + Then the import looks like + | StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | + | 5/19/2021 1:00:00 PM | 2 | 2 | true | NewLoans 2021-05-19-2.txt | Active | + And the loans now look like + | ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | ExportedReserveDate | Term | + | 1 | 1 | 7488740 | 144 | SWEENEY | JIM | C | 3434 | 1/06/2021 | 2.49 | 2.49 | New | | 63 | + | 1 | 2 | 71229741 | 143 | BARNES | BILL | R | 4545 | 1/06/2021 | 2.49 | 2.49 | New | | 63 | + And the vehicle for LoanId 1 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 1 | M733P33389Y85216 | 1 | Chevrolet | Cruze | 2013 | 0 | White | + And the vehicle for LoanId 2 looks like this + | VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color | + | 2 | M233333389Y853128921 | 2 | Buick | LaCrosse | 2017 | 56000 | Burgundy | + And ExportReservesAsync is called \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadServiceSteps.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadServiceSteps.cs new file mode 100644 index 0000000..0e85faf --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportReadServiceSteps.cs @@ -0,0 +1,434 @@ +using Microsoft.EntityFrameworkCore; +using Moq; +using System.Globalization; +using System.Text; +using TechTalk.SpecFlow; +using TechTalk.SpecFlow.Assist; +using TechTalk.SpecFlow.Assist.ValueRetrievers; +using Xunit; +using ReserveManagementSystemApi.ServiceClients; +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemLib.Configuration; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; +using ReserveManagementSystemLib.Repositories; +using LCU.Library.Internal.ApiSecurity.Enums; +using LCU.Library.Internal.ApiSecurity.Models; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan; +using LCU.Library.Internal.LcuHttpClient.HttpRequest; + +namespace ReserveManagementSystemApi.Tests.Features +{ + [Binding] + public sealed class ImportReadServiceSteps + { + // For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef + + private readonly ScenarioContext _scenarioContext; + private ApiSecurityConfiguration _apiSecurityConfiguration; + private ImportReadService _importReadService; + private LoanRepository _loanRepository; + private LoanTypeRepository _loanTypeRepository; + private DealerRepository _dealerRepository; + private DealerService _dealerService; + private Mock _mockAuditHistRepo; + private Mock _mockExportService; + private LoanService _loanService; + private ImportRepository _importRepository; + private ImportService _importService; + private AppDbContext _appDbContext; + private Import _importResult; + private Exception _exception; + private string _importFileName; + private StreamReader _importStreamReader; + private List _loansResult; + private List _dealersResult; + private List _loanDetailsFromDNA; + private List _closedLoansFromDNA; + private Mock _mockCoreApiConfiguration; + private Mock _mockgetRequest; + private Mock _mockpostRequest; + private Mock _mockputRequest; + + public ImportReadServiceSteps(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + Service.Instance.ValueRetrievers.Register(new NullValueRetriever("")); + } + + [BeforeScenario] + public void CreateDatabase() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + _apiSecurityConfiguration = new ApiSecurityConfiguration(ApiKeyAuthenticationFailureBehavior.Warn); + _mockCoreApiConfiguration = new Mock(); + _mockCoreApiConfiguration.Object.ApiTimeOutInSeconds = "420"; + _mockpostRequest = new Mock(); + _mockputRequest = new Mock(); + _mockgetRequest = new Mock(); + _appDbContext = new AppDbContext(options); + _loanRepository = new LoanRepository(_appDbContext); + _loanTypeRepository = new LoanTypeRepository(_appDbContext); + _dealerRepository = new DealerRepository(_appDbContext); + _importRepository = new ImportRepository(_appDbContext); + _mockAuditHistRepo = new Mock(); + _mockExportService = new Mock(); + _dealerService = new DealerService(_dealerRepository); + _loanService = new LoanService(_loanRepository, _loanTypeRepository, new AuditHistoryService(_mockAuditHistRepo.Object), + new Mock().Object); + _importService = new ImportService(_importRepository); + _importReadService = new ImportReadService( + _loanService, _importService, + _dealerService, + _mockExportService.Object, + new CoreApiServiceClient(_apiSecurityConfiguration, _mockCoreApiConfiguration.Object, _mockgetRequest.Object, _mockpostRequest.Object, _mockputRequest.Object), + new Mock().Object + ); + } + + [AfterScenario] + public void DisposeAppDbContext() + { + _appDbContext.Database.EnsureDeleted(); + _appDbContext.Dispose(); + } + + [Given(@"an import file named ""(.*)"" that contains")] + public void GivenAnImportFileNamedThatContains(string fileName, string fileContent) + { + _importFileName = fileName; + var byteArray = Encoding.UTF8.GetBytes(fileContent); + var stream = new MemoryStream(byteArray); + _importStreamReader = new StreamReader(stream); + } + + [Given(@"that I get these closed loans back from DNA")] + public void GivenThatIGetTheseClosedLoansBackFromDNA(Table table) + { + _closedLoansFromDNA = table.CreateSet().ToList(); + } + + [Given(@"an import from DNA that contains")] + public void GivenAnImportFromDNAThatContains(Table table) + { + _loanDetailsFromDNA = table.CreateSet().ToList(); + } + + [Given(@"a loan table consisting of")] + public void GivenALoanTableConsistingOf(Table table) + { + var loans = table.CreateSet().ToList(); + _appDbContext.Loans.AddRange(loans); + _appDbContext.SaveChanges(); + } + + [Given(@"a loan type table consisting of")] + public void GivenALoanTypeTableConsistingOf(Table table) + { + var types = table.CreateSet().ToList(); + _appDbContext.LoanTypes.AddRange(types); + _appDbContext.SaveChanges(); + } + + [Given(@"dealers that look like")] + public void GivenDealersThatLookLike(Table table) + { + var dealers = table.CreateSet().ToList(); + _appDbContext.Dealers.AddRange(dealers); + _appDbContext.SaveChanges(); + } + + [Given(@"the current vehicle for loanId (.*) is")] + public void GivenTheCurrentVehicleForLoanIdIs(int loanId, Table table) + { + var vehicle = table.CreateInstance(); + vehicle.LoanId = loanId; + _appDbContext.Vehicles.Add(vehicle); + _appDbContext.SaveChanges(); + } + + [When(@"I read this new loan import file at ""(.*)""")] + public async void WhenIReadThisNewLoanImportFileAt(DateTime currentTime) + { + try + { + _importResult = await _importReadService.ReadNewLoanImportFile(_importStreamReader, _importFileName, currentTime); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I process this DNA new/updated loan import at ""(.*)""")] + public async void WhenIReadThisNewDNALoanImportFileAt(DateTime currentTime) + { + try + { + _importResult = await _importReadService.ProcessActiveLoanImport(_loanDetailsFromDNA, currentTime); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I process this DNA new/updated loan import at ""(.*)"" and skip the export")] + public async void WhenIProcessThisDNANewUpdatedLoanImportAtAndSkipTheExport(DateTime currentTime) + { + try + { + _importResult = await _importReadService.ProcessActiveLoanImport(_loanDetailsFromDNA, currentTime, true); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I process the closed loan import from DNA at ""(.*)""")] + public async void WhenIReadTheClosedLoanImportFromDNAAt(DateTime currentTime) + { + try + { + _importResult = await _importReadService.ProcessClosedLoanImport(_closedLoansFromDNA, currentTime, string.Empty); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I process the closed loan import with missing loan ""(.*)"" with date from DNA at ""(.*)"" and select from ""(.*)""")] + public async void WhenIProcessTheClosedLoanImportWithMissingLoanWithDateFromDNAAt(string missingLoanNumber, DateTime currentTime, DateTime newerThan) + { + try + { + var stringDate = newerThan.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + _importResult = await _importReadService.ProcessClosedLoanImport(_closedLoansFromDNA.Where(l => l.LoanNumber != missingLoanNumber).ToList(), + currentTime, stringDate); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I process the missing active loans with detail from DNA at ""(.*)"" and export")] + public async void WhenIProcessTheMissingActiveLoansWithDetailFromDNAAtAndExport(DateTime currentTime) + { + try + { + _importResult = await _importReadService.ProcessClosedLoansWithUpdates(_loanDetailsFromDNA, currentTime, false); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I process the closed loan import with detail from DNA at ""(.*)""")] + public async void WhenIReadTheInitialClosedLoanImportFromDNAAt(DateTime currentTime) + { + try + { + _importResult = await _importReadService.ProcessClosedLoansWithUpdates(_loanDetailsFromDNA, currentTime, false); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I process the closed loan import with detail from DNA at ""(.*)"" and skip the export")] + public async void WhenIReadTheInitialClosedLoanImportFromDNAAtAndSkipTheExport(DateTime currentTime) + { + try + { + _importResult = await _importReadService.ProcessClosedLoansWithUpdates(_loanDetailsFromDNA, currentTime, true); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I read this closed loan import file at ""(.*)""")] + public async void WhenIReadThisClosedLoanImportFileAt(DateTime currentTime) + { + try + { + _importResult = await _importReadService.ReadClosedLoanImportFile(_importStreamReader, _importFileName, currentTime); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I get the record from the ImportTable")] + public async void WhenIGetTheRecordFromTheImportTable() + { + _importResult = (await _importRepository.GetAll().ToListAsync()).First(); + } + + [Then(@"the import has start date ""(.*)""")] + public void ThenTheImportHasStartDate(DateTime startDate) + { + Assert.Equal(startDate, _importResult.StartDate); + } + + [Then(@"the import has an end date that is greater or equal to the start date")] + public void ThenTheImportHasAnEndDateThatIsGreaterOrEqualToTheStartDate() + { + Assert.True(_importResult.EndDate >= _importResult.StartDate); + } + + [Then(@"the import has record count (.*)")] + public void ThenTheImportHasRecordCount(int recordCount) + { + Assert.Equal(recordCount, _importResult.RecordCount); + } + + [Then(@"the import has success ""(.*)""")] + public void ThenTheImportHasSuccess(bool success) + { + Assert.Equal(success, _importResult.Success); + } + + [Then(@"the import has file name ""(.*)""")] + public void ThenTheImportHasFileName(string fileName) + { + Assert.Equal(fileName, _importResult.FileName); + } + + [Then(@"an exception is thrown")] + public void ThenAnExceptionIsThrown() + { + Assert.NotNull(_exception); + } + + [Then(@"the import looks like")] + public void ThenTheImportLooksLike(Table table) + { + table.CompareToInstance(_importResult); + } + + [Then(@"there is a record added to the Audit History table")] + public void ThenThereIsARecordAddedToTheAuditHistoryTable() + { + _mockAuditHistRepo.Verify(x => x.AddHistoriesAsync(It.IsAny>())); + } + + [Then(@"no audit history record is added")] + public void ThenNoAuditHistoryRecordIsAdded() + { + _mockAuditHistRepo.Verify(x => x.AddHistoriesAsync(It.IsAny>()), Times.Never); + } + + [Then(@"AddHistoriesAsync is called with (.*) history/ies having OldValue ""(.*)"" and NewValue ""(.*)""")] + public void ThenAddHistoriesAsyncIsCalledWithHistoryIesHavingOldValueAndNewValue(int number, string oldValue, string newValue) + { + _mockAuditHistRepo.Verify(x => + x.AddHistoriesAsync(It.Is>(a => a.Count(x => x.NewValue == newValue && x.OldValue == oldValue) == number))); + } + + [Then(@"AddHistoriesAsync is called with (.*) history/ies having ActivityType ""(.*)"" and AuditDescription ""(.*)""")] + public void ThenAddHistoriesAsyncIsCalledWithHistoryHavingActivityTypeAndAuditDescription(int number, string type, string description) + { + _mockAuditHistRepo.Verify(x => + x.AddHistoriesAsync(It.Is>(a => a.Count(x => x.ActivityType == type && x.AuditDescription == description) == number))); + } + + [Then(@"AddHistoriesAsync is called with (.*) history/ies having LoanNumber ""(.*)"" and MemberNumber ""(.*)""")] + public void ThenAddHistoriesAsyncIsCalledWithHistoryHavingLoanNumberAndMemberNumber(int number, string loanNumber, string memberNumber) + { + _mockAuditHistRepo.Verify(x => + x.AddHistoriesAsync(It.Is>(a => a.Count(x => x.MemberNumber == memberNumber && x.LoanNumber == loanNumber) == number))); + } + + [Then(@"ExportChargebacksAsync is called")] + public void ThenExportChargebacksAsyncIsCalled() + { + _mockExportService.Verify(x => x.ExportChargebacksAsync(It.IsAny())); + } + + [Then(@"ExportChargebacksAsync is not called")] + public void ThenExportChargebacksAsyncIsNotCalled() + { + _mockExportService.Verify(x => x.ExportChargebacksAsync(It.IsAny()), Times.Never); + } + + [Then(@"MarkReservesAndChargebacksAsExportedAsync is called")] + public void ThenMarkReservesAndChargebacksAsExportedAsyncIsCalled() + { + _mockExportService.Verify(x => x.MarkReservesAndChargebacksAsExportedAsync(It.IsAny>(), It.IsAny())); + } + + [Then(@"all loans have non-null ExportedChargebackDate")] + public void ThenAllLoansHaveNon_NullExportedChargebackDate() + { + Assert.DoesNotContain(_loansResult, l => l.ExportedChargebackDate == null); + } + + [Then(@"ExportReservesAsync is called")] + public void ThenExportReservesAsyncIsCalled() + { + _mockExportService.Verify(x => x.ExportReservesAsync(It.IsAny())); + } + + [Then(@"ExportReservesAsync is not called")] + public void ThenExportReservesAsyncIsNotCalled() + { + _mockExportService.Verify(x => x.ExportReservesAsync(It.IsAny()), Times.Never); + } + + [Then(@"MarkReservesAsExportedAsync is called")] + public void ThenMarkReservesAsExportedAsyncIsCalled() + { + _mockExportService.Verify(x => x.MarkReservesAsExportedAsync(It.IsAny>(), It.IsAny())); + } + + [Then(@"all loans have non-null ExportedReserveDate")] + public void ThenAllLoansHaveNon_NullExportedReserveDate() + { + Assert.DoesNotContain(_loansResult, l => l.ExportedReserveDate == null); + } + + [Then(@"the loans now look like")] + public async void ThenTheLoansNowLookLike(Table table) + { + _loansResult = await _loanRepository.GetAll().AsNoTracking().ToListAsync(); + table.CompareToSet(_loansResult); + } + + [Then(@"the dealers now look like")] + public async void ThenTheDealersNowLookLike(Table table) + { + _dealersResult = await _dealerRepository.GetAll().ToListAsync(); + table.CompareToSet(_dealersResult); + } + + [Then(@"the vehicle for LoanId (.*) looks like this")] + public void ThenTheVehicleForLoanIdLooksLikeThis(int loanId, Table table) + { + var result = _loansResult.FirstOrDefault(v => v.LoanId == loanId)?.Vehicle; + table.CompareToInstance(result); + } + + [Then(@"the vehicle for LoanId (.*) is null")] + public void ThenTheVehicleForLoanIdIsNull(int loanId) + { + Assert.Null(_loansResult.FirstOrDefault(v => v.LoanId == loanId).Vehicle); + } + + [Then(@"the loans with details now look like")] + public void ThenTheLoansWithDetailsNowLookLike(Table table) + { + var loans = table.CreateInstance(); + _loansResult.Add(loans); + } + } +} \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportService.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportService.feature new file mode 100644 index 0000000..943ec2d --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportService.feature @@ -0,0 +1,92 @@ +Feature: ImportService + Gets Imports from database + +Scenario: GetAllAsync Success + Given the Import table has the following imports + | ImportId | Start Date | End Date | RecordCount | Success | FileName | + | 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | New Loans 1-2-2021.txt | + | 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | New Loans 1-3-2021.txt | + | 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt | + When I get the list of all imports with the import service + Then the result from the import service should have 3 imports + And the first import from the import service has ImportId 3 + +Scenario: GetAllAsync Exception + Given the Import Repository throws an exception in ListAll + When I get the list of all imports with the import service + Then the Import Service throws an exception with the message "Exception of type 'System.Exception' was thrown." + +Scenario: GetRecentAsync Success + Given the Import table has the following imports + | ImportId | Start Date | End Date | RecordCount | Success | FileName | ImportType | + | 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | New Loans 1-2-2021.txt | Active | + | 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | New Loans 1-3-2021.txt | Active | + | 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt | Active | + When I get the list of the top 2 imports with the import service + Then the result from the import service should have 2 imports + And the first import from the import service has ImportId 3 + +Scenario: GetRecentAsync Success - more imports asked for then are in the table + Given the Import table has the following imports + | ImportId | Start Date | End Date | RecordCount | Success | FileName |ImportType | + | 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | New Loans 1-2-2021.txt |Active | + | 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | New Loans 1-3-2021.txt |Active | + | 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt |Active | + When I get the list of the top 5 imports with the import service + Then the result from the import service should have 3 imports + And the first import from the import service has ImportId 3 + +Scenario: GetRecentAsync Exception + Given the Import Repository throws an exception in ListAll + When I get the list of the top 5 imports with the import service + Then the Import Service throws an exception with the message "Exception of type 'System.Exception' was thrown." + +Scenario: GetByIdAsync Success + Given the Import table has the following imports + | ImportId | Start Date | End Date | RecordCount | Success | FileName |ImportType | + | 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | New Loans 1-2-2021.txt |Active | + | 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | New Loans 1-3-2021.txt |Active | + | 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt |Active | + When I get the import with the importId 2 from the import service + Then the import from the import service has ImportId 2 + +Scenario: GetByIdAsync Exception - Not Found + Given the Import table has the following imports + | ImportId | Start Date | End Date | RecordCount | Success | FileName |ImportType | + | 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | New Loans 1-2-2021.txt |Active | + | 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | New Loans 1-3-2021.txt |Active | + | 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt |Active | + When I get the import with the importId 4 from the import service + Then the Import Service throws an exception with the message "Import with id 4 not found." + +Scenario: GetByIdAsync Exception - Import Repository exception + Given the Import Repository throws an exception in FindByIdAsync + When I get the import with the importId 3 from the import service + Then the Import Service throws an exception with the message "Exception of type 'System.Exception' was thrown." + +Scenario: GetLastLoanImport - active when there are only active + Given the Import table has the following imports + | ImportId | Start Date | End Date | RecordCount | Success | FileName | ImportType | + | 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | | Active | + | 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | | Active | + | 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | | Active | + When I get the last loan import of type "Active" + Then the import from the import service has ImportId 3 + +Scenario: GetLastLoanImport - ignore imports that have a non-null filename + Given the Import table has the following imports + | ImportId | Start Date | End Date | RecordCount | Success | FileName | ImportType | + | 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | | Active | + | 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | | Active | + | 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt | Active | + When I get the last loan import of type "Active" + Then the import from the import service has ImportId 1 + +Scenario: GetLastLoanImport - closed when there are both types + Given the Import table has the following imports + | ImportId | Start Date | End Date | RecordCount | Success | FileName | ImportType | + | 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | | Closed | + | 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | | Active | + | 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | | Active | + When I get the last loan import of type "Closed" + Then the import from the import service has ImportId 1 \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportServiceSteps.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportServiceSteps.cs new file mode 100644 index 0000000..c313b31 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ImportServiceSteps.cs @@ -0,0 +1,149 @@ +using Microsoft.EntityFrameworkCore; +using Moq; +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; +using ReserveManagementSystemLib.Repositories; +using System; +using System.Collections.Generic; +using System.Linq; +using TechTalk.SpecFlow; +using TechTalk.SpecFlow.Assist; +using TechTalk.SpecFlow.Assist.ValueRetrievers; +using Xunit; +using ReserveManagementSystemLib.Enums; + +namespace ReserveManagementSystemApi.Tests.ServiceTests +{ + [Binding] + public sealed class ImportServiceSteps + { + // For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef + + private readonly ScenarioContext _scenarioContext; + private ImportService _importService; + private AppDbContext _appDbContext; + private IEnumerable _importsResult; + private Import _importResult; + private Exception _exception; + + public ImportServiceSteps(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + Service.Instance.ValueRetrievers.Register(new NullValueRetriever("")); + } + + [BeforeScenario] + public void CreateDatabase() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + _appDbContext = new AppDbContext(options); + _importService = new ImportService(new ImportRepository(_appDbContext)); + } + + [AfterScenario] + public void DisposeAppDbContext() + { + _appDbContext.Database.EnsureDeleted(); + _appDbContext.Dispose(); + } + + [Given(@"the Import table has the following imports")] + public void GivenTheImportTableHasTheFollowingImports(Table table) + { + var imports = table.CreateSet(); + + _appDbContext.Imports.AddRange(imports); + _appDbContext.SaveChanges(); + } + + [Given(@"the Import Repository throws an exception in ListAll")] + public void GivenTheImportRepositoryThrowsAnExceptionInListAll() + { + var mockImportRepo = new Mock(); + mockImportRepo.Setup(a => a.GetAll()).Throws(new Exception()); + _importService = new ImportService(mockImportRepo.Object); + } + + [Given(@"the Import Repository throws an exception in FindByIdAsync")] + public void GivenTheImportRepositoryThrowsAnExceptionInFindByIdAsync() + { + var mockImportRepo = new Mock(); + mockImportRepo.Setup(a => a.FindByIdAsync(It.IsAny())).Throws(new Exception()); + _importService = new ImportService(mockImportRepo.Object); + } + + [When(@"I get the list of all imports with the import service")] + public async void WhenIGetTheListOfAllImportsWithTheImportService() + { + try + { + _importsResult = await _importService.GetAllAsync(); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I get the list of the top (.*) imports with the import service")] + public async void WhenIGetTheListOfAllImportsWithTheImportService(int numberOfImports) + { + try + { + _importsResult = await _importService.GetRecentAsync(numberOfImports); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I get the import with the importId (.*) from the import service")] + public async void WhenIGetTheImportWithTheImportIdFromTheImportService(int importId) + { + try + { + _importResult = await _importService.GetByIdAsync(importId); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I get the last loan import of type ""(.*)""")] + public async void WhenIGetTheLastLoanImportOfType(string importType) + { + var type = (ImportType)Enum.Parse(typeof(ImportType), importType, true); + _importResult = await _importService.GetLastLoanImportByType(type); + } + + [Then(@"the result from the import service should have (.*) imports")] + public void ThenTheResultFromTheImportServiceShouldHaveImports(int importCount) + { + Assert.Equal(importCount, _importsResult.Count()); + } + + [Then(@"the first import from the import service has ImportId (.*)")] + public void ThenTheFirstImportFromTheImportServiceHasImportId(int importId) + { + Assert.Equal(importId, _importsResult.First().ImportId); + } + + [Then(@"the Import Service throws an exception with the message ""(.*)""")] + public void ThenTheImportServiceThrowsAnExceptionWithTheMessage(string message) + { + Assert.NotNull(_exception); + Assert.Equal(message, _exception.Message); + } + + [Then(@"the import from the import service has ImportId (.*)")] + public void ThenTheImportFromTheImportServiceHasImportId(int importId) + { + Assert.Equal(importId, _importResult.ImportId); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanCalculationServiceSteps.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanCalculationServiceSteps.cs new file mode 100644 index 0000000..f1cda50 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanCalculationServiceSteps.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore; +using Moq; +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; +using ReserveManagementSystemLib.Repositories; +using System; +using System.Collections.Generic; +using System.Linq; +using TechTalk.SpecFlow; +using TechTalk.SpecFlow.Assist; +using Xunit; + +namespace ReserveManagementSystemApi.Tests +{ + [Binding] + public sealed class LoanCalculationServiceSteps + { + // For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef + + private readonly ScenarioContext _scenarioContext; + private Loan _loan; + private LoanReserve _reserve; + private LoanChargeback _chargeback; + private List _loanTypes; + private AppDbContext _appDbContext; + + public LoanCalculationServiceSteps(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + } + + [BeforeScenario] + public void CreateDatabase() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + _appDbContext = new AppDbContext(options); + } + + [AfterScenario] + public void DisposeAppDbContext() + { + _appDbContext.Database.EnsureDeleted(); + _appDbContext.Dispose(); + } + + [Given(@"a loan that looks like")] + public void GivenALoanThatLooksLike(Table table) + { + _loan = table.CreateInstance(); + } + + [Given(@"a loan type table that looks like")] + public void GivenALoanTypeTableThatLooksLike(Table table) + { + _loanTypes = table.CreateSet().ToList(); + _appDbContext.LoanTypes.AddRange(_loanTypes); + _appDbContext.SaveChanges(); + } + + [When(@"the reserve is calculated")] + public async void WhenTheReserveIsCalculated() + { + _loan.LoanType = await new LoanTypeRepository(_appDbContext).GetLoanTypeAsync(_loan.LoanTypeId); + var loanCalculationService = new LoanCalculationService(); + _reserve = loanCalculationService.CalculateSingleReserve(_loan); + } + + [When(@"the chargeback is calculated")] + public async void WhenTheChargebackIsCalculated() + { + _loan.LoanType = await new LoanTypeRepository(_appDbContext).GetLoanTypeAsync(_loan.LoanTypeId); + var loanCalculationService = new LoanCalculationService(); + _chargeback = loanCalculationService.CalculateSingleChargeback(_loan); + } + + [Then(@"FlatFeePaid should be ""(.*)""")] + public void ThenFlatFeePaidShouldBe(bool flatFeePaid) + { + Assert.Equal(flatFeePaid, _reserve.FlatFeePaid); + } + + [Then(@"the reserve amount should be (.*)")] + public void ThenTheReserveAmountShouldBe(decimal reserve) + { + Assert.Equal(reserve, _reserve.ReserveAmount); + } + + [Then(@"the reserve message should be (.*)")] + public void ThenTheReserveMessageShouldBe(string message) + { + Assert.Equal(message, _reserve.ReserveTypeMessage); + } + + [Then(@"the chargeback returned is")] + public void ThenTheChargebackReturnedIs(Table table) + { + table.CompareToInstance(_chargeback); + } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Filtering and Pagination.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Filtering and Pagination.feature new file mode 100644 index 0000000..2f43704 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Filtering and Pagination.feature @@ -0,0 +1,475 @@ +Feature: Loan Service - Filtering, Pagination + Get filtered list of loans, get paginated list of loans + +Scenario: GetFilteredAsync Success - ImportId - sorting will be by Dealer Code ASC importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 345 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 | + | 2 | 2 | 45 | 2 | 678 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 3 | 2 | 22 | 6 | 901 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | ImportId | DealerCodeText | ActionRequired | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | | 2 | | | | | | | DealerCode | ASC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "3,2" + +Scenario: GetFilteredAsync Success - MemberNumber - sorting will be by Action Required DESC importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 345 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 | + | 2 | 2 | 45 | 6 | 678 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 12 | 4 | 901 | Smith | Joe | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | 12 | | | | | | | | | ActionRequired | DESC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "3,1" + +Scenario: GetFilteredAsync Success - MemberName, searches both first and last name - sorting will be by MemberLastName DESC importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 345 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 | + | 2 | 2 | 45 | 6 | 678 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 12 | 4 | 901 | SMITH | JOE | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | Jo | | | | | | | | MemberLastName | ASC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "2,3,1" + +Scenario: GetFilteredAsync Success - MemberName, returns one - sorting will be by MemberLastName DESC importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number |Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 |345 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 | + | 2 | 2 | 45 | 6 |678 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 12 | 4 |901 | SMITH | JOE | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | Jon | | | | | | | | MemberLastName | ASC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "2" + +Scenario: GetFilteredAsync Success - LoanNumber - sorting will be by Action Required DESC importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number |Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 |345 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 | + | 2 | 2 | 45 | 6 |678 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 56 | 3 |901 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | | 3 | | | | | | | ActionRequired | DESC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "3,1" + +Scenario: GetFilteredAsync Success - Application Id - sorting will be by Action Required DESC importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number |Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 |345 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 | + | 2 | 2 | 45 | 6 |378 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 56 | 3 |901 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | | | 3 | | | | | | ActionRequired | DESC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "2,1" + +Scenario: GetFilteredAsync Success - DealerCode - sorting will be by Action Required ASC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 345 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 | + | 2 | 2 | 45 | 6 | 678 | Jones | Laura | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 3 | 2 | 22 | 2 | 901 | Johnson | Laura | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And the dealer table has these dealers + | Dealer Code | Name | DealerOrganizationNumber | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | + | A1 | ACURA OF BROOKFIELD | 5678 | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | | | H21 | | ActionRequired | ASC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "2,3" + +Scenario: GetFilteredAsync Success - ActionRequired - sorting will be by Dealer Code ASC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number |Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate |Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 |345 | SMITH | JOE | | H21 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 |NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 |678 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 |BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 3 | 2 | 22 | 2 |901 | JOHNSON | LAURA | M | S7 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 |BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | | | | 1 | DealerCode | ASC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "1,3" + +Scenario: GetFilteredAsync Success - Loan Date - start and end - sorting will be by Dealer Code ASC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id |Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 345 |SMITH | JOE | | H21 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 678 |JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 3 | 2 | 22 | 2 | 901 |JOHNSON | LAURA | M | S7 | 2/23/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And a loan filter that looks like + | MemberNumberText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | | |2/23/2021 | 2/24/2021 | | | | DealerCode | ASC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "2,3" + +Scenario: GetFilteredAsync Success - Loan Date - only start date - sorting will be by Dealer Code ASC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number |Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 |345 | SMITH | JOE | | H21 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 |678 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 3 | 2 | 22 | 2 |901 | JOHNSON | LAURA | M | S7 | 2/23/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And a loan filter that looks like + | MemberNumberText | MemberNameText |LoanNumberText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | | | 2/24/2021 | | | | | DealerCode | ASC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "2,1" + +Scenario: GetFilteredAsync Success - Loan Date - only end date - sorting will be by Dealer Code ASC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | SMITH | JOE | | H21 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 3 | 2 | 22 | 2 | JOHNSON | LAURA | M | S7 | 2/23/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | 2/24/2021 | | | | DealerCode | ASC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "2,3" + +Scenario: GetFilteredAsync Success - all filters used - sorting will be by DealerCode ASC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 345 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 678 | JONES | LAURA | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 3 | 2 | 22 | 2 | 901 | JOHNSON | LAURA | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + And the dealer table has these dealers + | Dealer Code | Name | DealerOrganizationNumber | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | + | A1 | ACURA OF BROOKFIELD | 5678 | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | 22 | Joh | 2 | 9 | 2 | 2/24/2021 | 2/24/2021 | H21 | 1 | DealerCode | ASC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the loan ids from the loan service look like "3" + +Scenario: GetFilteredAsync Success - returning nothing - sorting will be by Dealer Code DESC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id |Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 345 |SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 678 |JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 22 | 2 | 901 |JOHNSON | LAURA | M | H22 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange | + | | | | |1 | | | H21 | 0 | DealerCode | DESC | 15 | 7 | + When I get the list of the filtered loans with the loan service + Then the result from the loan service should have 0 loans + +Scenario: GetPagedListAsync Success - returning page count with one index and one record validation - no filtering sorting will be by MemberFirstName DESC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 1 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 7 | 1 | 34 | 3 | 7 | Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 10 | 1 | 12 | 3 | 10 | Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 16 | 1 | 34 | 3 | 16 | Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | | | | | MemberFirstName | DESC | 2 | 15 | 7 | + When I get the list of paged loans + Then the result from the loan service should have 2 pages + And the first result from the loan service should have loanid 13 + +Scenario: GetPagedListAsync Success - returning one index record count and one record validation - no filtering sorting will be by MemberLastName DESC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id |Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 1 |Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 2 |Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 22 | 2 | 3 |Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 4 | 1 | 55 | 5 | 4 |Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 5 | 2 | 99 | 8 | 5 |Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 6 | 2 | 78 | 9 | 6 |Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 7 | 1 | 34 | 3 | 7 |Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 8 | 2 | 11 | 1 | 8 |Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 9 | 2 | 11 | 2 | 9 |McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 10 | 1 | 12 | 3 | 10 |Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 11 | 2 | 45 | 6 | 11 |Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 12 | 2 | 22 | 2 | 12 |Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 13 | 1 | 55 | 5 | 13 |Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 14 | 2 | 99 | 8 | 14 |Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 15 | 2 | 78 | 9 | 15 |Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 16 | 1 | 34 | 3 | 16 |Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 17 | 2 | 11 | 1 | 17 |Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 18 | 2 | 11 | 2 | 18 |McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | | | | | MemberLastName | DESC | 1 | 15 | 7 | + When I get the list of paged loans + Then the result from the loan service should have 15 records + And the first result from the loan service should have loanid 1 + +Scenario: GetPagedListAsync Fail - not returning pages no records - no sorting or filtering + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id |Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 1 |Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 2 |Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 22 | 2 | 3 |Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 4 | 1 | 55 | 5 | 4 |Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 5 | 2 | 99 | 8 | 5 |Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 6 | 2 | 78 | 9 | 6 |Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 7 | 1 | 34 | 3 | 7 |Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 8 | 2 | 11 | 1 | 8 |Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 9 | 2 | 11 | 2 | 9 |McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 10 | 1 | 12 | 3 | 10 |Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 11 | 2 | 45 | 6 | 11 |Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 12 | 2 | 22 | 2 | 12 |Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 13 | 1 | 55 | 5 | 13 |Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 14 | 2 | 99 | 8 | 14 |Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 15 | 2 | 78 | 9 | 15 |Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 16 | 1 | 34 | 3 | 16 |Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 17 | 2 | 11 | 1 | 17 |Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 18 | 2 | 11 | 2 | 18 |McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + Then the result from the loan service should have 0 total items + +Scenario: GetPagedListAsync HasPrevious Page Success HasNext Page Fail - returning one index record count checking previous next - no sorting or filtering + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 1 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 7 | 1 | 34 | 3 | 7 | Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 10 | 1 | 12 | 3 | 10 | Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 16 | 1 | 34 | 3 | 16 | Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | | | | | | | 2 | 15 | 7 | + When I get the list of paged loans + Then the result from the loan service should have 3 records + And the result from the loan service has previous page should be true + And the result from the loan service has next page should be false + +Scenario: GetPagedListAsync HasPrevious Page Fail HasNext Page Success - returning one index record count checking previous next - no sorting or filtering + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id |Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 1 |Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 2 |Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 22 | 2 | 3 |Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 4 | 1 | 55 | 5 | 4 |Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 5 | 2 | 99 | 8 | 5 |Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 6 | 2 | 78 | 9 | 6 |Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 7 | 1 | 34 | 3 | 7 |Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 8 | 2 | 11 | 1 | 8 |Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 9 | 2 | 11 | 2 | 9 |McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 10 | 1 | 12 | 3 | 10 |Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 11 | 2 | 45 | 6 | 11 |Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 12 | 2 | 22 | 2 | 12 |Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 13 | 1 | 55 | 5 | 13 |Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 14 | 2 | 99 | 8 | 14 |Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 15 | 2 | 78 | 9 | 15 |Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 16 | 1 | 34 | 3 | 16 |Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 17 | 2 | 11 | 1 | 17 |Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 18 | 2 | 11 | 2 | 18 |McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | | | | | | | 1 | 15 | 7 | + When I get the list of paged loans + Then the result from the loan service should have 15 records + And the result from the loan service has previous page should be false + And the result from the loan service has next page should be true + +Scenario: GetPagedListAsync Success - returning page count with one index and one record validation - Filter by DealerCodeText sorting will be by MemberFirstName DESC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 1 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 7 | 1 | 34 | 3 | 7 | Best | Ronald | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 10 | 1 | 12 | 3 | 10 | Smith | Joey | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 16 | 1 | 34 | 3 | 16 | Best | Rainier | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + And the dealer table has these dealers + | Dealer Code | Name | DealerOrganizationNumber | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | + | A1 | ACURA OF BROOKFIELD | 5678 | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | + And a loan filter that looks like + | MemberNumberText | MemberFirstNameText | MemberLastNameText | ApplicationIdText | LoanNumberText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | | | | H21 | | MemberFirstName | DESC | 1 | 15 | 7 | + When I get the list of paged loans + Then the result from the loan service should have 1 pages + And the first result from the loan service should have loanid 8 + +Scenario: GetPagedListAsync Success - returning one index record count and one record validation - Filtering by ActionRequired sorting will be by MemberLastName DESC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 1 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 7 | 1 | 34 | 3 | 7 | Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 10 | 1 | 12 | 3 | 10 | Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 16 | 1 | 34 | 3 | 16 | Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | | | | 1 | MemberLastName | DESC | 1 | 15 | 7 | + When I get the list of paged loans + Then the result from the loan service should have 8 records + And the first result from the loan service should have loanid 1 + +Scenario: GetPagedListAsync Success Page Range Values Check Last Page Index First and Last Page - returning one index record count and one record validation - Filtering by ImportId sorting will be by MemberLastName DESC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 1 | Zeman | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 1 | 45 | 6 | 2 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 1 | 22 | 2 | 3 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 5 | 1 | 99 | 8 | 5 | Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 6 | 1 | 78 | 9 | 6 | Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 7 | 1 | 34 | 3 | 7 | Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 8 | 1 | 11 | 1 | 8 | Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 9 | 1 | 11 | 2 | 9 | McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 10 | 1 | 12 | 3 | 10 | Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 11 | 1 | 45 | 6 | 11 | Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 12 | 1 | 22 | 2 | 12 | Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 14 | 1 | 99 | 8 | 14 | Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 15 | 1 | 78 | 9 | 15 | Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 16 | 1 | 34 | 3 | 16 | Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 17 | 1 | 11 | 1 | 17 | Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 18 | 1 | 11 | 2 | 18 | McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 19 | 2 | 11 | 1 | 19 | Sorenson | Jill | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 20 | 2 | 11 | 2 | 20 | Berry | Bill | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | 1 | | | | | MemberLastName | DESC | 9 | 2 | 3 | + When I get the list of paged loans + Then the result from the loan service should have 2 records + And the first result from the loan service should have loanid 7 + And the result from the loan service has previous page should be true + And the result from the loan service has next page should be false + And the result from the loan service first page should be 7 + And the result from the loan service last page should be 9 + +Scenario: GetPagedListAsync Success - Page Range Values Check First and Last Page - returning one index record count and one record validation - Filtering by Dealer Code sorting will be by MemberLastName DESC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 1 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 7 | 1 | 34 | 3 | 7 | Best | Ronald | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 10 | 1 | 12 | 3 | 10 | Smith | Joey | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 16 | 1 | 34 | 3 | 16 | Best | Rainier | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + And the dealer table has these dealers + | Dealer Code | Name | DealerOrganizationNumber | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | + | A1 | ACURA OF BROOKFIELD | 5678 | + | S21 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | | | S21 | | MemberLastName | DESC | 5 | 2 | 3 | + When I get the list of paged loans + Then the result from the loan service should have 2 records + And the first result from the loan service should have loanid 11 + And the result from the loan service has previous page should be true + And the result from the loan service has next page should be true + And the result from the loan service first page should be 4 + And the result from the loan service last page should be 6 + +Scenario: GetPagedListAsync Success - Page Range Values Check Below Page Range Max - returning one index record count and one record validation - Filtering by Dealer Code sorting will be by MemberLastName DESC then importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required | + | 1 | 1 | 12 | 3 | 1 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 7 | 1 | 34 | 3 | 7 | Best | Ronald | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 10 | 1 | 12 | 3 | 10 | Smith | Joey | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 | + | 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + | 16 | 1 | 34 | 3 | 16 | Best | Rainier | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 | + | 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 | + | 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | + And the dealer table has these dealers + | Dealer Code | Name | DealerOrganizationNumber | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | + | A1 | ACURA OF BROOKFIELD | 5678 | + | S21 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | + And a loan filter that looks like + | MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange | + | | | | | | | | S21 | 1 | MemberLastName | DESC | 2 | 4 | 4 | + When I get the list of paged loans + Then the result from the loan service should have 4 records + And the first result from the loan service should have loanid 4 + And the result from the loan service has previous page should be true + And the result from the loan service has next page should be false + And the result from the loan service first page should be 1 + And the result from the loan service last page should be 2 \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Get All and Get By Id.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Get All and Get By Id.feature new file mode 100644 index 0000000..fdb5aaa --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Get All and Get By Id.feature @@ -0,0 +1,183 @@ +Feature: Loan Service Get all, Get By Id + Gets all loans, get loan by loanId + +Scenario: GetListAsync Success and sorting will be by importid descending and then by member number ascending + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 1 | 12 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 2 | 45 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + | 3 | 2 | 22 | 2 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + When I get the list of all loans with the loan service + Then the result from the loan service should have 3 loans + And the first loan from the loan service has LoanId 3 + +Scenario: GetListAsync Exception + Given the Loan Repository throws an exception in ListAll + When I get the list of all loans with the loan service with the mock loan repo + Then the Loan Service throws an exception with the message "Exception of type 'System.Exception' was thrown." + +Scenario: GetListAsync - get dealer info + Given the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 1 | 12 | 3 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 2 | 45 | 6 | Jones | Laura | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + | 3 | 2 | 22 | 2 | Johnson | Laura | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + And the dealer table has these dealers + | Dealer Code | Name | DealerOrganizationNumber | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | + | A1 | ACURA OF BROOKFIELD | 5678 | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | + + When I get the list of all loans with the loan service + Then the result from the loan service should have 3 loans + And the first loan from the loan service has LoanId 3 + And the loan with LoanId 1 has dealer name "SUBURBAN MOTORS HARLEY DAVIDSON" + And the loan with LoanId 2 has dealer name "HEISER CHEVROLET CADILLAC OF WEST BEND" + +Scenario: GetByIdAsync Success + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 12 | 3 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | BB | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 45 | 6 | Jones | Laura | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | + And the dealer table has these dealers + | Dealer Code | Name | DealerOrganizationNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | + And loan types that look like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + | BB | Standard | S | 0 | 0 | 0 | 1.00 | 0.25 | + When I get the loan with the loanId 2 from the loan service + Then the loan from the loan service has LoanId 2 + And the loan from the loan service has LoanType.LoanComment "75% Reserve" + And the loan from the loan service has dealer name "HEISER CHEVROLET CADILLAC OF WEST BEND" + +Scenario: GetByIdAsync - unknown loan type + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 12 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | BB | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 45 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | + And loan types that look like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When I get the loan with the loanId 1 from the loan service + Then the loan from the loan service has LoanId 1 + And the loan from the loan service has LoanType.LoanComment "" + +Scenario: GetByIdAsync Exception - Not Found + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 12 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 45 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + And loan types that look like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When I get the loan with the loanId 3 from the loan service + Then the Loan Service throws an exception with the message "Loan with id 3 not found." + +Scenario: GetByIdAsync Exception - Loan Repository exception + Given the Loan Repository throws an exception in FindByIdAsync + When I get the loan with the loanId 2 from the loan service using the Mock Loan Repo + Then the Loan Service throws an exception with the message "Exception of type 'System.Exception' was thrown." + +Scenario: Get Members - searching member number, don't return duplicates + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 452 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + | 3 | 451 | 7 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + When I get the list of members with search string "45" + Then the list of members returned is in this order + | Member Number | + | 451 | + | 452 | + +Scenario: Get Members - searching member number, return in number order, not string order + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate |Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 452 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 |NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 4510 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 |BB | 60 | 1450.2923 | 500 | false | | | Message | + When I get the list of members with search string "45" + Then the list of members returned is in this order + | Member Number | + | 452 | + | 4510 | + +Scenario: Get Members - only get numbers starting with the search string + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 245 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + | 3 | 451 | 7 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + When I get the list of members with search string "45" + Then the list of members returned is in this order + | Member Number | + | 451 | + +Scenario: Get Members - not searching name + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate |Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 452 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 |NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 |BB | 60 | 1450.2923 | 500 | false | | | Message | + | 3 | 451 | 7 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 |BB | 60 | 1450.2923 | 500 | false | | | Message | + When I get the list of members with search string "Jo" + Then the list of members returned is in this order + | Member Number | + +Scenario: Get Loan Numbers + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 452 | 731 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + | 3 | 451 | 73 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + When I get the list of loan numbers with search string "73" + Then the list of loan numbers looks like + | Loan Number | + | 73 | + | 731 | + +Scenario: Get Loan Numbers, return in number order, not string order + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 452 | 731 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + | 3 | 451 | 7307 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + When I get the list of loan numbers with search string "73" + Then the list of loan numbers looks like + | Loan Number | + | 731 | + | 7307 | + +Scenario: Get Loan Numbers - only get numbers starting with the search string + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 452 | 173 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + | 3 | 451 | 73 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + When I get the list of loan numbers with search string "73" + Then the list of loan numbers looks like + | Loan Number | + | 73 | + +Scenario: Get Application Ids, return in number order + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 452 | 731 | 112 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 451 | 6 | 334 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + | 3 | 451 | 73 | 35 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + When I get the list of application ids with search string "3" + Then the list of application ids looks like + |Application Id | + | 35 | + | 334 | + +Scenario: Get Application Ids - only get ids starting with the search string + Given the Loan table has the following loans + | LoanId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | + | 1 | 452 | 173 | 133 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | + | 2 | 451 | 6 | 334 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + | 3 | 451 | 73 | 35 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | + When I get the list of application ids with search string "33" + Then the list of application ids looks like + |Application Id | + | 334 | \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Saving.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Saving.feature new file mode 100644 index 0000000..7f7197b --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanService - Saving.feature @@ -0,0 +1,77 @@ +Feature: Loan Service - Saving + Save loan with Reserve, save loan with chargeback + +Background: +Given dealers that look like + | Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber | + | S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 | + | H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | | + | | | 1234 | 924324324324324 | +And loan types that look like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + + +Scenario: Save with Reserve Success + Given I have these loans to save + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial |DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status | + | 1 | 3250 | 144 | MUELLER | STEVEN | C |1234 | 2/10/2021 | 3/20/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 48 | 0 | false | 0.00 | | | New | + When I save the loan with the reserve + Then the loans from the loan service look like + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status | + | 1 | 3250 | 144 | MUELLER | STEVEN | C | 1234 | 2/10/2021 | 3/20/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 48 | 566.98877654773011071101556050 | false | 0.00 | | Standard Reserve | New | + +Scenario: Save with Reserve Failure + Given the Loan Repository throws an exception in UpdateAsync + And I have these loans to save + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status | + | 1 | 3250 | 144 | MUELLER | STEVEN | C | 1234 | 2/10/2021 | 3/15/2021 | 3/12/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | | | false | 0.00 | | | New | + When I save the loan with the reserve with the mock loan repo + Then the Loan Service throws an exception with the message "Exception of type 'System.Exception' was thrown." + +Scenario: Save with Reserve - Set ActionRequired - Dealer organization null + Given I have these loans to save + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial |DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status | VIN Number | Make | Model | Year | Mileage | Color | + | 1 | 3250 | 144 | MUELLER | STEVEN | C | | 2/10/2021 | 3/20/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 48 | 0 | false | 0.00 | | | New | M733P33389Y85216 | | | | | | + When I save the loan with the reserve + Then the loans from the loan service look like + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ActionRequired | Loan Status | + | 1 | 3250 | 144 | MUELLER | STEVEN | C | | 2/10/2021 | 3/20/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 48 | 566.98877654773011071101556050 | false | 0.00 | | Standard Reserve | true | New | + +Scenario: Save with Reserve - Set ActionRequired - first payment date before loan date + Given I have these loans to save + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status | OriginalTermInDays | + | 1 | 3250 | 144 | MUELLER | STEVEN | C | 1234 | 2/10/2021 | 1/12/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 50 | 0 | false | 0.00 | | | New | 1491 | + When I save the loan with the reserve + Then the loans from the loan service look like + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ActionRequired | Loan Status | + | 1 | 3250 | 144 | MUELLER | STEVEN | C | 1234 | 2/10/2021 | 1/12/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 50 | 584.06654602341792014440573200 | false | 0.00 | | Standard Reserve | true | New | + +Scenario: Save with Reserve - Set ActionRequired True - Invalid Dealer Code + Given I have these loans to save + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status | DealerOrganizationNumber | OriginalTermInDays | + | 1 | 3250 | 144 | MUELLER | STEVEN | C | 6222 | 2/10/2021 | 1/12/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 50 | 0 | false | 0.00 | | | New | 2345 | 1491 | + When I save the loan with the reserve + Then the loans from the loan service look like + | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ActionRequired | Loan Status | + | 1 | 3250 | 144 | MUELLER | STEVEN | C | 6222 | 2/10/2021 | 1/12/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 50 | 584.06654602341792014440573200 | false | 0.00 | | Standard Reserve | true | New | + +Scenario: SaveWithChargeback + Given I have these loans to save with chargebacks + | ImportId | Member Number | Loan Number | PaidLoanDate | + | 2 | 5841 | 147 | 2/19/2021 | + | 2 | 10094 | 143 | 12/22/2020 | + And the Loan table has the following loans + | LoanId | ImportId | Member Number | Loan Number | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Type Id | Term | Reserve Amount | Chargeback | Paid Loan Date | Reserve Message | Loan Status | + | 1 | 1 | 12 | 3 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | NC | 63 | 210.0375 | | | Standard | New | + | 2 | 1 | 5841 | 147 | 2/10/2021 | 2/27/2021 | 3/12/2021 | 2/12/2028 | NC | 84 | 1206.8106 | | | Message | New | + | 3 | 1 | 10094 | 143 | 12/7/2020 | 12/15/2020 | 1/21/2021 | 12/21/2025 | NC | 60 | 878.8602 | | | Message | New | + When I save the loans with chargebacks + And I get the list of all loans with the loan service + Then the loans in the loan table will look like + | LoanId | ImportId | Member Number | Loan Number | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Type Id | Term | Reserve Amount | Chargeback | Chargeback Message | Paid Loan Date | Reserve Message | Loan Status | Remaining Months at Close | + | 1 | 1 | 12 | 3 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | NC | 63 | 210.0375 | | | | Standard | New | | + | 2 | 2 | 5841 | 147 | 2/10/2021 | 2/27/2021 | 3/12/2021 | 2/12/2028 | NC | 84 | 1206.8106 | 1206.8106 | Paid On or Before Chargeback Date | 2/19/2021 | Message | Closed | 84 | + | 3 | 2 | 10094 | 143 | 12/7/2020 | 12/15/2020 | 1/21/2021 | 12/21/2025 | NC | 60 | 878.8602 | 878.8602 | Paid On or Before Chargeback Date | 12/22/2020 | Message | Closed | 60 | + + \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanServiceSteps.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanServiceSteps.cs new file mode 100644 index 0000000..2a4c0c0 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/LoanServiceSteps.cs @@ -0,0 +1,436 @@ +using FluentAssertions; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using Moq; +using ReserveManagementSystemApi.Models; +using ReserveManagementSystemApi.ServiceClients; +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; +using ReserveManagementSystemLib.Repositories; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TechTalk.SpecFlow; +using TechTalk.SpecFlow.Assist; +using TechTalk.SpecFlow.Assist.ValueRetrievers; +using Xunit; + +namespace ReserveManagementSystemApi.Tests.ServiceTests +{ + [Binding] + public sealed class LoanServiceSteps + { + // For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef + + private readonly ScenarioContext _scenarioContext; + private LoanService _loanService; + private AppDbContext _appDbContext; + private IEnumerable _loansResult; + private List _loansToSave; + private Loan _loanResult; + private Exception _exception; + private LoanFilter _loanFilter; + private List _memberResult; + private List _loanNumberResult; + private List _applicationIdResult; + PaginatedListResponse _pagedListResponse = new PaginatedListResponse(); + private IEnumerable _closedLoans; + private IEnumerable _loansInSystem; + private IEnumerable _loansNotInSystem; + private Mock _mockLoanRepo = new Mock(); + private Mock _mockAuditHistRepo = new Mock(); + + public LoanServiceSteps(ScenarioContext scenarioContext) + { + _scenarioContext = scenarioContext; + } + + [BeforeScenario] + public void CreateDatabase() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + _appDbContext = new AppDbContext(options); + _loanService = new LoanService(new LoanRepository(_appDbContext), new LoanTypeRepository(_appDbContext), new AuditHistoryService(_mockAuditHistRepo.Object), new Mock().Object); + } + + [AfterScenario] + public void Dispose() + { + _appDbContext.Database.EnsureDeleted(); + _appDbContext.Dispose(); + } + + [Given(@"the Loan table has the following loans")] + public void GivenTheLoanTableHasTheFollowingLoans(Table table) + { + var loans = table.CreateSet(); + + _appDbContext.Loans.AddRange(loans); + _appDbContext.SaveChanges(); + } + + [Given(@"I have these loans to save")] + public void GivenIHaveThisLoanToSave(Table table) + { + _loansToSave = table.CreateSet().ToList(); + } + + [Given(@"a loan filter that looks like")] + public void GivenALoanFilterThatLooksLike(Table table) + { + _loanFilter = table.CreateInstance(); + } + + [Then(@"the first loan from the loan service has ImportId (.*)")] + public void ThenTheFirstLoanFromTheLoanServiceHasImportId(int importId) + { + Assert.Equal(importId, _loansResult.First().ImportId); + } + + [Given(@"the Loan Repository throws an exception in ListAll")] + public void GivenTheLoanRepositoryThrowsAnExceptionInListAll() + { + _mockLoanRepo.Setup(a => a.GetAll()).Throws(new Exception()); + _loanService = new LoanService(_mockLoanRepo.Object, new LoanTypeRepository(_appDbContext), new AuditHistoryService(_mockAuditHistRepo.Object), new Mock().Object); + } + + [Given(@"the Loan Repository throws an exception in FindByIdAsync")] + public void GivenTheLoanRepositoryThrowsAnExceptionInFindByIdAsync() + { + _mockLoanRepo.Setup(a => a.FindByIdAsync(It.IsAny())).Throws(new Exception()); + _loanService = new LoanService(_mockLoanRepo.Object, new LoanTypeRepository(_appDbContext), new AuditHistoryService(_mockAuditHistRepo.Object), new Mock().Object); + } + + [Given(@"the Loan Repository throws an exception in UpdateAsync")] + public void GivenTheLoanRepositoryThrowsAnExceptionInAdd() + { + _mockLoanRepo.Setup(a => a.UpdateAsync(It.IsAny>())).Throws(new Exception()); + _loanService = new LoanService(_mockLoanRepo.Object, new LoanTypeRepository(_appDbContext), new AuditHistoryService(_mockAuditHistRepo.Object), new Mock().Object); + } + + [Given(@"loan types that look like")] + public void GivenLoanTypesThatLookLike(Table table) + { + var loanTypes = table.CreateSet().ToList(); + _appDbContext.LoanTypes.AddRange(loanTypes); + _appDbContext.SaveChanges(); + } + + [Given(@"I have these loans to save with chargebacks")] + public void GivenIHaveTheseLoansToSaveWithChargebacks(Table table) + { + _closedLoans = table.CreateSet().ToList(); + } + + [Given(@"the dealer table has these dealers")] + public void GivenTheDealerTableHasTheseDealers(Table table) + { + var dealers = table.CreateSet().ToList(); + _appDbContext.Dealers.AddRange(dealers); + _appDbContext.SaveChanges(); + } + + [When(@"I save the loans with chargebacks")] + public async void WhenISaveTheLoansWithChargebacks() + { + try + { + var result = await _loanService.SaveWithChargebackAsync(_closedLoans.ToList()); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I get the list of all loans with the loan service")] + public async void WhenIGetTheListOfAllLoansWithTheLoanService() + { + try + { + _loansResult = await _loanService.GetAllAsync(); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I get the list of all loans with the loan service with the mock loan repo")] + public async void WhenIGetTheListOfAllLoansWithTheLoanServiceWithTheMockLoanRepo() + { + try + { + _loansResult = await _loanService.GetAllAsync(); + } + catch (Exception ex) + { + _exception = ex; + } + } + + + [When(@"I get the list of the filtered loans with the loan service")] + public async void WhenIGetTheListOfTheFilteredLoansWithTheLoanService() + { + try + { + _pagedListResponse = await _loanService.GetFilteredAsync(_loanFilter); + _loansResult = _pagedListResponse.LoanList; + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I get the list of paged loans")] + public async void WhenIGetTheListOfPagedLoans() + { + try + { + _pagedListResponse = await _loanService.GetFilteredAsync(_loanFilter); + _loansResult = _pagedListResponse.LoanList; + } + catch (Exception ex) + { + _exception = ex; + } + } + + [Then(@"the result from the loan service should have (.*) pages")] + public void ThenTheResultFromTheLoanServiceShouldHavePages(int pageCount) + { + Assert.Equal(pageCount, _pagedListResponse.TotalPages); + } + + [Then(@"the result from the loan service should have (.*) records")] + public void ThenTheResultFromTheLoanServiceShouldHaveRecords(int recordCount) + { + Assert.Equal(recordCount, _pagedListResponse.LoanList.Count()); + } + + [Then(@"the result from the loan service should have (.*) total items")] + public void ThenTheResultFromTheLoanServiceShouldHaveTotalItems(int recordCount) + { + Assert.Equal(recordCount, _pagedListResponse.TotalItems); + } + + [Then(@"the first result from the loan service should have loanid (.*)")] + public void ThenTheFirstResultFromTheLoanServiceShouldHaveLoanId(int loanId) + { + Assert.Equal(loanId, _pagedListResponse.LoanList.First().LoanId); + } + + //have to rethink this if we get rid of the other one + [When(@"I save the loan with the reserve")] + public async void WhenISaveTheLoan() + { + try + { + await _loanService.SaveLoansAsync(_loansToSave); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I save the loan with the reserve with the mock loan repo")] + public async void WhenISaveTheLoanWithTheReserveWithTheMockLoanRepo() + { + try + { + await _loanService.SaveLoansAsync(_loansToSave); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [Then(@"the result from the loan service has previous page should be (.*)")] + public void ThenTheResultFromTheLoanServiceHasPreviousPageShouldBe(bool hasPreviousPage) + { + Assert.Equal(hasPreviousPage, _pagedListResponse.HasPreviousPage); + } + + [Then(@"the result from the loan service has next page should be (.*)")] + public void ThenTheResultFromTheLoanServiceHasNextPageShouldBe(bool hasNextPage) + { + Assert.Equal(hasNextPage, _pagedListResponse.HasNextPage); + } + + [Then (@"the result from the loan service first page should be (.*)")] + public void ThenTheResultFromTheLoanServiceFirstPageShouldBe(int firstPage) + { + Assert.Equal(firstPage, _pagedListResponse.FirstPage); + } + + [Then(@"the result from the loan service last page should be (.*)")] + public void ThenTheResultFromTheLoanServiceLastPageShouldBe(int lastPage) + { + Assert.Equal(lastPage, _pagedListResponse.LastPage); + } + + [When(@"I get the loan with the loanId (.*) from the loan service")] + public async void WhenIGetTheLoanWithTheLoanIdFromTheLoanService(int loanId) + { + try + { + _loanResult = await _loanService.GetByIdAsync(loanId); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I get the loan with the loanId (.*) from the loan service using the Mock Loan Repo")] + public async void WhenIGetTheLoanWithTheLoanIdFromTheLoanServiceUsingTheMockLoanRepo(int loanId) + { + try + { + _loanResult = await _loanService.GetByIdAsync(loanId); + } + catch (Exception ex) + { + _exception = ex; + } + } + + [When(@"I get the list of members with search string ""(.*)""")] + public async void WhenIGetTheListOfMembersWithSearchString(string searchString) + { + _memberResult = await _loanService.SearchMembers(searchString); + } + + [When(@"I get the list of loan numbers with search string ""(.*)""")] + public async void WhenIGetTheListOfLoanNumbersWithSearchString(string searchString) + { + _loanNumberResult = await _loanService.SearchLoanNumbers(searchString); + } + + [When(@"I get the list of application ids with search string ""(.*)""")] + public async void WhenIGetTheListOfApplicationIdsWithSearchString(string searchString) + { + _applicationIdResult = await _loanService.SearchApplicationIds(searchString); + } + + + [Then(@"the result from the loan service should have (.*) loans")] + public void ThenTheResultFromTheLoanServiceShouldHaveLoans(int loanCount) + { + Assert.Equal(loanCount, _loansResult.Count()); + } + + [Then(@"the list of members returned is in this order")] + public void ThenTheListOfMembersReturnedIsInThisOrder(Table table) + { + var expectedMemberNumbers = table.Rows.Select(x => x[0]).ToList(); + _memberResult.Should().BeEquivalentTo(expectedMemberNumbers).And.ContainInOrder(expectedMemberNumbers); + } + + [Then(@"the list of loan numbers looks like")] + public void ThenTheListOfLoanNumbersLooksLike(Table table) + { + var expectedLoanNumbers = table.Rows.Select(x => x[0]).ToList(); + _loanNumberResult.Should().BeEquivalentTo(expectedLoanNumbers).And.ContainInOrder(expectedLoanNumbers); + } + + [Then(@"the list of application ids looks like")] + public void ThenTheListOfApplicationIdsLooksLike(Table table) + { + var expectedApplicationIds = table.Rows.Select(x => x[0]).ToList(); + _applicationIdResult.Should().BeEquivalentTo(expectedApplicationIds).And.ContainInOrder(expectedApplicationIds); + } + + + [Then(@"the loans from the loan service look like")] + public async void ThenTheLoanFromTheLoanServiceLooksLike(Table table) + { + var actualLoans = (await _loanService.GetAllAsync()).ToList(); + table.CompareToSet(actualLoans); + } + + [Then(@"the first loan from the loan service has LoanId (.*)")] + public void ThenTheFirstLoanFromTheLoanServiceHasLoanId(int loanId) + { + Assert.Equal(loanId, _loansResult.First().LoanId); + } + + [Then(@"the loan ids from the loan service look like")] + public void ThenTheLoanIdsFromTheLoanServiceLookLike(Table table) + { + var expectedLoanIds = table.CreateSet().ToList(); + _loansResult.Select(x => x.LoanId).Should().BeEquivalentTo(expectedLoanIds).And.ContainInOrder(expectedLoanIds); + } + + [Then(@"the loan ids from the loan service look like ""(.*)""")] + public void ThenTheLoanIdsFromTheLoanServiceLookLike(string listOfIds) + { + var expected = listOfIds.Split(',').Select(x => int.Parse(x)).ToList(); + _loansResult.Select(x => x.LoanId).Should().BeEquivalentTo(expected); + } + + + [Then(@"the loan with LoanId (.*) has dealer name ""(.*)""")] + public void ThenTheLoanWithLoanIdHasDealerName(int loanId, string dealerName) + { + Assert.Equal(dealerName, _loansResult.First(x => x.LoanId == loanId).Dealer.Name); + } + + [Then(@"the Loan Service throws an exception with the message ""(.*)""")] + public void ThenTheLoanServiceThrowsAnExceptionWithTheMessage(string message) + { + Assert.NotNull(_exception); + Assert.Equal(message, _exception.Message); + } + + [Then(@"the loan from the loan service has LoanId (.*)")] + public void ThenTheLoanFromTheLoanServiceHasLoanId(int loanId) + { + Assert.Equal(loanId, _loanResult.LoanId); + } + + [Then(@"the loan from the loan service has dealer name ""(.*)""")] + public void ThenTheLoanFromTheLoanServiceHasDealerName(string dealerName) + { + Assert.Equal(dealerName, _loanResult.Dealer.Name); + } + + [Then(@"the flat fee result for MemberNumber (.*) should be (.*)")] + public void ThenTheReserveAmountResultShouldBe(string member, bool feePaid) + { + Assert.Equal(feePaid, _loansResult.FirstOrDefault(x => x.MemberNumber == member).FlatFeePaid); + } + + [Then(@"the loan from the loan service has LoanType.LoanComment ""(.*)""")] + public void ThenTheLoanFromTheLoanServiceHasLoanTypeDescription(string description) + { + Assert.Equal(description, _loanResult.LoanType?.LoanComment ?? string.Empty); + } + + [Then(@"the loans in the system look like")] + public void ThenTheLoansInTheSystemLookLike(Table table) + { + table.CompareToSet(_loansInSystem); + } + + [Then(@"the loans not in the system look like")] + public void ThenTheLoansNotInTheSystemLookLike(Table table) + { + table.CompareToSet(_loansNotInSystem); + } + + [Then(@"the loans in the loan table will look like")] + public void ThenTheLoansInTheLoanTableWillLookLike(Table table) + { + table.CompareToSet(_loansResult); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ReserveCalculation.feature b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ReserveCalculation.feature new file mode 100644 index 0000000..e2e77ad --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/Features/ReserveCalculation.feature @@ -0,0 +1,230 @@ +Feature: Reserve Calculation + Calculates reserve amount for a loan + +Scenario: Calculate Reserve with Standard Markup + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | + | 5/28/2015 | 6/27/2015 | 5/27/2017 | 6200.20 | 1.48 | 3.89 | NC | 24 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 118.73687752592344707611833965 + And the reserve message should be Standard Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Boat Reserve with Standard Markup + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | AccountType | + | 5/28/2015 | 6/27/2015 | 5/27/2017 | 6200.20 | 1.50 | 3.00 | NC | 24 | DL09 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 310.01 + And the reserve message should be Standard Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Reserve with Standard Markup and Days to First Payment is less than 30 + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | + | 2/27/2021 | 3/27/2021 | 2/27/2028 | 55799.16 | 4.59 | 6.34 | NC | 84 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 2917.8694562514364202612072482 + And the reserve message should be Standard Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Reserve with Standard Markup - first pay date is less than 30 days from loan date (this calculation might be wrong) + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | + | 4/23/2021 | 5/21/2021 | 4/21/2026 | 6474.80 | 3.24 | 5.24 | NC | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 263.93215613213476391531161260 + And the reserve message should be Standard Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Reserve with Standard Markup - loan type id is missing, but will calculate the same as NC + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | + | 5/28/2015 | 6/27/2015 | 5/27/2017 | 6200.20 | 1.48 | 3.89 | | 24 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 118.73687752592344707611833965 + And the reserve message should be Standard Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Reserve with Standard Markup - BB + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | + | 5/28/2015 | 6/27/2015 | 5/27/2017 | 6200.20 | 1.48 | 3.89 | BB | 24 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | BB | Standard | S | 0 | 0 | 0 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 158.3158367012312627681577862 + And the reserve message should be Standard Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Reserve with Standard Markup - NR + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | + | 5/28/2015 | 6/27/2015 | 5/27/2017 | 6200.20 | 3.89 | 3.89 | NR | 24 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 0 + And the reserve message should be No Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Reserve with Standard Markup Flat Fee Applied + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | + | 2/18/2021 | 4/4/2021 | 3/1/2024 | 12000.95 | 2.89 | 3.49 | NC | 36 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 90.007125 + And the reserve message should be Flat Fee Reserve + And FlatFeePaid should be "true" + +Scenario: Calculate Reserve with No Markup Flat Fee Applied + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | + | 7/10/2019 | 8/24/2019 | 7/24/2024 | 20500 | 3.49 | 3.49 | NC | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 153.75 + Then the reserve message should be Flat Fee Reserve + And FlatFeePaid should be "true" + +Scenario: Calculate Boat Reserve with No Markup Standard Reserve Applied @ 1% Reserve + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | AccountType | + | 7/10/2019 | 8/24/2019 | 7/24/2024 | 20500 | 3.49 | 3.49 | NC | 60 | DL12 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 205 + Then the reserve message should be Standard Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Boat Reserve with Non Standard Markup + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | AccountType | + | 1/2/2021 | 2/16/2021 | 1/26/2026 | 28423.05 | 1.83 | 11.73 | NC | 60 | DL10 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 1705.383 + Then the reserve message should be Standard Reserve - Exceeds Max Markup + And FlatFeePaid should be "false" + +Scenario: Calculate Reserve with Non Standard Markup + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | + | 1/2/2021 | 2/16/2021 | 1/26/2026 | 28423.05 | 1.83 | 11.73 | NC | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 6104.6234279062733014747185835 + Then the reserve message should be Standard Reserve - Exceeds Max Markup + And FlatFeePaid should be "false" + +Scenario: Calculate Buydown Reserve + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | + | 4/19/2018 | 5/19/2018 | 4/19/2024 | 27042.25 | 4.6 | 1.9 | NC | 72 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be -2362.679817869475221821891298 + Then the reserve message should be Buy Down Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Boat Buydown Reserve + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | AccountType | + | 4/19/2018 | 5/19/2018 | 4/19/2024 | 27042.25 | 4.6 | 1.9 | NC | 72 | DL11 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be -2362.679817869475221821891298 + Then the reserve message should be Buy Down Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Buydown Reserve - 0.5 difference + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | + | 5/15/2019 | 7/1/2019 | 6/1/2022 | 6261 | 5.27 | 4.77 | NC | 36 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be -52.284980016715778985051587237 + Then the reserve message should be Buy Down Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Boat Buydown Reserve - 0.05 difference (reserve should be 0) + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | AccountType | + | 8/4/2017 | 9/3/2017 | 8/3/2022 | 15721.42 | 9.95 | 9.9 | NC | 60 | DL09 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 0 + Then the reserve message should be No Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Buydown Reserve - 0.05 difference (reserve should be 0) + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | + | 8/4/2017 | 9/3/2017 | 8/3/2022 | 15721.42 | 9.95 | 9.9 | NC | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 0 + Then the reserve message should be No Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Buydown Reserve - 0.25 difference (reserve should be 0) + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | + | 8/4/2017 | 9/3/2017 | 8/3/2022 | 15721.42 | 8.25 | 8 | NC | 60 | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 0 + Then the reserve message should be No Reserve + And FlatFeePaid should be "false" + +Scenario: Calculate Reserve when there are weird payment dates + Given a loan that looks like + | Loan Date | First Pmt Date | Final Pmt Date | PostedDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | + | 6/14/2021 | 7/24/2021 | 6/30/2021 | 6/18/2021 | 20500 | 3.49 | 3.49 | NC | + And a loan type table that looks like + | LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold | + | NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 | + When the reserve is calculated + Then the reserve amount should be 0 + Then the reserve message should be Exception while calculating reserve + And FlatFeePaid should be "false" \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/ReserveManagementSystemApi.Tests.csproj b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/ReserveManagementSystemApi.Tests.csproj new file mode 100644 index 0000000..3aab1b2 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/ReserveManagementSystemApi.Tests.csproj @@ -0,0 +1,129 @@ + + + + net6.0 + + false + + enable + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + ImportReadService Closed from DNA.feature + + + ImportReadService Closed.feature + + + ImportReadService New from DNA.feature + + + ImportReadService New.feature + + + ImportService.feature + + + LoanService - Saving.feature + + + LoanService - Filtering and Pagination.feature + + + LoanService - Get All and Get By Id.feature + + + Code + + + Code + + + Code + + + Code + + + ChargebackCalculation.feature + + + ReserveCalculation.feature + + + + + + PreserveNewest + + + + + + $(UsingMicrosoftNETSdk) + %(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension) + + + $(UsingMicrosoftNETSdk) + %(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension) + + + $(UsingMicrosoftNETSdk) + %(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension) + + + $(UsingMicrosoftNETSdk) + %(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension) + + + $(UsingMicrosoftNETSdk) + %(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension) + + + $(UsingMicrosoftNETSdk) + %(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension) + + + $(UsingMicrosoftNETSdk) + %(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension) + + + $(UsingMicrosoftNETSdk) + %(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension) + + + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/xunit.runner.json b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/xunit.runner.json new file mode 100644 index 0000000..671206e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "parallelizeTestCollections": false +} \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/ApiConfiguration.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/ApiConfiguration.cs new file mode 100644 index 0000000..9925ad3 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/ApiConfiguration.cs @@ -0,0 +1,11 @@ +namespace ReserveManagementSystemApi.Configurations +{ + public class ApiConfiguration + { + public string AppName { get; set; } + public string SeqUrl { get; set; } + public string SeqApiKey { get; set; } + public string[] CorsAllowedOrigins { get; set; } + public FeatureFlags FeatureFlags { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/FeatureFlags.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/FeatureFlags.cs new file mode 100644 index 0000000..065d8d2 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/FeatureFlags.cs @@ -0,0 +1,9 @@ +namespace ReserveManagementSystemApi.Configurations +{ + public class FeatureFlags + { + public bool RunRmsActiveLoanConsumer { get; set; } + public bool EnableImportExportActive { get; set; } + public bool EnableImportExportClosed { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/ReceiveEndPointConfigurations.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/ReceiveEndPointConfigurations.cs new file mode 100644 index 0000000..7c19478 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/ReceiveEndPointConfigurations.cs @@ -0,0 +1,9 @@ +using LCU.Library.Internal.LcuMassTransitModels; + +namespace ReserveManagementSystemApi.Configurations +{ + public class ReceiveEndpointConfigurations + { + public ReceiveEndpointConfiguration RmsActiveLoanReceiveEndpoint { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/SqlConnectionConfiguration.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/SqlConnectionConfiguration.cs new file mode 100644 index 0000000..9768c06 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Configurations/SqlConnectionConfiguration.cs @@ -0,0 +1,9 @@ +namespace ReserveManagementSystemApi.Configurations +{ + public class SqlConnectionConfiguration + { + public string ConnectionString { get; set; } + public int MaxRetryCount { get; set; } + public TimeSpan MaxRetryDelay { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Consumers/RmsActiveImportConsumer.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Consumers/RmsActiveImportConsumer.cs new file mode 100644 index 0000000..3f2fa0a --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Consumers/RmsActiveImportConsumer.cs @@ -0,0 +1,40 @@ +using MassTransit; +using LCU.Api.Internal.MaidBotLib.Models; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemApi.Services; + +namespace ReserveManagementSystemApi.Consumers; + +public class RmsActiveImportConsumer : IConsumer +{ + private readonly IServiceScopeFactory _scopeFactory; + + public RmsActiveImportConsumer(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + } + + public async Task Consume(ConsumeContext context) + { + try + { + Log.Debug("Starting RmsActiveImportConsumer."); + using var scope = _scopeFactory.CreateScope(); + await Task.Run(async () => await scope.ServiceProvider.GetRequiredService().HandleRmsActiveImportRequest(context.Message)); + Log.Debug("Ending RmsActiveImportConsumer."); + } + catch (ServiceException) + { + throw; + } + catch (Exception exception) + { + Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group), + Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)) + .Fatal(exception, $"Unexpected RmsActiveImportConsumer Failure. " + + $"The RmsActiveImportRequest is placed on ReserveManagementSystemApi.ActiveImport_error."); + throw; + } + } +} + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/AuditHistoryController.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/AuditHistoryController.cs new file mode 100644 index 0000000..a58a973 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/AuditHistoryController.cs @@ -0,0 +1,34 @@ +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class AuditHistoryController : ControllerBase + { + private readonly IAuditHistoryService _auditHistoryService; + + public AuditHistoryController(IAuditHistoryService auditHistoryService) + { + _auditHistoryService = auditHistoryService; + } + + [HttpGet(Name = "GetAllHistory")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [ProducesResponseType(500)] + public async Task Get() + { + try + { + var result = await _auditHistoryService.GetAllAsync(); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, "Failure returning Audit Histories"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/DealerController.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/DealerController.cs new file mode 100644 index 0000000..117e160 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/DealerController.cs @@ -0,0 +1,51 @@ +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class DealerController : ControllerBase + { + private readonly IDealerService _dealerService; + + public DealerController(IDealerService dealerService) + { + _dealerService = dealerService; + } + + [HttpGet("Search/{dealerSearchText}", Name = "Search")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [ProducesResponseType(500)] + public async Task Search(string dealerSearchText) + { + try + { + var result = await _dealerService.Search(dealerSearchText); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, "Failure returning dealers"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpGet("GetByDealerCode/{dealerCode}", Name = "GetDealerByCode")] + [ProducesResponseType(typeof(Dealer), 200)] + [ProducesResponseType(500)] + public async Task GetByDealerCode(string dealerCode) + { + try + { + var result = await _dealerService.GetDealerByCodeAsync(dealerCode); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, "Failure returning dealer"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/ImportController.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/ImportController.cs new file mode 100644 index 0000000..a9f49b3 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/ImportController.cs @@ -0,0 +1,183 @@ +using ReserveManagementSystemApi.Models; +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemApi.Services.QueueServices; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ImportController : ControllerBase + { + private readonly IImportReadService _importReadService; + private readonly IImportService _importService; + private readonly IQueueRmsActiveLoanService _queueRmsActiveLoanService; + + public ImportController(IImportReadService importReadService, IImportService importService, IQueueRmsActiveLoanService queueRmsActiveLoanService) + { + _importReadService = importReadService; + _importService = importService; + _queueRmsActiveLoanService = queueRmsActiveLoanService; + } + + [HttpGet(Name = "GetAllImports")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [ProducesResponseType(500)] + public async Task Get() + { + try + { + var result = await _importService.GetAllAsync(); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, "Failure returning Imports"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpGet("GetRecentImports/{numberOfImports}", Name = "GetRecentImports")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [ProducesResponseType(500)] + public async Task GetRecent(int numberOfImports) + { + try + { + var result = await _importService.GetRecentAsync(numberOfImports); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, "Failure returning Imports"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpGet("GetById/{importId}", Name = "GetImportById")] + [ProducesResponseType(typeof(Loan), 200)] + [ProducesResponseType(500)] + public async Task GetById(int importId) + { + try + { + var result = await _importService.GetByIdAsync(importId); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, $"Failure returning loan with id {importId}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpPost("ImportNewLoanFile", Name ="ImportNewLoanFile")] + [ProducesResponseType(typeof(Import), 200)] + [ProducesResponseType(500)] + public async Task ImportNewLoanFile(IFormFile file) + { + try + { + var stream = new StreamReader(file.OpenReadStream()); + var import = await _importReadService.ReadNewLoanImportFile(stream, file.FileName, DateTime.Now); + return Ok(import); + } + catch (Exception e) + { + Log.Error("Failure to import new loan file, message: {0} {1}", e.Message, e.StackTrace); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpPost("ImportBoatLoanFile", Name = "ImportBoatLoanFile")] + [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(500)] + public async Task ImportBoatLoanFile(IFormFile file) + { + try + { + var stream = new StreamReader(file.OpenReadStream()); + var import = await _importReadService.ReadBoatLoanImportFile(stream); + return Ok(import); + } + catch (Exception e) + { + Log.Error("Failure to import boat loan file, message: {0} {1}", e.Message, e.StackTrace); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpPost("ImportClosedLoanFile", Name = "ImportClosedLoanFile")] + [ProducesResponseType(typeof(Import), 200)] + [ProducesResponseType(500)] + public async Task ImportClosedLoanFile(IFormFile file) + { + try + { + var stream = new StreamReader(file.OpenReadStream()); + var import = await _importReadService.ReadClosedLoanImportFile(stream, file.FileName, DateTime.Now); + return Ok(import); + } + catch (Exception e) + { + Log.Error("Failure to import closed loan file, message: {0} {1}", e.Message, e.StackTrace); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpPut("ImportActiveLoans", Name = "ImportActiveLoans")] + [ProducesResponseType(typeof(Import), 200)] + [ProducesResponseType(500)] + public async Task ImportActiveLoans(string? newerThanDateTime = "", bool skipExport = false) + { + try + { + var request = new RmsActiveImportRequest() + { + ImportDateTime = newerThanDateTime, + SkipExport = skipExport + }; + await _queueRmsActiveLoanService.QueueRmsActiveLoanServiceRequest(request); + return Accepted(); + } + catch (Exception ex) + { + Log.Error(ex, $"Failure importing Active Loans"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpPut("ImportClosedLoans", Name = "ImportClosedLoans")] + [ProducesResponseType(typeof(Import), 200)] + [ProducesResponseType(500)] + public async Task ImportClosedLoans(string? newerThanDateTime) + { + try + { + var import = await _importReadService.ImportClosedLoansFromDNA(newerThanDateTime); + return Ok(import); + } + catch (Exception ex) + { + Log.Error(ex, $"Failure importing Closed Loans"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpPut("ImportClosedLoansWithUpdates", Name = "ImportClosedLoansWithUpdates")] + [ProducesResponseType(typeof(Import), 200)] + [ProducesResponseType(500)] + public async Task ImportClosedLoansWithUpdates(string? newerThanDateTime, bool skipExport = false) + { + try + { + var import = await _importReadService.ImportClosedLoansWithUpdatesFromDNA(newerThanDateTime, skipExport); + return Ok(import); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/LoanController.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/LoanController.cs new file mode 100644 index 0000000..1e2be69 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/LoanController.cs @@ -0,0 +1,146 @@ +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class LoanController : ControllerBase + { + private readonly ILoanService _loanService; + public LoanController(ILoanService loanService) + { + _loanService = loanService; + } + + [HttpGet(Name = "GetAllLoans")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [ProducesResponseType(500)] + public async Task Get() + { + try + { + var result = await _loanService.GetAllAsync(); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, "Failure returning Loans"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpPost("SearchLoans", Name = "SearchLoans")] + [ProducesResponseType(typeof(PaginatedListResponse), 200)] + [ProducesResponseType(500)] + public async Task Post([FromBody]LoanFilter filter) + { + try + { + var result = await _loanService.GetFilteredAsync(filter); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, "Failure returning Filtered Loans"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpGet("SearchMembers/{searchText}", Name = "SearchMembers")] + [ProducesResponseType(typeof(string), 200)] + [ProducesResponseType(500)] + public async Task SearchMembers(string searchText) + { + try + { + var result = await _loanService.SearchMembers(searchText); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, $"Failure returning members"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpGet("SearchLoanNumbers/{searchText}", Name = "SearchLoanNumbers")] + [ProducesResponseType(typeof(string), 200)] + [ProducesResponseType(500)] + public async Task SearchLoanNumbers(string searchText) + { + try + { + var result = await _loanService.SearchLoanNumbers(searchText); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, $"Failure returning loan numbers"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpGet("SearchApplicationIds/{searchText}", Name = "SearchApplicationIds")] + [ProducesResponseType(typeof(string), 200)] + [ProducesResponseType(500)] + public async Task SearchApplicationIds(string searchText) + { + try + { + var result = await _loanService.SearchApplicationIds(searchText); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, $"Failure returning application ids"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpGet("GetById/{loanId}", Name = "GetLoanById")] + [ProducesResponseType(typeof(Loan), 200)] + [ProducesResponseType(500)] + public async Task GetById(int loanId) + { + try + { + var result = await _loanService.GetByIdAsync(loanId); + return Ok(result); + } + catch (Exception e) + { + Log.Error(e, $"Failure returning loan with id {loanId}"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + [HttpPost("CalculateReserve", Name = "CalculateReserve")] + public async Task Post([FromBody] LoanReserveInput input) + { + try + { + return await _loanService.GetLoanReserveAsync(input); + } + catch (Exception e) + { + Log.Error("Failure to Calculate Loan Reserve: {0} {1}", e.Message, e.StackTrace); + return null; + } + } + + [HttpPost("CalculateChargeback", Name = "CalculateChargeback")] + public async Task Post([FromBody] LoanChargebackInput input) + { + try + { + return await _loanService.GetLoanChargebackAsync(input); + } + catch (Exception e) + { + Log.Error("Failure to Calculate Loan Chargeback: {0} {1}", e.Message, e.StackTrace); + return null; + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/StatusController.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/StatusController.cs new file mode 100644 index 0000000..3a1d5d0 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Controllers/StatusController.cs @@ -0,0 +1,33 @@ +using ReserveManagementSystemApi.Services; +namespace ReserveManagementSystemApi.Controllers; + + [ApiController] + [Route("api/[controller]")] + public class StatusController : ControllerBase + { + private readonly IStatusService _statusService; + public StatusController(IStatusService statusService) + { + _statusService = statusService; + } + + [HttpGet] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public IActionResult Get() + { + try + { + return Ok(_statusService.GetEnviromentView()); + } + catch (ServiceException exception) + { + return StatusCode(StatusCodes.Status500InternalServerError, exception.Message); + } + catch (Exception exception) + { + Log.Error(exception, "Unexpected Exception"); + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + } \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Enums/ActivityType.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Enums/ActivityType.cs new file mode 100644 index 0000000..8e8c412 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Enums/ActivityType.cs @@ -0,0 +1,10 @@ +namespace ReserveManagementSystemApi.Enums +{ + public enum ActivityType + { + Warning, + Error, + LoanStatusChange, + NewLoanUpdate + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Enums/LoanStatus.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Enums/LoanStatus.cs new file mode 100644 index 0000000..da6df49 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Enums/LoanStatus.cs @@ -0,0 +1,10 @@ +namespace ReserveManagementSystemApi.Enums +{ + public enum LoanStatus + { + New, + Updated, + Closed, + MissingActiveLoan + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/BatchHelper.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/BatchHelper.cs new file mode 100644 index 0000000..154a499 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/BatchHelper.cs @@ -0,0 +1,27 @@ +namespace ReserveManagementSystemApi.Helpers +{ + public static class BatchHelper + { + public static IEnumerable> CreateBatch ( + List source, int batchSize) + { + var items = new TSource[batchSize]; + var count = 0; + foreach (var item in source) + { + items[count++] = item; + if (count == batchSize) + { + yield return items; + items = new TSource[batchSize]; + count = 0; + } + } + + if (count > 0) + { + yield return items.Take(count); + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/BoatLoanCalculationHelper.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/BoatLoanCalculationHelper.cs new file mode 100644 index 0000000..9548ea6 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/BoatLoanCalculationHelper.cs @@ -0,0 +1,56 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Helpers +{ + public class BoatLoanCalculationHelper + { + private LoanReserve loanReserve = new LoanReserve(); + + public BoatLoanCalculationHelper() + { + + } + + public LoanReserve CalculateStandardMarkup(CalculationConfig cc) + { + var markup = cc.LoanData().ContractRate - cc.LoanData().BuyRate; + loanReserve.ReserveTypeMessage = "Standard Reserve"; + switch (markup) + { + case decimal d when (d >= (decimal)0.00 && d <= (decimal)0.49): + loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).01; + break; + case decimal d when (d >= (decimal)0.50 && d <= (decimal)0.74): + loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).02; + break; + case decimal d when (d >= (decimal)0.75 && d <= (decimal)0.99): + loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).03; + break; + case decimal d when (d >= (decimal)1.00 && d <= (decimal)1.49): + loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).04; + break; + case decimal d when (d >= (decimal)1.50 && d <= (decimal)1.99): + loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).05; + break; + case decimal d when (d == (decimal)2.00): + loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).06; + break; + case decimal d when (d > (decimal)2.00): + loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).06; + loanReserve.ReserveTypeMessage = "Standard Reserve - Exceeds Max Markup"; + break; + default: + loanReserve.ReserveAmount = 0; + loanReserve.ReserveTypeMessage = "No Reserve"; + break; + } + return loanReserve; + + } + + internal DateTime GetLastChargebackDate (DateTime firstPmtDate, int numOfChargebackPayments) + { + return firstPmtDate.AddMonths(numOfChargebackPayments - 1); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/ImportHelper.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/ImportHelper.cs new file mode 100644 index 0000000..8f01411 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/ImportHelper.cs @@ -0,0 +1,210 @@ +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan; +using ReserveManagementSystemApi.Models; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Helpers +{ + public class ImportHelper + { + private string _firstName; + private string _middleInitial; + private const double MonthInDays = 30.4375; + + public ImportHelper() + { + + } + + public List TransformNewLoan(List iImportLoans, int iImportId) + { + var result = new List(); + + foreach (var iLoan in iImportLoans) + { + Loan loan = new Loan(); + loan.ImportId = iImportId; + loan.MemberNumber = iLoan.MemberNumber; + loan.LoanNumber = iLoan.LoanNumber; + SplitFirstMiddleName(iLoan.FirstAndMiddleName); + loan.MemberFirstName = _firstName?.ToUpper(); + loan.MemberInitial = _middleInitial?.ToUpper(); + + loan.MemberLastName = iLoan.LastName?.ToUpper(); + loan.DealerOrganizationNumber = iLoan.DealerOrganizationNumber; + loan.LoanDate = iLoan.LoanDate; + loan.PostedDate = iLoan.PostedDate; + loan.FirstPmtDate = iLoan.FirstPmtDate; + loan.FinalPmtDate = iLoan.FinalPmtDate; + loan.LoanAmount = iLoan.LoanAmount; + loan.BuyRate = Math.Round(iLoan.BuyRate, 3, MidpointRounding.AwayFromZero); + loan.ContractRate = Math.Round(iLoan.ContractRate, 3, MidpointRounding.AwayFromZero); + loan.MonthlyPmt = iLoan.MonthlyPmt; + loan.LoanTypeId = iLoan.LoanType; + loan.ChargeBack = null; + loan.PaidLoanDate = null; + loan.ApplicationId = iLoan.ApplicationId; + loan.AccountType = iLoan.AccountType; + loan.Gap = !string.IsNullOrEmpty(iLoan.Gap) && Convert.ToBoolean(iLoan.Gap); + loan.Warranty = !string.IsNullOrEmpty(iLoan.Warranty) && Convert.ToBoolean(iLoan.Warranty); + loan.CreditLife = !string.IsNullOrEmpty(iLoan.CreditLife) && Convert.ToBoolean(iLoan.CreditLife); + loan.CreditDisability = !string.IsNullOrEmpty(iLoan.CreditDisability) && Convert.ToBoolean(iLoan.CreditDisability); + loan.Mbp = !string.IsNullOrEmpty(iLoan.Mbp) && Convert.ToBoolean(iLoan.Mbp); + loan.Term = (int)Math.Round(Convert.ToDecimal(iLoan.OriginalTermInDays) / (decimal)MonthInDays, 0); + + Vehicle vehicle = new Vehicle(); + vehicle.VINNumber = !string.IsNullOrEmpty(iLoan.VINNumber) ? iLoan.VINNumber : null; + vehicle.Make = iLoan.Make; + vehicle.Model = iLoan.Model; + vehicle.Year = iLoan.Year; + vehicle.Mileage = ValidateMileage(iLoan.Mileage); + vehicle.Color = iLoan.Color; + loan.Vehicle = vehicle; + result.Add(loan); + } + + return result; + } + + public List TransformLoanFromDNA(List importLoans, int iImportId) + { + var result = new List(); + if (!importLoans.Any()) + { + return result; + } + + foreach (var iLoan in importLoans) + { + Loan loan = new Loan + { + ImportId = iImportId, + MemberNumber = iLoan.MemberNumber, + LoanNumber = iLoan.LoanNumber, + MemberFirstName = iLoan.MemberFirstName?.ToUpper(), + MemberInitial = iLoan.MemberInitial?.ToUpper(), + MemberLastName = iLoan.MemberLastName?.ToUpper(), + DealerOrganizationNumber = (!String.IsNullOrWhiteSpace(iLoan.DealerOrganizationNumber)) ? iLoan.DealerOrganizationNumber : string.Empty, + LoanDate = iLoan.LoanDate, + FirstPmtDate = iLoan.FirstPmtDate, + FinalPmtDate = iLoan.FinalPmtDate, + LoanAmount = iLoan.LoanAmount, + BuyRate = (iLoan.BuyRate != 0) ? Math.Round(iLoan.BuyRate, 3, MidpointRounding.AwayFromZero) : 0, + ContractRate = Math.Round(iLoan.ContractRate, 3, MidpointRounding.AwayFromZero), + MonthlyPmt = iLoan.MonthlyPmt, + LoanTypeId = iLoan.LoanType, + ApplicationId = iLoan.ApplicationId, + Gap = iLoan.GAP, + Warranty = iLoan.Warranty, + CreditLife = iLoan.CreditLife, + CreditDisability = iLoan.CreditDisability, + Mbp = iLoan.Mbp, + AccountType = iLoan.AccountType, + PaidLoanDate = iLoan.PaidLoanDate, + Term = (int)Math.Round(Convert.ToDecimal(iLoan.OriginalTermInDays) / (decimal)MonthInDays, 0) + }; + + if (iLoan.PostedDate == DateTime.MinValue) + { + loan.PostedDate = iLoan.LoanDate; + } + else + { + loan.PostedDate = iLoan.PostedDate; + } + + loan.Vehicle = new Vehicle + { + VINNumber = !string.IsNullOrEmpty(iLoan.Vin) ? iLoan.Vin : null, + Make = iLoan.Make, + Model = iLoan.Model, + Year = iLoan.Year.ToString(), + Mileage = ValidateMileage(iLoan.Mileage.ToString()), + Color = iLoan.Color + }; + + loan.Dealer = new Dealer + { + DealerCode = (!String.IsNullOrWhiteSpace(iLoan.DealerCode)) && (iLoan.DealerCode.Length <= 4) ? iLoan.DealerCode : string.Empty, + Name = (!String.IsNullOrWhiteSpace(iLoan.DealerName)) ? iLoan.DealerName : string.Empty, + DealerAccountNumber = (!String.IsNullOrWhiteSpace(iLoan.DealerAccountNumber)) ? iLoan.DealerAccountNumber : string.Empty, + DealerOrganizationNumber = (!String.IsNullOrWhiteSpace(iLoan.DealerOrganizationNumber)) ? iLoan.DealerOrganizationNumber : string.Empty + }; + + result.Add(loan); + } + + return result; + } + + public List TransformBoatLoan(List importLoans) + { + var result = new List(); + if (!importLoans.Any()) + { + return result; + } + + foreach (var iLoan in importLoans) + { + Loan loan = new Loan + { + LoanNumber = iLoan.LoanNumber, + ReserveAmount = (!String.IsNullOrWhiteSpace(iLoan.IsChargeback) && iLoan.IsChargeback.ToUpper() == "FALSE") ? iLoan.Amount : 0, + ChargeBack = (!String.IsNullOrWhiteSpace(iLoan.IsChargeback) && iLoan.IsChargeback.ToUpper() == "TRUE") ? iLoan.Amount : 0, + Adjustment = (!String.IsNullOrWhiteSpace(iLoan.IsAdjustment) && iLoan.IsAdjustment.ToUpper() == "TRUE") ? iLoan.Amount : 0, + AccountType = iLoan.AccountType + }; + + loan.Dealer = new Dealer + { + DealerAccountNumber = iLoan.DealerAccountNumber + }; + + result.Add(loan); + } + + return result; + } + + private double ValidateMileage(string mileage) + { + double convertedMileage; + if (double.TryParse(mileage, out convertedMileage)) + { + return convertedMileage; + } + return convertedMileage; + } + + public IEnumerable TransformClosedLoans(List importLoans, int importId) + { + return importLoans.Select(x => new Loan + { + ImportId = importId, + LoanNumber = x.LoanNumber, + PaidLoanDate = x.PaidLoanDate + //will update LoanStatus when saving + }); + } + + private void SplitFirstMiddleName(string name) + { + //initial first name last name rules with first name character max length set in DB as 20, and middle initial max as 1 + + _middleInitial = String.Empty; + _firstName = name; + + string[] names = name.Trim().Split(" "); + if (names != null && names.Length > 1) //when true there is a middle initial + { + _firstName = _firstName.Substring(0, name.Length - names.Last().Length).Trim(); + _middleInitial = names.Last().Substring(0, 1); + + } + else + { + _firstName = name; //no middle initial + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/LoanCalculationHelper.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/LoanCalculationHelper.cs new file mode 100644 index 0000000..57f7b77 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Helpers/LoanCalculationHelper.cs @@ -0,0 +1,120 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Helpers +{ + public class LoanCalculationHelper + { + private decimal _daysInterest; + private LoanReserve loanReserve = new LoanReserve(); + + public LoanCalculationHelper() + { + + } + public LoanReserve CalculateBuyDown(CalculationConfig cc) + { + // Case - Buy down Below Threshold where difference in rates is less than or equal to 0.25 percent + if (cc.BuydownRateDifference() <= cc.NoReserveThreshold()) //we already know it's positive + { + loanReserve.ReserveAmount = 0; + loanReserve.ReserveTypeMessage = "No Reserve"; + loanReserve.FlatFeePaid = false; + return loanReserve; + } + + // Case - This is a regular Buy Down + _daysInterest = cc.BuydownRateDifference() / 100 * cc.DaysToFirstPmt() * cc.LoanData().LoanAmount / + 365 * (decimal)Math.Pow(1 + (double)cc.LoanData().ContractRate / 100, (double)cc.LoanData().Term / 12); + + loanReserve.ReserveAmount = -(cc.BuyFinanceCharge - cc.ContractFinanceCharge + _daysInterest); + loanReserve.ReserveTypeMessage = "Buy Down Reserve"; + loanReserve.FlatFeePaid = false; + return loanReserve; + } + + public LoanReserve CalculateNoMarkup(CalculationConfig cc) + { + // Case - Contract rate is equal to the Buy rate + _daysInterest = cc.StdRate() * cc.DaysToFirstPmt() * cc.LoanData().LoanAmount / + 365 * (decimal)Math.Pow(1 + (double)cc.LoanData().ContractRate / 100, (double)cc.LoanData().Term / 12); + + loanReserve.ReserveAmount = (cc.ContractFinanceCharge - cc.BuyFinanceCharge + _daysInterest); + loanReserve.ReserveTypeMessage = "No Reserve"; + loanReserve.FlatFeePaid = false; + return loanReserve; + } + + public LoanReserve CalculateStandardMarkup(CalculationConfig cc) + { + // Case - Contract rate is greater than Buy rate + _daysInterest = (1 - cc.ReserveDiscount()/100) * cc.StdRate() / 100 * cc.DaysToFirstPmt() * cc.LoanData().LoanAmount / + 365 * (decimal)Math.Pow(1 + (double)cc.LoanData().ContractRate / 100, (double)cc.LoanData().Term / 12); + + loanReserve.ReserveAmount = ((1 - cc.ReserveDiscount() / 100) * (cc.ContractFinanceCharge - cc.BuyFinanceCharge) + _daysInterest); + + if ((double)cc.StdRate() <= 2.5) + { + loanReserve.ReserveTypeMessage = "Standard Reserve"; + } + else + { + loanReserve.ReserveTypeMessage = "Standard Reserve - Exceeds Max Markup"; + } + return loanReserve; + } + + public LoanReserve AssignFlatFee(LoanReserve loanReserve, CalculationConfig cc) + { + loanReserve.FlatFeePaid = false; + if (loanReserve.ReserveAmount < (cc.FlatFeePercent() / 100 * cc._loanData.LoanAmount * (1 - cc.ReserveDiscount() / 100))) + { + loanReserve.ReserveAmount = (cc.FlatFeePercent() / 100 * cc._loanData.LoanAmount * (1 - cc.ReserveDiscount() / 100)); + loanReserve.ReserveTypeMessage = "Flat Fee Reserve"; + loanReserve.FlatFeePaid = true; + } + + return loanReserve; + } + + public decimal FinanceCharge(int term, decimal loanAmount, decimal rate) + { + if (rate == 0) { return 0; } + double paymentRate = (double)rate / (double)1200; + decimal rateFactor = (decimal)Math.Pow((1 + paymentRate), (double)term); + return (loanAmount * term * rate / 1200 * rateFactor) / (rateFactor - 1) - loanAmount; + } + + internal DateTime GetLastChargebackDate(DateTime loanDate, DateTime firstPmtDate, int numOfChargebackPayments, Int16? numOfChargebackDays) + { + // in rare cases that date from loan date is most recent, check table values and compare between loan date vs. first payment date + int result = DateTime.Compare(loanDate.AddDays((Double)numOfChargebackDays), firstPmtDate.AddMonths(numOfChargebackPayments - 1)); + if (result > 0) + { + return loanDate.AddDays((Double)numOfChargebackDays); //Loan Date is most recent + } + return firstPmtDate.AddMonths(numOfChargebackPayments - 1); + } + + internal decimal CalculateChargeback(decimal reserve, int term, int remainingMonths) + { + return reserve * MonthFactor(remainingMonths) / MonthFactor(term); + } + + private decimal MonthFactor(int numberOfMonths) + { + return numberOfMonths * (numberOfMonths + 1) / 2; + } + + public int GetRemainingMonths(Loan loanData) + { + if (loanData.PaidLoanDate >= loanData.FinalPmtDate) + return 0; + + var monthsOccurred = (loanData.PaidLoanDate.Value - loanData.FirstPmtDate).TotalDays / (365) * 12 + 1; + + var remainingMonths = (int)(loanData.Term - Math.Round((decimal)monthsOccurred, 0)); + return remainingMonths; + } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/HostedService/HostedImportExportActive.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/HostedService/HostedImportExportActive.cs new file mode 100644 index 0000000..52997af --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/HostedService/HostedImportExportActive.cs @@ -0,0 +1,72 @@ +using NCrontab; +using ReserveManagementSystemApi.Services; + +namespace ReserveManagementSystemApi.HostedService +{ + public class HostedImportExportActive : BackgroundService + { + private readonly IConfiguration _config; + private readonly IServiceScopeFactory _scopeFactory; + private readonly CrontabSchedule _schedule; + private DateTime _nextRun; + public const string DefaultCronPattern = "0 19 * * 0-6"; + + private CancellationToken _cancellationToken; + + public HostedImportExportActive(IConfiguration config, IServiceScopeFactory scopeFactory) + { + _config = config; + _scopeFactory = scopeFactory; + try + { + _schedule = CrontabSchedule.Parse(_config.GetSection("HostedServices").GetSection("ImportExportActiveCronSchedule").Value); + } + catch (Exception ex) + { + Log.Error(ex, "Failure parsing crontab in appsettings."); + _schedule = CrontabSchedule.Parse(DefaultCronPattern); + } + _nextRun = _schedule.GetNextOccurrence(DateTime.Now); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + Log.Information("Hosted ImportExportActive is starting."); + Log.Information($"The import export active job will run at {_nextRun:M/d/yyyy HH:mm:ss tt}"); + + _cancellationToken = cancellationToken; + try + { + await ImportExportScheduler(cancellationToken); + } + catch (TaskCanceledException ex) when (_cancellationToken.IsCancellationRequested) + { + Log.Information(ex, "Hosted ImportExportActive shut down normally."); + } + catch (Exception ex) + { + Log.Error(ex, "Error trying to startup main thread for Hosted ImportExportActive"); + } + + Log.Information("Hosted ImportExportActive is stopping."); + } + + private async Task ImportExportScheduler(CancellationToken cancellationToken) + { + do + { + if (DateTime.Now > _nextRun) + { + using (var scope = _scopeFactory.CreateScope()) + { + await scope.ServiceProvider.GetRequiredService().RunImportExportActive(_cancellationToken); + } + + _nextRun = _schedule.GetNextOccurrence(DateTime.Now); + } + + await Task.Delay(5000, cancellationToken); //5 seconds delay + } while (!cancellationToken.IsCancellationRequested); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/HostedService/HostedImportExportClosed.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/HostedService/HostedImportExportClosed.cs new file mode 100644 index 0000000..4a9594f --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/HostedService/HostedImportExportClosed.cs @@ -0,0 +1,72 @@ +using NCrontab; +using ReserveManagementSystemApi.Services; + +namespace ReserveManagementSystemApi.HostedService +{ + public class HostedImportExportClosed : BackgroundService + { + private readonly IConfiguration _config; + private readonly IServiceScopeFactory _scopeFactory; + private readonly CrontabSchedule _schedule; + private DateTime _nextRun; + public const string DefaultCronPattern = "15 19 * * 0-6"; + + private CancellationToken _cancellationToken; + + public HostedImportExportClosed(IConfiguration config, IServiceScopeFactory scopeFactory) + { + _config = config; + _scopeFactory = scopeFactory; + try + { + _schedule = CrontabSchedule.Parse(_config.GetSection("HostedServices").GetSection("ImportExportClosedCronSchedule").Value); + } + catch (Exception ex) + { + Log.Error(ex, "Failure parsing crontab in appsettings."); + _schedule = CrontabSchedule.Parse(DefaultCronPattern); + } + _nextRun = _schedule.GetNextOccurrence(DateTime.Now); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + Log.Information("Hosted ImportExportClosed is starting."); + Log.Information($"The import export closed job will run at {_nextRun:M/d/yyyy HH:mm:ss tt}"); + + _cancellationToken = cancellationToken; + try + { + await ImportExportScheduler(cancellationToken); + } + catch (TaskCanceledException ex) when (_cancellationToken.IsCancellationRequested) + { + Log.Information(ex, "Hosted ImportExportClosed shut down normally."); + } + catch (Exception ex) + { + Log.Error(ex, "Error trying to startup main thread for Hosted ImportExportClosed"); + } + + Log.Information("Hosted ImportExportClosed is stopping."); + } + + private async Task ImportExportScheduler(CancellationToken cancellationToken) + { + do + { + if (DateTime.Now > _nextRun) + { + using (var scope = _scopeFactory.CreateScope()) + { + await scope.ServiceProvider.GetRequiredService().RunImportExportClosed(_cancellationToken); + } + + _nextRun = _schedule.GetNextOccurrence(DateTime.Now); + } + + await Task.Delay(5000, cancellationToken); //5 seconds delay + } while (!cancellationToken.IsCancellationRequested); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Mappers/ExportMapper.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Mappers/ExportMapper.cs new file mode 100644 index 0000000..4cac352 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Mappers/ExportMapper.cs @@ -0,0 +1,180 @@ +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Mappers +{ + public static class ExportMapper + { + //used for Chargeback transactions + private const string GeneralLedgerReceipt = "GLR"; + private const string DealerInterestRebate = "DRIR"; + private const string ChargeBkDesc = "ChrgBk"; + private const string ChargeBkAdjDesc = "ChrgBk Adj"; + private const string GeneralLedgerCategory = "NOTE"; + private const string GeneralLedgerType = "BAL"; + + //used for Reserve transactions + private const string DealerExpense = "DLRE"; + private const string DealerIncome = "DLRI"; + private const string ReserveAdjDesc = "Adjustment"; + + //used for all transaction types + private const string UpfrontCategory = "UPFR"; + private const string DealerType = "DLR"; + private const string ElectronicFundType = "EL"; + private const string NoHoldClearCategory = "IMED"; + private const string FundTypeDetail = "INTR"; + + public static GroupedMonetaryTransactionRequest MapLoanToIndirectLoanForReserve(Loan loan) + { + return new GroupedMonetaryTransactionRequest + { + MonetaryTransactionRequests = BuildReserveTransactions(loan), + ParentRequestNumber = loan.LoanNumber + }; + } + + private static List BuildReserveTransactions(Loan loan) + { + var listTransaction = new List(); + var isAdjustment = loan.Adjustment != null && loan.Adjustment != 0; + + var reserveRounded = Math.Round(loan.ReserveAmount, 4, MidpointRounding.AwayFromZero); //this represents the amount saved to DB + var amount = isAdjustment ? Math.Round((decimal)loan.Adjustment, 2, MidpointRounding.AwayFromZero) : Math.Round(reserveRounded, 2, MidpointRounding.AwayFromZero); + var transactionDescription = isAdjustment ? ReserveAdjDesc : null; + + var loanAmount = (double)amount * -1; + listTransaction.Add(BuildParentTransaction(loanAmount, loan.LoanNumber, DealerExpense, transactionDescription)); + + var dealerAmount = (double)amount; + listTransaction.Add(BuildDealerTransaction(dealerAmount, loan.Dealer.DealerAccountNumber, DealerIncome, transactionDescription)); + + return listTransaction; + } + + public static GroupedMonetaryTransactionRequest MapLoanToIndirectLoanForChargeback(Loan loan, MinorAccountType accountType) + { + var request = new GroupedMonetaryTransactionRequest(); + if (accountType != null) { + request.MonetaryTransactionRequests = BuildChargebackTransactions(loan, accountType.GeneralLedgerAccountNumber); + request.ParentRequestNumber = loan.LoanNumber; + } + + return request; + } + + private static List BuildChargebackTransactions(Loan loan, string accountNumber) + { + var listTransaction = new List(); + _balanceTypeList = new List(); + _fundList = new List(); + _fundBalanceTypeList = new List(); + + var isAdjustment = loan.Adjustment != null && loan.Adjustment != 0; + + var chargeBackRounded = Math.Round((decimal)loan.ChargeBack, 4, MidpointRounding.AwayFromZero); //this represents the amount saved to DB + var amount = isAdjustment ? Math.Round((decimal)loan.Adjustment, 2, MidpointRounding.AwayFromZero) : Math.Round(chargeBackRounded, 2, MidpointRounding.AwayFromZero); + var transactionDescription = isAdjustment ? $"{ChargeBkAdjDesc}-{loan.LoanNumber}" : $"{ChargeBkDesc}-{loan.LoanNumber}"; + + var loanAmount = (double)amount; + listTransaction.Add(BuildParentTransaction(loanAmount, accountNumber, GeneralLedgerReceipt, transactionDescription)); + + var dealerAmount = (double)amount * -1; + listTransaction.Add(BuildDealerTransaction(dealerAmount, loan.Dealer.DealerAccountNumber, DealerInterestRebate, transactionDescription)); + + return listTransaction; + } + + private static MonetaryTransactionRequest BuildParentTransaction(double amount, string accountNumber, string transactionType, string transactionDescription) + { + //if a reserve the parent transaction contains the loan number, if chargeback the account is the ledger account for minor account type + _balanceTypeList = new List(); + _fundList = new List(); + _fundBalanceTypeList = new List(); + + _balanceTypeList.Add(BuildBalanceType(amount, transactionType)); + _fundList.Add(BuildFund(amount)); + _fundBalanceTypeList.Add(BuildFundBalanceType(amount)); + + var transaction = new MonetaryTransactionRequest() + { + AccountNumber = (long)System.Convert.ToUInt64(accountNumber), + TransactionAmount = (transactionType != GeneralLedgerReceipt) ? amount : (double?)null, + RequestNumber = "1", + ParentRequestNumber = null, + TransactionTypeCode = transactionType, + Description = transactionDescription, + BalanceTypes = _balanceTypeList, + Funds = _fundList, + FundBalanceTypes = (transactionType == GeneralLedgerReceipt) ? _fundBalanceTypeList : null, + }; + + return transaction; + } + + private static MonetaryTransactionRequest BuildDealerTransaction(double amount, string dealerAccount, string transactionType, string transactionDescription) + { + //for second request take the offset amount value of parent request amount + _balanceTypeList = new List(); + _fundList = new List(); + _fundBalanceTypeList = new List(); + + _balanceTypeList.Add(BuildBalanceType(amount, transactionType)); + _fundList.Add(BuildFund(amount)); + _fundBalanceTypeList.Add(BuildFundBalanceType(amount)); + + var transaction = new MonetaryTransactionRequest() + { + AccountNumber = (string.IsNullOrEmpty(dealerAccount) == true) ? 0 : (long)System.Convert.ToUInt64(dealerAccount), + TransactionAmount = amount, + RequestNumber = "2", + ParentRequestNumber = "1", + TransactionTypeCode = transactionType, + Description = transactionDescription, + BalanceTypes = _balanceTypeList, + Funds = _fundList, + }; + + return transaction; + } + + private static List _fundBalanceTypeList; + private static FundBalanceType BuildFundBalanceType(double amount) + { + return new FundBalanceType + { + FundBalanceAmount = amount, + FundTypeCode = ElectronicFundType, + FundTypeDetailCode = NoHoldClearCategory + }; + } + + private static List _balanceTypeList; + private static BalanceType1 BuildBalanceType(double amount, string transactionType) + { + return new BalanceType1 + { + BalanceTypeAmount = amount, + BalanceCategoryCode = (transactionType == GeneralLedgerReceipt) ? GeneralLedgerCategory : UpfrontCategory, + BalanceTypeCode = (transactionType == GeneralLedgerReceipt) ? GeneralLedgerType : DealerType, + }; + } + + private static List _fundList; + private static Fund BuildFund(double amount) + { + return new Fund + { + ClearingCategoryCode = NoHoldClearCategory, + FundAmount = amount, + FundTypeCode = ElectronicFundType, + FundTypeDetailCode = FundTypeDetail, + NumberOfItems = 1, + }; + } + + } +} + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Models/ImportBoatLoan.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Models/ImportBoatLoan.cs new file mode 100644 index 0000000..98ee5f9 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Models/ImportBoatLoan.cs @@ -0,0 +1,43 @@ +using CsvHelper.Configuration.Attributes; +using System; +using System.Text.Json.Serialization; + +namespace ReserveManagementSystemApi.Models +{ + public class ImportBoatLoan + { + + [Index(0)] + public string LoanNumber { get; set; } + + [JsonIgnore] + [Index(1)] + public string DealerAccountNumber { get; set; } + + [JsonIgnore] + [Index(2)] + public decimal Amount { get; set; } + + [JsonIgnore] + [Index(3)] + public string IsChargeback { get; set; } + + [JsonIgnore] + [Index(4)] + public string IsAdjustment { get; set; } + + [JsonIgnore] + [Index(5)] + [Optional] + public string AccountType { get; set; } + + [Index(6)] + [Optional] + public string ErrorMessage { get; set; } + + [Index(7)] + [Optional] + public bool? WasSuccessful { get; set; } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Models/ImportNewLoan.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Models/ImportNewLoan.cs new file mode 100644 index 0000000..7d0c380 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Models/ImportNewLoan.cs @@ -0,0 +1,113 @@ +using CsvHelper.Configuration.Attributes; +using System; + +namespace ReserveManagementSystemApi.Models +{ + public class ImportNewLoan + { + [Index(0)] + public string DealerOrganizationNumber { get; set; } + + [Index(1)] + public string MemberNumber { get; set; } + + [Index(2)] + public string LoanNumber { get; set; } + + [Index(3)] + public string LastName { get; set; } + + [Index(4)] + public string FirstAndMiddleName { get; set; } + + [Index(5)] + public DateTime LoanDate { get; set; } + + [Index(6)] + public DateTime PostedDate { get; set; } + + [Index(7)] + public DateTime FirstPmtDate { get; set; } + + [Index(8)] + public DateTime FinalPmtDate { get; set; } + + [Index(9)] + public decimal LoanAmount { get; set; } + + [Index(10)] + public decimal BuyRate { get; set; } + + [Index(11)] + public decimal ContractRate { get; set; } + + [Index(12)] + public decimal MonthlyPmt { get; set; } + + [Index(13)] + public string LoanType { get; set; } + + [Index(14)] + public string InsuranceIndicator { get; set; } + + [Index(15)] + public string NewMember { get; set; } + + [Index(16)] + [Optional] + public string OriginalTermInDays { get; set; } + + [Index(17)] + [Optional] + public string VINNumber { get; set; } + + [Index(18)] + [Optional] + public string Make { get; set; } + + [Index(19)] + [Optional] + public string Model { get; set; } + + [Index(20)] + [Optional] + public string Year { get; set; } + + [Index(21)] + [Optional] + public string Mileage { get; set; } + + [Index(22)] + [Optional] + public string Color { get; set; } + + [Index(23)] + [Optional] + public string ApplicationId { get; set; } + + [Index(24)] + [Optional] + public string Warranty { get; set; } + + [Index(25)] + [Optional] + public string Gap { get; set; } + + [Index(26)] + [Optional] + public string CreditLife { get; set; } + + [Index(27)] + [Optional] + public string CreditDisability { get; set; } + + [Index(28)] + [Optional] + public string Mbp { get; set; } + + [Index(29)] + [Optional] + public string AccountType { get; set; } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Program.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Program.cs new file mode 100644 index 0000000..2d641e1 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Program.cs @@ -0,0 +1,249 @@ +using ReserveManagementSystemApi.Services; +using ReserveManagementSystemApi.HostedService; +using ReserveManagementSystemApi.ServiceClients; +using ReserveManagementSystemApi.Configurations; +using ReserveManagementSystemLib.Configuration; +using ReserveManagementSystemLib.Persistence; +using ReserveManagementSystemLib.Repositories; +using MassTransit; +using Microsoft.OpenApi.Models; +using Microsoft.Extensions.FileProviders; +using Newtonsoft.Json; +using LCU.Library.Internal.LcuHttpClient; +using LCU.Library.Internal.ApiSecurity; +using LCU.Library.Internal.Logging.Models; +using LCU.Library.Internal.Logging; +using LCU.Library.Internal.Logging.Middleware; +using LCU.Library.Internal.Configuration; +using LCU.Library.Internal.LcuMassTransit; +using LCU.Library.Internal.LcuMassTransitModels; +using ReserveManagementSystemApi.Consumers; +using ReserveManagementSystemApi.Services.QueueServices; +using LCU.Library.Internal.Status; + +namespace ReserveManagementSystemApi +{ + public class Program + { + public static string MyAllowSpecificOrigins { get; private set; } + + static void Main(string[] args) + { + MyAllowSpecificOrigins = "_myAllowSpecificOrigins"; + WebApplicationBuilder builder = BuildBuilder(args); + BuildServicesAndConfigurations(builder); + BuildAppAndRun(builder); + } + + public static WebApplicationBuilder BuildBuilder(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.Configuration.AddJsonFile("/app/config/environment-appsettings/appsettings.json", true, true); + builder.Configuration.AddJsonFile("/app/config/workload-appsettings/appsettings.json", true, true); + builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-seq-api-key.json", true, true); + builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-rms-database-connection.json", true, true); + builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-connection-configuration-queue-username.json", true, true); + builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-connection-configuration-queue-password.json", true, true); + builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-api-security-configuration-internal-api-key-set.json", true, true); + builder.Host.UseSerilog(); + return builder; + } + + public static void BuildServicesAndConfigurations(WebApplicationBuilder builder) + { + var services = builder.Services; + var configuration = builder.Configuration; + + services.AddControllers(); + services.AddEndpointsApiExplorer(); + services.AddLCUHttpServices(); + + var apiConfiguration = configuration.Bind(services, "Primary"); + + services.AddCors(options => + { + options.AddPolicy(MyAllowSpecificOrigins, + builder => + { + builder.WithOrigins(apiConfiguration.CorsAllowedOrigins).AllowAnyHeader() + .AllowAnyMethod(); + }); + }); + + services.AddDbContext(options => + { + options.UseSqlServer(configuration.GetSection("SqlConnectionConfiguration").GetSection("ConnectionString").Value); + }); + + var connectionConfiguration = MassTransitConfigurationSetUp.AddConnectionConfiguration(ref services, configuration); + Log.Debug(JsonConvert.SerializeObject(connectionConfiguration)); + + var receiveEndpointConfigurations = new ReceiveEndpointConfigurations() + { + RmsActiveLoanReceiveEndpoint = MassTransitConfigurationSetUp.GetReceiveEndpointConfiguration(configuration, "RmsActiveLoanReceiveEndpoint") + }; + + services.AddSingleton(receiveEndpointConfigurations); + Log.Debug(JsonConvert.SerializeObject(receiveEndpointConfigurations)); + + AddMassTransitServices(ref services, connectionConfiguration, receiveEndpointConfigurations, apiConfiguration); + AddQueueServices(services); + + var loggerConfiguration = new LoggingConfiguration(apiConfiguration.AppName, + apiConfiguration.SeqApiKey, + apiConfiguration.SeqUrl); + var lcuLogging = new LcuLogging(loggerConfiguration); + lcuLogging.UseLcuLogging(); + + var rmsApiConfiguration = configuration.Bind(services, "RmsApiConfiguration"); + var coreApiConfiguration = configuration.Bind(services, "CoreApiConfiguration"); + + services.AddApiKeySecurity(configuration); + services.AddLCUStatusServices(); + + //Services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + //Service Clients + services.AddScoped(); + + //Repositories + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + //HostedServices + if (apiConfiguration.FeatureFlags.EnableImportExportActive) + { + services.AddHostedService(); + } + + if (apiConfiguration.FeatureFlags.EnableImportExportClosed) + { + services.AddHostedService(); + } + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = $"{apiConfiguration.AppName}", + Version = "v1", + Contact = new OpenApiContact { Name = "SpecFlow Tests", Url = new Uri("/Specs/index.html", UriKind.Relative) } + }); + }); + } + + public static void BuildAppAndRun(WebApplicationBuilder builder) + { + var app = builder.Build(); + + app.UseSwagger(c => + { + c.PreSerializeFilters.Add((swaggerDoc, request) => + { + const string prefixHeader = "X-Forwarded-Prefix"; + if (!request.Headers.ContainsKey(prefixHeader)) return; + + var serverUrl = request.Headers[prefixHeader]; + swaggerDoc.Servers = new List() + { + new() { Description = serverUrl, Url = serverUrl } + }; + }); + }); + app.UseSwaggerUI(c => + { + c.RoutePrefix = "swagger"; + c.SwaggerEndpoint("v1/swagger.json", "ReserveManagementSystemApi v1"); + }); + + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + var specsFolder = Path.Combine(Directory.GetCurrentDirectory(), "Specs"); + + if (Directory.Exists(specsFolder)) + { + app.UseFileServer(new FileServerOptions() + { + FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "Specs")), + RequestPath = new PathString("/Specs"), + EnableDefaultFiles = true + }); + } + + app.UseMiddleware(); + app.UseSerilogRequestLogging(options => + { + options.EnrichDiagnosticContext = RequestResponseLoggingMiddleware.RequestDataParser.EnrichFromRequest; + }); + + app.UseAuthentication(); + app.UseHttpsRedirection(); + app.UseRouting(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + app.Run(); + } + + public static void AddMassTransitServices(ref IServiceCollection services, + ConnectionConfiguration connectionConfiguration, + ReceiveEndpointConfigurations receiveEndpointConfigurations, + ApiConfiguration apiConfiguration) + { + services.AddMassTransit(x => + { + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.Host(connectionConfiguration.HostUrl, "/", host => + { + host.Username(connectionConfiguration.UserName); + host.Password(connectionConfiguration.Password); + }); + cfg.UseTimeout(t => t.Timeout = TimeSpan.FromSeconds(420)); + + if (apiConfiguration.FeatureFlags.RunRmsActiveLoanConsumer) + { + cfg.ReceiveEndpoint(receiveEndpointConfigurations.RmsActiveLoanReceiveEndpoint.ReceiveEndpoint, e => + { + e.ConfigureConsumer(context); + e.SetQuorumQueue(); + e.PrefetchCount = receiveEndpointConfigurations.RmsActiveLoanReceiveEndpoint.PrefetchLimit; + e.UseTimeout(t => t.Timeout = TimeSpan.FromSeconds(420)); + }); + } + }); + }); + services.AddMassTransitHostedService(); + } + + public static void AddQueueServices(IServiceCollection services) + { + services.AddScoped(); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ReserveManagementSystemApi.csproj b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ReserveManagementSystemApi.csproj new file mode 100644 index 0000000..65ec89a --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ReserveManagementSystemApi.csproj @@ -0,0 +1,45 @@ + + + + net6.0 + 1.1.1 + false + + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ReserveManagementSystemApi.sln b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ReserveManagementSystemApi.sln new file mode 100644 index 0000000..f7edbde --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ReserveManagementSystemApi.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31025.194 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReserveManagementSystemApi", "ReserveManagementSystemApi.csproj", "{954C8B67-1471-4FB2-81BD-713BEE5E679E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReserveManagementSystemLib", "..\ReserveManagementSystemLib\ReserveManagementSystemLib.csproj", "{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}" +EndProject +Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "ReserveManagementSystemApi.Sql", "..\ReserveManagementSystemApi.Sql\ReserveManagementSystemApi.Sql.sqlproj", "{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReserveManagementSystemApi.Tests", "..\ReserveManagementSystemApi.Tests\ReserveManagementSystemApi.Tests.csproj", "{CFE504B7-58D7-444B-A4B8-8A219400BA42}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveManagementSystemApi.Test", "..\ReserveManagementSystemApi.Test\ReserveManagementSystemApi.Test.csproj", "{5BC5FBBC-2B72-4055-95E0-41192F581784}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {954C8B67-1471-4FB2-81BD-713BEE5E679E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {954C8B67-1471-4FB2-81BD-713BEE5E679E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {954C8B67-1471-4FB2-81BD-713BEE5E679E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {954C8B67-1471-4FB2-81BD-713BEE5E679E}.Release|Any CPU.Build.0 = Release|Any CPU + {BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Release|Any CPU.Build.0 = Release|Any CPU + {5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Release|Any CPU.Build.0 = Release|Any CPU + {5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Release|Any CPU.Deploy.0 = Release|Any CPU + {CFE504B7-58D7-444B-A4B8-8A219400BA42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CFE504B7-58D7-444B-A4B8-8A219400BA42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CFE504B7-58D7-444B-A4B8-8A219400BA42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CFE504B7-58D7-444B-A4B8-8A219400BA42}.Release|Any CPU.Build.0 = Release|Any CPU + {5BC5FBBC-2B72-4055-95E0-41192F581784}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BC5FBBC-2B72-4055-95E0-41192F581784}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BC5FBBC-2B72-4055-95E0-41192F581784}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BC5FBBC-2B72-4055-95E0-41192F581784}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {27D5180A-D1E5-4786-9F4D-99C4652488F4} + EndGlobalSection +EndGlobal diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ServiceClients/CoreApiServiceClient.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ServiceClients/CoreApiServiceClient.cs new file mode 100644 index 0000000..33d6dd8 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ServiceClients/CoreApiServiceClient.cs @@ -0,0 +1,120 @@ +using ReserveManagementSystemLib.Configuration; +using LCU.Library.Internal.ApiSecurity.Models; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction; +using LCU.Library.Internal.LcuHttpClient.HttpRequest; +using LCU.Library.Internal.ApiSecurity.Enums; +using LCU.Library.Internal.LcuHttpClient.Utilities; + +namespace ReserveManagementSystemApi.ServiceClients +{ + public class CoreApiServiceClient : ICoreApiServiceClient + { + private readonly ApiSecurityConfiguration _apiSecurityConfiguration; + private readonly CoreApiConfiguration _coreApiWrapperConfiguration; + private readonly IPostRequest _postRequest; + private readonly IGetRequest _getRequest; + private readonly IPutRequest _putRequest; + + private const string ClientKeyValue = "ReserveManagementSystem"; + + public CoreApiServiceClient( + ApiSecurityConfiguration apiSecurityConfiguration, + CoreApiConfiguration coreApiWrapperConfiguration, + IGetRequest getRequest, + IPostRequest postRequest, + IPutRequest putRequest + ) + { + _apiSecurityConfiguration = apiSecurityConfiguration; + _coreApiWrapperConfiguration = coreApiWrapperConfiguration; + _getRequest = getRequest; + _postRequest = postRequest; + _putRequest = putRequest; + } + + public async Task> PostMonetaryTransactions(List loanReserves) + { + try + { + var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.PostMonetaryTransactions}"; + var uri = new Uri(path); + var response = await _postRequest.Async, List>(uri, loanReserves, + HttpUtilities.GetApiKeyHeaders(ClientKeyValue, _apiSecurityConfiguration.ExtractApiKey(ClientKeyValue, ApiKeySetName.InternalKeySet)), + 0, Convert.ToInt16(_coreApiWrapperConfiguration.ApiTimeOutInSeconds)); + return response; + } + catch (Exception ex) + { + Log.Error(ex, "Error in posting MonetaryTransactions."); + throw; + } + } + + public async Task> PutDealerOriginalBalance(string accountNumber) + { + try + { + var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.PutDealerOriginalBalance}"; + var uri = new Uri(path); + var response = await _putRequest.Async>(uri, accountNumber, + HttpUtilities.GetApiKeyHeaders(ClientKeyValue, _apiSecurityConfiguration.ExtractApiKey(ClientKeyValue, ApiKeySetName.InternalKeySet)), + 0, Convert.ToUInt16(_coreApiWrapperConfiguration.ApiTimeOutInSeconds)); + return response; + } + catch (Exception ex) + { + Log.Error(ex, "Error updating Dealer Original Balance"); + throw; + } + } + + public async Task> GetIndirectLoansByPostedDate(string afterPostDate) + { + try + { + var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.GetIndirectLoansByPostedDate}{afterPostDate}"; + var response = await _getRequest.AsyncWithNewton>(path, null, 0, Convert.ToUInt16(_coreApiWrapperConfiguration.ApiTimeOutInSeconds)); + return response; + } + catch (Exception ex) + { + Log.Error(ex, "Error getting Indirect Loans By Posted Date"); + return null; + } + } + + public async Task> GetClosedLoansByPostedDate(string afterPostDate) + { + try + { + var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.GetClosedLoansByPostedDate}{afterPostDate}"; + var response = await _getRequest.AsyncWithNewton>(path); + return response; + } + catch (Exception ex) + { + Log.Error(ex, "Error getting Closed Loans By Posted Date"); + } + + return null; + } + + public async Task> GetClosedLoansWithDetailByClosedDate(string afterClosedDate) + { + try + { + var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.GetClosedIndirectLoansWithDetailByPostedDate}{afterClosedDate}"; + var response = await _getRequest.AsyncWithNewton>(path); + return response; + } + catch (Exception ex) + { + Log.Error(ex, "Error getting Closed Loans With Detail"); + } + + return null; + } + } +} \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ServiceClients/ICoreApiServiceClient.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ServiceClients/ICoreApiServiceClient.cs new file mode 100644 index 0000000..776b160 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/ServiceClients/ICoreApiServiceClient.cs @@ -0,0 +1,19 @@ +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests; + +namespace ReserveManagementSystemApi.ServiceClients +{ + public interface ICoreApiServiceClient + { + Task> PostMonetaryTransactions(List loans); + + Task> GetIndirectLoansByPostedDate(string afterPostDate); + + Task> GetClosedLoansByPostedDate(string afterPostDate); + + Task> GetClosedLoansWithDetailByClosedDate(string afterClosedDate); + + Task> PutDealerOriginalBalance(string accountNumber); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/AuditHistoryService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/AuditHistoryService.cs new file mode 100644 index 0000000..54bc17e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/AuditHistoryService.cs @@ -0,0 +1,146 @@ +using Newtonsoft.Json; +using ReserveManagementSystemApi.Enums; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Repositories; + +namespace ReserveManagementSystemApi.Services +{ + public class AuditHistoryService : IAuditHistoryService + { + private readonly IAuditHistoryRepository _auditHistoryRepository; + const string SYSTEM_MODIFIED_BY = "RMS"; + + public AuditHistoryService(IAuditHistoryRepository auditHistoryRepository) + { + _auditHistoryRepository = auditHistoryRepository; + } + + public async Task> GetAllAsync() + { + try + { + return await _auditHistoryRepository.GetAll().OrderByDescending(o => o.ModifiedDate).ToListAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving audit histories."); + throw; + } + } + + public async Task FindByIdAsync(int id) + { + AuditHistory history; + try + { + history = await _auditHistoryRepository.FindByIdAsync(id); + } + catch (Exception ex) + { + Log.Error(ex, $"Failure retrieving history with id {id}."); + throw; + } + + if (history == null) + { + throw new Exception($"Audit history with id {id} not found."); + } + + return history; + } + + public async Task AddHistoryAsync(AuditHistory history) + { + return await _auditHistoryRepository.AddHistoryAsync(history); + } + + public async Task AddAuditHistoriesForNonexistingLoansAsync(List loans) + { + var currentDate = DateTime.Now; + var histories = loans.Select(loan => new AuditHistory + { + LoanId = null, + MemberNumber = loan.MemberNumber, + LoanNumber = loan.LoanNumber, + ModifiedBy = SYSTEM_MODIFIED_BY, + ModifiedDate = currentDate, + ActivityType = ActivityType.Error.ToString(), + AuditDescription = "Loan in closed file not found", + OldValue = null, + NewValue = null + }).ToList(); + + return await _auditHistoryRepository.AddHistoriesAsync(histories); + } + + public AuditHistory GetAuditHistoryForNonexistingClosedLoan(Loan loan) + { + var currentDate = DateTime.Now; + return new AuditHistory + { + LoanId = null, + MemberNumber = loan.MemberNumber, + LoanNumber = loan.LoanNumber, + ModifiedBy = SYSTEM_MODIFIED_BY, + ModifiedDate = currentDate, + ActivityType = ActivityType.Error.ToString(), + AuditDescription = "Loan in closed import not found", + OldValue = null, + NewValue = null + }; + } + + public AuditHistory GetExistingLoanUpdateHistoryAsync(Loan loan, Loan originalLoan) + { + var currentDate = DateTime.Now; + return new AuditHistory + { + LoanId = loan.LoanId, + MemberNumber = loan.MemberNumber, + LoanNumber = loan.LoanNumber, + ModifiedBy = SYSTEM_MODIFIED_BY, + ModifiedDate = currentDate, + ActivityType = ActivityType.NewLoanUpdate.ToString(), + AuditDescription = "Loan found in table, record updated", + OldValue = JsonConvert.SerializeObject(originalLoan), + NewValue = JsonConvert.SerializeObject(loan) + }; + } + + public AuditHistory GetLoanStatusChangeHistory(Loan loan, string oldStatus, string description) + { + return new AuditHistory + { + LoanId = loan.LoanId, + MemberNumber = loan.MemberNumber, + LoanNumber = loan.LoanNumber, + ModifiedBy = SYSTEM_MODIFIED_BY, + ModifiedDate = DateTime.Now, + ActivityType = ActivityType.LoanStatusChange.ToString(), + OldValue = oldStatus, + NewValue = loan.LoanStatus, + AuditDescription = description + }; + } + + public async Task AddHistoriesAsync(List histories) + { + return await _auditHistoryRepository.AddHistoriesAsync(histories); + } + + public async Task AddErrorHistoryAsync(string memberNumber, string loanNumber, string description) + { + var auditHist = new AuditHistory + { + MemberNumber = memberNumber, + LoanNumber = loanNumber, + ActivityType = ActivityType.Error.ToString(), + ModifiedDate = DateTime.Now, + AuditDescription = description, + ModifiedBy = SYSTEM_MODIFIED_BY + }; + + await _auditHistoryRepository.AddHistoryAsync(auditHist); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/DealerService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/DealerService.cs new file mode 100644 index 0000000..9dfc6a8 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/DealerService.cs @@ -0,0 +1,100 @@ +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Repositories; + +namespace ReserveManagementSystemApi.Services +{ + public class DealerService: IDealerService + { + private readonly IDealerRepository _dealerRepository; + + public DealerService(IDealerRepository dealerRepository) + { + _dealerRepository = dealerRepository; + } + + public async Task> Search(string dealerSearchText) + { + try + { + return await _dealerRepository.GetAll().Where(d => d.DealerCode.Contains(dealerSearchText) || d.Name.Contains(dealerSearchText)).OrderBy(x => x.DealerCode).ToListAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving dealers."); + throw; + } + } + + public async Task GetDealerByCodeAsync(string dealerCode) + { + try + { + return await _dealerRepository.FindByCodeAsync(dealerCode); + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving dealer by code."); + throw; + } + } + + public async Task> GetAllDealersAsync() + { + return await _dealerRepository.GetAll().ToListAsync(); + } + + public async Task UpdateDealersAsync(List dealers) + { + try + { + var allDealers = await GetAllDealersAsync(); + var dealersToAdd = new List(); + var dealersToUpdate = new List(); + dealers.ForEach(d => + { + var currentDealer = allDealers.FirstOrDefault(a => a.DealerOrganizationNumber == d.DealerOrganizationNumber); + if (currentDealer != null && !currentDealer.Equals(d)) + { + currentDealer.Name = d.Name ?? currentDealer.Name; + currentDealer.DealerCode = d.DealerCode?.ToUpper() ?? currentDealer.DealerCode; + currentDealer.DealerAccountNumber = d.DealerAccountNumber ?? currentDealer.DealerAccountNumber; + currentDealer.DealerOrganizationNumber = d.DealerOrganizationNumber ?? currentDealer.DealerOrganizationNumber; + dealersToUpdate.Add(currentDealer); + } + else if (currentDealer == null) + { + //write the new dealer record to set Action required to true if invalid + dealersToAdd.Add(d); + } + }); + if (dealersToUpdate.Any()) + { + await _dealerRepository.UpdateDealersAsync(dealersToUpdate); + } + if (dealersToAdd.Any()) + { + await _dealerRepository.AddDealersAsync(dealersToAdd); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failure updating dealers."); + throw; + } + } + + public async Task UpdateDealersAsync(List loans) + { + var dealers = loans.Select(l => new Dealer + { + DealerCode = l.Dealer.DealerCode, + Name = l.Dealer.Name, + DealerOrganizationNumber = l.DealerOrganizationNumber, + DealerAccountNumber = l.Dealer.DealerAccountNumber + }) + .GroupBy(d => d.DealerOrganizationNumber).Select(g => g.First()).ToList(); + + await UpdateDealersAsync(dealers); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ExportService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ExportService.cs new file mode 100644 index 0000000..3e92e23 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ExportService.cs @@ -0,0 +1,275 @@ +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction; +using ReserveManagementSystemApi.Mappers; +using ReserveManagementSystemApi.Helpers; +using ReserveManagementSystemApi.Models; +using ReserveManagementSystemApi.ServiceClients; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Repositories; +using ReserveManagementSystemLib.Configuration; + +namespace ReserveManagementSystemApi.Services +{ + public class ExportService: IExportService + { + private readonly ILoanRepository _loanRepository; + private readonly ICoreApiServiceClient _coreApiServiceClient; + private readonly IMinorAccountTypeRepository _minorAccountTypeRepository; + private readonly RmsApiConfiguration _rmsApiConfig; + + public ExportService(ILoanRepository loanRepository, ICoreApiServiceClient coreApiServiceClient, IMinorAccountTypeRepository minorAccountTypeRepository, RmsApiConfiguration rmsApiConfig) + { + _loanRepository = loanRepository; + _coreApiServiceClient = coreApiServiceClient; + _minorAccountTypeRepository = minorAccountTypeRepository; + _rmsApiConfig = rmsApiConfig; + } + + public async Task ExportReservesAsync(DateTime currentDate) + { + var numberOfLoansUpdated = 0; + try + { + var loansToUpdate = await _loanRepository.GetAll().Where(l => l.ExportedReserveDate == null).ToListAsync(); + var groupedLoanRequests = loansToUpdate.Select(ExportMapper.MapLoanToIndirectLoanForReserve).ToList(); + var exportResult = await SendBatchListMonetaryTransactionRequests(groupedLoanRequests); + + if (exportResult != null || exportResult.Any()) + { + var loansExported = new List(); + + loansToUpdate.ForEach(loan => + { + var validLoan = exportResult.Where(e => e.AccountNumber.ToString() == loan.LoanNumber).FirstOrDefault(); + if (validLoan != null && validLoan.WasSuccessful) + { + loan.ExportedReserveDate = currentDate; + numberOfLoansUpdated += 1; + loan.Adjustment = 0; + loansExported.Add(loan); //update successful loan export with exported date + } + else + { + Log.Error($"Failure to export loan reserve account number: {loan.LoanNumber}"); + } + }); + + await _loanRepository.UpdateAsync(loansExported); + Log.Information($"Exported {numberOfLoansUpdated} reserves"); + } + } + + catch (Exception ex) + { + Log.Error(ex, "Failure exporting loan reserves."); + throw; + } + + return numberOfLoansUpdated; + } + + public async Task ExportChargebacksAsync(DateTime currentDate) + { + var numberOfLoansUpdated = 0; + try + { + var loansToUpdate = await _loanRepository.GetAll().Where(l => l.ExportedChargebackDate == null && l.PaidLoanDate != null).ToListAsync(); + var groupedLoanRequests = new List(); + loansToUpdate.ForEach(loan => + { + var accountType = GetMinorAccountType(loan.AccountType); + groupedLoanRequests.Add(ExportMapper.MapLoanToIndirectLoanForChargeback(loan, accountType)); + } + ); + + var exportResult = await SendBatchListMonetaryTransactionRequests(groupedLoanRequests); + + if (exportResult != null || exportResult.Any()) + { + var loansExported = new List(); + loansToUpdate.ForEach(loan => + { + var accountType = GetMinorAccountType(loan.AccountType); + var validLoan = exportResult.Where(e => e.AccountNumber.ToString() == accountType.GeneralLedgerAccountNumber).FirstOrDefault(); + if (validLoan != null && validLoan.WasSuccessful) + { + loan.ExportedChargebackDate = currentDate; + numberOfLoansUpdated += 1; + loan.Adjustment = 0; + loansExported.Add(loan); //update successful loan export with exported date + } + else + { + Log.Error($"Failure to export loan reserve account number: {loan.LoanNumber}"); + } + }); + + await _loanRepository.UpdateAsync(loansExported); + Log.Information($"Exported {numberOfLoansUpdated} chargebacks"); + } + else + { + Log.Error("Failure exporting loan chargebacks."); + } + } + + catch (Exception ex) + { + Log.Error(ex, "Failure exporting loan chargebacks."); + throw; + } + + return numberOfLoansUpdated; + } + + public async Task> ExportBoatLoansAsync(List loans) + { + var loansExported = new List(); + try + { + var reserveLoans = loans.Where(l => l.ReserveAmount != 0); + if (reserveLoans != null || reserveLoans.Any()) + { + var groupedReserveRequests = reserveLoans.Select(ExportMapper.MapLoanToIndirectLoanForReserve).ToList(); + var exportReserveResult = await SendBatchListMonetaryTransactionRequests(groupedReserveRequests); + loansExported.AddRange(MapBoatExportResults(exportReserveResult, reserveLoans.ToList())); + } + + var chargebackLoans = loans.Where(l => l.ChargeBack != 0).ToList(); + if (chargebackLoans != null || chargebackLoans.Any()) + { + var groupedLoanRequests = new List(); + chargebackLoans.ForEach(loan => + { + var accountType = GetMinorAccountType(loan.AccountType); + groupedLoanRequests.Add(ExportMapper.MapLoanToIndirectLoanForChargeback(loan, accountType)); + } + ); + + var exportResult = await _coreApiServiceClient.PostMonetaryTransactions(groupedLoanRequests); + loansExported.AddRange(MapBoatExportResults(exportResult, chargebackLoans.ToList())); + } + } + + catch (Exception ex) + { + Log.Error(ex, "Failure exporting boat loans."); + throw; + } + + return loansExported; + } + + public async Task MarkReservesAsExportedAsync(List loans, DateTime currentDate) + { + try + { + loans.ForEach(l => + { + l.ExportedReserveDate = currentDate; + }); + + await _loanRepository.UpdateAsync(loans); + Log.Information($"Marked {loans.Count} loan reserves as exported (without exporting)"); + } + catch (Exception ex) + { + Log.Error(ex, "Failure marking loan reserves as exported."); + throw; + } + + return loans.Count; + } + + public async Task MarkReservesAndChargebacksAsExportedAsync(List loans, DateTime currentDate) + { + try + { + loans.ForEach(l => + { + l.ExportedReserveDate = currentDate; + l.ExportedChargebackDate = currentDate; + }); + + await _loanRepository.UpdateAsync(loans); + Log.Information($"Marked {loans.Count} loan reserves and chargebacks as exported (without exporting)"); + } + catch (Exception ex) + { + Log.Error(ex, "Failure marking loan reserves and chargebacks as exported."); + throw; + } + + return loans.Count; + } + + private List MapBoatExportResults(List exportResult, List loansToUpdate) + { + var numberOfLoansUpdated = 0; + var loansExported = new List(); + + if (exportResult != null || exportResult.Any()) + { + loansToUpdate.ForEach(loan => + { + var boatLoan = new ImportBoatLoan(); + string acccountNumber; + if (loan.ReserveAmount != 0) + { + acccountNumber = loan.LoanNumber; + } + else + { + var accountType = GetMinorAccountType(loan.AccountType); + acccountNumber = accountType.GeneralLedgerAccountNumber; + } + + var validLoan = exportResult.Where(e => e.AccountNumber.ToString() == acccountNumber).Any(); + var validDealer = exportResult.Where(e => e.AccountNumber.ToString() == loan.Dealer.DealerAccountNumber).Any(); + + if (validLoan && validDealer) + { + numberOfLoansUpdated += 1; + boatLoan.LoanNumber = loan.LoanNumber; + boatLoan.WasSuccessful = true; + } + else + { + var error = $"Failure to export boat loan account number: {loan.LoanNumber}"; + boatLoan.LoanNumber = loan.LoanNumber; + boatLoan.WasSuccessful = false; + boatLoan.ErrorMessage = error; + Log.Error(error); + } + + loansExported.Add(boatLoan); + }); + + Log.Information($"Exported {numberOfLoansUpdated} boat loans"); + } + + return loansExported; + } + + private MinorAccountType GetMinorAccountType(string loanAccountType) + { + var result = _minorAccountTypeRepository.GetMinorAccountType(loanAccountType); + return result; + } + + public async Task> SendBatchListMonetaryTransactionRequests(List groupedLoanRequests) + { + var result = new List(); + var batchSize = _rmsApiConfig.DefaultBatchSize; + + foreach (var batch in BatchHelper.CreateBatch(groupedLoanRequests, batchSize)) + { + var response = await _coreApiServiceClient.PostMonetaryTransactions(batch.ToList()); + result.AddRange(response); + }; + + return result; + } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IAuditHistoryService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IAuditHistoryService.cs new file mode 100644 index 0000000..00c8a55 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IAuditHistoryService.cs @@ -0,0 +1,25 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public interface IAuditHistoryService + { + Task> GetAllAsync(); + + Task FindByIdAsync(int id); + + Task AddHistoryAsync(AuditHistory history); + + Task AddAuditHistoriesForNonexistingLoansAsync(List loans); + + AuditHistory GetExistingLoanUpdateHistoryAsync(Loan loan, Loan originalLoan); + + AuditHistory GetAuditHistoryForNonexistingClosedLoan(Loan loan); + + AuditHistory GetLoanStatusChangeHistory(Loan loan, string oldStatus, string description); + + Task AddHistoriesAsync(List histories); + + Task AddErrorHistoryAsync(string memberNumber, string loanNumber, string description); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IDealerService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IDealerService.cs new file mode 100644 index 0000000..9b748af --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IDealerService.cs @@ -0,0 +1,17 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public interface IDealerService + { + Task> Search(string dealerSearch); + + Task GetDealerByCodeAsync(string dealerCode); + + Task UpdateDealersAsync(List dealers); + + Task UpdateDealersAsync(List loans); + + Task> GetAllDealersAsync(); + } +} \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IExportService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IExportService.cs new file mode 100644 index 0000000..d38481d --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IExportService.cs @@ -0,0 +1,18 @@ +using ReserveManagementSystemApi.Models; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public interface IExportService + { + Task ExportReservesAsync(DateTime currentDate); + + Task ExportChargebacksAsync(DateTime currentDate); + + Task> ExportBoatLoansAsync(List loans); + + Task MarkReservesAsExportedAsync(List loans, DateTime currentDate); + + Task MarkReservesAndChargebacksAsExportedAsync(List loans, DateTime currentDate); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IImportReadService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IImportReadService.cs new file mode 100644 index 0000000..d9ed519 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IImportReadService.cs @@ -0,0 +1,28 @@ +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan; +using ReserveManagementSystemApi.Models; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public interface IImportReadService + { + Task ImportActiveLoansFromDNA(string newerThanDateTime, bool skipExport = false); + + Task ImportClosedLoansFromDNA(string newerThanDateTime); + + Task ImportClosedLoansWithUpdatesFromDNA(string newerThanDateTime, bool skipExport); + + Task ProcessActiveLoanImport(List loanDetails, DateTime currentTime, bool skipExport); + + Task ProcessClosedLoanImport(List closedLoans, DateTime currentTime, string newerThanDateTime); + + Task ProcessClosedLoansWithUpdates(List dnaLoans, DateTime currentTime, bool skipExport); + + Task ReadNewLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime); + + Task> ReadBoatLoanImportFile(StreamReader importFile); + + Task ReadClosedLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime); + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IImportService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IImportService.cs new file mode 100644 index 0000000..74b290f --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IImportService.cs @@ -0,0 +1,20 @@ +using ReserveManagementSystemLib.Enums; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public interface IImportService + { + Task> GetAllAsync(); + + Task> GetRecentAsync(int numberOfImports); + + Task GetByIdAsync(int id); + + Task AddImportAsync(Import import); + + Task UpdateImportAsync(Import import); + + Task GetLastLoanImportByType(ImportType importType); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ILoanCalculationService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ILoanCalculationService.cs new file mode 100644 index 0000000..d491920 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ILoanCalculationService.cs @@ -0,0 +1,13 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public interface ILoanCalculationService + { + LoanReserve CalculateSingleReserve(Loan loan); + + LoanChargeback CalculateSingleChargeback(Loan loan); + + Loan CalculateLoanReserve(Loan loan, decimal? originalReserve = null, DateTime? originalExportedReserveDate = null); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ILoanService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ILoanService.cs new file mode 100644 index 0000000..ebbdec5 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ILoanService.cs @@ -0,0 +1,31 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public interface ILoanService + { + Task> GetAllAsync(); + + Task GetFilteredAsync(LoanFilter filter); + + Task GetByIdAsync(int id); + + Task GetLoanReserveAsync(LoanReserveInput input); + + Task GetLoanChargebackAsync(LoanChargebackInput input); + + Task> SaveLoansAsync(List loans); + + Task> SaveWithChargebackAsync(List loans); + + Task> SaveWithReserveAndChargebackAsync(List loans); + + Task> SearchMembers(string searchText); + + Task> SearchLoanNumbers(string searchText); + + Task> SearchApplicationIds(string searchText); + + Task UpdateDealerOriginalBalanceAsync(List accountNumbers); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IRmsActiveLoanService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IRmsActiveLoanService.cs new file mode 100644 index 0000000..8a70528 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IRmsActiveLoanService.cs @@ -0,0 +1,9 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public interface IRmsActiveLoanService + { + Task HandleRmsActiveImportRequest(RmsActiveImportRequest request); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IScheduleService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IScheduleService.cs new file mode 100644 index 0000000..3c9ff76 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IScheduleService.cs @@ -0,0 +1,8 @@ +namespace ReserveManagementSystemApi.Services +{ + interface IScheduleService + { + Task RunImportExportActive(CancellationToken cancellationToken); + Task RunImportExportClosed(CancellationToken cancellationToken); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IStatusService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IStatusService.cs new file mode 100644 index 0000000..67a2d48 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IStatusService.cs @@ -0,0 +1,7 @@ +using LCU.Library.Internal.StatusModels.Models; +namespace ReserveManagementSystemApi.Services; + +public interface IStatusService +{ + EnvironmentView GetEnviromentView(); +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IVehicleService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IVehicleService.cs new file mode 100644 index 0000000..407319e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/IVehicleService.cs @@ -0,0 +1,10 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public interface IVehicleService + { + + Task GetVehicleByLoanIdAsync(int? loanId); + } +} \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ImportReadService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ImportReadService.cs new file mode 100644 index 0000000..b360c91 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ImportReadService.cs @@ -0,0 +1,402 @@ +using CsvHelper; +using CsvHelper.Configuration; +using System.Globalization; +using ReserveManagementSystemApi.Helpers; +using ReserveManagementSystemApi.Models; +using ReserveManagementSystemApi.ServiceClients; +using ReserveManagementSystemLib.Enums; +using ReserveManagementSystemLib.Helpers; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Configuration; +using ReserveManagementSystemApi.Enums; +using LCU.Api.Internal.MaidBotLib.Models; +using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan; + +namespace ReserveManagementSystemApi.Services +{ + public class ImportReadService : IImportReadService + { + private readonly ILoanService _loanService; + private readonly IImportService _importService; + private readonly IDealerService _dealerService; + private readonly IExportService _exportService; + private readonly ICoreApiServiceClient _coreApiServiceClient; + private readonly RmsApiConfiguration _rmsApiConfig; + private ImportHelper _importHelper = new ImportHelper(); + + public ImportReadService(ILoanService loanService, + IImportService importService, + IDealerService dealerService, + IExportService exportService, + ICoreApiServiceClient coreApiServiceClient, + RmsApiConfiguration rmsApiConfig) + { + _loanService = loanService; + _importService = importService; + _dealerService = dealerService; + _exportService = exportService; + _coreApiServiceClient = coreApiServiceClient; + _rmsApiConfig = rmsApiConfig; + } + + public async Task ImportActiveLoansFromDNA(string newerThanDateTime, bool skipExport = false) + { + if (string.IsNullOrEmpty(newerThanDateTime)) + { + Import lastImport = await _importService.GetLastLoanImportByType(ImportType.Active); + newerThanDateTime = GetStartDateForNextImport(lastImport); + } + Log.Information("Get Active Loans By Posted Date is starting."); + var dnaLoans = await _coreApiServiceClient.GetIndirectLoansByPostedDate(newerThanDateTime); + Log.Information($"Get Active Loans By Posted Date has ended. Found {dnaLoans.Count()} active loan records."); + + Log.Information("Starting ProcessActiveLoanImport"); + var import = await ProcessActiveLoanImport(dnaLoans, DateTime.Now, skipExport); + Log.Information("ProcessActiveLoanImport has ended"); + + return import; + } + + public async Task ImportClosedLoansFromDNA(string newerThanDateTime = "") + { + if (string.IsNullOrEmpty(newerThanDateTime)) + { + var lastImport = await _importService.GetLastLoanImportByType(ImportType.Closed); + newerThanDateTime = GetStartDateForNextImport(lastImport); + } + var dnaLoans = await _coreApiServiceClient.GetClosedLoansByPostedDate(newerThanDateTime); + var import = await ProcessClosedLoanImport(dnaLoans, DateTime.Now, newerThanDateTime); + + return import; + } + + public async Task ImportClosedLoansWithUpdatesFromDNA(string newerThanDateTime, bool skipExport) + { + var dnaLoans = await _coreApiServiceClient.GetClosedLoansWithDetailByClosedDate(newerThanDateTime); + var newImport = new Import(); + + try + { + newImport = await ProcessClosedLoansWithUpdates(dnaLoans, DateTime.Now, skipExport); + } + catch (Exception ex) + { + Log.Error(ex, "Failure to import closed loans from DNA."); + Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group), + Enum.GetName(typeof(GroupEnum), GroupEnum.IndirectLending)) + .Error(ex, $"Reserve Management System Failure. " + + $"Error while importing Closed Loans from DNA."); + throw; + } + + return newImport; + } + + public async Task ProcessActiveLoanImport(List loanDetails, DateTime currentTime, bool skipExport = false) + { + var newImport = new Import + { + ImportType = ImportType.Active.ToString(), + StartDate = currentTime + }; + + newImport = await _importService.AddImportAsync(newImport); + + try + { + var loans = _importHelper.TransformLoanFromDNA(loanDetails, newImport.ImportId); //Transform import to loan object + + await _dealerService.UpdateDealersAsync(loans); + + var allDealers = await _dealerService.GetAllDealersAsync(); + loans.ForEach(loan => + { + var currentDealer = allDealers.FirstOrDefault(a => a.DealerOrganizationNumber == loan.DealerOrganizationNumber); + loan.Dealer = currentDealer; // assign the loan the updated dealer then check validation in save method + }); + + await SaveLoansExportAndImportAsync(newImport, loans, currentTime, skipExport); + } + catch (Exception ex) + { + Log.Error(ex, "Failure to import active loans from DNA."); + Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group), + Enum.GetName(typeof(GroupEnum), GroupEnum.IndirectLending)) + .Error(ex, $"Reserve Management System Failure. " + + $"Error while importing Active Loans from DNA."); + throw; + } + + return newImport; + } + + public async Task ProcessClosedLoansWithUpdates(List dnaLoans, DateTime currentTime, bool skipExport) + { + var closedImport = new Import + { + ImportType = ImportType.Closed.ToString(), + StartDate = currentTime + }; + + closedImport = await _importService.AddImportAsync(closedImport); + var loans = _importHelper.TransformLoanFromDNA(dnaLoans, closedImport.ImportId); + + await _dealerService.UpdateDealersAsync(loans); + + var loansClosed = await _loanService.SaveWithReserveAndChargebackAsync(loans); + + if (!skipExport) + { + //prevent converted loans from exporting + DateTime defaultConvertedPostDate = Convert.ToDateTime(_rmsApiConfig.DefaultConvertedLoanPostDate); + var convertedLoansMarkExported = loansClosed.Where(loan => loan.PostedDate < defaultConvertedPostDate && loan.LoanStatus == "Closed").ToList(); + await _exportService.MarkReservesAsExportedAsync(convertedLoansMarkExported, currentTime); + + //update dealer original balance with only non-converted loans + await _exportService.ExportReservesAsync(DateTime.Now); + var nonConvertedLoans = loansClosed.Where(loan => loan.PostedDate > defaultConvertedPostDate).ToList(); + var listAccountNumbers = nonConvertedLoans.Select(loan => loan.LoanNumber).ToList(); + await _loanService.UpdateDealerOriginalBalanceAsync(listAccountNumbers); + + await _exportService.ExportChargebacksAsync(DateTime.Now); + } + else + { + await _exportService.MarkReservesAndChargebacksAsExportedAsync(loansClosed, DateTime.Now); + } + + closedImport.EndDate = DateTime.Now; + closedImport.RecordCount = loansClosed.Count; + closedImport.RecordsSaved = loansClosed.Count; + closedImport.Success = true; + + await _importService.UpdateImportAsync(closedImport); + + return closedImport; + } + + public async Task ProcessClosedLoanImport(List closedLoans, DateTime currentTime, string newerThanDateTime) + { + List loansUpdated; + var closedImport = new Import + { + ImportType = ImportType.Closed.ToString(), + StartDate = currentTime + }; + + closedImport = await _importService.AddImportAsync(closedImport); + + try + { + loansUpdated = await CloseLoans(closedLoans, closedImport); + var needDetails = loansUpdated.Where(l => l.LoanStatus == LoanStatus.MissingActiveLoan.ToString()).ToList(); + if (needDetails.Count > 0) + { + await ImportClosedLoansWithUpdatesFromDNA(newerThanDateTime, false); + } + + } + catch (Exception ex) + { + Log.Error(ex, "Failure to import closed loans from DNA."); + throw; + } + + return closedImport; + } + + public async Task ReadNewLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime) + { + var loansFromImport = new List(); + var newImport = new Import + { + FileName = fileName.Truncate(50), + StartDate = currentTime, + ImportType = ImportType.Active.ToString() + }; + + newImport = await _importService.AddImportAsync(newImport); + + var config = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false, + TrimOptions = TrimOptions.Trim, + HeaderValidated = null, + MissingFieldFound = null + }; + + try + { + using (var csv = new CsvReader(importFile, config)) + { + loansFromImport = csv.GetRecords().ToList(); + } + + var dealers = loansFromImport.Select(l => new Dealer + { + DealerOrganizationNumber = l.DealerOrganizationNumber + }) + .GroupBy(d => d.DealerOrganizationNumber).First().ToList(); + + await _dealerService.UpdateDealersAsync(dealers); + + var loans = _importHelper.TransformNewLoan(loansFromImport, newImport.ImportId); //Transform import to loan object + + await SaveLoansExportAndImportAsync(newImport, loans, currentTime); + } + catch (Exception ex) + { + Log.Error("Failure to read new loan file, message: {0} {1}", ex.Message, ex.StackTrace); + throw; + } + + return newImport; + } + + public async Task> ReadBoatLoanImportFile(StreamReader importFile) + { + var loansFromImport = new List(); + var config = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false, + TrimOptions = TrimOptions.Trim, + HeaderValidated = null, + MissingFieldFound = null, + BadDataFound = null + }; + + try + { + using (var csv = new CsvReader(importFile, config)) + { + loansFromImport = csv.GetRecords().ToList(); + } + + var loans = _importHelper.TransformBoatLoan(loansFromImport); + loansFromImport = await ExportBoatLoansAsync(loans); + } + catch (Exception ex) + { + Log.Error(ex, "Failure to read boat loan file"); + throw; + } + + return loansFromImport; + } + + public async Task ReadClosedLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime) + { + var loansFromImport = new List(); + List loansUpdated; + + var closedImport = new Import + { + FileName = fileName.Truncate(50), + StartDate = currentTime, + ImportType = ImportType.Closed.ToString() + }; + + closedImport = await _importService.AddImportAsync(closedImport); + + var config = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = true, + TrimOptions = TrimOptions.Trim, + PrepareHeaderForMatch = args => args.Header.ToLower() + }; + + try + { + using (var csv = new CsvReader(importFile, config)) + { + loansFromImport = csv.GetRecords().ToList(); + } + + loansUpdated = await CloseLoans(loansFromImport, closedImport); + Log.Information($"Closed {loansUpdated.Count} loans"); + await _exportService.ExportChargebacksAsync(DateTime.Now); + } + catch (Exception ex) + { + Log.Error("Failure to read closed loan file, message: {0} {1}", ex.Message, ex.StackTrace); + throw; + } + + return closedImport; + } + + private async Task> CloseLoans(List loansFromImport, Import closedImport) + { + var loans = _importHelper.TransformClosedLoans(loansFromImport, closedImport.ImportId).ToList(); + var loansUpdated = await _loanService.SaveWithChargebackAsync(loans); + + var missingActiveLoans = loansUpdated.Where(l => l.LoanStatus == LoanStatus.MissingActiveLoan.ToString()).ToList(); + + await _exportService.ExportChargebacksAsync(DateTime.Now); + + closedImport.EndDate = DateTime.Now; + closedImport.RecordCount = loansFromImport.Count; + closedImport.RecordsSaved = loansUpdated.Count - missingActiveLoans.Count; + closedImport.Success = true; + + await _importService.UpdateImportAsync(closedImport); + Log.Information($"Closed {loansUpdated.Count} loans"); + + return loansUpdated; + } + + private async Task> ExportBoatLoansAsync(List loans, bool skipExport = false) + { + var boatResults = new List(); + if (!skipExport) + { + boatResults = await _exportService.ExportBoatLoansAsync(loans); + var reserveLoans = loans.Where(l => l.ReserveAmount != 0); + var listAccounts = reserveLoans.Select(loan => loan.LoanNumber).ToList(); + await _loanService.UpdateDealerOriginalBalanceAsync(listAccounts); + } + + Log.Information($"Imported {loans.Count} boat loans"); + return boatResults; + } + + private string GetStartDateForNextImport(Import lastImport) + { + if (lastImport?.StartDate != null) + { + return lastImport.StartDate.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + } + return _rmsApiConfig.DefaultImportStartDate; + } + + private async Task SaveLoansExportAndImportAsync(Import import, List loans, DateTime currentTime, bool skipExport = false) + { + var loansImported = await _loanService.SaveLoansAsync(loans); + if (!skipExport) + { + //prevent converted loans from exporting + DateTime defaultConvertedPostDate = Convert.ToDateTime(_rmsApiConfig.DefaultConvertedLoanPostDate); + var convertedLoansMarkExported = loansImported.Where(loan => loan.PostedDate < defaultConvertedPostDate && loan.LoanStatus == "New").ToList(); + await _exportService.MarkReservesAsExportedAsync(convertedLoansMarkExported, currentTime); + + //update dealer original balance with only non-converted loans + await _exportService.ExportReservesAsync(currentTime); + var nonConvertedLoans = loansImported.Where(loan => loan.PostedDate > defaultConvertedPostDate).ToList(); + var listAccountNumbers = nonConvertedLoans.Select(loan => loan.LoanNumber).ToList(); + await _loanService.UpdateDealerOriginalBalanceAsync(listAccountNumbers); + } + else + { + await _exportService.MarkReservesAsExportedAsync(loansImported, currentTime); + } + + Log.Information($"Imported {loansImported.Count} active loans"); + import.EndDate = DateTime.Now; + import.RecordCount = loansImported.Count; + import.RecordsSaved = loansImported.Count; + import.Success = true; + + await _importService.UpdateImportAsync(import); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ImportService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ImportService.cs new file mode 100644 index 0000000..0ee8fc5 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ImportService.cs @@ -0,0 +1,89 @@ +using ReserveManagementSystemLib.Enums; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Repositories; + +namespace ReserveManagementSystemApi.Services +{ + public class ImportService: IImportService + { + private readonly IImportRepository _importRepository; + + public ImportService(IImportRepository importRepository) + { + _importRepository = importRepository; + } + + public async Task> GetAllAsync() + { + try + { + return await _importRepository.GetAll().OrderByDescending(imp => imp.StartDate).ToListAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving imports."); + throw; + } + } + + public async Task GetLastLoanImportByType(ImportType importType) + { + Import import; + try + { + import = await _importRepository.GetLastLoanDnaImportByType(importType).FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error(ex, $"Failure retrieving last import."); + throw; + } + + return import; + } + + public async Task> GetRecentAsync(int numberOfImports) + { + try + { + return await _importRepository.GetAll().OrderByDescending(imp => imp.StartDate).Take(numberOfImports).ToListAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving imports."); + throw; + } + } + + public async Task GetByIdAsync(int id) + { + Import import; + try + { + import = await _importRepository.FindByIdAsync(id); + } + catch (Exception ex) + { + Log.Error(ex, $"Failure retrieving import with id {id}."); + throw; + } + + if (import == null) + { + throw new Exception($"Import with id {id} not found."); + } + + return import; + } + + public async Task AddImportAsync(Import newImport) + { + return await _importRepository.AddImportAsync(newImport); + } + + public async Task UpdateImportAsync(Import import) + { + return await _importRepository.UpdateImportAsync(import); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/LoanCalculationService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/LoanCalculationService.cs new file mode 100644 index 0000000..359388f --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/LoanCalculationService.cs @@ -0,0 +1,314 @@ +using ReserveManagementSystemApi.Helpers; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public class LoanCalculationService : ILoanCalculationService + { + private readonly LoanCalculationHelper _loanCalculationHelper; + private readonly BoatLoanCalculationHelper _boatLoanCalculationHelper; + private CalculationConfig _calculationConfig; + + public LoanCalculationService() + { + _loanCalculationHelper = new LoanCalculationHelper(); + _boatLoanCalculationHelper = new BoatLoanCalculationHelper(); + } + + public LoanReserve CalculateSingleReserve(Loan loan) + { + var result = new LoanReserve(); + try + { + _calculationConfig = new CalculationConfig(loan); + if (!loan.IsBoatLoan(loan)) + { + result = ReserveCalculation(loan); + } + else + { + result = BoatReserveCalculation(loan); + } + } + catch (Exception e) + { + Log.Error("Failure to Calculate Loan: {0} {1}", e.Message, e.StackTrace); + result.ReserveTypeMessage = "Exception while calculating reserve"; + } + + return result; + } + + public LoanChargeback CalculateSingleChargeback(Loan loan) + { + var result = new LoanChargeback(); + try + { + _calculationConfig = new CalculationConfig(loan); + if (!loan.IsBoatLoan(loan)) + { + result = ChargebackCalculation(loan); + } + else + { + result = BoatChargebackCalculation(loan); + } + + } + catch (Exception e) + { + Log.Error("Failure to Calculate Chargeback: {0} {1}", e.Message, e.StackTrace); + } + + return result; + } + + public Loan CalculateLoanReserve(Loan loan, decimal? originalReserve = null, DateTime? originalExportReserveDate = null) + { + try + { + _calculationConfig = new CalculationConfig(loan); + var loanReserve = new LoanReserve(); + if (!loan.IsBoatLoan(loan)) + { + loanReserve = ReserveCalculation(loan); + } + else + { + loanReserve = BoatReserveCalculation(loan); + } + + loan = AssignReserveValues(loan, loanReserve); + if (originalReserve.HasValue) + { + CalculateReserveAdjustment(loan, originalReserve, originalExportReserveDate); + } + } + catch (Exception e) + { + Log.Error("Failure to Calculate Reserve: {0} {1}", e.Message, e.StackTrace); + loan.ReserveMessage = "Exception while calculating reserve"; + } + + return loan; + } + + private static void CalculateReserveAdjustment(Loan loan, decimal? originalReserve, DateTime? originalExportReserveDate) + { + var updatedReserveSaved = Math.Round(loan.ReserveAmount, 4, MidpointRounding.AwayFromZero); //this represents the amount saved to DB + var updatedReserveRounded = Math.Round(updatedReserveSaved, 2, MidpointRounding.AwayFromZero); + var originalReserveRounded = Math.Round(originalReserve.Value, 2, MidpointRounding.AwayFromZero); + + if (updatedReserveRounded != originalReserveRounded) + { + loan.Adjustment = (updatedReserveRounded - originalReserveRounded); + loan.ExportedReserveDate = null; //we only want to remove last export date if reserve amount changed + } + else + { + loan.ExportedReserveDate = originalExportReserveDate; + } + } + + private LoanReserve ReserveCalculation(Loan loanData) + { + var loanReserve = new LoanReserve(); + _calculationConfig.BuyFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.BuyRate); // Finance charge using BuyRate% + _calculationConfig.ContractFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.ContractRate); // Finance charge using ContractRate% + + if (loanData.ContractRate < loanData.BuyRate) + { + //Calculate Buy Down. + return _loanCalculationHelper.CalculateBuyDown(_calculationConfig); + } + else + { + // Calculate Reserve Amount for all other loans. + if ((double)loanData.ContractRate == (double)loanData.BuyRate) + { + loanReserve = _loanCalculationHelper.CalculateNoMarkup(_calculationConfig); + } + else + { + loanReserve = _loanCalculationHelper.CalculateStandardMarkup(_calculationConfig); + } + + //Assign any Flat Fees if applicable + return _loanCalculationHelper.AssignFlatFee(loanReserve, _calculationConfig); + } + } + + private LoanReserve BoatReserveCalculation(Loan loanData) + { + if (loanData.ContractRate < loanData.BuyRate) + { + _calculationConfig.BuyFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.BuyRate); // Finance charge using BuyRate% + _calculationConfig.ContractFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.ContractRate); // Finance charge using ContractRate% + + return _loanCalculationHelper.CalculateBuyDown(_calculationConfig); + + } + else + { + // Calculate Reserve Amount for all other loans. + return _boatLoanCalculationHelper.CalculateStandardMarkup(_calculationConfig); + } + } + + private Loan AssignReserveValues(Loan loan, LoanReserve loanReserve) + { + loan.ReserveAmount = loanReserve.ReserveAmount; + loan.ReserveMessage = loanReserve.ReserveTypeMessage; + loan.Term = _calculationConfig.LoanData().Term; + loan.FlatFeePaid = loanReserve.FlatFeePaid; + + return loan; + } + + private LoanChargeback ChargebackCalculation(Loan loanData) + { + if (loanData.ReserveAmount < 0 || loanData.ReserveMessage != null && loanData.ReserveMessage.ToUpper().Contains("BUYDOWN")) + { + return SetBuyDownChargeback(loanData); + } + + if (loanData.LoanType?.ChargebackMethod == "S") + { + return CalculateBBLoans(loanData); + } + else + { + return CalculateLoanWithChargebackDate(loanData); + } + } + + private LoanChargeback BoatChargebackCalculation(Loan loanData) + { + if (loanData.ReserveAmount < 0 || loanData.ReserveMessage != null && loanData.ReserveMessage.ToUpper().Contains("BUYDOWN")) + { + return SetBuyDownChargeback(loanData); + } + else + { + return CalculateBoatLoanWithChargebackDate(loanData); + } + } + + private LoanChargeback CalculateBoatLoanWithChargebackDate(Loan loanData) + { + var numOfChargebackPayments = loanData.Term <= 63 ? 6 : 12; //term is 63 or less chargeback date is 6 monthly payments, otherwise 12 + var lastChargebackDate = _boatLoanCalculationHelper.GetLastChargebackDate(loanData.FirstPmtDate, numOfChargebackPayments); + + if (loanData.PaidLoanDate > lastChargebackDate) + { + return new LoanChargeback + { + ChargebackAmount = 0M, + ChargebackTypeMessage = loanData.ReserveAmount == 0 ? "No Reserve - No Chargeback" : "Paid After Chargeback Date", + RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData) + }; + } + + return ChargebackWithRemainingMonths(loanData); + + } + + private LoanChargeback CalculateLoanWithChargebackDate(Loan loanData) + { + var numOfChargebackPayments = loanData.LoanType?.NoChargebackPayments ?? 3; + var numOfChargebackDays = loanData.LoanType?.NoChargebackDays ?? 90; + var lastChargebackDate = _loanCalculationHelper.GetLastChargebackDate(loanData.LoanDate, loanData.FirstPmtDate, numOfChargebackPayments, numOfChargebackDays); + + if (loanData.PaidLoanDate > lastChargebackDate) + { + return new LoanChargeback + { + ChargebackAmount = 0M, + ChargebackTypeMessage = loanData.ReserveAmount == 0 ? "No Reserve - No Chargeback" : "Paid After Chargeback Date", + RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData) + }; + } + + return ChargebackWithRemainingMonths(loanData); + + } + + private LoanChargeback ChargebackWithRemainingMonths(Loan loanData) + { + int remainingMonths = 0; + string message = "Paid On or Before Chargeback Date"; + + remainingMonths = _loanCalculationHelper.GetRemainingMonths(loanData); + + return new LoanChargeback + { + //this needs to be the original term formula + ChargebackAmount = _loanCalculationHelper.CalculateChargeback(loanData.ReserveAmount, loanData.Term, remainingMonths), + ChargebackTypeMessage = loanData.ReserveAmount == 0 ? "No Reserve - No Chargeback" : message, + RemainingMonthsAtClose = (byte)remainingMonths + }; + } + + private LoanChargeback ChargebackWithRemainingMonthsBB(Loan loanData) + { + int remainingMonths = 0; + string message = "Paid After Chargeback Date"; + + //add chargeback date for BB Loans + var chargeBackDate = loanData.FirstPmtDate.AddMonths(loanData.Term - 1); + + if (loanData.PaidLoanDate <= chargeBackDate) + { + remainingMonths = _loanCalculationHelper.GetRemainingMonths(loanData); + if (remainingMonths <= 0) + { + return new LoanChargeback + { + ChargebackAmount = 0M, + ChargebackTypeMessage = message, + RemainingMonthsAtClose = 0 + }; + } + else + { + message = "Paid On or Before Chargeback Date"; + } + } + + return new LoanChargeback + { + ChargebackAmount = _loanCalculationHelper.CalculateChargeback(loanData.ReserveAmount, loanData.Term, remainingMonths), + ChargebackTypeMessage = message, + RemainingMonthsAtClose = (byte)remainingMonths + }; + } + + private LoanChargeback CalculateBBLoans(Loan loanData) + { + if (loanData.ReserveAmount != 0) + { + return ChargebackWithRemainingMonthsBB(loanData); + } + else + { + return new LoanChargeback + { + ChargebackAmount = 0M, + ChargebackTypeMessage = "No Reserve - No Chargeback", + RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData) + }; + } + } + + private LoanChargeback SetBuyDownChargeback(Loan loanData) + { + //Buydown scenarios will not calculate a chargeback and return a message indicating this. + return new LoanChargeback + { + ChargebackAmount = 0M, + ChargebackTypeMessage = "Buy Down Reserve - No Chargeback", + RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData) + }; + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/LoanService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/LoanService.cs new file mode 100644 index 0000000..3aa090c --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/LoanService.cs @@ -0,0 +1,448 @@ +using System.Linq.Dynamic.Core; +using ReserveManagementSystemApi.Enums; +using ReserveManagementSystemApi.ServiceClients; +using ReserveManagementSystemLib.Helpers; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Repositories; + +namespace ReserveManagementSystemApi.Services +{ + public class LoanService : ILoanService + { + private readonly ILoanRepository _loanRepository; + private readonly ILoanTypeRepository _loanTypeRepository; + private readonly IAuditHistoryService _auditHistoryService; + private ILoanCalculationService _loanCalculationService; + private const double MonthInDays = 30.4375; + + private readonly ICoreApiServiceClient _coreApiServiceClient; + + public LoanService(ILoanRepository loanRepository, ILoanTypeRepository loanTypeRepository, IAuditHistoryService auditHistoryService, ICoreApiServiceClient coreApiServiceClient) + { + _loanRepository = loanRepository; + _loanTypeRepository = loanTypeRepository; + _auditHistoryService = auditHistoryService; + _coreApiServiceClient = coreApiServiceClient; + _loanCalculationService = new LoanCalculationService(); + } + + public async Task> GetAllAsync() + { + try + { + return await _loanRepository.GetAll().OrderByDescending(x => x.ImportId).ThenBy(y => y.MemberNumber).ThenBy(z => z.LoanNumber).ToListAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving loans."); + throw; + } + } + + public async Task GetByIdAsync(int id) + { + Loan loan; + try + { + loan = await _loanRepository.FindByIdAsync(id); + if (loan != null) + { + loan.ValidationErrors = ValidationHelper.SetValidationErrors(loan); + } + } + catch (Exception ex) + { + Log.Error(ex, $"Failure retrieving loan with id {id}."); + throw; + } + + if (loan == null) + { + throw new Exception($"Loan with id {id} not found."); + } + + return loan; + } + + public async Task GetFilteredAsync(LoanFilter filter) + { + try + { + IQueryable loans; + PaginatedListResponse pagedListResponse = new PaginatedListResponse(); + var query = _loanRepository.GetAll(); + var filteredQuery = filter.ApplyFilter(query); + + List listOfFieldNames = typeof(Loan).GetProperties().Select(f => f.Name).ToList(); + listOfFieldNames.AddRange(typeof(Dealer).GetProperties().Select(f => $"Dealer.{f.Name}").ToList()); + listOfFieldNames.AddRange(typeof(Vehicle).GetProperties().Select(f => $"Vehicle.{f.Name}").ToList()); + + var result = listOfFieldNames.Where(n => n == filter.SortField).FirstOrDefault(); + + if (!string.IsNullOrEmpty(filter.SortField) && result != null) + { + loans = filteredQuery.OrderBy($"{filter.SortField} {filter.SortDirection}, ImportId desc, MemberNumber, LoanNumber"); + } + else + { + loans = filteredQuery.OrderBy($"ImportId desc, MemberNumber, LoanNumber"); + } + pagedListResponse = await PaginatedListResponse.CreateListAsync(loans, filter.PageIndex, filter.MaxRecordsPerPage, filter.MaxPagesPerRange); + return pagedListResponse; + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving loans with filter."); + throw; + } + } + + public async Task GetLoanReserveAsync(LoanReserveInput input) + { + var result = new LoanReserve(); + var loan = new Loan + { + LoanDate = input.LoanDate, + FirstPmtDate = input.FirstPmtDate, + LoanAmount = input.LoanAmount, + LoanTypeId = input.LoanType, + BuyRate = input.BuyRate, + ContractRate = input.ContractRate, + Term = (int)Math.Round(Convert.ToDecimal(input.OriginalTermInDays) / (decimal)MonthInDays, 0), + AccountType = input.AccountType + }; + + loan.LoanType = await _loanTypeRepository.GetLoanTypeAsync(loan.LoanTypeId); + try + { + _loanCalculationService = new LoanCalculationService(); + result = _loanCalculationService.CalculateSingleReserve(loan); + } + catch (Exception e) + { + Log.Error("Failure to Calculate Loan Reserve: {0} {1}", e.Message, e.StackTrace); + } + + return result; + } + + public async Task GetLoanChargebackAsync(LoanChargebackInput input) + { + var result = new LoanChargeback(); + var loan = new Loan + { + LoanDate = input.LoanDate, + FirstPmtDate = input.FirstPmtDate, + FinalPmtDate = input.FinalPmtDate, + LoanTypeId = input.LoanType, + ReserveAmount = input.ReserveAmount, + PaidLoanDate = input.PaidLoanDate, + Term = input.Term, + AccountType = input.AccountType + }; + + loan.LoanType = await _loanTypeRepository.GetLoanTypeAsync(loan.LoanTypeId); + try + { + result = _loanCalculationService.CalculateSingleChargeback(loan); + } + catch (Exception e) + { + Log.Error("Failure to Calculate Loan Chargeback: {0} {1}", e.Message, e.StackTrace); + } + + return result; + } + + public async Task> SaveLoansAsync(List loans) + { + var loansToAddOrUpdate = new List(); + var historyRecords = new List(); + var loanTypes = await _loanTypeRepository.GetLoanTypesAsync(); + var numberOfLoansToUpdateReserve = 0; + var numberOfLoansToAdd = 0; + var numberOfLoansToUpdate = 0; + + try + { + Log.Information($"Have {loans.Count} loans to add or update"); + loans.ForEach(loan => + { + var validationErrors = ValidationHelper.SetValidationErrors(loan); + loan.ActionRequired = validationErrors.Count > 0; + loan.LoanType = loanTypes.FirstOrDefault(x => x.LoanTypeId == loan.LoanTypeId); + var originalLoan = _loanRepository.GetAll().FirstOrDefault(l => l.LoanNumber == loan.LoanNumber); + + if (originalLoan != null) //have an updated loan + { + if (!loan.Equals(originalLoan) || (loan.Vehicle != null && !loan.Vehicle.Equals(originalLoan.Vehicle))) + { + numberOfLoansToUpdate++; + loan.LoanId = originalLoan.LoanId; + var needToUpdateReserve = !loan.HasSameReserve(originalLoan); + if (needToUpdateReserve) + { + numberOfLoansToUpdateReserve++; + loan = _loanCalculationService.CalculateLoanReserve(loan, originalLoan.ReserveAmount, originalLoan.ExportedReserveDate); + } + + historyRecords.Add(_auditHistoryService.GetExistingLoanUpdateHistoryAsync(loan, originalLoan)); + CopyFromDnaLoanToOriginalLoan(loan, originalLoan, needToUpdateReserve); + + originalLoan.LoanStatus = LoanStatus.Updated.ToString(); + + loansToAddOrUpdate.Add(originalLoan); + } + } + else //have a new loan + { + numberOfLoansToAdd++; + loan = _loanCalculationService.CalculateLoanReserve(loan); + + loan.Dealer = null; + loan.LoanStatus = LoanStatus.New.ToString(); + loansToAddOrUpdate.Add(loan); + } + }); + + Log.Information($"{numberOfLoansToUpdate} loans were updated"); + Log.Information($"{numberOfLoansToUpdateReserve} loans had reserve recalculated"); + Log.Information($"{numberOfLoansToAdd} loans were added"); + + if (loansToAddOrUpdate.Any()) + { + await _loanRepository.UpdateAsync(loansToAddOrUpdate); + } + if (historyRecords.Any()) + { + await _auditHistoryService.AddHistoriesAsync(historyRecords); + } + } + catch (Exception e) + { + Log.Error("Failure to Save Loans: {0} {1}", e.Message, e.StackTrace); + throw; + } + + return loansToAddOrUpdate; + } + + public async Task> SaveWithChargebackAsync(List loans) + { + var historiesForClosedLoans = new List(); + var historiesForLoansNotFound = new List(); + var loansToClose = new List(); + var description = "Importing closed loan changed loan status"; + loans.ForEach(loan => + { + var originalLoan = _loanRepository.GetAllWithoutDealer().FirstOrDefault(l => l.LoanNumber == loan.LoanNumber); + + if (originalLoan != null) //have an updated loan + { + if (!originalLoan.PaidLoanDate.HasValue || originalLoan.PaidLoanDate != loan.PaidLoanDate) + { + var oldStatus = originalLoan.LoanStatus; + originalLoan.PaidLoanDate = loan.PaidLoanDate; + originalLoan.ImportId = loan.ImportId; + var chargebackResult = _loanCalculationService.CalculateSingleChargeback(originalLoan); + + AssignChargebackResult(originalLoan, chargebackResult); + + historiesForClosedLoans.Add(_auditHistoryService.GetLoanStatusChangeHistory(originalLoan, oldStatus, description)); + + loansToClose.Add(originalLoan); + } + } + else //active loan not found to close + { + //set this temporary status to flag selecting original loan with details in order to calculate both reserve and chargeback + loan.LoanStatus = LoanStatus.MissingActiveLoan.ToString(); + loansToClose.Add(loan); + historiesForLoansNotFound.Add(_auditHistoryService.GetAuditHistoryForNonexistingClosedLoan(loan)); + } + }); + + await _auditHistoryService.AddHistoriesAsync(historiesForClosedLoans); + await _auditHistoryService.AddHistoriesAsync(historiesForLoansNotFound); + await _loanRepository.UpdateAsync(loansToClose.Where(l => l.LoanStatus != "MissingActiveLoan")); + return loansToClose; + } + + /// This method was used for one time import, we want to save the original reserve info for an already closed loan pre Go-Live. + public async Task> SaveWithReserveAndChargebackAsync(List loans) + { + try + { + var loanTypes = await _loanTypeRepository.GetLoanTypesAsync(); + var historiesForClosedLoans = new List(); + var loansToClose = new List(); + var description = "Importing closed loan changed loan status"; + + loans.ForEach(loan => + { + var originalLoan = _loanRepository.GetAllWithoutDealer().FirstOrDefault(l => l.LoanNumber == loan.LoanNumber); + + if (originalLoan == null) + { + loan.LoanType = loanTypes.FirstOrDefault(x => x.LoanTypeId == loan.LoanTypeId); + loan = _loanCalculationService.CalculateLoanReserve(loan, null, null); + var chargebackResult = _loanCalculationService.CalculateSingleChargeback(loan); + AssignChargebackResult(loan, chargebackResult); + loansToClose.Add(loan); + historiesForClosedLoans.Add(_auditHistoryService.GetLoanStatusChangeHistory(loan, "", description)); + } + }); + + await _auditHistoryService.AddHistoriesAsync(historiesForClosedLoans); + await _loanRepository.UpdateAsync(loansToClose); + + return loansToClose; + } + catch (Exception ex) + { + Log.Error(ex.Message); + throw; + } + } + + public async Task> SearchMembers(string searchText) + { + var memberNumbers = new List(); + try + { + memberNumbers = await _loanRepository.GetAll().Where(x => x.MemberNumber.StartsWith(searchText)) + .Select(y => y.MemberNumber) + .Distinct() + .ToListAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving list of members."); + throw; + } + + return memberNumbers.OrderBy(x => long.Parse(x)).ToList(); + } + + public async Task> SearchLoanNumbers(string searchText) + { + var loanNumbers = new List(); + try + { + loanNumbers = await _loanRepository.GetAll().Where(x => x.LoanNumber.StartsWith(searchText)) + .Select(x => x.LoanNumber) + .Distinct() + .ToListAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving list of loan numbers."); + throw; + } + + return loanNumbers.OrderBy(x => long.Parse(x)).ToList(); + } + + public async Task> SearchApplicationIds(string searchText) + { + var applicationIds = new List(); + try + { + applicationIds = await _loanRepository.GetAll().Where(x => x.ApplicationId.StartsWith(searchText)) + .Select(x => x.ApplicationId) + .Distinct() + .ToListAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving list of application ids."); + throw; + } + + return applicationIds.OrderBy(x => long.Parse(x)).ToList(); + } + + public async Task UpdateDealerOriginalBalanceAsync(List accountNumbers) + { + foreach(var account in accountNumbers) + { + var exportResult = await _coreApiServiceClient.PutDealerOriginalBalance(account); + + if (exportResult != null) + { + foreach (var result in exportResult) + if (!string.IsNullOrEmpty(result.ErrorMessage)) + { + Log.Error($"Failure to update dealer original balance loan: {result.AccountNumber} {result.ErrorMessage} "); + } + } + else //if nothing is returned all updates succeeded + { + Log.Information($"Update to dealers original balances completed successfully"); + } + } + + } + + private static void AssignChargebackResult(Loan loan, LoanChargeback chargebackResult, bool skipExport = false) + { + loan.ChargeBack = Math.Round(chargebackResult.ChargebackAmount, 4); + loan.ChargebackMessage = chargebackResult.ChargebackTypeMessage; + loan.RemainingMonthsAtClose = chargebackResult.RemainingMonthsAtClose; + loan.ActionRequired = false; + + //if skipping export then loan will already have ExportedChargebackDate set + loan.ExportedChargebackDate = skipExport ? loan.ExportedChargebackDate : null; + loan.LoanStatus = LoanStatus.Closed.ToString(); + loan.Dealer = null; + } + + private static void CopyFromDnaLoanToOriginalLoan(Loan loan, Loan originalLoan, bool needToUpdateReserve) + { + originalLoan.ActionRequired = loan.ActionRequired; + originalLoan.ImportId = loan.ImportId; + originalLoan.MemberNumber = loan.MemberNumber; + originalLoan.MemberFirstName = loan.MemberFirstName; + originalLoan.MemberInitial = loan.MemberInitial; + originalLoan.MemberLastName = loan.MemberLastName; + originalLoan.DealerOrganizationNumber = loan.DealerOrganizationNumber; + originalLoan.LoanDate = loan.LoanDate; + originalLoan.PostedDate = loan.PostedDate; + originalLoan.FirstPmtDate = loan.FirstPmtDate; + originalLoan.FinalPmtDate = loan.FinalPmtDate; + originalLoan.LoanAmount = loan.LoanAmount; + originalLoan.BuyRate = loan.BuyRate; + originalLoan.ContractRate = loan.ContractRate; + originalLoan.MonthlyPmt = loan.MonthlyPmt; + originalLoan.LoanTypeId = loan.LoanTypeId; + originalLoan.ApplicationId = loan.ApplicationId; + //suppressing these insurance updates until DNA has correct value + //originalLoan.Warranty = loan.Warranty; + //originalLoan.Gap = loan.Gap; + originalLoan.CreditLife = loan.CreditLife; + originalLoan.CreditDisability = loan.CreditDisability; + //originalLoan.Mbp = loan.Mbp; + originalLoan.AccountType = loan.AccountType; + + if (originalLoan.Vehicle != null) + { + originalLoan.Vehicle.VINNumber = loan.Vehicle?.VINNumber; + originalLoan.Vehicle.Make = loan.Vehicle?.Make; + originalLoan.Vehicle.Model = loan.Vehicle?.Model; + originalLoan.Vehicle.Mileage = loan.Vehicle?.Mileage ?? 0; + originalLoan.Vehicle.Year = loan.Vehicle?.Year; + originalLoan.Vehicle.Color = loan.Vehicle?.Color; + } + + if (needToUpdateReserve) + { + originalLoan.ReserveAmount = loan.ReserveAmount; + originalLoan.ReserveMessage = loan.ReserveMessage; + originalLoan.ExportedReserveDate = loan.ExportedReserveDate; + originalLoan.Term = loan.Term; + originalLoan.FlatFeePaid = loan.FlatFeePaid; + originalLoan.Adjustment = loan.Adjustment; + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/QueueServices/IQueueRmsActiveLoanService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/QueueServices/IQueueRmsActiveLoanService.cs new file mode 100644 index 0000000..41a1de3 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/QueueServices/IQueueRmsActiveLoanService.cs @@ -0,0 +1,9 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services.QueueServices +{ + public interface IQueueRmsActiveLoanService + { + Task QueueRmsActiveLoanServiceRequest(RmsActiveImportRequest request); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/QueueServices/QueueRmsActiveLoanService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/QueueServices/QueueRmsActiveLoanService.cs new file mode 100644 index 0000000..3bf504d --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/QueueServices/QueueRmsActiveLoanService.cs @@ -0,0 +1,36 @@ +using LCU.Library.Internal.LcuMassTransitModels; +using MassTransit; +using Newtonsoft.Json; +using ReserveManagementSystemApi.Configurations; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services.QueueServices +{ + public class QueueRmsActiveLoanService : IQueueRmsActiveLoanService + { + private readonly ISendEndpoint _sendEndpoint; + private readonly ReceiveEndpointConfiguration _rmsActiveLoanImportReceiveEndpoint; + + public QueueRmsActiveLoanService(IBusControl busControl, ReceiveEndpointConfigurations receiveEndpointConfigurations) + { + _rmsActiveLoanImportReceiveEndpoint = receiveEndpointConfigurations.RmsActiveLoanReceiveEndpoint; + _sendEndpoint = busControl.GetSendEndpoint(_rmsActiveLoanImportReceiveEndpoint.ReceiveEndpointUri).Result; + } + + public async Task QueueRmsActiveLoanServiceRequest(RmsActiveImportRequest request) + { + try + { + var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + await _sendEndpoint.Send(request, cancellationTokenSource.Token); + } + catch (Exception exception) + { + var message = "Failed to Queue RmsActiveLoanImport Request. " + + $"RmsActiveLoanImport: {JsonConvert.SerializeObject(request)}"; + Log.Error(exception, message); + throw new ServiceException($"{message}, Exception Message: {exception.Message}"); + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/RmsActiveLoanService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/RmsActiveLoanService.cs new file mode 100644 index 0000000..3874244 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/RmsActiveLoanService.cs @@ -0,0 +1,31 @@ +using ReserveManagementSystemApi.Services.QueueServices; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemApi.Services +{ + public class RmsActiveLoanService : IRmsActiveLoanService + { + private readonly IQueueRmsActiveLoanService _queueRmsActiveLoanService; + private readonly IImportReadService _importReadService; + + public RmsActiveLoanService( + IQueueRmsActiveLoanService queueRmsActiveLoanService, IImportReadService importReadService +) + { + _queueRmsActiveLoanService = queueRmsActiveLoanService; + _importReadService = importReadService; + } + public async Task HandleRmsActiveImportRequest(RmsActiveImportRequest request) + { + try + { + await _importReadService.ImportActiveLoansFromDNA(request.ImportDateTime, request.SkipExport); + } + catch (Exception ex) + { + Log.Error(ex, $"Failure importing Active Loans from DNA"); + throw new ServiceException($"Exception Message: {ex.Message} - Failure importing Active Loans from DNA"); + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ScheduleService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ScheduleService.cs new file mode 100644 index 0000000..84f4106 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/ScheduleService.cs @@ -0,0 +1,42 @@ +namespace ReserveManagementSystemApi.Services +{ + public class ScheduleService : IScheduleService + { + private readonly IImportReadService _importReadService; + private readonly IExportService _exportService; + + public ScheduleService(IImportReadService importReadService, IExportService exportService) + { + _importReadService = importReadService; + _exportService = exportService; + } + + public async Task RunImportExportActive(CancellationToken cancellationToken) + { + try + { + Log.Information("Importing active loans from DNA"); + var activeImport = await _importReadService.ImportActiveLoansFromDNA(string.Empty); + Log.Information($"{activeImport.RecordsSaved} active loans were saved"); + } + catch (Exception ex) + { + Log.Error(ex, $"Failed to perform scheduled active loan import."); + } + } + + public async Task RunImportExportClosed(CancellationToken cancellationToken) + { + try + { + Log.Information("Importing closed loans from DNA"); + var closedImport = await _importReadService.ImportClosedLoansFromDNA(string.Empty); + Log.Information($"{closedImport.RecordsSaved} loans were closed"); + } + catch (Exception ex) + { + Log.Error(ex, $"Failed to perform scheduled closed loan import."); + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/StatusService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/StatusService.cs new file mode 100644 index 0000000..8533b49 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/StatusService.cs @@ -0,0 +1,28 @@ +using LCU.Library.Internal.Status.Services; +using LCU.Library.Internal.StatusModels.Models; +namespace ReserveManagementSystemApi.Services; + +public class StatusService : IStatusService +{ + private readonly IEnvironmentViewService _environmentViewService; + + public StatusService(IEnvironmentViewService environmentViewService) + { + _environmentViewService = environmentViewService; + } + + public EnvironmentView GetEnviromentView() + { + try + { + var environmentView = _environmentViewService.BuildEnvironmentView(); + Log.Verbose("Status Environment View Heartbeat. EnvironmentView: {@EnvironmentView}", environmentView); + return environmentView; + } + catch (Exception exception) + { + Log.Error(exception, $"Failure Getting Enviroment View Information."); + throw new ServiceException($"Failure Getting Enviroment View Information."); + } + } +} \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/VehicleService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/VehicleService.cs new file mode 100644 index 0000000..0d79b60 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Services/VehicleService.cs @@ -0,0 +1,29 @@ +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Repositories; + +namespace ReserveManagementSystemApi.Services +{ + public class VehicleService: IVehicleService + { + private readonly IVehicleRepository _vehicleRepository; + + public VehicleService(IVehicleRepository vehicleRepository) + { + _vehicleRepository = vehicleRepository; + } + + + public async Task GetVehicleByLoanIdAsync(int? loanId) + { + try + { + return await _vehicleRepository.FindByLoanIdAsync(loanId); + } + catch (Exception ex) + { + Log.Error(ex, "Failure retrieving vehicle by loan id."); + throw; + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Using.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Using.cs new file mode 100644 index 0000000..972162b --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/Using.cs @@ -0,0 +1,8 @@ +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.EntityFrameworkCore; +global using Serilog; +global using System; +global using System.Linq; +global using System.Threading.Tasks; +global using LCU.Library.Internal.Exceptions; + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/appsettings.json b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/appsettings.json new file mode 100644 index 0000000..cca5fdb --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemApi/appsettings.json @@ -0,0 +1,59 @@ +{ + "Primary": { + "AppName": "Reserve Management System", + "SeqUrl": "http://localhost", + "SeqApiKey": "VALUE-IN-KEYVAULT", + "CorsAllowedOrigins": [ + "http://localhost:3000", + "http://lcu-internal.com", + "https://lcu-internal.com", + "https://qa-api.lcu-internal.com" + ], + "FeatureFlags": { + "RunRmsActiveLoanConsumer": true, + "EnableImportExportActive": false, + "EnableImportExportClosed": false + } + }, + "SqlConnectionConfiguration": { + "ConnectionString": "data source=localhost;initial catalog=ReserveManagementSystem;Integrated Security=SSPI;MultipleActiveResultSets=True;App=EntityFramework", + "MaxRetryCount": 3, + "MaxRetryDelay": "00:00:30" + }, + "ConnectionConfiguration": { + "HostUrl": "localhost", + "UserName": "guest", + "Password": "guest" + }, + "RmsActiveLoanReceiveEndpoint": { + "ReceiveEndpoint": "LCU.ReserveManagementSystem.ActiveImport", + "ReceiveEndpointType": "exchange", + "PrefetchLimit": 1 + }, + "ApiSecurityConfiguration": { + "ApiKeyAuthenticationFailureBehavior": "Error", + "InternalApiKeySet": { + "ReserveManagementSystem": "VALUE-IN-KEYVAULT" + }, + "ExternalApiKeySet": {} + }, + "CoreApiConfiguration": { + "ApiTimeOutInSeconds": 420, + "BaseUrl": "https://localhost:59303", + "GetIndirectLoansByPostedDate": "/api/core/IndirectLoan/", + "GetClosedLoansByPostedDate": "/api/core/IndirectLoan/Closed/", + "GetClosedIndirectLoansWithDetailByPostedDate": "/api/core/IndirectLoan/ClosedWithDetail/", + "PostMonetaryTransactions": "/api/core/Transaction/PostMonetaryTransactions", + "PutDealerOriginalBalance": "/api/core/IndirectLoan/PutDealerOriginalBalance" + }, + "AllowedHosts": "*", + "HostedServices": { + "ImportExportActiveCronSchedule": "0 19 * * 0-6", + "ImportExportClosedCronSchedule": "15 19 * * 0-6" + }, + "RmsApiConfiguration": { + "DefaultImportStartDate": "2021-07-01 00:00:00", + "DefaultBatchSize": 100, + "DefaultConvertedLoanPostDate": "2021-10-31" + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Configuration/CoreApiConfiguration.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Configuration/CoreApiConfiguration.cs new file mode 100644 index 0000000..bdcff6a --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Configuration/CoreApiConfiguration.cs @@ -0,0 +1,14 @@ +namespace ReserveManagementSystemLib.Configuration +{ + public class CoreApiConfiguration + { + public string ClientName { get; set; } + public string ApiTimeOutInSeconds { get; set; } + public string BaseUrl { get; set; } + public string GetIndirectLoansByPostedDate { get; set; } + public string GetClosedLoansByPostedDate { get; set; } + public string GetClosedIndirectLoansWithDetailByPostedDate { get; set; } + public string PostMonetaryTransactions { get; set; } + public string PutDealerOriginalBalance { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Configuration/RmsApiConfiguration.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Configuration/RmsApiConfiguration.cs new file mode 100644 index 0000000..c3648c6 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Configuration/RmsApiConfiguration.cs @@ -0,0 +1,10 @@ +namespace ReserveManagementSystemLib.Configuration +{ + public class RmsApiConfiguration + { + public string DefaultImportStartDate { get; set; } + public int DefaultBatchSize { get; set; } + public string DefaultConvertedLoanPostDate { get; set; } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Enums/ImportType.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Enums/ImportType.cs new file mode 100644 index 0000000..875610b --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Enums/ImportType.cs @@ -0,0 +1,8 @@ +namespace ReserveManagementSystemLib.Enums +{ + public enum ImportType + { + Active, + Closed + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Helpers/StringExtensions.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Helpers/StringExtensions.cs new file mode 100644 index 0000000..1b12a8f --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Helpers/StringExtensions.cs @@ -0,0 +1,17 @@ +namespace ReserveManagementSystemLib.Helpers +{ + public static class StringExtensions + { + public static string Truncate(this string inputString, int numberOfCharacters) + { + if (inputString.Length <= numberOfCharacters) + { + return inputString; + } + else + { + return inputString.Substring(0, numberOfCharacters); + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Helpers/ValidationHelper.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Helpers/ValidationHelper.cs new file mode 100644 index 0000000..8a83aa4 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Helpers/ValidationHelper.cs @@ -0,0 +1,34 @@ +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Components.Forms; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemLib.Helpers +{ + public class ValidationHelper + { + + public static List SetValidationErrors(Loan loanInfo) + { + var validator = GetValidatorForModel(); + var editContext = new EditContext(loanInfo); + + var context = new ValidationContext(editContext.Model); + var validationResults = validator.Validate(context); + + return new List(validationResults.Errors); + } + + private static IValidator GetValidatorForModel() + { + LoanValidator baseclass = new LoanValidator(); + var modelValidatorInstance = (IValidator)Activator.CreateInstance(baseclass.GetType()); + + return modelValidatorInstance; + + } + + } + + +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/AuditHistory.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/AuditHistory.cs new file mode 100644 index 0000000..368d45c --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/AuditHistory.cs @@ -0,0 +1,18 @@ +namespace ReserveManagementSystemLib.Models +{ + public class AuditHistory + { + [Key] + public int AuditId { get; set; } + public int? LoanId { get; set; } + public string MemberNumber { get; set; } + public string LoanNumber { get; set; } + public string ModifiedBy { get; set; } + public DateTime ModifiedDate { get; set; } + public string ActivityType { get; set; } + public string OldValue { get; set; } + public string NewValue { get; set; } + public string AuditDescription { get; set; } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/CalculationConfig.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/CalculationConfig.cs new file mode 100644 index 0000000..2e5a32d --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/CalculationConfig.cs @@ -0,0 +1,33 @@ +namespace ReserveManagementSystemLib.Models +{ + public class CalculationConfig + { + public readonly Loan _loanData; + + public CalculationConfig(Loan loanData) + { + _loanData = loanData; + } + + public Loan LoanData() { return _loanData; } + + public decimal BuydownRateDifference() { return _loanData.BuyRate - _loanData.ContractRate; } + + public decimal BuyFinanceCharge { get; set; } + + public decimal ContractFinanceCharge { get; set; } + + public int DaysToFirstPmt() { return (_loanData.FirstPmtDate - _loanData.LoanDate).Days - 30 > 0 ? (_loanData.FirstPmtDate - _loanData.LoanDate).Days - 30 : 0; } + + public decimal ReserveDiscount() { return _loanData.LoanType?.ReserveDiscount ?? 25M; } + + public decimal FlatFeePercent() { return _loanData.LoanType?.FlatFeePercent ?? 1M; } + + public decimal NoReserveThreshold() { return _loanData.LoanType?.NoReserveThreshold ?? 0.25M; } + + public decimal StdRate() { return (_loanData.ContractRate - _loanData.BuyRate); } + + } + +} + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Dealer.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Dealer.cs new file mode 100644 index 0000000..9af21a3 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Dealer.cs @@ -0,0 +1,32 @@ +namespace ReserveManagementSystemLib.Models +{ + public class Dealer + { + + public string DealerCode { get; set; } + + string _dealerName; + public string Name { get { return _dealerName?.ToUpper(); } set => _dealerName = value?.ToUpper(); } + + public string DealerGroupCode { get; set; } + + public string DealerAccountNumber { get; set; } + + [Key] + public string DealerOrganizationNumber { get; set; } + + public override bool Equals(object obj) + { + return obj is Dealer dealer && + DealerCode == dealer.DealerCode && + Name == dealer.Name?.ToUpper() && + DealerAccountNumber == dealer.DealerAccountNumber && + DealerOrganizationNumber == dealer.DealerOrganizationNumber; + } + + public override int GetHashCode() + { + return HashCode.Combine(DealerCode, DealerAccountNumber, DealerOrganizationNumber); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Import.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Import.cs new file mode 100644 index 0000000..bc17530 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Import.cs @@ -0,0 +1,22 @@ +namespace ReserveManagementSystemLib.Models +{ + public class Import + { + [Key] + public int ImportId { get; set; } + + public DateTime StartDate { get; set; } + + public DateTime? EndDate { get; set; } + + public int RecordCount { get; set; } + + public int RecordsSaved { get; set; } + + public bool Success { get; set; } + + public string FileName { get; set; } + + public string ImportType { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Loan.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Loan.cs new file mode 100644 index 0000000..18cbcf4 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Loan.cs @@ -0,0 +1,204 @@ +using FluentValidation; +using FluentValidation.Results; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ReserveManagementSystemLib.Models +{ + public class Loan + { + [Key] + public int LoanId { get; set; } + + public int ImportId { get; set; } + + public string MemberNumber { get; set; } + + public string LoanNumber { get; set; } + + public string MemberLastName { get; set; } + + public string MemberFirstName { get; set; } + + public string MemberInitial { get; set; } + + public string DealerOrganizationNumber { get; set; } + + public DateTime LoanDate { get; set; } + + public DateTime PostedDate { get; set; } + + public DateTime FirstPmtDate { get; set; } + + public DateTime FinalPmtDate { get; set; } + + public decimal LoanAmount { get; set; } + + public decimal BuyRate { get; set; } + + public decimal ContractRate { get; set; } + + public decimal MonthlyPmt { get; set; } + + public string LoanTypeId { get; set; } + + public int Term { get; set; } + + public decimal ReserveAmount { get; set; } + + public bool FlatFeePaid { get; set; } + + public decimal? ChargeBack { get; set; } + + public string ChargebackMessage { get; set; } + + public byte? RemainingMonthsAtClose { get; set; } + + public DateTime? PaidLoanDate { get; set; } + + public string ReserveMessage { get; set; } + + public bool ActionRequired { get; set; } + + public DateTime? StatementDate { get; set; } + + public decimal? Adjustment { get; set; } + + public string LoanStatus { get; set; } + + public DateTime? ExportedReserveDate { get; set; } + + public DateTime? ExportedChargebackDate { get; set; } + + public string ApplicationId { get; set; } + + public bool Warranty { get; set; } + + public bool Gap { get; set; } + + public bool CreditLife { get; set; } + + public bool CreditDisability { get; set; } + + public bool Mbp { get; set; } + + public string AccountType { get; set; } + + [ForeignKey("DealerOrganizationNumber")] + public Dealer Dealer { get; set; } + + [ForeignKey("LoanTypeId")] + public LoanType LoanType { get; set; } + + public Vehicle Vehicle { get; set; } + + [NotMapped] + public List ValidationErrors { get; set; } + + public bool HasSameReserve(object obj) + { + return obj is Loan loan && + LoanDate == loan.LoanDate && + Term == loan.Term && + FirstPmtDate == loan.FirstPmtDate && + FinalPmtDate == loan.FinalPmtDate && + LoanAmount == loan.LoanAmount && + LoanTypeId == loan.LoanTypeId && + BuyRate == loan.BuyRate && + ContractRate == loan.ContractRate; + } + + public bool IsBoatLoan(object obj) + { + return obj is Loan loan && + AccountType == "DL09" || AccountType == "DL10" || AccountType == "DL11" || AccountType == "DL12"; + } + + public override bool Equals(object obj) + { + return obj is Loan loan && + MemberNumber == loan.MemberNumber && + LoanNumber == loan.LoanNumber && + MemberLastName == loan.MemberLastName && + MemberFirstName == loan.MemberFirstName && + MemberInitial == loan.MemberInitial && + DealerOrganizationNumber == loan.DealerOrganizationNumber && + LoanDate == loan.LoanDate && + PostedDate == loan.PostedDate && + Term == loan.Term && + FirstPmtDate == loan.FirstPmtDate && + FinalPmtDate == loan.FinalPmtDate && + LoanAmount == loan.LoanAmount && + BuyRate == loan.BuyRate && + ContractRate == loan.ContractRate && + MonthlyPmt == loan.MonthlyPmt && + LoanTypeId == loan.LoanTypeId && + ApplicationId == loan.ApplicationId && + Warranty == loan.Warranty && + Gap == loan.Gap && + Mbp == loan.Mbp && + CreditLife == loan.CreditLife && + CreditDisability == loan.CreditDisability && + ActionRequired == loan.ActionRequired; + } + + public override int GetHashCode() + { + HashCode hash = new HashCode(); + hash.Add(MemberNumber); + hash.Add(LoanNumber); + hash.Add(MemberLastName); + hash.Add(MemberFirstName); + hash.Add(MemberInitial); + hash.Add(DealerOrganizationNumber); + hash.Add(LoanDate); + hash.Add(PostedDate); + hash.Add(FirstPmtDate); + hash.Add(FinalPmtDate); + hash.Add(LoanAmount); + hash.Add(BuyRate); + hash.Add(ContractRate); + hash.Add(MonthlyPmt); + hash.Add(LoanTypeId); + hash.Add(ApplicationId); + hash.Add(Warranty); + hash.Add(Gap); + hash.Add(Mbp); + hash.Add(CreditLife); + hash.Add(CreditDisability); + hash.Add(ActionRequired); + return hash.ToHashCode(); + } + } + public class LoanValidator : AbstractValidator + { + public LoanValidator() + { + + RuleFor(m => m.LoanTypeId).NotEmpty().WithMessage("Loan type cannot be empty!"); + RuleFor(m => m.DealerOrganizationNumber).NotEmpty().WithMessage("Dealer Organization Number cannot be empty!"); + When(m => m.Dealer != null && string.IsNullOrEmpty(m.Dealer.DealerCode), () => + { + RuleFor(m => m.Dealer.DealerCode).NotEmpty().WithMessage("Dealer Code cannot be empty!"); + + }); + + When(m => m.Dealer != null && string.IsNullOrEmpty(m.Dealer.DealerAccountNumber), () => + { + RuleFor(m => m.Dealer.DealerAccountNumber).NotEmpty().WithMessage("Dealer Account Number cannot be empty!"); + }); + + When(m => m.Dealer != null && string.IsNullOrEmpty(m.Dealer.Name), () => + { + RuleFor(m => m.Dealer.Name).NotEmpty().WithMessage("Dealer Name cannot be empty!"); + }); + + When(m => m.Vehicle != null && string.IsNullOrEmpty(m.Vehicle.VINNumber), () => + { + RuleFor(m => m.Vehicle.VINNumber).NotEmpty().WithMessage("VIN cannot be empty!"); + }); + + RuleFor(m => m.LoanDate).LessThanOrEqualTo(m => m.FirstPmtDate).WithMessage("First Payment Date cannot be before Loan Date!"); + } + } +} + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanChargeback.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanChargeback.cs new file mode 100644 index 0000000..b117188 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanChargeback.cs @@ -0,0 +1,11 @@ +namespace ReserveManagementSystemLib.Models +{ + public class LoanChargeback + { + public decimal ChargebackAmount { get; set; } + + public string ChargebackTypeMessage { get; set; } + + public byte? RemainingMonthsAtClose { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanChargebackInput.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanChargebackInput.cs new file mode 100644 index 0000000..c21ba5e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanChargebackInput.cs @@ -0,0 +1,22 @@ +namespace ReserveManagementSystemLib.Models +{ + public class LoanChargebackInput + { + public DateTime LoanDate { get; set; } + + public DateTime FirstPmtDate { get; set; } + + public DateTime FinalPmtDate { get; set; } + + public string LoanType { get; set; } + + public decimal ReserveAmount { get; set; } + + public DateTime? PaidLoanDate { get; set; } + + public int Term { get; set; } + + public string AccountType { get; set; } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanFilter.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanFilter.cs new file mode 100644 index 0000000..167c5de --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanFilter.cs @@ -0,0 +1,89 @@ +namespace ReserveManagementSystemLib.Models +{ + public class LoanFilter + { + public bool? ActionRequired { get; set; } + + public string DealerCodeText { get; set; } + + public string MemberNumberText { get; set; } + + public string MemberNameText { get; set; } + + public string LoanNumberText { get; set; } + + public string ApplicationIdText { get; set; } + + public int? ImportId { get; set; } + + public DateTime? LoanDateStart { get; set; } + + public DateTime? LoanDateEnd { get; set; } + + public int PageIndex { get; set; } + + public int MaxRecordsPerPage { get; set; } + + public int MaxPagesPerRange { get; set; } + + public string SortField { get; set; } + + public string SortDirection { get; set; } + + public IQueryable ApplyFilter(IQueryable query) + { + if (!string.IsNullOrEmpty(MemberNumberText)) + { + query = query.Where(x => x.MemberNumber.Contains(this.MemberNumberText)); + } + + if (!string.IsNullOrEmpty(MemberNameText)) + { + query = query.Where(x => x.MemberFirstName.Contains(this.MemberNameText.ToUpper()) || x.MemberLastName.Contains(this.MemberNameText.ToUpper())); + } + + if (!string.IsNullOrEmpty(LoanNumberText)) + { + query = query.Where(x => x.LoanNumber.Contains(this.LoanNumberText)); + } + + if (!string.IsNullOrEmpty(ApplicationIdText)) + { + query = query.Where(x => x.ApplicationId.Contains(this.ApplicationIdText)); + } + + if (ImportId.HasValue) + { + query = query.Where(x => x.ImportId == ImportId.Value); + } + + if (ActionRequired.HasValue && !string.IsNullOrEmpty(ActionRequired.ToString())) + { + query = query.Where(x => x.ActionRequired == (this.ActionRequired)); + } + + if (!string.IsNullOrEmpty(DealerCodeText)) + { + query = query.Where(x => x.Dealer.DealerCode == (this.DealerCodeText)); + } + + if (LoanDateStart.HasValue || LoanDateEnd.HasValue) + { + if (LoanDateStart.HasValue && LoanDateEnd.HasValue) + { + query = query.Where(x => x.LoanDate >= LoanDateStart.Value && x.LoanDate <= LoanDateEnd.Value); + } + else if (LoanDateStart.HasValue) + { + query = query.Where(x => x.LoanDate >= LoanDateStart.Value); + } + else if (LoanDateEnd.HasValue) + { + query = query.Where(x => x.LoanDate <= LoanDateEnd.Value); + } + } + + return query; + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanReserve.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanReserve.cs new file mode 100644 index 0000000..aa35da8 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanReserve.cs @@ -0,0 +1,12 @@ +namespace ReserveManagementSystemLib.Models +{ + public class LoanReserve + { + public decimal ReserveAmount { get; set;} + + public string ReserveTypeMessage { get; set; } + + public bool FlatFeePaid { get; set; } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanReserveInput.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanReserveInput.cs new file mode 100644 index 0000000..8eb5349 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanReserveInput.cs @@ -0,0 +1,22 @@ +namespace ReserveManagementSystemLib.Models +{ + public class LoanReserveInput + { + public DateTime LoanDate { get; set; } + + public DateTime FirstPmtDate { get; set; } + + public decimal LoanAmount { get; set; } + + public decimal BuyRate { get; set; } + + public decimal ContractRate { get; set; } + + public int OriginalTermInDays { get; set; } + + public string LoanType { get; set; } + + public string AccountType { get; set; } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanType.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanType.cs new file mode 100644 index 0000000..92aed83 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/LoanType.cs @@ -0,0 +1,22 @@ +namespace ReserveManagementSystemLib.Models +{ + public class LoanType + { + [Key] + public string LoanTypeId{ get; set; } + + public string LoanComment { get; set; } + + public string ChargebackMethod { get; set; } + + public short NoChargebackDays { get; set; } + + public short NoChargebackPayments { get; set; } + + public decimal ReserveDiscount { get; set; } + + public decimal FlatFeePercent { get; set; } + + public decimal NoReserveThreshold { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/MinorAccountType.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/MinorAccountType.cs new file mode 100644 index 0000000..29ea612 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/MinorAccountType.cs @@ -0,0 +1,13 @@ +namespace ReserveManagementSystemLib.Models +{ + public class MinorAccountType + { + [Key] + public int MinorAccountTypeId{ get; set; } + + public string AccountType { get; set; } + + public string GeneralLedgerAccountNumber { get; set; } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/PaginatedListResponse.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/PaginatedListResponse.cs new file mode 100644 index 0000000..9ce760a --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/PaginatedListResponse.cs @@ -0,0 +1,78 @@ +namespace ReserveManagementSystemLib.Models +{ + public class PaginatedListResponse + { + public List LoanList { get; set; } + + public int PageIndex { get; set; } + + public int TotalPages { get; set; } + + public int TotalItems { get; set; } + + public int FirstPage { get; set; } + + public int LastPage { get; set; } + + public int PageRangeSize { get; set; } + + public PaginatedListResponse() {} + + public void SetFirstAndLastPageToShow() + { + if (TotalPages < PageRangeSize) { PageRangeSize = TotalPages; } + var half = PageRangeSize / 2; //this should be set to an odd number to evenly set page range + if (PageIndex - half <= 0) + { + FirstPage = 1; + LastPage = (PageRangeSize > TotalPages) ? TotalPages : PageRangeSize; + } + else if (PageIndex == TotalPages || PageIndex + half > TotalPages) + { + FirstPage = TotalPages - PageRangeSize + 1; + LastPage = TotalPages; + } + else //increment page range slightly by the half difference + { + FirstPage = PageIndex - half; + LastPage = PageIndex + half; + } + } + + public PaginatedListResponse(List items, int count, int pageIndex, int pageSize, int pageRangeSize) + { + PageIndex = pageIndex; + TotalItems = count; + TotalPages = (int)Math.Ceiling(count / (double)pageSize); + PageRangeSize = pageRangeSize; + + SetFirstAndLastPageToShow(); + List loanList = new List(); + loanList.AddRange(items); + LoanList = loanList; + } + + public bool HasPreviousPage + { + get + { + return (PageIndex > 1); + } + } + + public bool HasNextPage + { + get + { + return (PageIndex < TotalPages); + } + } + + public static async Task CreateListAsync(IQueryable source, int pageIndex, int pageSize, int pageRangeSize) + { + var count = source.Count(); + var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); + return new PaginatedListResponse(items, count, pageIndex, pageSize, pageRangeSize); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/RmsActiveImportRequest.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/RmsActiveImportRequest.cs new file mode 100644 index 0000000..5204df4 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/RmsActiveImportRequest.cs @@ -0,0 +1,8 @@ +namespace ReserveManagementSystemLib.Models +{ + public class RmsActiveImportRequest + { + public string ImportDateTime { get; set; } + public bool SkipExport { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Vehicle.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Vehicle.cs new file mode 100644 index 0000000..85845a5 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Models/Vehicle.cs @@ -0,0 +1,38 @@ +namespace ReserveManagementSystemLib.Models +{ + public class Vehicle + { + [Key] + public int VehicleId { get; set; } + + public int? LoanId { get; set; } + + public string VINNumber { get; set; } + + public string Make { get; set; } + + public string Model { get; set; } + + public string Year { get; set; } + + public double Mileage { get; set; } + + public string Color { get; set; } + + public override bool Equals(object obj) + { + return obj is Vehicle vehicle && + VINNumber == vehicle.VINNumber && + Make == vehicle.Make && + Model == vehicle.Model && + Year == vehicle.Year && + Mileage == vehicle.Mileage && + Color == vehicle.Color; + } + + public override int GetHashCode() + { + return HashCode.Combine(VINNumber, Make, Model, Year, Mileage, Color); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Persistence/AppDbContext.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Persistence/AppDbContext.cs new file mode 100644 index 0000000..8f7fd4b --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Persistence/AppDbContext.cs @@ -0,0 +1,42 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemLib.Persistence +{ + public class AppDbContext : DbContext + { + public DbSet Loans { get; set; } + + public DbSet LoanTypes { get; set; } + + public DbSet Imports { get; set; } + + public DbSet AuditHistories { get; set; } + + public DbSet Dealers { get; set; } + + public DbSet Vehicles { get; set; } + + public DbSet MinorAccountTypes { get; set; } + + public AppDbContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity().ToTable("Loan"); + + builder.Entity().ToTable("LoanType"); + + builder.Entity().ToTable("Import"); + + builder.Entity().ToTable("LoanAudit"); + + builder.Entity().ToTable("Dealer"); + + builder.Entity().ToTable("Vehicle"); + + builder.Entity().ToTable("MinorAccountType"); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/AuditHistoryRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/AuditHistoryRepository.cs new file mode 100644 index 0000000..96bb991 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/AuditHistoryRepository.cs @@ -0,0 +1,41 @@ +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; + +namespace ReserveManagementSystemLib.Repositories +{ + public class AuditHistoryRepository: BaseRepository, IAuditHistoryRepository + { + public AuditHistoryRepository(AppDbContext context) : base(context) { } + + public IQueryable GetAll() + { + return _context.AuditHistories; + } + + public async Task FindByIdAsync(int id) + { + return await _context.AuditHistories.FirstOrDefaultAsync(l => l.AuditId == id); + } + + public async Task AddHistoryAsync(AuditHistory history) + { + try + { + var newAudit = await _context.AuditHistories.AddAsync(history); + await _context.SaveChangesAsync(); + return newAudit.Entity; + } + catch (Exception ex) + { + Log.Error(ex, $"Failure to add audit history"); + return null; + } + } + + public async Task AddHistoriesAsync(List histories) + { + await _context.AuditHistories.AddRangeAsync(histories); + return await _context.SaveChangesAsync(); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/BaseRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/BaseRepository.cs new file mode 100644 index 0000000..0d9a6a8 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/BaseRepository.cs @@ -0,0 +1,14 @@ +using ReserveManagementSystemLib.Persistence; + +namespace ReserveManagementSystemLib.Repositories +{ + public class BaseRepository + { + protected readonly AppDbContext _context; + + public BaseRepository(AppDbContext context) + { + _context = context; + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/DealerRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/DealerRepository.cs new file mode 100644 index 0000000..095ebef --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/DealerRepository.cs @@ -0,0 +1,47 @@ +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; + +namespace ReserveManagementSystemLib.Repositories +{ + public class DealerRepository : BaseRepository, IDealerRepository + { + public DealerRepository(AppDbContext context) : base(context) { } + + public async Task FindByCodeAsync(string code) + { + return await _context.Dealers.FirstOrDefaultAsync(d => d.DealerCode == code); + } + + public IQueryable GetAll() + { + return _context.Dealers; + } + + public async Task AddDealerAsync(Dealer dealer) + { + try + { + var newDealer = await _context.Dealers.AddAsync(dealer); + await _context.SaveChangesAsync(); + return newDealer.Entity; + } + catch (Exception ex) + { + Log.Error(ex, $"Failure to add dealer."); + return null; + } + } + + public async Task UpdateDealersAsync(List dealers) + { + _context.Dealers.UpdateRange(dealers); + return await _context.SaveChangesAsync(); + } + + public async Task AddDealersAsync(List dealers) + { + _context.Dealers.AddRange(dealers); + return await _context.SaveChangesAsync(); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IAuditHistoryRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IAuditHistoryRepository.cs new file mode 100644 index 0000000..63d708d --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IAuditHistoryRepository.cs @@ -0,0 +1,15 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemLib.Repositories +{ + public interface IAuditHistoryRepository + { + IQueryable GetAll(); + + Task FindByIdAsync(int id); + + Task AddHistoryAsync(AuditHistory history); + + Task AddHistoriesAsync(List histories); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IDealerRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IDealerRepository.cs new file mode 100644 index 0000000..5de9178 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IDealerRepository.cs @@ -0,0 +1,17 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemLib.Repositories +{ + public interface IDealerRepository + { + Task FindByCodeAsync(string dealerCode); + + IQueryable GetAll(); + + Task AddDealerAsync(Dealer dealer); + + Task UpdateDealersAsync(List dealers); + + Task AddDealersAsync(List dealers); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IImportRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IImportRepository.cs new file mode 100644 index 0000000..88a4d76 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IImportRepository.cs @@ -0,0 +1,18 @@ +using ReserveManagementSystemLib.Enums; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemLib.Repositories +{ + public interface IImportRepository + { + IQueryable GetAll(); + + Task FindByIdAsync(int id); + + Task AddImportAsync(Import import); + + Task UpdateImportAsync(Import import); + + IQueryable GetLastLoanDnaImportByType(ImportType importType); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ILoanRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ILoanRepository.cs new file mode 100644 index 0000000..f4ce2c5 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ILoanRepository.cs @@ -0,0 +1,17 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemLib.Repositories +{ + public interface ILoanRepository + { + IQueryable GetAll(); + + IQueryable GetAllWithoutDealer(); + + Task FindByIdAsync(int id); + + Loan Add(Loan loan); + + Task UpdateAsync(IEnumerable loans); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ILoanTypeRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ILoanTypeRepository.cs new file mode 100644 index 0000000..a74648a --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ILoanTypeRepository.cs @@ -0,0 +1,11 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemLib.Repositories +{ + public interface ILoanTypeRepository + { + Task> GetLoanTypesAsync(); + + Task GetLoanTypeAsync(string loanTypeId); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IMinorAccountTypeRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IMinorAccountTypeRepository.cs new file mode 100644 index 0000000..dd4356b --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IMinorAccountTypeRepository.cs @@ -0,0 +1,9 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemLib.Repositories +{ + public interface IMinorAccountTypeRepository + { + MinorAccountType GetMinorAccountType(string accountTypeId); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IVehicleRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IVehicleRepository.cs new file mode 100644 index 0000000..52d40f1 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/IVehicleRepository.cs @@ -0,0 +1,10 @@ +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemLib.Repositories +{ + public interface IVehicleRepository + { + Task FindByLoanIdAsync(int? loanId); + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ImportRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ImportRepository.cs new file mode 100644 index 0000000..72afd47 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/ImportRepository.cs @@ -0,0 +1,49 @@ +using ReserveManagementSystemLib.Enums; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; + +namespace ReserveManagementSystemLib.Repositories +{ + public class ImportRepository: BaseRepository, IImportRepository + { + public ImportRepository(AppDbContext context) : base(context) { } + + public IQueryable GetAll() + { + return _context.Imports; + } + + public async Task FindByIdAsync(int id) + { + return await _context.Imports.FirstOrDefaultAsync(l => l.ImportId == id); + } + + public async Task AddImportAsync(Import import) + { + try + { + var newImport = await _context.Imports.AddAsync(import); + await _context.SaveChangesAsync(); + return newImport.Entity; + } + catch (Exception ex) + { + Log.Error(ex, $"Failure to add import"); + return null; + } + } + + public async Task UpdateImportAsync(Import import) + { + var updatedImport = _context.Imports.Update(import); + await _context.SaveChangesAsync(); + return updatedImport.Entity; + } + + public IQueryable GetLastLoanDnaImportByType(ImportType importType) + { + var result = _context.Imports.Where(i => i.ImportType == importType.ToString() && i.Success == true && i.FileName == null).OrderByDescending(i => i.StartDate); + return result; + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/LoanRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/LoanRepository.cs new file mode 100644 index 0000000..4310c23 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/LoanRepository.cs @@ -0,0 +1,40 @@ +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; + +namespace ReserveManagementSystemLib.Repositories +{ + public class LoanRepository : BaseRepository, ILoanRepository + { + public LoanRepository(AppDbContext context) : base(context) { } + + public IQueryable GetAll() + { + return _context.Loans.Include(d => d.Dealer).Include(lt => lt.LoanType).Include("Vehicle"); + } + + public IQueryable GetAllWithoutDealer() + { + return _context.Loans.Include(lt => lt.LoanType).Include("Vehicle"); + } + + public async Task FindByIdAsync(int id) + { + return await _context.Loans.Where(l => l.LoanId == id).Include(d => d.Dealer).Include(lt => lt.LoanType).Include("Vehicle").FirstOrDefaultAsync(); + } + + public Loan Add(Loan loan) + { + var result =_context.Loans.Add(loan); + _context.SaveChanges(); + + return result.Entity; + } + + public async Task UpdateAsync(IEnumerable loans) + { + _context.Loans.UpdateRange(loans); + + await _context.SaveChangesAsync(); //can use this for update and add + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/LoanTypeRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/LoanTypeRepository.cs new file mode 100644 index 0000000..e00ce5b --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/LoanTypeRepository.cs @@ -0,0 +1,20 @@ +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; + +namespace ReserveManagementSystemLib.Repositories +{ + public class LoanTypeRepository: BaseRepository, ILoanTypeRepository + { + public LoanTypeRepository(AppDbContext context) : base(context) { } + + public async Task> GetLoanTypesAsync() + { + return await _context.LoanTypes.ToListAsync(); + } + + public async Task GetLoanTypeAsync(string loanTypeId) + { + return await _context.LoanTypes.FirstOrDefaultAsync(x => x.LoanTypeId == loanTypeId); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/MinorAccountTypeRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/MinorAccountTypeRepository.cs new file mode 100644 index 0000000..40aa58b --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/MinorAccountTypeRepository.cs @@ -0,0 +1,20 @@ +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; + +namespace ReserveManagementSystemLib.Repositories +{ + public class MinorAccountTypeRepository : BaseRepository, IMinorAccountTypeRepository + { + public MinorAccountTypeRepository(AppDbContext context) : base(context) { } + + /// + /// Try to get this to work asynchronously like Loan Types will have to add a get all and then call get by id async + /// + /// + /// + public MinorAccountType GetMinorAccountType(string accountTypeId) + { + return _context.MinorAccountTypes.FirstOrDefault(x => x.AccountType == accountTypeId); + } + } +} \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/VehicleRepository.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/VehicleRepository.cs new file mode 100644 index 0000000..17fd49b --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Repositories/VehicleRepository.cs @@ -0,0 +1,16 @@ +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemLib.Persistence; + +namespace ReserveManagementSystemLib.Repositories +{ + public class VehicleRepository : BaseRepository, IVehicleRepository + { + public VehicleRepository(AppDbContext context) : base(context) { } + + public async Task FindByLoanIdAsync(int? loanId) + { + return await _context.Vehicles.FirstOrDefaultAsync(v => v.LoanId == loanId); + } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/ReserveManagementSystemLib.csproj b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/ReserveManagementSystemLib.csproj new file mode 100644 index 0000000..7016a0b --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/ReserveManagementSystemLib.csproj @@ -0,0 +1,53 @@ + + + + net6.0 + true + 2.2.1 + + 1.1.0 Updated Loan Model, Add History Model + 1.1.1 Added Loan Filter + 1.1.2 Updated Loan and Import Models + 1.1.3 Updated Loan Audit Table/Model Add Repo + 1.1.4 More updates to Audit History table/model + 1.1.5 Update to LoanFilter model + 1.1.6 Pagination model + 1.1.7 Changes to Loan, Import, and AuditHistory models + 1.1.8 Change to LoanFilter model and Get Filtered Loans return type. + 1.1.9 Added Dealer + 1.1.10 Update to LoanFilter model and Paging model. + 1.1.11 Update to Loan model for Vehicle. + 1.1.12 Update to Vehicle model. + 1.1.13 Update to Loan Model + 1.2.0 Updated models to add LoanNumber + 1.2.1 Updated model for Filter and Paging + 1.2.2 Updated Filter to add LoanDateStart and LoanDateEnd, took out Names + 1.2.3 Added MemberNameText to LoanFilter + 1.2.4 Added LoanValidator to Model. + 1.2.5 Added Validation Results to Model. + 1.2.6 Changes to Loan and Dealer Model. + 1.2.7 Changes to Loan and Dealer Model for all caps. Validation changes. + 1.2.8 Changes to Loan Model + 1.2.9 Changes to Loan and Dealer Model + 1.2.10 Adding ApplicationId to LoanFilter + 1.2.11 Changes to Dealer and Loan Model + 1.2.12 Changes to Loan and Import models + 1.2.13 Changed Term on Loan to int + 2.0.0 Add client name to CoreApiConfiguration, fix typo in PostMonetaryTransactions + 2.1.0 Add ApiTimeOut setting for HttpClient + 2.2.0 Updates Core Api model + 2.2.1 Added queue for Active Imports + + + + + + + + + + + + + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Using.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Using.cs new file mode 100644 index 0000000..d744581 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/ReserveManagementSystemLib/Using.cs @@ -0,0 +1,9 @@ +global using Microsoft.EntityFrameworkCore; +global using Serilog; +global using System; +global using System.Collections.Generic; +global using System.ComponentModel.DataAnnotations; +global using System.Linq; +global using System.Threading.Tasks; + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-k8s-pipeline.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-k8s-pipeline.yaml new file mode 100644 index 0000000..24cc0a7 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-k8s-pipeline.yaml @@ -0,0 +1,74 @@ +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.reservemanagementsystem-reservemanagementsystem + +resources: + repositories: + - repository: azdo_pipeline_templates + type: git + name: Infrastructure Platform/azdo-pipeline-templates + ref: main + +stages: +# DOTNET BUILD AND NUGET PUSH +- template: dotnet-nuget-build-push.yaml@azdo_pipeline_templates + parameters: + azdo_agent_pool: $(pvar_azdo_agent_pool_nuget) + azure_artifacts_feed: 'LCU-Internal' + branches_to_push: + - develop + - main + +# DACPAC BUILD AND PUSH +- template: db-build-deploy.yaml@azdo_pipeline_templates + parameters: + azdo_agent_pool: $(pvar_azdo_agent_pool_db) + azdo_deployment_environment: $(pvar_azdo_deployment_environment_db) + sql_project_path: './Api/ReserveManagementSystemApi.Sql/ReserveManagementSystemApi.Sql.sqlproj' + built_sql_dacpac_file_path: './Api/ReserveManagementSystemApi.Sql/bin/Release/ReserveManagementSystemApi.Sql.dacpac' + name_of_artifact: ReserveManagementSystemApi_Sql_dacpac + db_name: $(pvar_ReserveManagementSystemApi_Sql_db_name) + sql_server_name: $(pvar_ReserveManagementSystemApi_Sql_sql_server_name) + +# 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: ./Api/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: ./Api/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.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-feature-branches.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-feature-branches.yaml new file mode 100644 index 0000000..ee44aef --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/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.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-prd.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-prd.yaml new file mode 100644 index 0000000..7d13684 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-prd.yaml @@ -0,0 +1,9 @@ +trigger: + branches: + include: + - 'main' +extends: + template: azure-k8s-pipeline.yaml + parameters: + target_environment: prd + k8s_namespace: landmarkcu diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-qa.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-qa.yaml new file mode 100644 index 0000000..f581dd4 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-qa.yaml @@ -0,0 +1,9 @@ +trigger: + branches: + include: + - 'develop' +extends: + template: azure-k8s-pipeline.yaml + parameters: + target_environment: qa + k8s_namespace: landmarkcu diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-uat.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-uat.yaml new file mode 100644 index 0000000..4747234 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/azure-pipelines-uat.yaml @@ -0,0 +1,9 @@ +trigger: + branches: + include: + - 'main' +extends: + template: azure-k8s-pipeline.yaml + parameters: + target_environment: uat + k8s_namespace: landmarkcu diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/prd-pvars.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/prd-pvars.yaml new file mode 100644 index 0000000..08bb21f --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/prd-pvars.yaml @@ -0,0 +1,12 @@ +--- +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 + pvar_azdo_agent_pool_db: LCUOnPrem + pvar_azdo_deployment_environment_db: DB-PRD + + pvar_ReserveManagementSystemApi_Sql_db_name: ReserveManagementSystem + pvar_ReserveManagementSystemApi_Sql_sql_server_name: D1CLS01P diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/qa-pvars.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/qa-pvars.yaml new file mode 100644 index 0000000..50f7578 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/qa-pvars.yaml @@ -0,0 +1,12 @@ +--- +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 + pvar_azdo_agent_pool_db: LCUOnPrem + pvar_azdo_deployment_environment_db: DB-QA + + pvar_ReserveManagementSystemApi_Sql_db_name: ReserveManagementSystem + pvar_ReserveManagementSystemApi_Sql_sql_server_name: DAZCDTBWEB01Q diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/uat-pvars.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/uat-pvars.yaml new file mode 100644 index 0000000..5bca69e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/azdo_pipelines/config-files/uat-pvars.yaml @@ -0,0 +1,12 @@ +--- +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 + pvar_azdo_agent_pool_db: LCUOnPrem + pvar_azdo_deployment_environment_db: DB-UAT + + pvar_ReserveManagementSystemApi_Sql_db_name: ReserveManagementSystem + pvar_ReserveManagementSystemApi_Sql_sql_server_name: D1CLS01U diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/Deployment.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/Deployment.yaml new file mode 100644 index 0000000..2f8991e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/Deployment.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: reserve-management-system-api +spec: + replicas: 1 + selector: + matchLabels: + app: reserve-management-system-api + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: reserve-management-system-api + spec: + volumes: + - name: volume-lcu-internal-cert + configMap: + name: configmap-lcu-internal-cert + containers: + - name: reserve-management-system-api + env: [] + volumeMounts: + - name: volume-lcu-internal-cert + mountPath: /etc/ssl/certs/lcu-internal-cert.pem + subPath: lcu-internal-cert.pem + image: reserve-management-system-api + resources: + requests: + cpu: "50m" + memory: "512Mi" + limits: + memory: "512Mi" + ports: + - containerPort: 80 + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + httpGet: + path: /api/status + port: 80 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + httpGet: + path: /api/status + port: 80 + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/Service.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/Service.yaml new file mode 100644 index 0000000..052dea5 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/Service.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: reserve-management-system-api +spec: + selector: + app: reserve-management-system-api + ports: + - port: 80 + targetPort: 80 diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/kustomization.yaml new file mode 100644 index 0000000..65a10a4 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/kustomization.yaml @@ -0,0 +1,9 @@ +resources: + - Deployment.yaml + - Service.yaml + - virtual-service.yaml + - secrets-provider-class.yaml + +images: +- name: reserve-management-system-api + newName: landmarkcu/lcu.api.internal.reservemanagementsystem-reservemanagementsystem \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/secrets-provider-class.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/secrets-provider-class.yaml new file mode 100644 index 0000000..f617302 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/secrets-provider-class.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: reserve-management-system-api-csi-key-vault-secrets-provider +spec: + provider: azure + parameters: + usePodIdentity: "false" + useVMManagedIdentity: "true" + tenantId: 63cd1554-2783-4a86-ae50-b9e6c21c9499 \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/virtual-service.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/virtual-service.yaml new file mode 100644 index 0000000..9f46a54 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/base/virtual-service.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: reserve-management-system-api +spec: + hosts: [] + gateways: + - istio-system/ingress-gateway + http: + - name: reserve-management-system-api + match: + - uri: + prefix: / + route: + - destination: + host: reserve-management-system-api + port: + number: 80 diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/kustomization.yaml new file mode 100644 index 0000000..0ae1552 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +patches: + - target: + kind: Deployment + path: set_secrets_deployment.yaml + - target: + kind: SecretProviderClass + path: set_secrets_provider.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/set_secrets_deployment.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/set_secrets_deployment.yaml new file mode 100644 index 0000000..87c25e1 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/set_secrets_deployment.yaml @@ -0,0 +1,49 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: reserve-management-system-api-secrets + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: reserve-management-system-api-csi-key-vault-secrets-provider + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: reserve-management-system-api-secrets + mountPath: /vault/secrets/reserve-management-system-seq-api-key.json + readOnly: true + subPath: reserve-management-system-seq-api-key + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: reserve-management-system-api-secrets + mountPath: /vault/secrets/reserve-management-system-rms-database-connection.json + readOnly: true + subPath: reserve-management-system-rms-database-connection + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: reserve-management-system-api-secrets + mountPath: /vault/secrets/reserve-management-system-connection-configuration-queue-username.json + readOnly: true + subPath: reserve-management-system-connection-configuration-queue-username + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: reserve-management-system-api-secrets + mountPath: /vault/secrets/reserve-management-system-connection-configuration-queue-password.json + readOnly: true + subPath: reserve-management-system-connection-configuration-queue-password + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: reserve-management-system-api-secrets + mountPath: /vault/secrets/reserve-management-system-api-security-configuration-internal-api-key-set.json + readOnly: true + subPath: reserve-management-system-api-security-configuration-internal-api-key-set \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/set_secrets_provider.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/set_secrets_provider.yaml new file mode 100644 index 0000000..7d137b0 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/keyvaults/normal/set_secrets_provider.yaml @@ -0,0 +1,24 @@ +- op: add + path: /spec/parameters/objects + value: | + array: + - | + objectName: reserve-management-system-seq-api-key + objectType: secret + objectVersion: "" + - | + objectName: reserve-management-system-rms-database-connection + objectType: secret + objectVersion: "" + - | + objectName: reserve-management-system-connection-configuration-queue-username + objectType: secret + objectVersion: "" + - | + objectName: reserve-management-system-connection-configuration-queue-password + objectType: secret + objectVersion: "" + - | + objectName: reserve-management-system-api-security-configuration-internal-api-key-set + objectType: secret + objectVersion: "" \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/appsettings/appsettings.json b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/appsettings/appsettings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/appsettings/appsettings.json @@ -0,0 +1 @@ +{} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/kustomization.yaml new file mode 100644 index 0000000..ae1d1a4 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +configMapGenerator: + - name: reserve-management-system-api-workload-appsettings + files: + - appsettings/appsettings.json +generatorOptions: + disableNameSuffixHash: true + +patches: + - target: + kind: Deployment + path: set_appsettings_deployment.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/set_appsettings_deployment.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/set_appsettings_deployment.yaml new file mode 100644 index 0000000..cc5e581 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/components/workloads/normal/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: volume-workload-appsettings + configMap: + name: reserve-management-system-api-workload-appsettings + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: volume-workload-appsettings + mountPath: /app/config/workload-appsettings diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/appsettings/appsettings.json b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/appsettings/appsettings.json new file mode 100644 index 0000000..930d707 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/appsettings/appsettings.json @@ -0,0 +1,22 @@ + { + "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" + ], + "FeatureFlags": { + "RunRmsActiveLoanConsumer": true, + "RunWithQuorumQueues": false + } + }, + "ConnectionConfiguration": { + "HostUrl": "d1msg01q.lcu-internal.com" + }, + "CoreApiConfiguration": { + "ApiTimeOutInSeconds": 420, + "BaseUrl": "https://qa-core-api-wrapper-api.apps.azure.lcu-internal.com/" + } + } diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/kustomization.yaml new file mode 100644 index 0000000..0616f74 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/kustomization.yaml @@ -0,0 +1,12 @@ +resources: + - ../../../base +patches: + - target: + kind: Deployment + path: set_appsettings_deployment.yaml + - target: + kind: SecretProviderClass + path: set_secrets_provider_class.yaml +images: +- name: reserve-management-system-api + newName: landmarkcu/lcu.api.internal.reservemanagementsystem-reservemanagementsystem diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/set_appsettings_deployment.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/set_appsettings_deployment.yaml new file mode 100644 index 0000000..59eeef4 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/set_appsettings_deployment.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: reserve-management-system-api +spec: + template: + spec: + containers: + - name: reserve-management-system-api + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Development" diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/set_secrets_provider_class.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/set_secrets_provider_class.yaml new file mode 100644 index 0000000..0f3d934 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/common/set_secrets_provider_class.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: reserve-management-system-api-csi-key-vault-secrets-provider +spec: + parameters: + userAssignedIdentityID: 3286ed9a-f85c-4c50-b742-dbd3c1c3fe1f + keyvaultName: kv-lz-apps-qa-uscz diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/kustomization.yaml new file mode 100644 index 0000000..96febc8 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/kustomization.yaml @@ -0,0 +1,14 @@ +resources: + - normal + +configMapGenerator: + - name: reserve-management-system-api-environment-appsettings + files: + - appsettings/appsettings.json +generatorOptions: + disableNameSuffixHash: true + +patches: + - target: + kind: Deployment + path: set_configmap_deployment.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/normal/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/normal/kustomization.yaml new file mode 100644 index 0000000..c8ce4f0 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/normal/kustomization.yaml @@ -0,0 +1,6 @@ +resources: + - ../common +patches: + - target: + kind: VirtualService + path: set_virtual_service.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/normal/set_virtual_service.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/normal/set_virtual_service.yaml new file mode 100644 index 0000000..6abd407 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/normal/set_virtual_service.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: reserve-management-system-api +spec: + hosts: + - qa-reserve-management-system-api.apps.azure.lcu-internal.com diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/set_configmap_deployment.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/set_configmap_deployment.yaml new file mode 100644 index 0000000..d234917 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/local/set_configmap_deployment.yaml @@ -0,0 +1,12 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: volume-environment-appsettings + configMap: + name: reserve-management-system-api-environment-appsettings + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: volume-environment-appsettings + mountPath: /app/config/environment-appsettings diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/appsettings/appsettings.json b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/appsettings/appsettings.json new file mode 100644 index 0000000..cb95d49 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/appsettings/appsettings.json @@ -0,0 +1,25 @@ +{ + "Primary": { + "SeqUrl": "https://seq.lcu-internal.com", + "CorsAllowedOrigins": [ + "http://lcu-internal.com", + "https://lcu-internal.com" + ], + "FeatureFlags": { + "RunRmsActiveLoanConsumer": true, + "EnableImportExportActive": true, + "EnableImportExportClosed": true + } + }, + "ConnectionConfiguration": { + "HostUrl": "rabbitmq.lcu-internal.com" + }, + "CoreApiConfiguration": { + "ApiTimeOutInSeconds": 420, + "BaseUrl": "https://prd-core-api-wrapper-api.apps.azure.lcu-internal.com" + }, + "HostedServices": { + "ImportExportActiveCronSchedule": "0 19 * * 0-6", + "ImportExportClosedCronSchedule": "15 19 * * 0-6" + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/component/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/component/kustomization.yaml new file mode 100644 index 0000000..6e732a6 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/component/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +patches: + - target: + kind: SecretProviderClass + path: set_secrets_provider_class.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/component/set_secrets_provider_class.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/component/set_secrets_provider_class.yaml new file mode 100644 index 0000000..c455781 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/component/set_secrets_provider_class.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: reserve-management-system-api-csi-key-vault-secrets-provider +spec: + parameters: + userAssignedIdentityID: 9eaefc49-e5d3-48d4-a2b9-b42cf2d8ca03 + keyvaultName: kv-lz-apps-prod-uscz diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/kustomization.yaml new file mode 100644 index 0000000..96febc8 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/kustomization.yaml @@ -0,0 +1,14 @@ +resources: + - normal + +configMapGenerator: + - name: reserve-management-system-api-environment-appsettings + files: + - appsettings/appsettings.json +generatorOptions: + disableNameSuffixHash: true + +patches: + - target: + kind: Deployment + path: set_configmap_deployment.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/normal/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/normal/kustomization.yaml new file mode 100644 index 0000000..a1e6b06 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/normal/kustomization.yaml @@ -0,0 +1,12 @@ +resources: + - ../../../base + +components: + - ../component + - ../../../components/keyvaults/normal + - ../../../components/workloads/normal + +patches: + - target: + kind: VirtualService + path: set_virtual_service.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/normal/set_virtual_service.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/normal/set_virtual_service.yaml new file mode 100644 index 0000000..7ed9f83 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/normal/set_virtual_service.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: reserve-management-system-api +spec: + hosts: + - prd-reserve-management-system-api.apps.azure.lcu-internal.com diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/set_configmap_deployment.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/set_configmap_deployment.yaml new file mode 100644 index 0000000..d234917 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/prd/set_configmap_deployment.yaml @@ -0,0 +1,12 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: volume-environment-appsettings + configMap: + name: reserve-management-system-api-environment-appsettings + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: volume-environment-appsettings + mountPath: /app/config/environment-appsettings diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/appsettings/appsettings.json b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/appsettings/appsettings.json new file mode 100644 index 0000000..13c95d6 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/appsettings/appsettings.json @@ -0,0 +1,27 @@ +{ + "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" + ], + "FeatureFlags": { + "RunRmsActiveLoanConsumer": true, + "EnableImportExportActive": true, + "EnableImportExportClosed": true + } + }, + "ConnectionConfiguration": { + "HostUrl": "qa-rabbitmq.lcu-internal.com" + }, + "CoreApiConfiguration": { + "ApiTimeOutInSeconds": 420, + "BaseUrl": "https://qa-core-api-wrapper-api.apps.azure.lcu-internal.com" + }, + "HostedServices": { + "ImportExportActiveCronSchedule": "0 19 * * 0-6", + "ImportExportClosedCronSchedule": "15 19 * * 0-6" + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/component/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/component/kustomization.yaml new file mode 100644 index 0000000..6e732a6 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/component/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +patches: + - target: + kind: SecretProviderClass + path: set_secrets_provider_class.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/component/set_secrets_provider_class.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/component/set_secrets_provider_class.yaml new file mode 100644 index 0000000..0f3d934 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/component/set_secrets_provider_class.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: reserve-management-system-api-csi-key-vault-secrets-provider +spec: + parameters: + userAssignedIdentityID: 3286ed9a-f85c-4c50-b742-dbd3c1c3fe1f + keyvaultName: kv-lz-apps-qa-uscz diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/kustomization.yaml new file mode 100644 index 0000000..96febc8 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/kustomization.yaml @@ -0,0 +1,14 @@ +resources: + - normal + +configMapGenerator: + - name: reserve-management-system-api-environment-appsettings + files: + - appsettings/appsettings.json +generatorOptions: + disableNameSuffixHash: true + +patches: + - target: + kind: Deployment + path: set_configmap_deployment.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/normal/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/normal/kustomization.yaml new file mode 100644 index 0000000..a1e6b06 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/normal/kustomization.yaml @@ -0,0 +1,12 @@ +resources: + - ../../../base + +components: + - ../component + - ../../../components/keyvaults/normal + - ../../../components/workloads/normal + +patches: + - target: + kind: VirtualService + path: set_virtual_service.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/normal/set_virtual_service.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/normal/set_virtual_service.yaml new file mode 100644 index 0000000..6abd407 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/normal/set_virtual_service.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: reserve-management-system-api +spec: + hosts: + - qa-reserve-management-system-api.apps.azure.lcu-internal.com diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/set_configmap_deployment.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/set_configmap_deployment.yaml new file mode 100644 index 0000000..d234917 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/qa/set_configmap_deployment.yaml @@ -0,0 +1,12 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: volume-environment-appsettings + configMap: + name: reserve-management-system-api-environment-appsettings + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: volume-environment-appsettings + mountPath: /app/config/environment-appsettings diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/appsettings/appsettings.json b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/appsettings/appsettings.json new file mode 100644 index 0000000..378ff9d --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/appsettings/appsettings.json @@ -0,0 +1,25 @@ +{ + "Primary": { + "SeqUrl": "https://uat-seq.lcu-internal.com", + "CorsAllowedOrigins": [ + "http://lcu-internal.com", + "https://lcu-internal.com" + ], + "FeatureFlags": { + "RunRmsActiveLoanConsumer": true, + "EnableImportExportActive": true, + "EnableImportExportClosed": true + } + }, + "ConnectionConfiguration": { + "HostUrl": "uat-rabbitmq.lcu-internal.com" + }, + "CoreApiConfiguration": { + "ApiTimeOutInSeconds": 420, + "BaseUrl": "https://uat-core-api-wrapper-api.apps.azure.lcu-internal.com" + }, + "HostedServices": { + "ImportExportActiveCronSchedule": "0 19 * * 0-6", + "ImportExportClosedCronSchedule": "15 19 * * 0-6" + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/component/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/component/kustomization.yaml new file mode 100644 index 0000000..6e732a6 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/component/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +patches: + - target: + kind: SecretProviderClass + path: set_secrets_provider_class.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/component/set_secrets_provider_class.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/component/set_secrets_provider_class.yaml new file mode 100644 index 0000000..6bd525a --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/component/set_secrets_provider_class.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: reserve-management-system-api-csi-key-vault-secrets-provider +spec: + parameters: + userAssignedIdentityID: 64ba01ed-1ecc-4c9b-b56e-8d618f2b81ce + keyvaultName: kv-lz-apps-uat-uscz diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/kustomization.yaml new file mode 100644 index 0000000..96febc8 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/kustomization.yaml @@ -0,0 +1,14 @@ +resources: + - normal + +configMapGenerator: + - name: reserve-management-system-api-environment-appsettings + files: + - appsettings/appsettings.json +generatorOptions: + disableNameSuffixHash: true + +patches: + - target: + kind: Deployment + path: set_configmap_deployment.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/normal/kustomization.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/normal/kustomization.yaml new file mode 100644 index 0000000..a1e6b06 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/normal/kustomization.yaml @@ -0,0 +1,12 @@ +resources: + - ../../../base + +components: + - ../component + - ../../../components/keyvaults/normal + - ../../../components/workloads/normal + +patches: + - target: + kind: VirtualService + path: set_virtual_service.yaml diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/normal/set_virtual_service.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/normal/set_virtual_service.yaml new file mode 100644 index 0000000..61fc9ee --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/normal/set_virtual_service.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: reserve-management-system-api +spec: + hosts: + - uat-reserve-management-system-api.apps.azure.lcu-internal.com diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/set_configmap_deployment.yaml b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/set_configmap_deployment.yaml new file mode 100644 index 0000000..d234917 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/Api/kustomize/overlays/uat/set_configmap_deployment.yaml @@ -0,0 +1,12 @@ +- op: add + path: /spec/template/spec/volumes/- + value: + name: volume-environment-appsettings + configMap: + name: reserve-management-system-api-environment-appsettings + +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: volume-environment-appsettings + mountPath: /app/config/environment-appsettings diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/README.md b/LCU.WebApps.Indirect.ReserveManagementSystem/README.md new file mode 100644 index 0000000..7e47ec6 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/README.md @@ -0,0 +1,39 @@ +# Introduction +We are building a new version of the Dealer Reserve System that will support the Indirect Lending team and will be compatible with the new FISERV solution, called DNA. + +This new system is named the Reserve Management System (RMS) and the main features that will be built will include: +- Calculating Reserves - this is a "commission" that is paid to the dealer after they close on a sale and they use a Landmark loan to finance the sale +- Calculating Chargebacks - this is the "refund" that the dealership owes back to Landmark if a loan is paid off early +- Creating Billing and Payment notices (Statements) that meet compliance requirements + +# Getting Started +Our repo has two solutions: the UI and the API. The UI is built with server-side Blazor, based on the LCU Blazor base project. + +Our API is built with WebApi and Entity Framework, .NET Core 3.1. It also uses Swagger tools to show and allow interaction with the endpoints. + +We are using SpecFlow and xUnit for our API unit tests (see more information below). You should install the Visual Studio extension SpecFlow for Visual Studio 2019 to make SpecFlow easier to use in the IDE. + +# Build and Test +The SpecFlow NuGet packages will make sure the test files are created when building the solution. + +#SpecFlow +We are using SpecFlow for the unit tests in our API ([SpecFlow Getting Started](https://specflow.org/getting-started/)). SpecFlow allows us to set up the tests in the Gherkin language with Given - When - Then steps - these are the feature files. Each step is then linked to a method in a steps .cs file. The SpecFlow NuGet package compiles the feature file into a corresponding feature.cs file as part of the MSBuild process. This means that the feature.cs files do not need to be in the Git repo, and our .gitignore makes sure of that. + +There is also a SpecFlow xUnit package that enables SpecFlow to be used with xUnit. + +Another useful NuGet package that we are using with SpecFlow is Pickles.CommandLine. Pickles is a package that generates documents from Gherkin files. This is installed in our main project so that it will be installed on Azure DevOps. We then have a step in our YAML file that calls pickles.exe (from its location on the DevOps server) to generate these documents and put them in a folder called Specs in the ReserveManagementSystemApi project (.gitignore makes sure that these aren't commited to the repo). The Startup file for the Api looks for this folder, and if it exists, it will set up the folder as a static website, found in QA at [RMS Specs](https://qa-reservemanagementsystemapi.lcu-internal.com/specs/index.html). Now every time we build our API solution, the latest version of the feature files are used to create this documentation. + +If you having trouble with the SpecFlow extension not working correctly (this can happen after it is updated), look at this link: [Troubleshooting SpecFlow extension](https://docs.specflow.org/projects/specflow/en/latest/Help/Troubleshooting-Visual-Studio-Integration.html#troubleshooting). The first section helped me fix this. + +#CsvHelper +We are using the NuGet package CsvHelper to import csv files. We configured this to have a header row which will match the properties of a model. (Currently one of our imports is still headerless, but we will most likely change that.) It also is configured to trim both fields and headers. This package is easy to use in that it doesn't let you import a file that has values that aren't the right type for the column (i.e. not a date for a DateTime column). It will also allow us to change the column order (when we have a header row), and mark some columns as optional. + +This will soon be unnecessary because we will be getting new/updated loans or closed loans from DNA via the CoreApiWrapper + +#CoreApiWrapper +We have built endpoints in the CoreApiWrapper that connect to DNA and get the loans that we need. There is an import for new and updated loans with a date parameter that returns a list of all the loans that have been added or updated after the date passed in. There is a similar endpoint for getting all loans closed past a certain date. + +We also have endpoints for exporting the reserve or chargeback information back to DNA. These currently are just placeholders for the future export. + +#Notes about NuGet packages +In the API, we have left all Entity Framework packages at version 3.1.17, instead of upgrading to 5 since that we aren't using .Net Core 5. We have also left Blazored Fluent Validation at 1.4.1 in both the UI and API since we had issues when upgrading to 2.0.1. \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI.sln b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI.sln new file mode 100644 index 0000000..849ac8e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29905.134 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReserveManagementSystemUI", "ReserveManagementSystemUI\ReserveManagementSystemUI.csproj", "{57132F3E-B964-4260-A5BC-CAD83B635A7F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {57132F3E-B964-4260-A5BC-CAD83B635A7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57132F3E-B964-4260-A5BC-CAD83B635A7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57132F3E-B964-4260-A5BC-CAD83B635A7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57132F3E-B964-4260-A5BC-CAD83B635A7F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {52C37430-1E89-47B3-8D54-8F0836A59C8D} + EndGlobalSection +EndGlobal diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/App.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/App.razor new file mode 100644 index 0000000..2d8fa2e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/App.razor @@ -0,0 +1,19 @@ + + + + + + +

Sorry!

+

You're not authorized to reach this page.

+
+
+
+ + +

Sorry, there's nothing at this address.

+
+
+
+
+
diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Dockerfile b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Dockerfile new file mode 100644 index 0000000..a3c02b2 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Dockerfile @@ -0,0 +1,40 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build + +# Copy project files first for restore layer +COPY ReserveManagementSystemUI.csproj ./source/ + +# Optional credential provider for Azure Artifacts (matches API pattern) +ARG PAT=bh2lq3jibktw67b2wdbqyfln7cd3je6lk52kijntpj43v6kimpgq +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && \ + update-ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +ADD https://github.com/microsoft/artifacts-credprovider/releases/latest/download/Microsoft.Net8.NuGet.CredentialProvider.tar.gz /tmp/ +# Extract and install Credential Provider +RUN mkdir -p /root/.nuget/plugins/netcore && \ + tar --strip-components=3 -xzf /tmp/Microsoft.Net8.NuGet.CredentialProvider.tar.gz -C /root/.nuget/plugins/netcore && \ + rm /tmp/Microsoft.Net8.NuGet.CredentialProvider.tar.gz + +ENV NUGET_PLUGIN_PATHS=/root/.nuget/plugins/netcore/CredentialProvider.Microsoft.dll +ENV NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED=true +ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{\"endpointCredentials\": [{\"endpoint\":\"https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json\", \"password\":\"${PAT}\"}]}" + +# Restore +RUN dotnet restore -s "https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json" -s "https://api.nuget.org/v3/index.json" ./source/ReserveManagementSystemUI.csproj + +# Copy all sources and publish +COPY . ./source +WORKDIR /source +RUN dotnet publish -c Release -o /app/publish + +# Runtime image +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine +RUN apk add --no-cache icu-libs && apk add --no-cache tzdata; +ENV TZ=America/Chicago +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +ENV ASPNETCORE_URLS=http://+:80 + +WORKDIR /app +COPY --from=build /app/publish . +EXPOSE 80 +ENTRYPOINT ["dotnet", "ReserveManagementSystemUI.dll"] diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/GridHelpers.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/GridHelpers.cs new file mode 100644 index 0000000..e54b6cf --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/GridHelpers.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; +using ReserveManagementSystemLib.Models; + +namespace ReserveManagementSystemUI.Helpers +{ + public class GridHelpers + { + public List OrderByAscending(List imports, string sortByField) + { + return sortByField switch + { + "FileName" => imports.OrderBy(loan => loan.FileName).ToList(), + "StartDate" => imports.OrderBy(loan => loan.StartDate).ToList(), + "EndDate" => imports.OrderBy(loan => loan.EndDate).ToList(), + "RecordCount" => imports.OrderBy(loan => loan.RecordCount).ToList(), + "RecordsSaved" => imports.OrderBy(loan => loan.RecordsSaved).ToList(), + "Success" => imports.OrderBy(loan => loan.Success).ToList(), + _ => imports + }; + } + + public List OrderByDescending(List imports, string sortByField) + { + return sortByField switch + { + "FileName" => imports.OrderByDescending(loan => loan.FileName).ToList(), + "StartDate" => imports.OrderByDescending(loan => loan.StartDate).ToList(), + "EndDate" => imports.OrderByDescending(loan => loan.EndDate).ToList(), + "RecordCount" => imports.OrderByDescending(loan => loan.RecordCount).ToList(), + "RecordsSaved" => imports.OrderByDescending(loan => loan.RecordsSaved).ToList(), + "Success" => imports.OrderByDescending(loan => loan.Success).ToList(), + _ => imports + }; + } + + public List OrderByAscending(List historyData, string sortByField) + { + return sortByField switch + { + "MemberNumber" => historyData.OrderBy(history => history.MemberNumber).ToList(), + "LoanNumber" => historyData.OrderBy(history => history.LoanNumber).ToList(), + "ActivityType" => historyData.OrderBy(history => history.ActivityType).ToList(), + "OldValue" => historyData.OrderBy(history => history.OldValue).ToList(), + "NewValue" => historyData.OrderBy(history => history.NewValue).ToList(), + "ModifiedDate" => historyData.OrderBy(history => history.ModifiedDate).ToList(), + "ModifiedBy" => historyData.OrderBy(history => history.ModifiedBy).ToList(), + "Description" => historyData.OrderBy(history => history.AuditDescription).ToList(), + _ => historyData + }; + } + + public List OrderByDescending(List historyData, string sortByField) + { + return sortByField switch + { + "MemberNumber" => historyData.OrderByDescending(history => history.MemberNumber).ToList(), + "LoanNumber" => historyData.OrderByDescending(history => history.LoanNumber).ToList(), + "ActivityType" => historyData.OrderByDescending(history => history.ActivityType).ToList(), + "OldValue" => historyData.OrderByDescending(history => history.OldValue).ToList(), + "NewValue" => historyData.OrderByDescending(history => history.NewValue).ToList(), + "ModifiedDate" => historyData.OrderByDescending(history => history.ModifiedDate).ToList(), + "ModifiedBy" => historyData.OrderByDescending(history => history.ModifiedBy).ToList(), + "Description" => historyData.OrderByDescending(history => history.AuditDescription).ToList(), + _ => historyData + }; + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/IEHelpers.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/IEHelpers.cs new file mode 100644 index 0000000..b7ade61 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/IEHelpers.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Serilog; +using System.Collections.Generic; + +namespace ReserveManagementSystemUI.Helpers +{ + public class IEHelpers + { + private IHttpContextAccessor _httpContextAccessor; + + public IEHelpers(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public bool IsIE() + { + string userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"]; + return userAgent.Contains("MSIE") || userAgent.Contains("Trident"); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/ServiceHelpers.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/ServiceHelpers.cs new file mode 100644 index 0000000..2300141 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Helpers/ServiceHelpers.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Components.Authorization; +using System.Reflection; +using ReserveManagementSystemUI.Model.Configuration; +using Serilog; + +namespace ReserveManagementSystemUI.Helpers +{ + public class ServiceHelpers + { + private AuthenticationStateProvider _authenticationStateProvider; + private readonly RmsUIConfiguration _rmsUIConfig; + + public ServiceHelpers(RmsUIConfiguration rmsUIConfig, AuthenticationStateProvider authenticationStateProvider) + { + _rmsUIConfig = rmsUIConfig; + _authenticationStateProvider = authenticationStateProvider; + } + + public string GetControllerApiUrl(string apiUrl, string controller) + { + PropertyInfo propInfo = typeof(RmsUIConfiguration).GetProperty(controller); + var propValue = propInfo.GetValue(_rmsUIConfig, null); + + return _rmsUIConfig.RmsApiUrl + propValue; + } + + public string GetReportServerBaseUrl() + { + return _rmsUIConfig.ReportServerUrl; + } + + public string GetUserName() + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + + if (authState?.User?.Identity?.Name == null) + { + Log.Error("Could not get user identity by AuthenticationState"); + return ""; + } + + var userName = authState.User.Identity.Name; + return userName[4..]; + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Configuration/AppConfiguration.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Configuration/AppConfiguration.cs new file mode 100644 index 0000000..46a5077 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Configuration/AppConfiguration.cs @@ -0,0 +1,9 @@ +namespace ReserveManagementSystemUI.Model.Configuration +{ + public class AppConfiguration + { + public string AppName { get; set; } + public string SeqApiKey { get; set; } + public string SeqUrl { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Configuration/RmsUIConfiguration.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Configuration/RmsUIConfiguration.cs new file mode 100644 index 0000000..b269968 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Configuration/RmsUIConfiguration.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ReserveManagementSystemUI.Model.Configuration +{ + public class RmsUIConfiguration + { + public int DefaultImportTimeout { get; set; } + public string RmsApiUrl { get; set; } + public string LoanController { get; set; } + public string ImportController { get; set; } + public string DealerController { get; set; } + public string AuditHistoryController { get; set; } + public string RmsAssociatesRoles { get; set; } + public string ReportServerUrl { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/TestModel.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/TestModel.cs new file mode 100644 index 0000000..58ae21e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/TestModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace ReserveManagementSystemUI.Model +{ + public class TestModel + { + [Required(ErrorMessage = "Action is required.")] + public string Action { get; set; } + public string NoResponse { get; set; } + public string YesResponse { get; set; } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Validators/TestModelValidator.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Validators/TestModelValidator.cs new file mode 100644 index 0000000..db91276 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Model/Validators/TestModelValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ReserveManagementSystemUI.Model.Validators +{ + public class TestModelValidator : AbstractValidator + { + + public TestModelValidator() + { + RuleFor(tm => tm.YesResponse).NotEmpty().When(tm => tm.Action == "Yes").WithMessage("Yes response is required when yes action is selected."); + RuleFor(tm => tm.NoResponse).NotEmpty().When(tm => tm.Action == "No").WithMessage("No response is required when no action is selected."); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Error.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Error.razor new file mode 100644 index 0000000..79929d7 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Error.razor @@ -0,0 +1,16 @@ +@page "/error" + + +

Error.

+

An error occurred while processing your request.

+ +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

\ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/History.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/History.razor new file mode 100644 index 0000000..3083afa --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/History.razor @@ -0,0 +1,47 @@ +@page "/History" +@using ReserveManagementSystemUI.Shared.Components.Table; + +@attribute [Authorize(Policy = "IsRMSAssociate")] + + +

Audit/Activity History

+ + + @TheTab.Text + + + +
We found @reserveHistory.Count history record(s)
+ + + + + + + + + + + + + + + @foreach (var history in reserveHistory) + { + + + + + + + + + + + } + +
+
+
+
+ diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/History.razor.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/History.razor.cs new file mode 100644 index 0000000..2c85300 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/History.razor.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Components; +using System.Collections.Generic; +using System.Threading.Tasks; +using Blazored.Modal; +using Blazored.Toast.Services; +using Blazored.Modal.Services; +using Models = ReserveManagementSystemLib.Models; +using ReserveManagementSystemUI.Helpers; +using ReserveManagementSystemUI.Services; +using ReserveManagementSystemUI.Shared.Components; + +namespace ReserveManagementSystemUI.Pages +{ + public partial class History : ComponentBase + { + [Inject] + private ReserveService ReserveService { get; set; } + + [Inject] + IToastService ToastService { get; set; } + + [Inject] + IModalService Modal { get; set; } + + [Inject] + GridHelpers GridHelpers { get; set; } + + private List reserveHistory = new List(); + private bool ascending; + private string currentSortedHeader; + private IModalReference openModal; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + openModal = Modal.Show(); + await LoadHistoryData(); + } + + await base.OnAfterRenderAsync(firstRender); + } + + private async Task LoadHistoryData() + { + var result = true; + reserveHistory = await ReserveService.GetHistory(); + + if (reserveHistory == null) + { + ToastService.ShowError("There was an issue retrieving history data."); + reserveHistory = new List(); + result = false; + } + + StateHasChanged(); + openModal.Close(); + return result; + } + + private void OnSortClick(string sortBy) + { + var sameClick = sortBy == currentSortedHeader; + if (sameClick) + { + ascending = !ascending; + } + + if (ascending) + { + reserveHistory = GridHelpers.OrderByAscending(reserveHistory, sortBy); + } + else + { + reserveHistory = GridHelpers.OrderByDescending(reserveHistory, sortBy); + } + + currentSortedHeader = sortBy; + + StateHasChanged(); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Index.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Index.razor new file mode 100644 index 0000000..a62f827 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Index.razor @@ -0,0 +1,9 @@ +@page "/" +@using ReserveManagementSystemUI.Shared.Components; +@inject IConfiguration Configuration; +@using ReserveManagementSystemUI.Model.Configuration; +@attribute [Authorize(Policy = "IsRMSAssociate")] + +

Home

+ + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Index.razor.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Index.razor.cs new file mode 100644 index 0000000..e991322 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Index.razor.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Components; + +namespace ReserveManagementSystemUI.Pages +{ + public partial class Index: ComponentBase + { + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/LoanView.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/LoanView.razor new file mode 100644 index 0000000..a016872 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/LoanView.razor @@ -0,0 +1,255 @@ +@page "/LoanView/{LoanId:int}" + +@attribute [Authorize(Policy = "IsRMSAssociate")] + + + + +
+

Member Information

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ @if (loanDetails.ValidationErrors != null) + { + @foreach (var error in loanDetails.ValidationErrors) + { +

@error.ErrorMessage

+ } + } +
+
+ + + + @TheTab.Text + + + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+ +
+
+ + +
+ +
+
+ + +
+
+ +
+
+ + +
+
+
+ + +
+
+ +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ @if (loanDetails.Dealer != null) + { +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ } +
+ + @if (loanDetails.Vehicle != null) + { +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ } + + @if (loanDetails.Vehicle == null) + { +
Sorry no vehicle detail information for this loan!
+ } + +
+
+
+
diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/LoanView.razor.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/LoanView.razor.cs new file mode 100644 index 0000000..95b1ef8 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/LoanView.razor.cs @@ -0,0 +1,93 @@ +using Blazored.Modal; +using Blazored.Modal.Services; +using Blazored.Toast.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemUI.Services; +using ReserveManagementSystemUI.Shared.Components; +using System.Threading.Tasks; + +namespace ReserveManagementSystemUI.Pages +{ + public partial class LoanView : ComponentBase + { + [Inject] + IToastService ToastService { get; set; } + + [Inject] + IModalService Modal { get; set; } + + [Inject] + ReserveService ReserveService { get; set; } + + [Parameter] + public int LoanId { get; set; } + + private Loan loanDetails = new Loan(); + + private IModalReference openModal; + + private string LoanTypeDetailString => !string.IsNullOrEmpty(loanDetails.LoanType?.ToString()) ? loanDetails.LoanTypeId?.ToString() + " - " + @loanDetails.LoanType?.LoanComment : ""; + + private string PaidLoanDateString => loanDetails.PaidLoanDate?.ToString("M/d/yyyy"); + + private string ChargeBackString => loanDetails.ChargeBack?.ToString("C"); + + private string StatementDateString => loanDetails.StatementDate?.ToString("M/d/yyyy"); + + private string AdjustmentString => loanDetails.Adjustment?.ToString("C"); + + private string VehicleMileageFormat => loanDetails.Vehicle.Mileage.ToString("#,##0"); + + private string GapString => loanDetails.Gap == true ? "Y" : "N"; + + private string WarrantyString => loanDetails.Warranty == true ? "Y" : "N"; + + private string MbpString => loanDetails.Mbp == true ? "Y" : "N"; + + private string CreditLifeString => loanDetails.CreditLife == true ? "Y" : "N"; + + private string CreditDisabilityString => loanDetails.CreditDisability == true ? "Y" : "N"; + + [CascadingParameter] EditContext EditContext { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + + await LoadLoanDetails(); + } + await base.OnAfterRenderAsync(firstRender); + + } + + private string GetFieldValidationClass(string fieldName) + { + string className = "form-control"; + if (loanDetails.ValidationErrors != null) { + foreach(var error in loanDetails.ValidationErrors) + { + if (error.PropertyName == fieldName) { + return "form-control invalid"; + } + } + } + return className; + } + + private async Task LoadLoanDetails() + { + openModal = Modal.Show(); + loanDetails = await ReserveService.GetLoanById(LoanId); + + if (loanDetails == null) + { + ToastService.ShowError("There was an issue retrieving this loan."); + } + openModal.Close(); + StateHasChanged(); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Loans.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Loans.razor new file mode 100644 index 0000000..e538922 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Loans.razor @@ -0,0 +1,228 @@ +@page "/Loans/{ImportId:int}" +@page "/Loans/" + +@using ReserveManagementSystemUI.Shared.Components.Table; + +@using ReserveManagementSystemUI.Model; + +@using ReserveManagementSystemLib.Models; + +@attribute [Authorize(Policy = "IsRMSAssociate")] + +

Loans

+ + + + @TheTab.Text + + + +
This is a summary view list from the Loan table, please click Select to view loan details.
+ +
We found @pagedResponse.TotalItems loan(s)
+
+
+
+ + + + @context + + + @context + + +
+
+ + +
+
+ + + + @context + + + @context + + +
+
+ + + + @context + + + @context + + +
+
+
+ +
+
+
+ + + + @dealer.DealerCode - @dealer.Name + + + @dealer.DealerCode - @dealer.Name + + +
+
+ + + + @action + + + @action + + +
+
+ + +
+
+ + +
+
+ + + + @import?.ImportType @import?.StartDate.ToString("M/d/yyyy h:mm:ss tt") + + + @import?.ImportType @import?.StartDate.ToString("M/d/yyyy h:mm:ss tt") + + +
+
+
+ +
Page @(pagedResponse.TotalPages < pagedResponse.PageIndex ? 0 : pagedResponse.PageIndex) of @pagedResponse.TotalPages
+
+
+ + + + @for (int i = pagedResponse.FirstPage; i <= pagedResponse.LastPage; i++) + { + var currentPage = i; + + } + + + +
+
+ + + + + + + + + + + + + + + + + @foreach (var loan in pagedResponse.LoanList) + { + + + + + + + + + + + + + } + +
+
+ @if (loan.ActionRequired) + { + + } +
+
+
+ +
+
+
+
+
+ + + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Loans.razor.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Loans.razor.cs new file mode 100644 index 0000000..478c8da --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Loans.razor.cs @@ -0,0 +1,266 @@ +using Blazored.Modal; +using Blazored.Modal.Services; +using Blazored.Toast.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemUI.Services; +using ReserveManagementSystemUI.Shared.Components; + +namespace ReserveManagementSystemUI.Pages +{ + public partial class Loans : ComponentBase + { + [Inject] + private ReserveService ReserveService { get; set; } + + [Inject] + IToastService ToastService { get; set; } + + [Inject] + IModalService Modal { get; set; } + + [Inject] + NavigationManager NavigationManager { get; set; } + + [Inject] + IJSRuntime JSRuntime { get; set; } + + [Parameter] + public int? ImportId { get; set; } + + private LoanFilter currentLoanFilter = new LoanFilter(); + + private PaginatedListResponse pagedResponse = new PaginatedListResponse(); + + private const int TopImports = 5; + private List Imports = new List(); + private List Actions = new List(); + + private Import SelectedImport; + private Dealer SelectedDealer; + + private bool ascending = true; + + private IModalReference openModal; + + protected override async Task OnParametersSetAsync() + { + currentLoanFilter = new LoanFilter + { + PageIndex = 1, + MaxRecordsPerPage = 15, + MaxPagesPerRange = 7, + }; + Imports = await ReserveService.GetRecentImports(TopImports); + + AddActions(); + + if (ImportId.HasValue) + { + SelectedImport = Imports.Where(i => ImportId.Value == (i.ImportId)).FirstOrDefault(); + } + else + { + SelectedImport = null; + } + currentLoanFilter.ImportId = SelectedImport?.ImportId; + await LoadLoanDataAsync(1); + await base.OnParametersSetAsync(); + } + + protected async Task BlazorFocusElement() + { + try + { + await JSRuntime.InvokeVoidAsync("BlazorFocusElement"); + } + catch (Exception) + { + //do nothing + } + } + + private async Task LoadLoanDataAsync(int pageIndex) + { + currentLoanFilter.PageIndex = pageIndex; + + openModal = Modal.Show(); + + pagedResponse = await ReserveService.GetFilteredLoans(currentLoanFilter); + + if (pagedResponse.LoanList == null) + { + ToastService.ShowError("There was an issue retrieving loan data."); + pagedResponse.LoanList = new List(); + } + + StateHasChanged(); + await BlazorFocusElement(); + + openModal.Close(); + + GetPageButtonClass(currentLoanFilter.PageIndex); + } + + private async Task OnMemberNameTextChangeAsync(ChangeEventArgs args) + { + currentLoanFilter.MemberNameText = args.Value.ToString(); + await LoadLoanDataAsync(1); + } + + private async Task OnLoanDateStartChangeAsync(ChangeEventArgs args) + { + if (DateTime.TryParse(args.Value.ToString(), out DateTime startDate)) + { + currentLoanFilter.LoanDateStart = startDate; + } + else + { + currentLoanFilter.LoanDateStart = null; + } + await LoadLoanDataAsync(1); + } + + private async Task OnLoanDateEndChangeAsync(ChangeEventArgs args) + { + if (DateTime.TryParse(args.Value.ToString(), out DateTime endDate)) + { + currentLoanFilter.LoanDateEnd = endDate; + } + else + { + currentLoanFilter.LoanDateEnd = null; + } + await LoadLoanDataAsync(1); + } + + private async Task> GetMemberNumbersAsync(string searchText) + { + var result = await ReserveService.GetMemberNumbers(searchText); + return result; + } + + private async Task SelectedMemberNumberResultChangedAsync(string result) + { + currentLoanFilter.MemberNumberText = result; + + await LoadLoanDataAsync(1); + } + + private async Task> GetLoanNumbersAsync(string searchText) + { + var result = await ReserveService.GetLoanNumbers(searchText); + return result; + } + + private async Task SelectedLoanNumberResultChangedAsync(string result) + { + currentLoanFilter.LoanNumberText = result; + await LoadLoanDataAsync(1); + } + + private async Task> GetApplicationIdsAsync(string searchText) + { + var result = await ReserveService.GetApplicationIds(searchText); + return result; + } + + private async Task SelectedApplicationIdResultChangedAsync(string result) + { + currentLoanFilter.ApplicationIdText = result; + await LoadLoanDataAsync(1); + } + + private async Task SelectedImportResultChangedAsync(Import result) + { + currentLoanFilter.ImportId = result?.ImportId; + SelectedImport = result; + await LoadLoanDataAsync(1); + } + + private async Task> GetImportsAsync(string searchText) + { + var search = await Task.FromResult(Imports); + return search; + } + + private string GetPageButtonClass(int currentPage) + { + return currentPage == pagedResponse.PageIndex ? "btn btn-page-focus" : "btn btn-page"; + } + + private async Task> GetDealersAsync(string searchText) + { + var dealers = new List(); + + if (!string.IsNullOrWhiteSpace(searchText)) + { + dealers = await ReserveService.GetDealers(searchText); + } + + return dealers; + } + + private async Task SelectedDealerResultChangedAsync(Dealer selectedDealer) + { + SelectedDealer = selectedDealer; + currentLoanFilter.DealerCodeText = selectedDealer?.DealerCode; + await LoadLoanDataAsync(1); + } + + private async Task> GetActionRequiredLoansAsync(string searchText) + { + bool action; + var results = new List(); + if (!string.IsNullOrWhiteSpace(searchText) && bool.TryParse(searchText, out action)) + { + results = await Task.FromResult(Actions.Where(x => x.Value.ToString() == searchText || x == null).ToList()); + } + else + { + results = await Task.FromResult(Actions.ToList()); + } + return results; + } + + private async Task SelectedActionResultChangedAsync(bool? result) + { + currentLoanFilter.ActionRequired = result; + + await LoadLoanDataAsync(1); + } + + private void AddActions() + { + if (Actions != null && Actions.Count == 0) + { + Actions.Add(true); + Actions.Add(false); + } + } + + private async void OnSortClickAsync(string sortBy) + { + if (sortBy == currentLoanFilter.SortField) + { + ascending = !ascending; + } + + currentLoanFilter.SortField = sortBy; + currentLoanFilter.SortDirection = ascending ? "ASC" : "DESC"; + currentLoanFilter.PageIndex = 1; + pagedResponse = await ReserveService.GetFilteredLoans(currentLoanFilter); + StateHasChanged(); + } + + void GoToLoan(int loanId) + { + NavigationManager.NavigateTo($"LoanView/{loanId}"); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Statements.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Statements.razor new file mode 100644 index 0000000..4494650 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Statements.razor @@ -0,0 +1,14 @@ +@page "/Statements" +@inject IConfiguration Configuration; +@using ReserveManagementSystemUI.Model.Configuration; +@attribute [Authorize(Policy = "IsRMSAssociate")] + +

Statements & Reports

+ + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Statements.razor.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Statements.razor.cs new file mode 100644 index 0000000..5e46c20 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Statements.razor.cs @@ -0,0 +1,31 @@ +using Blazored.Modal; +using Blazored.Modal.Services; +using Blazored.Toast.Services; +using Microsoft.AspNetCore.Components; +using ReserveManagementSystemUI.Helpers; +using Serilog; + +namespace ReserveManagementSystemUI.Pages +{ + public partial class Statements : ComponentBase + { + [Inject] + IToastService ToastService { get; set; } + + [Inject] + IModalService Modal { get; set; } + + [Inject] + ServiceHelpers ServiceHelpers { get; set; } + + string StatementLink => $"{ServiceHelpers.GetReportServerBaseUrl()}DealerReserveVehicleStatement"; + + string NoReserveLink => $"{ServiceHelpers.GetReportServerBaseUrl()}DealerReserveBuyDownNoReserveMarkups"; + + string ExceedMaxMarkupLink => $"{ServiceHelpers.GetReportServerBaseUrl()}DealerReserveExceedMaximumDealerMarkup"; + + string BuyDownLink => $"{ServiceHelpers.GetReportServerBaseUrl()}BuyDown"; + + string UpfrontDealerBalanceLink => $"{ServiceHelpers.GetReportServerBaseUrl()}UpfrontDealerBalance"; + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Test.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Test.razor new file mode 100644 index 0000000..4aa8478 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Test.razor @@ -0,0 +1,21 @@ +@page "/Test" +@inject IConfiguration Configuration; +@using ReserveManagementSystemUI.Model.Configuration; +@attribute [Authorize(Policy = "IsRMSAssociate")] + +

Hidden Page!

+ +
+
+
Test Logging
+
+ +
+
+ +
+
+ +
+
+
diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Test.razor.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Test.razor.cs new file mode 100644 index 0000000..df81f6a --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/Test.razor.cs @@ -0,0 +1,32 @@ +using Blazored.Toast.Services; +using Microsoft.AspNetCore.Components; +using Serilog; + + +namespace ReserveManagementSystemUI.Pages +{ + public partial class Test + { + + [Inject] + IToastService ToastService { get; set; } + + private void TestSeqError() + { + Log.Error("Test error logging from RMS"); + ToastService.ShowSuccess("Logged Error"); + } + + private void TestSeqInfo() + { + Log.Information("Test information logging from RMS"); + ToastService.ShowSuccess("Logged Information"); + } + + private void TestSeqWarn() + { + Log.Warning("Test warning logging from RMS"); + ToastService.ShowSuccess("Logged Warning"); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/_Host.cshtml b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/_Host.cshtml new file mode 100644 index 0000000..8913d10 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Pages/_Host.cshtml @@ -0,0 +1,55 @@ +@page "/" +@namespace ReserveManagementSystemUI.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + ReserveManagementSystem + + + + + + + + + + + + + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + @* *******Required in order to use in IE 11.******* + https://docs.microsoft.com/en-us/aspnet/core/blazor/supported-platforms?view=aspnetcore-3.1 + https://github.com/Daddoon/Blazor.Polyfill/tree/master/Publish/Blazor.Polyfill.Publish + *@ + @if (Request.Headers.TryGetValue("User-Agent", out var userAgents) && + (userAgents.FirstOrDefault() is string userAgent) && + System.Text.RegularExpressions.Regex.IsMatch(userAgent, @"MSIE \d|Trident.*rv:")) + { + + } + + + + + + + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Program.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Program.cs new file mode 100644 index 0000000..c07d528 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Program.cs @@ -0,0 +1,29 @@ + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace ReserveManagementSystemUI +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((_, config) => + { + config.AddJsonFile("/vault/secrets/reserve-management-system-ui-seq-api-key.json", true, true); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup() + .UseSetting(WebHostDefaults.DetailedErrorsKey, "true") + .UseSerilog(); + }); + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/ReserveManagementSystemUI.csproj b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/ReserveManagementSystemUI.csproj new file mode 100644 index 0000000..593f60a --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/ReserveManagementSystemUI.csproj @@ -0,0 +1,38 @@ + + + + net8.0 + + + + + + + + + + + + + + + + + + + + + + + true + PreserveNewest + + + + + + + + + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Services/NotificationService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Services/NotificationService.cs new file mode 100644 index 0000000..d01cd4e --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Services/NotificationService.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using LCU.Shared.Common.NotificationApiLib.Models; +using ReserveManagementSystemUI.Helpers; +using Serilog; +using LCU.Library.Internal.LcuHttpClient; + +namespace ReserveManagementSystemUI.Service +{ + public class NotificationService + { + private ILcuHttpClient _lcuHttpClient; + private IConfiguration _config; + private ServiceHelpers _serviceHelper; + + public NotificationService(ILcuHttpClient lcuHttpClient, + IConfiguration config, + ServiceHelpers serviceHelper) + { + _lcuHttpClient = lcuHttpClient; + _config = config; + _serviceHelper = serviceHelper; + } + + public async Task SendMessageAsync(string recipient) + { + List recipients = new List() { recipient }; + string subject = "!Test!"; + string body = "

This is a test notification sent from Blazor Base application!

"; + + var emailRequest = new EmailRequest + { + Recipient = recipients, + Subject = subject, + Body = body + }; + + try + { + var apiUrl = _serviceHelper.GetControllerApiUrl("NotificationsUrl", "EmailController"); + var encoded = JsonConvert.SerializeObject(emailRequest); + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + var content = new StringContent(encoded, Encoding.UTF8, "application/json"); + HttpResponseMessage response = await client.PostAsync(apiUrl, content); + + response.EnsureSuccessStatusCode(); + + Log.Information("Emailer SendMessageAsync Method message sent"); + + return response.IsSuccessStatusCode; + } + catch (Exception ex) + { + Log.Error("Emailer SendMessageAsync Method: {0} {1}", ex.Message, ex.StackTrace); + return false; + } + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Services/ReserveService.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Services/ReserveService.cs new file mode 100644 index 0000000..d6f0dba --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Services/ReserveService.cs @@ -0,0 +1,366 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemUI.Helpers; +using ReserveManagementSystemUI.Model.Configuration; +using Serilog; +using LCU.Library.Internal.LcuHttpClient; + +namespace ReserveManagementSystemUI.Services +{ + public class ReserveService + { + private ILcuHttpClient _lcuHttpClient; + private IConfiguration _config; + private RmsUIConfiguration _rmsUIconfig; + private AuthenticationStateProvider _authenticationStateProvider; + private ServiceHelpers _serviceHelper; + + public ReserveService(ILcuHttpClient lcuHttpClient, + AuthenticationStateProvider authenticationStateProvider, + ServiceHelpers serviceHelper, + RmsUIConfiguration rmsUIConfig) + { + _lcuHttpClient = lcuHttpClient; + _authenticationStateProvider = authenticationStateProvider; + _serviceHelper = serviceHelper; + _rmsUIconfig = rmsUIConfig; + } + + public async Task GetFilteredLoans(LoanFilter loanFilter) + { + PaginatedListResponse pagedLoanResponse = new PaginatedListResponse(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = $"{ _serviceHelper.GetControllerApiUrl("ApiUrl", "LoanController")}/SearchLoans"; + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + + string json = JsonConvert.SerializeObject(loanFilter); + HttpContent content = new StringContent(json, Encoding.UTF8, "application/json"); + + HttpResponseMessage response = client.PostAsync(apiUrl, content).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + pagedLoanResponse = JsonConvert.DeserializeObject(jsonString.Result); + } + + return pagedLoanResponse; + + } + catch (Exception ex) + { + Log.Error(ex, "Error getting loans."); + pagedLoanResponse = null; + } + + return await Task.FromResult(pagedLoanResponse); + } + + public async Task> GetMemberNumbers(string searchText) + { + var members = new List(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = $"{ _serviceHelper.GetControllerApiUrl("ApiUrl", "LoanController")}/SearchMembers/{searchText}"; + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + + var response = client.GetAsync(apiUrl).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + members = JsonConvert.DeserializeObject>(jsonString.Result); + } + + return members; + + } + catch (Exception ex) + { + Log.Error(ex, "Error getting members by search text."); + members = null; + } + + return await Task.FromResult(members); + } + + public async Task> GetLoanNumbers(string searchText) + { + var loanNumbers = new List(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = $"{ _serviceHelper.GetControllerApiUrl("ApiUrl", "LoanController")}/SearchLoanNumbers/{searchText}"; + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + + var response = client.GetAsync(apiUrl).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + loanNumbers = JsonConvert.DeserializeObject>(jsonString.Result); + } + + return loanNumbers; + } + catch (Exception ex) + { + Log.Error(ex, "Error getting loan numbers by search text."); + loanNumbers = null; + } + + return await Task.FromResult(loanNumbers); + } + + public async Task> GetApplicationIds(string searchText) + { + var applicationIds = new List(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = $"{ _serviceHelper.GetControllerApiUrl("ApiUrl", "LoanController")}/SearchApplicationIds/{searchText}"; + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + + var response = client.GetAsync(apiUrl).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + applicationIds = JsonConvert.DeserializeObject>(jsonString.Result); + } + + return applicationIds; + } + catch (Exception ex) + { + Log.Error(ex, "Error getting application ids by search text."); + applicationIds = null; + } + + return await Task.FromResult(applicationIds); + } + + public async Task> GetDealers(string dealerSearchText) + { + var dealers = new List(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = $"{ _serviceHelper.GetControllerApiUrl("ApiUrl", "DealerController")}/Search/{dealerSearchText}"; + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + + var response = client.GetAsync(apiUrl).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + dealers = JsonConvert.DeserializeObject>(jsonString.Result); + } + + return dealers; + + } + catch (Exception ex) + { + Log.Error(ex, "Error getting dealers by search text."); + dealers = null; + } + + return await Task.FromResult(dealers); + } + + public async Task GetLoanById(int loanId) + { + Loan loanInfo = new Loan(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = $"{_serviceHelper.GetControllerApiUrl("ApiUrl", "LoanController")}/GetById/{loanId}"; + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + HttpResponseMessage response = client.GetAsync(apiUrl).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + loanInfo = JsonConvert.DeserializeObject(jsonString.Result); + } + + } + catch (Exception ex) + { + Log.Error(ex, "Error getting loan."); + loanInfo = null; + } + + return await Task.FromResult(loanInfo); + } + + public async Task> GetAllImports() + { + var imports = new List(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = _serviceHelper.GetControllerApiUrl("ApiUrl", "ImportController"); + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + HttpResponseMessage response = client.GetAsync(apiUrl).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + imports = JsonConvert.DeserializeObject>(jsonString.Result) ?? new List(); + } + + } + catch (Exception ex) + { + Log.Error(ex, "Error getting imports."); + imports = null; + } + + return await Task.FromResult(imports); + } + + public async Task> GetRecentImports(int numberOfImports) + { + var imports = new List(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = $"{_serviceHelper.GetControllerApiUrl("ApiUrl", "ImportController")}/GetRecentImports/{numberOfImports}"; + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + HttpResponseMessage response = client.GetAsync(apiUrl).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + imports = JsonConvert.DeserializeObject>(jsonString.Result) ?? new List(); + } + + } + catch (Exception ex) + { + Log.Error(ex, "Error getting imports."); + imports = null; + } + + return await Task.FromResult(imports); + } + + public async Task> GetHistory() + { + var history = new List(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = _serviceHelper.GetControllerApiUrl("ApiUrl", "AuditHistoryController"); + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + HttpResponseMessage response = client.GetAsync(apiUrl).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + history = JsonConvert.DeserializeObject>(jsonString.Result); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error getting audit history."); + history = null; + } + return await Task.FromResult(history); + } + + public async Task GetActiveLoans() + { + var import = new Import(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = $"{_serviceHelper.GetControllerApiUrl("ApiUrl", "ImportController")}/ImportActiveLoans"; + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + var timeout = Convert.ToDouble(_rmsUIconfig.DefaultImportTimeout); + client.Timeout = TimeSpan.FromMinutes(timeout); + + HttpResponseMessage response = client.PutAsync(apiUrl, null).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + import = JsonConvert.DeserializeObject(jsonString.Result); + } + + } + catch (Exception ex) + { + Log.Error(ex, "Error running active loan import process."); + import = null; + } + + return await Task.FromResult(import); + } + + public async Task GetClosedLoans() + { + var import = new Import(); + + try + { + var authState = _authenticationStateProvider.GetAuthenticationStateAsync().Result; + var apiUrl = $"{_serviceHelper.GetControllerApiUrl("ApiUrl", "ImportController")}/ImportClosedLoans"; + var client = _lcuHttpClient.CreateClient(new Uri(apiUrl)); + var timeout = Convert.ToDouble(_rmsUIconfig.DefaultImportTimeout); + client.Timeout = TimeSpan.FromMinutes(timeout); + + HttpResponseMessage response = client.PutAsync(apiUrl, null).Result; + + if (response.IsSuccessStatusCode) + { + var jsonString = response.Content.ReadAsStringAsync(); + jsonString.Wait(); + import = JsonConvert.DeserializeObject(jsonString.Result); + } + + } + catch (Exception ex) + { + Log.Error(ex, "Error running closed loan import process."); + import = null; + } + + return await Task.FromResult(import); + } + } +} + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportBanner.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportBanner.razor new file mode 100644 index 0000000..da044a6 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportBanner.razor @@ -0,0 +1,6 @@ +@if (Loaded) +{ +
+ Last Import: Started @Import.StartDate.ToString("M/d/yyyy h:mm tt"), @GetCompletedText()@GetResultText() +
+} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportBanner.razor.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportBanner.razor.cs new file mode 100644 index 0000000..711481f --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportBanner.razor.cs @@ -0,0 +1,93 @@ +using Blazored.Modal; +using Blazored.Modal.Services; +using Blazored.Toast.Services; +using Microsoft.AspNetCore.Components; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemUI.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ReserveManagementSystemUI.Shared.Components +{ + public partial class ImportBanner: ComponentBase + { + [Inject] + ReserveService ReserveService { get; set; } + + [Inject] + IToastService ToastService { get; set; } + + [Inject] + IModalService Modal { get; set; } + + public Import Import = new Import(); + + public bool Loaded = false; + + public string ResultClass => Import.Success ? "alert-success" : "alert-danger"; + + [Parameter] + public bool Refresh { get; set; } + + protected override async Task OnParametersSetAsync() + { + if (Refresh) { + await GetImport(); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await GetImport(); + } + + await base.OnAfterRenderAsync(firstRender); + } + + public async Task GetImport() + { + var result = true; + + Import = (await ReserveService.GetRecentImports(1)).FirstOrDefault(); + + if (Import == null) + { + ToastService.ShowError("There was an issue retrieving import data."); + Import = new Import(); + result = false; + } + + Loaded = true; + + StateHasChanged(); + + return result; + } + + public string GetCompletedText() + { + if (Import.EndDate.HasValue) + { + return $"Completed {Import.EndDate?.ToString("M/d/yyyy h:mm tt")}, "; + } + return string.Empty; + } + + public string GetResultText() + { + if (Import.Success) + { + return $"Processed {Import.RecordCount} records"; + } + if (!Import.Success && Import.RecordCount > 0) + { + return $"Processed {Import.RecordCount} records, but only saved {Import.RecordsSaved}"; + } + return $"Failed to process"; + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportList.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportList.razor new file mode 100644 index 0000000..fc90a28 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportList.razor @@ -0,0 +1,56 @@ +@using ReserveManagementSystemUI.Shared.Components.Table; + +@attribute [Authorize(Policy = "IsRMSAssociate")] + + + + + + @TheTab.Text + + + + +
This is a list of @HeadingText imports; please click Select to view the loans in that import.
+
+
+ + +
+
+ + + + + + + + + + + + + + + + @foreach (var import in Imports) + { + + + + + + + + + + } + +
+
+ +
+
+
+
+
diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportList.razor.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportList.razor.cs new file mode 100644 index 0000000..72bcfce --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/ImportList.razor.cs @@ -0,0 +1,154 @@ +using Blazored.Modal; +using Blazored.Modal.Services; +using Blazored.Toast.Services; +using Microsoft.AspNetCore.Components; +using ReserveManagementSystemLib.Models; +using ReserveManagementSystemUI.Helpers; +using ReserveManagementSystemUI.Services; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using System.Globalization; +using System; +using Serilog; + +namespace ReserveManagementSystemUI.Shared.Components +{ + public partial class ImportList: ComponentBase + { + [Inject] + IToastService ToastService { get; set; } + + [Inject] + IModalService Modal { get; set; } + + [Inject] + ReserveService ReserveService { get; set; } + + [Inject] + NavigationManager NavigationManager { get; set; } + + [Inject] + GridHelpers GridHelpers { get; set; } + + [Inject] + ServiceHelpers ServiceHelpers { get; set; } + + [Parameter] + public bool ShowAllImports { get; set; } + + [Parameter] + public int NumberOfImportsToShow { get; set; } + + public List Imports = new List(); + + private bool ascending; + + private string currentSortedHeader; + + private bool updateBanner; + + private IModalReference openModal; + + private string HeadingText => ShowAllImports ? "all" : $"the last {NumberOfImportsToShow}"; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + openModal = Modal.Show(); + await GetImports(); + } + + await base.OnAfterRenderAsync(firstRender); + } + + public async Task GetImports() + { + var result = true; + + if (ShowAllImports) + { + Imports = await ReserveService.GetAllImports(); + } + else + { + Imports = await ReserveService.GetRecentImports(NumberOfImportsToShow); + } + + if (Imports == null) + { + ToastService.ShowError("There was an issue retrieving import data."); + Imports = new List(); + result = false; + } + + StateHasChanged(); + openModal.Close(); + + return result; + } + + private void OnSortClick(string sortBy) + { + var sameClick = sortBy == currentSortedHeader; + if (sameClick) + { + ascending = !ascending; + } + + if (ascending) + { + Imports = GridHelpers.OrderByAscending(Imports, sortBy); + } + else + { + Imports = GridHelpers.OrderByDescending(Imports, sortBy); + } + + currentSortedHeader = sortBy; + + StateHasChanged(); + } + + void GoToLoanSummary(int importId) + { + NavigationManager.NavigateTo($"Loans/{importId}"); + } + + private async Task ImportActiveLoans() + { + + openModal = Modal.Show(); + await Task.Delay(500); + + Log.Information("Import Active Loans started by user:" + ServiceHelpers.GetUserName()); + ToastService.ShowSuccess("Import Active Loans has started, please wait before clicking again may produce import errors."); + + //import the active loans since last successful active import start date + await ReserveService.GetActiveLoans(); + + await GetImports(); //update the banner and import list with new import + updateBanner = true; + + openModal.Close(); + } + + private async Task ImportClosedLoans() + { + openModal = Modal.Show(); + await Task.Delay(500); + + Log.Information("Import Closed Loans started by user:" + ServiceHelpers.GetUserName()); + + //import the closed loans since last successful closed import start date + await ReserveService.GetClosedLoans(); + + await GetImports(); //update the banner and import list with new import + updateBanner = true; + + openModal.Close(); + } + + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/LoadingLighthouse.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/LoadingLighthouse.razor new file mode 100644 index 0000000..568bfcf --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/LoadingLighthouse.razor @@ -0,0 +1,5 @@ +LCU Lighthouse + +@code { + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/LoadingSpinner.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/LoadingSpinner.razor new file mode 100644 index 0000000..1971345 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/LoadingSpinner.razor @@ -0,0 +1,9 @@ +
+
+ +
+
+ +@code { + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/Table/TableData.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/Table/TableData.razor new file mode 100644 index 0000000..5c09c9b --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/Table/TableData.razor @@ -0,0 +1,20 @@ + + +
+ @TableDataValue +
+ @if (@DisplayWarning) + { +
+ +
+ } + + +@code { + [Parameter] + public string TableDataValue { get; set; } = ""; + + [Parameter] + public bool DisplayWarning { get; set; } = false; +} \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/Table/TableHeader.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/Table/TableHeader.razor new file mode 100644 index 0000000..90d4cea --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/Components/Table/TableHeader.razor @@ -0,0 +1,29 @@ + +
+ @if (HeaderName == SelectedHeader) + { + if (Ascending) + { + + } + else + { + + } + } + @TableHeaderValue +
+ + +@code { + [Parameter] + public string TableHeaderValue { get; set; } = ""; + [Parameter] + public Action OnClick { get; set; } + [Parameter] + public string SelectedHeader { get; set; } = ""; + [Parameter] + public string HeaderName { get; set; } + [Parameter] + public bool Ascending { get; set; } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/LoginDisplay.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/LoginDisplay.razor new file mode 100644 index 0000000..f57da24 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/LoginDisplay.razor @@ -0,0 +1,5 @@ + + + diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/MainLayout.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/MainLayout.razor new file mode 100644 index 0000000..1cd49a7 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/MainLayout.razor @@ -0,0 +1,23 @@ +@inherits LayoutComponentBase +@using Blazored.Toast.Configuration + + + + + +
+
+ + +
+ @Body +
+
+
+

+ Copyright © @DateTime.Now.Year Landmark Credit Union + @GetAppVersion() +

+
+
+
\ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/MainLayout.razor.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/MainLayout.razor.cs new file mode 100644 index 0000000..bb08149 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/MainLayout.razor.cs @@ -0,0 +1,33 @@ +using Blazored.Modal.Services; +using Blazored.Toast.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace ReserveManagementSystemUI.Shared +{ + public partial class MainLayout: LayoutComponentBase + { + [Inject] + IToastService ToastService { get; set; } + + [Inject] + IConfiguration Configuration { get; set; } + + [Inject] + IModalService Modal { get; set; } + + private string GetAppVersion() + { + var version = string.IsNullOrEmpty(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion) ? + "" : + $"v{FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion}"; + return version; + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/NavMenu.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/NavMenu.razor new file mode 100644 index 0000000..914b13f --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Shared/NavMenu.razor @@ -0,0 +1,50 @@ +@using Microsoft.Extensions.Configuration; +@inject IConfiguration Configuration; + + +@code { + bool collapseNavMenu = true; + + string NavMenuCssClass => collapseNavMenu ? "collapse navbar-collapse" : "collapse navbar-collapse show"; + + void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } + + void ForceClosed() + { + collapseNavMenu = true; + } + +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Startup.cs b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Startup.cs new file mode 100644 index 0000000..9bd1027 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/Startup.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Blazored.Toast; +using LCU.Library.Internal.LcuHttpClient; +using ReserveManagementSystemUI.Service; +using ReserveManagementSystemUI.Services; +using ReserveManagementSystemUI.Helpers; +using Blazored.Modal; +using ReserveManagementSystemUI.Model.Configuration; +using LCU.Library.Internal.Logging; +using LCU.Library.Internal.Logging.Models; + +namespace ReserveManagementSystemUI +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + var appConfiguration = new AppConfiguration(); + Configuration.GetSection("Primary").Bind(appConfiguration); + + var loggerConfiguration = new LoggingConfiguration(appConfiguration.AppName, + appConfiguration.SeqApiKey, + appConfiguration.SeqUrl); + var lcuLogging = new LcuLogging(loggerConfiguration); + lcuLogging.UseLcuLogging(); + + services.AddRazorPages(); + services.AddServerSideBlazor(); + + LcuConfigureServicesAddons(services); + } + + //Added From Blazor Base + public void LcuConfigureServicesAddons(IServiceCollection services) + { + //Configuration + var rmsUIConfiguration = new RmsUIConfiguration(); + Configuration.GetSection("RmsUIConfiguration").Bind(rmsUIConfiguration); + services.AddSingleton(rmsUIConfiguration); + + services.AddSingleton(Configuration); + services.AddAuthentication(); + services.AddAuthorization(config => + { + config.AddPolicy("IsRMSAssociate", policy => policy.RequireRole(rmsUIConfiguration.RmsAssociatesRoles.Split(','))); + }); + services.AddLCUHttpServices(); + services.AddHttpContextAccessor(); + + //Added Components + services.AddBlazoredToast(); + services.AddBlazoredModal(); + + //Services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); + }); + } + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/_Imports.razor b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/_Imports.razor new file mode 100644 index 0000000..9a31496 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/_Imports.razor @@ -0,0 +1,18 @@ +@using System.Net.Http +@using Microsoft.Extensions.Configuration +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using ReserveManagementSystemUI +@using ReserveManagementSystemUI.Shared +@using Blazored.Toast +@using Blazored.Toast.Services +@using Blazored.Typeahead +@using Blazored.Modal +@using Blazored.Modal.Services +@using Blazored.FluentValidation +@using LCU.Library.Internal.RazorComponents.TabControl +@using ReserveManagementSystemUI.Model.Configuration \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/appsettings.json b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/appsettings.json new file mode 100644 index 0000000..5486533 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/appsettings.json @@ -0,0 +1,19 @@ +{ + "Primary": { + "AppName": "Reserve Management System", + "SeqApiKey": "VALUE-IN-KEYVAULT", + "SeqUrl": "https://qa-seq.lcu-internal.com" + }, + "RmsUIConfiguration": { + "AllowedHosts": "*", + "NotificationsUrl": "https://qa-notificationsapi.lcu-internal.com/api/", + "RmsApiUrl": "https://localhost:54779/", + "LoanController": "Loan", + "ImportController": "Import", + "DealerController": "Dealer", + "AuditHistoryController": "AuditHistory", + "DefaultImportTimeout": 10, + "RMSAssociatesRoles": "Reserve Management System - Associates", + "ReportServerUrl": "https://d2dtbetl01q.lcu-internal.com/reports/report/RMS/" + } +} diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/libman.json b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/libman.json new file mode 100644 index 0000000..168c94c --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/libman.json @@ -0,0 +1,10 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "font-awesome@5.15.3", + "destination": "wwwroot/css/font-awesome/" + } + ] +} \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/bootstrap/bootstrap.min.css b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/bootstrap/bootstrap.min.css new file mode 100644 index 0000000..92e3fe8 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/bootstrap/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #dee2e6;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:0s .6s opacity}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/bootstrap/bootstrap.min.css.map b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/bootstrap/bootstrap.min.css.map new file mode 100644 index 0000000..1e9cb78 --- /dev/null +++ b/LCU.WebApps.Indirect.ReserveManagementSystem/WebApp/ReserveManagementSystemUI/wwwroot/css/bootstrap/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","bootstrap.css","../../scss/mixins/_hover.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/mixins/_border-radius.scss","../../scss/_code.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/_tables.scss","../../scss/mixins/_table-row.scss","../../scss/_forms.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_forms.scss","../../scss/mixins/_gradients.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/mixins/_nav-divider.scss","../../scss/_button-group.scss","../../scss/_input-group.scss","../../scss/_custom-forms.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/mixins/_badge.scss","../../scss/_jumbotron.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_media.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/utilities/_align.scss","../../scss/mixins/_background-variant.scss","../../scss/utilities/_background.scss","../../scss/utilities/_borders.scss","../../scss/utilities/_display.scss","../../scss/utilities/_embed.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_float.scss","../../scss/utilities/_overflow.scss","../../scss/utilities/_position.scss","../../scss/utilities/_screenreaders.scss","../../scss/mixins/_screen-reader.scss","../../scss/utilities/_shadows.scss","../../scss/utilities/_sizing.scss","../../scss/utilities/_stretched-link.scss","../../scss/utilities/_spacing.scss","../../scss/utilities/_text.scss","../../scss/mixins/_text-truncate.scss","../../scss/mixins/_text-emphasis.scss","../../scss/mixins/_text-hide.scss","../../scss/utilities/_visibility.scss","../../scss/_print.scss"],"names":[],"mappings":"AAAA;;;;;ACAA,MAGI,OAAA,QAAA,SAAA,QAAA,SAAA,QAAA,OAAA,QAAA,MAAA,QAAA,SAAA,QAAA,SAAA,QAAA,QAAA,QAAA,OAAA,QAAA,OAAA,QAAA,QAAA,KAAA,OAAA,QAAA,YAAA,QAIA,UAAA,QAAA,YAAA,QAAA,UAAA,QAAA,OAAA,QAAA,UAAA,QAAA,SAAA,QAAA,QAAA,QAAA,OAAA,QAIA,gBAAA,EAAA,gBAAA,MAAA,gBAAA,MAAA,gBAAA,MAAA,gBAAA,OAKF,yBAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBACA,wBAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UCCF,ECqBA,QADA,SDjBE,WAAA,WAGF,KACE,YAAA,WACA,YAAA,KACA,yBAAA,KACA,4BAAA,YAMF,QAAA,MAAA,WAAA,OAAA,OAAA,OAAA,OAAA,KAAA,IAAA,QACE,QAAA,MAUF,KACE,OAAA,EACA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBEgFI,UAAA,KF9EJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,KACA,iBAAA,KGYF,sBHHE,QAAA,YASF,GACE,WAAA,YACA,OAAA,EACA,SAAA,QAaF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAOF,EACE,WAAA,EACA,cAAA,KCZF,0BDuBA,YAEE,gBAAA,UACA,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,cAAA,EACA,iCAAA,KAAA,yBAAA,KAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QCjBF,GDoBA,GCrBA,GDwBE,WAAA,EACA,cAAA,KAGF,MCpBA,MACA,MAFA,MDyBE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAGF,ECrBA,ODuBE,YAAA,OAGF,MEpFI,UAAA,IF6FJ,IC1BA,ID4BE,SAAA,SE/FE,UAAA,IFiGF,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAON,EACE,MAAA,QACA,gBAAA,KACA,iBAAA,YI5KA,QJ+KE,MAAA,QACA,gBAAA,UAUJ,8BACE,MAAA,QACA,gBAAA,KIxLA,oCAAA,oCJ2LE,MAAA,QACA,gBAAA,KANJ,oCAUI,QAAA,EC5BJ,KACA,IDoCA,ICnCA,KDuCE,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UErJE,UAAA,IFyJJ,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAQF,OAEE,OAAA,EAAA,EAAA,KAQF,IACE,eAAA,OACA,aAAA,KAGF,IAGE,SAAA,OACA,eAAA,OAQF,MACE,gBAAA,SAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAGF,GAGE,WAAA,QAQF,MAEE,QAAA,aACA,cAAA,MAMF,OAEE,cAAA,EAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBCvEF,OD0EA,MCxEA,SADA,OAEA,SD4EE,OAAA,EACA,YAAA,QEtPE,UAAA,QFwPF,YAAA,QAGF,OC1EA,MD4EE,SAAA,QAGF,OC1EA,OD4EE,eAAA,KAMF,OACE,UAAA,OC1EF,cACA,aACA,cD+EA,OAIE,mBAAA,OC9EF,6BACA,4BACA,6BDiFE,sBAKI,OAAA,QCjFN,gCACA,+BACA,gCDqFA,yBAIE,QAAA,EACA,aAAA,KCpFF,qBDuFA,kBAEE,WAAA,WACA,QAAA,EAIF,iBCvFA,2BACA,kBAFA,iBDiGE,mBAAA,QAGF,SACE,SAAA,KAEA,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAKF,OACE,QAAA,MACA,MAAA,KACA,UAAA,KACA,QAAA,EACA,cAAA,MElSI,UAAA,OFoSJ,YAAA,QACA,MAAA,QACA,YAAA,OAGF,SACE,eAAA,SGtGF,yCFGA,yCDyGE,OAAA,KGvGF,cH+GE,eAAA,KACA,mBAAA,KG3GF,yCHmHE,mBAAA,KAQF,6BACE,KAAA,QACA,mBAAA,OAOF,OACE,QAAA,aAGF,QACE,QAAA,UACA,OAAA,QAGF,SACE,QAAA,KGxHF,SH8HE,QAAA,eCvHF,IAAK,IAAK,IAAK,IAAK,IAAK,IIpWzB,GAAA,GAAA,GAAA,GAAA,GAAA,GAEE,cAAA,MAEA,YAAA,IACA,YAAA,IAIF,IAAA,GHgHM,UAAA,OG/GN,IAAA,GH+GM,UAAA,KG9GN,IAAA,GH8GM,UAAA,QG7GN,IAAA,GH6GM,UAAA,OG5GN,IAAA,GH4GM,UAAA,QG3GN,IAAA,GH2GM,UAAA,KGzGN,MHyGM,UAAA,QGvGJ,YAAA,IAIF,WHmGM,UAAA,KGjGJ,YAAA,IACA,YAAA,IAEF,WH8FM,UAAA,OG5FJ,YAAA,IACA,YAAA,IAEF,WHyFM,UAAA,OGvFJ,YAAA,IACA,YAAA,IAEF,WHoFM,UAAA,OGlFJ,YAAA,IACA,YAAA,ILyBF,GKhBE,WAAA,KACA,cAAA,KACA,OAAA,EACA,WAAA,IAAA,MAAA,eJmXF,OI3WA,MHMI,UAAA,IGHF,YAAA,IJ8WF,MI3WA,KAEE,QAAA,KACA,iBAAA,QAQF,eC/EE,aAAA,EACA,WAAA,KDmFF,aCpFE,aAAA,EACA,WAAA,KDsFF,kBACE,QAAA,aADF,mCAII,aAAA,MAUJ,YHjCI,UAAA,IGmCF,eAAA,UAIF,YACE,cAAA,KHeI,UAAA,QGXN,mBACE,QAAA,MH7CE,UAAA,IG+CF,MAAA,QAHF,2BAMI,QAAA,aEnHJ,WCIE,UAAA,KAGA,OAAA,KDDF,eACE,QAAA,OACA,iBAAA,KACA,OAAA,IAAA,MAAA,QEXE,cAAA,ODMF,UAAA,KAGA,OAAA,KDcF,QAEE,QAAA,aAGF,YACE,cAAA,MACA,YAAA,EAGF,gBLkCI,UAAA,IKhCF,MAAA,QGvCF,KRuEI,UAAA,MQrEF,MAAA,QACA,WAAA,WAGA,OACE,MAAA,QAKJ,IACE,QAAA,MAAA,MR0DE,UAAA,MQxDF,MAAA,KACA,iBAAA,QDZE,cAAA,MCQJ,QASI,QAAA,ERkDA,UAAA,KQhDA,YAAA,IVyMJ,IUlME,QAAA,MRyCE,UAAA,MQvCF,MAAA,QAHF,SR0CI,UAAA,QQlCA,MAAA,QACA,WAAA,OAKJ,gBACE,WAAA,MACA,WAAA,OCzCA,WCAA,MAAA,KACA,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KCmDE,yBFvDF,WCYI,UAAA,OC2CF,yBFvDF,WCYI,UAAA,OC2CF,yBFvDF,WCYI,UAAA,OC2CF,0BFvDF,WCYI,UAAA,QDAJ,iBCZA,MAAA,KACA,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KDkBA,KCJA,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,aAAA,MACA,YAAA,MDOA,YACE,aAAA,EACA,YAAA,EAFF,iBVyjBF,0BUnjBM,cAAA,EACA,aAAA,EGjCJ,KAAA,OAAA,QAAA,QAAA,QAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OAAA,ObylBF,UAEqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aAFqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aAFkJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACnG,aAEqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aa5lBI,SAAA,SACA,MAAA,KACA,cAAA,KACA,aAAA,KAmBE,KACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAEF,UACE,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KAIA,OFFN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEFM,OFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,OFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,OFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,OFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,OFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,OFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,OFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,OFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,QFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,QFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,QFFN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEGI,aAAwB,eAAA,GAAA,MAAA,GAExB,YAAuB,eAAA,GAAA,MAAA,GAGrB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,UAAwB,eAAA,GAAA,MAAA,GAAxB,UAAwB,eAAA,GAAA,MAAA,GAAxB,UAAwB,eAAA,GAAA,MAAA,GAMtB,UFTR,YAAA,UESQ,UFTR,YAAA,WESQ,UFTR,YAAA,IESQ,UFTR,YAAA,WESQ,UFTR,YAAA,WESQ,UFTR,YAAA,IESQ,UFTR,YAAA,WESQ,UFTR,YAAA,WESQ,UFTR,YAAA,IESQ,WFTR,YAAA,WESQ,WFTR,YAAA,WCWE,yBC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAEF,aACE,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KAIA,UFFN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,WFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,WFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,WFFN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEGI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFTR,YAAA,EESQ,aFTR,YAAA,UESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,aFTR,YAAA,WESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,aFTR,YAAA,WESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,cFTR,YAAA,WESQ,cFTR,YAAA,YCWE,yBC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAEF,aACE,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KAIA,UFFN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,WFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,WFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,WFFN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEGI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFTR,YAAA,EESQ,aFTR,YAAA,UESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,aFTR,YAAA,WESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,aFTR,YAAA,WESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,cFTR,YAAA,WESQ,cFTR,YAAA,YCWE,yBC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAEF,aACE,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KAIA,UFFN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,WFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,WFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,WFFN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEGI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFTR,YAAA,EESQ,aFTR,YAAA,UESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,aFTR,YAAA,WESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,aFTR,YAAA,WESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,cFTR,YAAA,WESQ,cFTR,YAAA,YCWE,0BC9BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAEF,aACE,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KAIA,UFFN,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,UFFN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IEFM,WFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,WFFN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WEFM,WFFN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEGI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAMtB,aFTR,YAAA,EESQ,aFTR,YAAA,UESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,aFTR,YAAA,WESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,aFTR,YAAA,WESQ,aFTR,YAAA,WESQ,aFTR,YAAA,IESQ,cFTR,YAAA,WESQ,cFTR,YAAA,YG7CF,OACE,MAAA,KACA,cAAA,KACA,MAAA,Qdy+CF,Uc5+CA,UAQI,QAAA,OACA,eAAA,IACA,WAAA,IAAA,MAAA,QAVJ,gBAcI,eAAA,OACA,cAAA,IAAA,MAAA,QAfJ,mBAmBI,WAAA,IAAA,MAAA,Qdy+CJ,ach+CA,aAGI,QAAA,MASJ,gBACE,OAAA,IAAA,MAAA,Qd49CF,mBc79CA,mBAKI,OAAA,IAAA,MAAA,Qd69CJ,yBcl+CA,yBAWM,oBAAA,Id89CN,8BAFA,qBcv9CA,qBdw9CA,2Bcn9CI,OAAA,EAQJ,yCAEI,iBAAA,gBX/DF,4BW2EI,MAAA,QACA,iBAAA,iBCnFJ,ef+hDF,kBADA,kBe1hDM,iBAAA,QfkiDN,2BAFA,kBepiDE,kBfqiDF,wBezhDQ,aAAA,QZLN,kCYiBM,iBAAA,QALN,qCf4hDF,qCenhDU,iBAAA,QA5BR,iBfqjDF,oBADA,oBehjDM,iBAAA,QfwjDN,6BAFA,oBe1jDE,oBf2jDF,0Be/iDQ,aAAA,QZLN,oCYiBM,iBAAA,QALN,uCfkjDF,uCeziDU,iBAAA,QA5BR,ef2kDF,kBADA,kBetkDM,iBAAA,Qf8kDN,2BAFA,kBehlDE,kBfilDF,wBerkDQ,aAAA,QZLN,kCYiBM,iBAAA,QALN,qCfwkDF,qCe/jDU,iBAAA,QA5BR,YfimDF,eADA,ee5lDM,iBAAA,QfomDN,wBAFA,eetmDE,efumDF,qBe3lDQ,aAAA,QZLN,+BYiBM,iBAAA,QALN,kCf8lDF,kCerlDU,iBAAA,QA5BR,efunDF,kBADA,kBelnDM,iBAAA,Qf0nDN,2BAFA,kBe5nDE,kBf6nDF,wBejnDQ,aAAA,QZLN,kCYiBM,iBAAA,QALN,qCfonDF,qCe3mDU,iBAAA,QA5BR,cf6oDF,iBADA,iBexoDM,iBAAA,QfgpDN,0BAFA,iBelpDE,iBfmpDF,uBevoDQ,aAAA,QZLN,iCYiBM,iBAAA,QALN,oCf0oDF,oCejoDU,iBAAA,QA5BR,afmqDF,gBADA,gBe9pDM,iBAAA,QfsqDN,yBAFA,gBexqDE,gBfyqDF,sBe7pDQ,aAAA,QZLN,gCYiBM,iBAAA,QALN,mCfgqDF,mCevpDU,iBAAA,QA5BR,YfyrDF,eADA,eeprDM,iBAAA,Qf4rDN,wBAFA,ee9rDE,ef+rDF,qBenrDQ,aAAA,QZLN,+BYiBM,iBAAA,QALN,kCfsrDF,kCe7qDU,iBAAA,QA5BR,cf+sDF,iBADA,iBe1sDM,iBAAA,iBZGJ,iCYiBM,iBAAA,iBALN,oCfqsDF,oCe5rDU,iBAAA,iBD8EV,sBAGM,MAAA,KACA,iBAAA,QACA,aAAA,QALN,uBAWM,MAAA,QACA,iBAAA,QACA,aAAA,QAKN,YACE,MAAA,KACA,iBAAA,QdgnDF,eclnDA,edmnDA,qBc5mDI,aAAA,QAPJ,2BAWI,OAAA,EAXJ,oDAgBM,iBAAA,sBXrIJ,uCW4IM,MAAA,KACA,iBAAA,uBFhFJ,4BEiGA,qBAEI,QAAA,MACA,MAAA,KACA,WAAA,KACA,2BAAA,MALH,qCASK,OAAA,GF1GN,4BEiGA,qBAEI,QAAA,MACA,MAAA,KACA,WAAA,KACA,2BAAA,MALH,qCASK,OAAA,GF1GN,4BEiGA,qBAEI,QAAA,MACA,MAAA,KACA,WAAA,KACA,2BAAA,MALH,qCASK,OAAA,GF1GN,6BEiGA,qBAEI,QAAA,MACA,MAAA,KACA,WAAA,KACA,2BAAA,MALH,qCASK,OAAA,GAdV,kBAOQ,QAAA,MACA,MAAA,KACA,WAAA,KACA,2BAAA,MAVR,kCAcU,OAAA,EE7KV,cACE,QAAA,MACA,MAAA,KACA,OAAA,2BACA,QAAA,QAAA,OfqHI,UAAA,KelHJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,QRbE,cAAA,OSCE,WAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAKF,uCDLJ,cCMM,WAAA,MDNN,0BAsBI,iBAAA,YACA,OAAA,EEhBF,oBACE,MAAA,QACA,iBAAA,KACA,aAAA,QACA,QAAA,EAKE,WAAA,EAAA,EAAA,EAAA,MAAA,oBFhBN,yCA+BI,MAAA,QAEA,QAAA,EAjCJ,gCA+BI,MAAA,QAEA,QAAA,EAjCJ,oCA+BI,MAAA,QAEA,QAAA,EAjCJ,qCA+BI,MAAA,QAEA,QAAA,EAjCJ,2BA+BI,MAAA,QAEA,QAAA,EAjCJ,uBAAA,wBA2CI,iBAAA,QAEA,QAAA,EAIJ,qCAOI,MAAA,QACA,iBAAA,KAKJ,mBhBm0DA,oBgBj0DE,QAAA,MACA,MAAA,KAUF,gBACE,YAAA,oBACA,eAAA,oBACA,cAAA,EfZE,UAAA,QecF,YAAA,IAGF,mBACE,YAAA,kBACA,eAAA,kBfoCI,UAAA,QelCJ,YAAA,IAGF,mBACE,YAAA,mBACA,eAAA,mBf6BI,UAAA,Qe3BJ,YAAA,IASF,wBACE,QAAA,MACA,MAAA,KACA,YAAA,QACA,eAAA,QACA,cAAA,EACA,YAAA,IACA,MAAA,QACA,iBAAA,YACA,OAAA,MAAA,YACA,aAAA,IAAA,EAVF,wCAAA,wCAcI,cAAA,EACA,aAAA,EAYJ,iBACE,OAAA,0BACA,QAAA,OAAA,MfXI,UAAA,QeaJ,YAAA,IRvIE,cAAA,MQ2IJ,iBACE,OAAA,yBACA,QAAA,MAAA,KfnBI,UAAA,QeqBJ,YAAA,IR/IE,cAAA,MQoJJ,8BAAA,0BAGI,OAAA,KAIJ,sBACE,OAAA,KAQF,YACE,cAAA,KAGF,WACE,QAAA,MACA,WAAA,OAQF,UACE,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,aAAA,KACA,YAAA,KAJF,ehBwyDA,wBgBhyDI,cAAA,IACA,aAAA,IASJ,YACE,SAAA,SACA,QAAA,MACA,aAAA,QAGF,kBACE,SAAA,SACA,WAAA,MACA,YAAA,SAHF,6CAMI,MAAA,QAIJ,kBACE,cAAA,EAGF,mBACE,QAAA,mBAAA,QAAA,YACA,eAAA,OAAA,YAAA,OACA,aAAA,EACA,aAAA,OAJF,qCAQI,SAAA,OACA,WAAA,EACA,aAAA,SACA,YAAA,EE3MF,gBACE,QAAA,KACA,MAAA,KACA,WAAA,OjBwCA,UAAA,IiBtCA,MAAA,QAGF,eACE,SAAA,SACA,IAAA,KACA,QAAA,EACA,QAAA,KACA,UAAA,KACA,QAAA,OAAA,MACA,WAAA,MjBmFE,UAAA,QiBjFF,YAAA,IACA,MAAA,KACA,iBAAA,mBV3CA,cAAA,OUgDA,uBAAA,mCAEE,aAAA,QAGE,cAAA,qBACA,iBAAA,2OACA,kBAAA,UACA,oBAAA,OAAA,MAAA,wBACA,gBAAA,sBAAA,sBATJ,6BAAA,yCAaI,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBlB2+D6C,uCACrD,sCkB1/DI,mDlBy/DJ,kDkBt+DQ,QAAA,MAOJ,2CAAA,+BAGI,cAAA,qBACA,oBAAA,IAAA,wBAAA,MAAA,wBAMJ,wBAAA,oCAEE,aAAA,QAGE,cAAA,uCACA,WAAA,0JAAA,UAAA,MAAA,OAAA,MAAA,CAAA,IAAA,IAAA,CAAA,2OAAA,KAAA,UAAA,OAAA,MAAA,OAAA,CAAA,sBAAA,sBANJ,8BAAA,0CAUI,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBlBg+D8C,wCACtD,uCkB5+DI,oDlB2+DJ,mDkB39DQ,QAAA,MlBi+DkD,4CAC1D,2CkB39DI,wDlB09DJ,uDkBt9DQ,QAAA,MAMJ,6CAAA,yDAGI,MAAA,QlBu9DiD,2CACzD,0CkB39DI,uDlB09DJ,sDkBl9DQ,QAAA,MAMJ,qDAAA,iEAGI,MAAA,QAHJ,6DAAA,yEAMM,aAAA,QlBo9DmD,+CAC7D,8CkB39DI,2DlB09DJ,0DkB98DQ,QAAA,MAZJ,qEAAA,iFAiBM,aAAA,QCnJN,iBAAA,QDkIA,mEAAA,+EAwBM,WAAA,EAAA,EAAA,EAAA,MAAA,oBAxBN,iFAAA,6FA4BM,aAAA,QAQN,+CAAA,2DAGI,aAAA,QlB08DkD,4CAC1D,2CkB98DI,wDlB68DJ,uDkBr8DQ,QAAA,MARJ,qDAAA,iEAaM,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBA7JR,kBACE,QAAA,KACA,MAAA,KACA,WAAA,OjBwCA,UAAA,IiBtCA,MAAA,QAGF,iBACE,SAAA,SACA,IAAA,KACA,QAAA,EACA,QAAA,KACA,UAAA,KACA,QAAA,OAAA,MACA,WAAA,MjBmFE,UAAA,QiBjFF,YAAA,IACA,MAAA,KACA,iBAAA,mBV3CA,cAAA,OUgDA,yBAAA,qCAEE,aAAA,QAGE,cAAA,qBACA,iBAAA,qRACA,kBAAA,UACA,oBAAA,OAAA,MAAA,wBACA,gBAAA,sBAAA,sBATJ,+BAAA,2CAaI,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBlBsmEiD,2CACzD,0CkBrnEI,uDlBonEJ,sDkBjmEQ,QAAA,MAOJ,6CAAA,iCAGI,cAAA,qBACA,oBAAA,IAAA,wBAAA,MAAA,wBAMJ,0BAAA,sCAEE,aAAA,QAGE,cAAA,uCACA,WAAA,0JAAA,UAAA,MAAA,OAAA,MAAA,CAAA,IAAA,IAAA,CAAA,qRAAA,KAAA,UAAA,OAAA,MAAA,OAAA,CAAA,sBAAA,sBANJ,gCAAA,4CAUI,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBlB2lEkD,4CAC1D,2CkBvmEI,wDlBsmEJ,uDkBtlEQ,QAAA,MlB4lEsD,gDAC9D,+CkBtlEI,4DlBqlEJ,2DkBjlEQ,QAAA,MAMJ,+CAAA,2DAGI,MAAA,QlBklEqD,+CAC7D,8CkBtlEI,2DlBqlEJ,0DkB7kEQ,QAAA,MAMJ,uDAAA,mEAGI,MAAA,QAHJ,+DAAA,2EAMM,aAAA,QlB+kEuD,mDACjE,kDkBtlEI,+DlBqlEJ,8DkBzkEQ,QAAA,MAZJ,uEAAA,mFAiBM,aAAA,QCnJN,iBAAA,QDkIA,qEAAA,iFAwBM,WAAA,EAAA,EAAA,EAAA,MAAA,oBAxBN,mFAAA,+FA4BM,aAAA,QAQN,iDAAA,6DAGI,aAAA,QlBqkEsD,gDAC9D,+CkBzkEI,4DlBwkEJ,2DkBhkEQ,QAAA,MARJ,uDAAA,mEAaM,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBFuEV,aACE,QAAA,YAAA,QAAA,KACA,cAAA,IAAA,KAAA,UAAA,IAAA,KACA,eAAA,OAAA,YAAA,OAHF,yBASI,MAAA,KJ9MA,yBIqMJ,mBAeM,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,OAAA,gBAAA,OACA,cAAA,EAlBN,yBAuBM,QAAA,YAAA,QAAA,KACA,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,cAAA,IAAA,KAAA,UAAA,IAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,EA3BN,2BAgCM,QAAA,aACA,MAAA,KACA,eAAA,OAlCN,qCAuCM,QAAA,ahBigEJ,4BgBxiEF,0BA4CM,MAAA,KA5CN,yBAkDM,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,OAAA,gBAAA,OACA,MAAA,KACA,aAAA,EAtDN,+BAyDM,SAAA,SACA,kBAAA,EAAA,YAAA,EACA,WAAA,EACA,aAAA,OACA,YAAA,EA7DN,6BAiEM,eAAA,OAAA,YAAA,OACA,cAAA,OAAA,gBAAA,OAlEN,mCAqEM,cAAA,GIhUN,KACE,QAAA,aAEA,YAAA,IACA,MAAA,QACA,WAAA,OACA,eAAA,OACA,oBAAA,KAAA,iBAAA,KAAA,gBAAA,KAAA,YAAA,KACA,iBAAA,YACA,OAAA,IAAA,MAAA,YCsFA,QAAA,QAAA,OpB0BI,UAAA,KoBxBJ,YAAA,IblGE,cAAA,OSCE,WAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAKF,uCGLJ,KHMM,WAAA,MdAJ,WiBQE,MAAA,QACA,gBAAA,KAfJ,WAAA,WAoBI,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBArBJ,cAAA,cA2BI,QAAA,IAeJ,epBi0EA,wBoB/zEE,eAAA,KASA,aCrDA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,mBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,mBAAA,mBAMI,WAAA,EAAA,EAAA,EAAA,MAAA,oBAKJ,sBAAA,sBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,kDAAA,kDrBq2EF,mCqBl2EI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,wDAAA,wDrBk2EJ,yCqB71EQ,WAAA,EAAA,EAAA,EAAA,MAAA,oBDKN,eCrDA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,qBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,qBAAA,qBAMI,WAAA,EAAA,EAAA,EAAA,MAAA,qBAKJ,wBAAA,wBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,oDAAA,oDrBu4EF,qCqBp4EI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,0DAAA,0DrBo4EJ,2CqB/3EQ,WAAA,EAAA,EAAA,EAAA,MAAA,qBDKN,aCrDA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,mBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,mBAAA,mBAMI,WAAA,EAAA,EAAA,EAAA,MAAA,mBAKJ,sBAAA,sBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,kDAAA,kDrBy6EF,mCqBt6EI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,wDAAA,wDrBs6EJ,yCqBj6EQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBDKN,UCrDA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,gBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,gBAAA,gBAMI,WAAA,EAAA,EAAA,EAAA,MAAA,oBAKJ,mBAAA,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,+CAAA,+CrB28EF,gCqBx8EI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,qDAAA,qDrBw8EJ,sCqBn8EQ,WAAA,EAAA,EAAA,EAAA,MAAA,oBDKN,aCrDA,MAAA,QFAE,iBAAA,QEEF,aAAA,QlBIA,mBkBAE,MAAA,QFNA,iBAAA,QEQA,aAAA,QAGF,mBAAA,mBAMI,WAAA,EAAA,EAAA,EAAA,MAAA,oBAKJ,sBAAA,sBAEE,MAAA,QACA,iBAAA,QACA,aAAA,QAOF,kDAAA,kDrB6+EF,mCqB1+EI,MAAA,QACA,iBAAA,QAIA,aAAA,QAEA,wDAAA,wDrB0+EJ,yCqBr+EQ,WAAA,EAAA,EAAA,EAAA,MAAA,oBDKN,YCrDA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,kBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,kBAAA,kBAMI,WAAA,EAAA,EAAA,EAAA,MAAA,mBAKJ,qBAAA,qBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,iDAAA,iDrB+gFF,kCqB5gFI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,uDAAA,uDrB4gFJ,wCqBvgFQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBDKN,WCrDA,MAAA,QFAE,iBAAA,QEEF,aAAA,QlBIA,iBkBAE,MAAA,QFNA,iBAAA,QEQA,aAAA,QAGF,iBAAA,iBAMI,WAAA,EAAA,EAAA,EAAA,MAAA,qBAKJ,oBAAA,oBAEE,MAAA,QACA,iBAAA,QACA,aAAA,QAOF,gDAAA,gDrBijFF,iCqB9iFI,MAAA,QACA,iBAAA,QAIA,aAAA,QAEA,sDAAA,sDrB8iFJ,uCqBziFQ,WAAA,EAAA,EAAA,EAAA,MAAA,qBDKN,UCrDA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,gBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,gBAAA,gBAMI,WAAA,EAAA,EAAA,EAAA,MAAA,kBAKJ,mBAAA,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,+CAAA,+CrBmlFF,gCqBhlFI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,qDAAA,qDrBglFJ,sCqB3kFQ,WAAA,EAAA,EAAA,EAAA,MAAA,kBDWN,qBCJA,MAAA,QACA,aAAA,QlBlDA,2BkBqDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,2BAAA,2BAEE,WAAA,EAAA,EAAA,EAAA,MAAA,mBAGF,8BAAA,8BAEE,MAAA,QACA,iBAAA,YAGF,0DAAA,0DrBykFF,2CqBtkFI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,gEAAA,gErBykFJ,iDqBpkFQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBD5BN,uBCJA,MAAA,QACA,aAAA,QlBlDA,6BkBqDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,6BAAA,6BAEE,WAAA,EAAA,EAAA,EAAA,MAAA,qBAGF,gCAAA,gCAEE,MAAA,QACA,iBAAA,YAGF,4DAAA,4DrBymFF,6CqBtmFI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,kEAAA,kErBymFJ,mDqBpmFQ,WAAA,EAAA,EAAA,EAAA,MAAA,qBD5BN,qBCJA,MAAA,QACA,aAAA,QlBlDA,2BkBqDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,2BAAA,2BAEE,WAAA,EAAA,EAAA,EAAA,MAAA,mBAGF,8BAAA,8BAEE,MAAA,QACA,iBAAA,YAGF,0DAAA,0DrByoFF,2CqBtoFI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,gEAAA,gErByoFJ,iDqBpoFQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBD5BN,kBCJA,MAAA,QACA,aAAA,QlBlDA,wBkBqDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,wBAAA,wBAEE,WAAA,EAAA,EAAA,EAAA,MAAA,oBAGF,2BAAA,2BAEE,MAAA,QACA,iBAAA,YAGF,uDAAA,uDrByqFF,wCqBtqFI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,6DAAA,6DrByqFJ,8CqBpqFQ,WAAA,EAAA,EAAA,EAAA,MAAA,oBD5BN,qBCJA,MAAA,QACA,aAAA,QlBlDA,2BkBqDE,MAAA,QACA,iBAAA,QACA,aAAA,QAGF,2BAAA,2BAEE,WAAA,EAAA,EAAA,EAAA,MAAA,mBAGF,8BAAA,8BAEE,MAAA,QACA,iBAAA,YAGF,0DAAA,0DrBysFF,2CqBtsFI,MAAA,QACA,iBAAA,QACA,aAAA,QAEA,gEAAA,gErBysFJ,iDqBpsFQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBD5BN,oBCJA,MAAA,QACA,aAAA,QlBlDA,0BkBqDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,0BAAA,0BAEE,WAAA,EAAA,EAAA,EAAA,MAAA,mBAGF,6BAAA,6BAEE,MAAA,QACA,iBAAA,YAGF,yDAAA,yDrByuFF,0CqBtuFI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,+DAAA,+DrByuFJ,gDqBpuFQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBD5BN,mBCJA,MAAA,QACA,aAAA,QlBlDA,yBkBqDE,MAAA,QACA,iBAAA,QACA,aAAA,QAGF,yBAAA,yBAEE,WAAA,EAAA,EAAA,EAAA,MAAA,qBAGF,4BAAA,4BAEE,MAAA,QACA,iBAAA,YAGF,wDAAA,wDrBywFF,yCqBtwFI,MAAA,QACA,iBAAA,QACA,aAAA,QAEA,8DAAA,8DrBywFJ,+CqBpwFQ,WAAA,EAAA,EAAA,EAAA,MAAA,qBD5BN,kBCJA,MAAA,QACA,aAAA,QlBlDA,wBkBqDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,wBAAA,wBAEE,WAAA,EAAA,EAAA,EAAA,MAAA,kBAGF,2BAAA,2BAEE,MAAA,QACA,iBAAA,YAGF,uDAAA,uDrByyFF,wCqBtyFI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,6DAAA,6DrByyFJ,8CqBpyFQ,WAAA,EAAA,EAAA,EAAA,MAAA,kBDjBR,UACE,YAAA,IACA,MAAA,QACA,gBAAA,KjBnEA,gBiBsEE,MAAA,QACA,gBAAA,UAPJ,gBAAA,gBAYI,gBAAA,UACA,WAAA,KAbJ,mBAAA,mBAkBI,MAAA,QACA,eAAA,KAWJ,mBAAA,QCLE,QAAA,MAAA,KpB0BI,UAAA,QoBxBJ,YAAA,IblGE,cAAA,MYyGJ,mBAAA,QCTE,QAAA,OAAA,MpB0BI,UAAA,QoBxBJ,YAAA,IblGE,cAAA,MYkHJ,WACE,QAAA,MACA,MAAA,KAFF,sBAMI,WAAA,MpBszFJ,6BADA,4BoBhzFA,6BAII,MAAA,KEtIJ,MLMM,WAAA,QAAA,KAAA,OAKF,uCKXJ,MLYM,WAAA,MKZN,iBAII,QAAA,EAIJ,qBAEI,QAAA,KAIJ,YACE,SAAA,SACA,OAAA,EACA,SAAA,OLXI,WAAA,OAAA,KAAA,KAKF,uCKGJ,YLFM,WAAA,MjB48FN,UACA,UAFA,WuBt9FA,QAIE,SAAA,SAGF,iBACE,YAAA,OCoBE,wBACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAhCJ,WAAA,KAAA,MACA,aAAA,KAAA,MAAA,YACA,cAAA,EACA,YAAA,KAAA,MAAA,YAqDE,8BACE,YAAA,ED1CN,eACE,SAAA,SACA,IAAA,KACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,MAAA,KACA,UAAA,MACA,QAAA,MAAA,EACA,OAAA,QAAA,EAAA,EtBsGI,UAAA,KsBpGJ,MAAA,QACA,WAAA,KACA,WAAA,KACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,gBf3BE,cAAA,OeoCA,oBACE,MAAA,KACA,KAAA,EAGF,qBACE,MAAA,EACA,KAAA,KXYF,yBWnBA,uBACE,MAAA,KACA,KAAA,EAGF,wBACE,MAAA,EACA,KAAA,MXYF,yBWnBA,uBACE,MAAA,KACA,KAAA,EAGF,wBACE,MAAA,EACA,KAAA,MXYF,yBWnBA,uBACE,MAAA,KACA,KAAA,EAGF,wBACE,MAAA,EACA,KAAA,MXYF,0BWnBA,uBACE,MAAA,KACA,KAAA,EAGF,wBACE,MAAA,EACA,KAAA,MAON,uBAEI,IAAA,KACA,OAAA,KACA,WAAA,EACA,cAAA,QC/BA,gCACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAzBJ,WAAA,EACA,aAAA,KAAA,MAAA,YACA,cAAA,KAAA,MACA,YAAA,KAAA,MAAA,YA8CE,sCACE,YAAA,EDUN,0BAEI,IAAA,EACA,MAAA,KACA,KAAA,KACA,WAAA,EACA,YAAA,QC7CA,mCACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAlBJ,WAAA,KAAA,MAAA,YACA,aAAA,EACA,cAAA,KAAA,MAAA,YACA,YAAA,KAAA,MAuCE,yCACE,YAAA,EA7BF,mCDmDE,eAAA,EAKN,yBAEI,IAAA,EACA,MAAA,KACA,KAAA,KACA,WAAA,EACA,aAAA,QC9DA,kCACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAJF,kCAgBI,QAAA,KAGF,mCACE,QAAA,aACA,aAAA,OACA,eAAA,OACA,QAAA,GA9BN,WAAA,KAAA,MAAA,YACA,aAAA,KAAA,MACA,cAAA,KAAA,MAAA,YAiCE,wCACE,YAAA,EAVA,mCDiDA,eAAA,EAON,oCAAA,kCAAA,mCAAA,iCAKI,MAAA,KACA,OAAA,KAKJ,kBE9GE,OAAA,EACA,OAAA,MAAA,EACA,SAAA,OACA,WAAA,IAAA,MAAA,QFkHF,eACE,QAAA,MACA,MAAA,KACA,QAAA,OAAA,OACA,MAAA,KACA,YAAA,IACA,MAAA,QACA,WAAA,QACA,YAAA,OACA,iBAAA,YACA,OAAA,EpBpHA,qBAAA,qBoBmIE,MAAA,QACA,gBAAA,KJ9IA,iBAAA,QIoHJ,sBAAA,sBAgCI,MAAA,KACA,gBAAA,KJrJA,iBAAA,QIoHJ,wBAAA,wBAuCI,MAAA,QACA,eAAA,KACA,iBAAA,YAQJ,oBACE,QAAA,MAIF,iBACE,QAAA,MACA,QAAA,MAAA,OACA,cAAA,EtBpDI,UAAA,QsBsDJ,MAAA,QACA,YAAA,OAIF,oBACE,QAAA,MACA,QAAA,OAAA,OACA,MAAA,QG1LF,W1B4sGA,oB0B1sGE,SAAA,SACA,QAAA,mBAAA,QAAA,YACA,eAAA,O1BgtGF,yB0BptGA,gBAOI,SAAA,SACA,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,K1BmtGJ,+BGltGE,sBuBII,QAAA,E1BqtGN,gCADA,gCADA,+B0BhuGA,uBAAA,uBAAA,sBAkBM,QAAA,EAMN,aACE,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,cAAA,MAAA,gBAAA,WAHF,0BAMI,MAAA,K1BstGJ,wC0BltGA,kCAII,YAAA,K1BmtGJ,4C0BvtGA,uDlBhBI,wBAAA,EACA,2BAAA,ER4uGJ,6C0B7tGA,kClBFI,uBAAA,EACA,0BAAA,EkBgCJ,uBACE,cAAA,SACA,aAAA,SAFF,8B1B0sGA,yCADA,sC0BlsGI,YAAA,EAGF,yCACE,aAAA,EAIJ,0CAAA,+BACE,cAAA,QACA,aAAA,QAGF,0CAAA,+BACE,cAAA,OACA,aAAA,OAoBF,oBACE,mBAAA,OAAA,eAAA,OACA,eAAA,MAAA,YAAA,WACA,cAAA,OAAA,gBAAA,OAHF,yB1B4rGA,+B0BrrGI,MAAA,K1B0rGJ,iD0BjsGA,2CAYI,WAAA,K1B0rGJ,qD0BtsGA,gElBlFI,2BAAA,EACA,0BAAA,ER6xGJ,sD0B5sGA,2ClBhGI,uBAAA,EACA,wBAAA,EkBuIJ,uB1B0qGA,kC0BvqGI,cAAA,E1B4qGJ,4C0B/qGA,yC1BirGA,uDADA,oD0BzqGM,SAAA,SACA,KAAA,cACA,eAAA,KCzJN,aACE,SAAA,SACA,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,eAAA,QAAA,YAAA,QACA,MAAA,K3Bg1GF,0BADA,4B2Bp1GA,2B3Bm1GA,qC2Bx0GI,SAAA,SACA,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAGA,MAAA,GACA,cAAA,E3Bw1GJ,uCADA,yCADA,wCADA,yCADA,2CADA,0CAJA,wCADA,0C2B91GA,yC3Bk2GA,kDADA,oDADA,mD2B30GM,YAAA,K3By1GN,sEADA,kC2B72GA,iCA6BI,QAAA,EA7BJ,mDAkCI,QAAA,E3Bq1GJ,6C2Bv3GA,4CnBeI,wBAAA,EACA,2BAAA,ER62GJ,8C2B73GA,6CnB6BI,uBAAA,EACA,0BAAA,EmB9BJ,0BA8CI,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OA/CJ,8D3B04GA,qEQ33GI,wBAAA,EACA,2BAAA,EmBhBJ,+DnB6BI,uBAAA,EACA,0BAAA,ERu3GJ,oB2Bv1GA,qBAEE,QAAA,YAAA,QAAA,K3B21GF,yB2B71GA,0BAQI,SAAA,SACA,QAAA,E3B01GJ,+B2Bn2GA,gCAYM,QAAA,E3B+1GN,8BACA,2CAEA,2CADA,wD2B72GA,+B3Bw2GA,4CAEA,4CADA,yD2Br1GI,YAAA,KAIJ,qBAAuB,aAAA,KACvB,oBAAsB,YAAA,KAQtB,kBACE,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,QAAA,QAAA,OACA,cAAA,E1BsBI,UAAA,K0BpBJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,OACA,YAAA,OACA,iBAAA,QACA,OAAA,IAAA,MAAA,QnB5GE,cAAA,OR48GJ,uC2B52GA,oCAkBI,WAAA,E3B+1GJ,+B2Br1GA,4CAEE,OAAA,yB3Bw1GF,+B2Br1GA,8B3By1GA,yCAFA,sDACA,0CAFA,uD2Bh1GE,QAAA,MAAA,K1BbI,UAAA,Q0BeJ,YAAA,InBzIE,cAAA,MRk+GJ,+B2Br1GA,4CAEE,OAAA,0B3Bw1GF,+B2Br1GA,8B3By1GA,yCAFA,sDACA,0CAFA,uD2Bh1GE,QAAA,OAAA,M1B9BI,UAAA,Q0BgCJ,YAAA,InB1JE,cAAA,MmB8JJ,+B3Bq1GA,+B2Bn1GE,cAAA,Q3B21GF,wFACA,+EAHA,uDACA,oE2B/0GA,uC3B60GA,oDQx+GI,wBAAA,EACA,2BAAA,EmBmKJ,sC3B80GA,mDAGA,qEACA,kFAHA,yDACA,sEQt+GI,uBAAA,EACA,0BAAA,EoB3BJ,gBACE,SAAA,SACA,QAAA,MACA,WAAA,OACA,aAAA,OAGF,uBACE,QAAA,mBAAA,QAAA,YACA,aAAA,KAGF,sBACE,SAAA,SACA,QAAA,GACA,QAAA,EAHF,4DAMI,MAAA,KACA,aAAA,QTtBA,iBAAA,QSeJ,0DAiBM,WAAA,EAAA,EAAA,EAAA,MAAA,oBAjBN,wEAsBI,aAAA,QAtBJ,0EA0BI,MAAA,KACA,iBAAA,QACA,aAAA,QA5BJ,qDAkCM,MAAA,QAlCN,6DAqCQ,iBAAA,QAUR,sBACE,SAAA,SACA,cAAA,EACA,eAAA,IAHF,8BAOI,SAAA,SACA,IAAA,OACA,KAAA,QACA,QAAA,MACA,MAAA,KACA,OAAA,KACA,eAAA,KACA,QAAA,GACA,iBAAA,KACA,OAAA,QAAA,MAAA,IAhBJ,6BAsBI,SAAA,SACA,IAAA,OACA,KAAA,QACA,QAAA,MACA,MAAA,KACA,OAAA,KACA,QAAA,GACA,WAAA,UAAA,GAAA,CAAA,IAAA,IASJ,+CpBrGI,cAAA,OoBqGJ,4EAOM,iBAAA,4LAPN,mFAaM,aAAA,QTjHF,iBAAA,QSoGJ,kFAkBM,iBAAA,yIAlBN,sFAwBM,iBAAA,mBAxBN,4FA2BM,iBAAA,mBASN,4CAGI,cAAA,IAHJ,yEAQM,iBAAA,sIARN,mFAcM,iBAAA,mBAUN,eACE,aAAA,QADF,6CAKM,KAAA,SACA,MAAA,QACA,eAAA,IAEA,cAAA,MATN,4CAaM,IAAA,mBACA,KAAA,qBACA,MAAA,iBACA,OAAA,iBACA,iBAAA,QAEA,cAAA,MXnLA,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,WAAA,CAAA,kBAAA,KAAA,YAAA,WAAA,UAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAAA,WAAA,UAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,WAAA,CAAA,kBAAA,KAAA,YAKF,uCW2JJ,4CX1JM,WAAA,MW0JN,0EA0BM,iBAAA,KACA,kBAAA,mBAAA,UAAA,mBA3BN,oFAiCM,iBAAA,mBAYN,eACE,QAAA,aACA,MAAA,KACA,OAAA,2BACA,QAAA,QAAA,QAAA,QAAA,O3BxFI,UAAA,K2B2FJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,eAAA,OACA,WAAA,0JAAA,UAAA,MAAA,OAAA,MAAA,CAAA,IAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,QpB3NE,cAAA,OoB8NF,mBAAA,KAAA,gBAAA,KAAA,WAAA,KAhBF,qBAmBI,aAAA,QACA,QAAA,EAIE,WAAA,EAAA,EAAA,EAAA,MAAA,oBAxBN,gCAiCM,MAAA,QACA,iBAAA,KAlCN,yBAAA,qCAwCI,OAAA,KACA,cAAA,OACA,iBAAA,KA1CJ,wBA8CI,MAAA,QACA,iBAAA,QA/CJ,2BAoDI,QAAA,KAIJ,kBACE,OAAA,0BACA,YAAA,OACA,eAAA,OACA,aAAA,M3BhJI,UAAA,Q2BoJN,kBACE,OAAA,yBACA,YAAA,MACA,eAAA,MACA,aAAA,K3BxJI,UAAA,Q2BiKN,aACE,SAAA,SACA,QAAA,aACA,MAAA,KACA,OAAA,2BACA,cAAA,EAGF,mBACE,SAAA,SACA,QAAA,EACA,MAAA,KACA,OAAA,2BACA,OAAA,EACA,QAAA,EANF,4CASI,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBAVJ,+CAcI,iBAAA,QAdJ,sDAmBM,QAAA,SAnBN,0DAwBI,QAAA,kBAIJ,mBACE,SAAA,SACA,IAAA,EACA,MAAA,EACA,KAAA,EACA,QAAA,EACA,OAAA,2BACA,QAAA,QAAA,OAEA,YAAA,IACA,YAAA,IACA,MAAA,QACA,iBAAA,KACA,OAAA,IAAA,MAAA,QpB5UE,cAAA,OoB+TJ,0BAkBI,SAAA,SACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,QAAA,EACA,QAAA,MACA,OAAA,qBACA,QAAA,QAAA,OACA,YAAA,IACA,MAAA,QACA,QAAA,ST1VA,iBAAA,QS4VA,YAAA,QpB7VA,cAAA,EAAA,OAAA,OAAA,EoBwWJ,cACE,MAAA,KACA,OAAA,mBACA,QAAA,EACA,iBAAA,YACA,mBAAA,KAAA,gBAAA,KAAA,WAAA,KALF,oBAQI,QAAA,EARJ,0CAY8B,WAAA,EAAA,EAAA,EAAA,IAAA,IAAA,CAAA,EAAA,EAAA,EAAA,MAAA,oBAZ9B,sCAa8B,WAAA,EAAA,EAAA,EAAA,IAAA,IAAA,CAAA,EAAA,EAAA,EAAA,MAAA,oBAb9B,+BAc8B,WAAA,EAAA,EAAA,EAAA,IAAA,IAAA,CAAA,EAAA,EAAA,EAAA,MAAA,oBAd9B,gCAkBI,OAAA,EAlBJ,oCAsBI,MAAA,KACA,OAAA,KACA,WAAA,QT/XA,iBAAA,QSiYA,OAAA,EpBlYA,cAAA,KSCE,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YWqYF,mBAAA,KAAA,WAAA,KXhYA,uCWkWJ,oCXjWM,WAAA,MWiWN,2CTvWI,iBAAA,QSuWJ,6CAsCI,MAAA,KACA,OAAA,MACA,MAAA,YACA,OAAA,QACA,iBAAA,QACA,aAAA,YpBnZA,cAAA,KoBwWJ,gCAiDI,MAAA,KACA,OAAA,KTzZA,iBAAA,QS2ZA,OAAA,EpB5ZA,cAAA,KSCE,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YW+ZF,gBAAA,KAAA,WAAA,KX1ZA,uCWkWJ,gCXjWM,WAAA,MWiWN,uCTvWI,iBAAA,QSuWJ,gCAgEI,MAAA,KACA,OAAA,MACA,MAAA,YACA,OAAA,QACA,iBAAA,QACA,aAAA,YpB7aA,cAAA,KoBwWJ,yBA2EI,MAAA,KACA,OAAA,KACA,WAAA,EACA,aAAA,MACA,YAAA,MTtbA,iBAAA,QSwbA,OAAA,EpBzbA,cAAA,KSCE,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YW4bF,WAAA,KXvbA,uCWkWJ,yBXjWM,WAAA,MWiWN,gCTvWI,iBAAA,QSuWJ,yBA6FI,MAAA,KACA,OAAA,MACA,MAAA,YACA,OAAA,QACA,iBAAA,YACA,aAAA,YACA,aAAA,MAnGJ,8BAwGI,iBAAA,QpBhdA,cAAA,KoBwWJ,8BA6GI,aAAA,KACA,iBAAA,QpBtdA,cAAA,KoBwWJ,6CAoHM,iBAAA,QApHN,sDAwHM,OAAA,QAxHN,yCA4HM,iBAAA,QA5HN,yCAgIM,OAAA,QAhIN,kCAoIM,iBAAA,QAKN,8B5Bi9GA,mBACA,eiBl8HM,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAKF,uCW2eJ,8B5Bw9GE,mBACA,eiBn8HI,WAAA,MYPN,KACE,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,aAAA,EACA,cAAA,EACA,WAAA,KAGF,UACE,QAAA,MACA,QAAA,MAAA,K1BCA,gBAAA,gB0BEE,gBAAA,KALJ,mBAUI,MAAA,QACA,eAAA,KACA,OAAA,QAQJ,UACE,cAAA,IAAA,MAAA,QADF,oBAII,cAAA,KAJJ,oBAQI,OAAA,IAAA,MAAA,YrB3BA,uBAAA,OACA,wBAAA,OLCF,0BAAA,0B0B6BI,aAAA,QAAA,QAAA,QAZN,6BAgBM,MAAA,QACA,iBAAA,YACA,aAAA,Y7Bm9HN,mC6Br+HA,2BAwBI,MAAA,QACA,iBAAA,KACA,aAAA,QAAA,QAAA,KA1BJ,yBA+BI,WAAA,KrBlDA,uBAAA,EACA,wBAAA,EqB4DJ,qBrBtEI,cAAA,OqBsEJ,4B7B48HA,2B6Br8HI,MAAA,KACA,iBAAA,QASJ,oBAEI,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,WAAA,OAIJ,yBAEI,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,WAAA,OASJ,uBAEI,QAAA,KAFJ,qBAKI,QAAA,MCpGJ,QACE,SAAA,SACA,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,QAAA,gBAAA,cACA,QAAA,MAAA,KANF,mB9B+iIA,yB8BniII,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,QAAA,gBAAA,cASJ,cACE,QAAA,aACA,YAAA,SACA,eAAA,SACA,aAAA,K7BkFI,UAAA,Q6BhFJ,YAAA,QACA,YAAA,O3BhCA,oBAAA,oB2BmCE,gBAAA,KASJ,YACE,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OACA,aAAA,EACA,cAAA,EACA,WAAA,KALF,sBAQI,cAAA,EACA,aAAA,EATJ,2BAaI,SAAA,OACA,MAAA,KASJ,aACE,QAAA,aACA,YAAA,MACA,eAAA,MAYF,iBACE,wBAAA,KAAA,WAAA,KACA,kBAAA,EAAA,UAAA,EAGA,eAAA,OAAA,YAAA,OAIF,gBACE,QAAA,OAAA,O7BmBI,UAAA,Q6BjBJ,YAAA,EACA,iBAAA,YACA,OAAA,IAAA,MAAA,YtB3GE,cAAA,OLWF,sBAAA,sB2BoGE,gBAAA,KAMJ,qBACE,QAAA,aACA,MAAA,MACA,OAAA,MACA,eAAA,OACA,QAAA,GACA,WAAA,UAAA,OAAA,OACA,gBAAA,KAAA,KlBxDE,4BkBkEC,6B9B0gIH,mC8BtgIQ,cAAA,EACA,aAAA,GlBpFN,yBkB+EA,kBAUI,cAAA,IAAA,OAAA,UAAA,IAAA,OACA,cAAA,MAAA,gBAAA,WAXH,8BAcK,mBAAA,IAAA,eAAA,IAdL,6CAiBO,SAAA,SAjBP,wCAqBO,cAAA,MACA,aAAA,MAtBP,6B9BmiIH,mC8BtgIQ,cAAA,OAAA,UAAA,OA7BL,mCAiCK,QAAA,sBAAA,QAAA,eAGA,wBAAA,KAAA,WAAA,KApCL,kCAwCK,QAAA,MlB1GN,4BkBkEC,6B9BojIH,mC8BhjIQ,cAAA,EACA,aAAA,GlBpFN,yBkB+EA,kBAUI,cAAA,IAAA,OAAA,UAAA,IAAA,OACA,cAAA,MAAA,gBAAA,WAXH,8BAcK,mBAAA,IAAA,eAAA,IAdL,6CAiBO,SAAA,SAjBP,wCAqBO,cAAA,MACA,aAAA,MAtBP,6B9B6kIH,mC8BhjIQ,cAAA,OAAA,UAAA,OA7BL,mCAiCK,QAAA,sBAAA,QAAA,eAGA,wBAAA,KAAA,WAAA,KApCL,kCAwCK,QAAA,MlB1GN,4BkBkEC,6B9B8lIH,mC8B1lIQ,cAAA,EACA,aAAA,GlBpFN,yBkB+EA,kBAUI,cAAA,IAAA,OAAA,UAAA,IAAA,OACA,cAAA,MAAA,gBAAA,WAXH,8BAcK,mBAAA,IAAA,eAAA,IAdL,6CAiBO,SAAA,SAjBP,wCAqBO,cAAA,MACA,aAAA,MAtBP,6B9BunIH,mC8B1lIQ,cAAA,OAAA,UAAA,OA7BL,mCAiCK,QAAA,sBAAA,QAAA,eAGA,wBAAA,KAAA,WAAA,KApCL,kCAwCK,QAAA,MlB1GN,6BkBkEC,6B9BwoIH,mC8BpoIQ,cAAA,EACA,aAAA,GlBpFN,0BkB+EA,kBAUI,cAAA,IAAA,OAAA,UAAA,IAAA,OACA,cAAA,MAAA,gBAAA,WAXH,8BAcK,mBAAA,IAAA,eAAA,IAdL,6CAiBO,SAAA,SAjBP,wCAqBO,cAAA,MACA,aAAA,MAtBP,6B9BiqIH,mC8BpoIQ,cAAA,OAAA,UAAA,OA7BL,mCAiCK,QAAA,sBAAA,QAAA,eAGA,wBAAA,KAAA,WAAA,KApCL,kCAwCK,QAAA,MA7CV,eAeQ,cAAA,IAAA,OAAA,UAAA,IAAA,OACA,cAAA,MAAA,gBAAA,WAhBR,0B9B6rIA,gC8BprIU,cAAA,EACA,aAAA,EAVV,2BAmBU,mBAAA,IAAA,eAAA,IAnBV,0CAsBY,SAAA,SAtBZ,qCA0BY,cAAA,MACA,aAAA,MA3BZ,0B9BitIA,gC8B/qIU,cAAA,OAAA,UAAA,OAlCV,gCAsCU,QAAA,sBAAA,QAAA,eAGA,wBAAA,KAAA,WAAA,KAzCV,+BA6CU,QAAA,KAaV,4BAEI,MAAA,e3BlLF,kCAAA,kC2BqLI,MAAA,eALN,oCAWM,MAAA,e3B3LJ,0CAAA,0C2B8LM,MAAA,eAdR,6CAkBQ,MAAA,e9B0qIR,4CAEA,2CADA,yC8B7rIA,0CA0BM,MAAA,eA1BN,8BA+BI,MAAA,eACA,aAAA,eAhCJ,mCAoCI,iBAAA,uOApCJ,2BAwCI,MAAA,eAxCJ,6BA0CM,MAAA,e3B1NJ,mCAAA,mC2B6NM,MAAA,eAOR,2BAEI,MAAA,K3BtOF,iCAAA,iC2ByOI,MAAA,KALN,mCAWM,MAAA,qB3B/OJ,yCAAA,yC2BkPM,MAAA,sBAdR,4CAkBQ,MAAA,sB9BsqIR,2CAEA,0CADA,wC8BzrIA,yCA0BM,MAAA,KA1BN,6BA+BI,MAAA,qBACA,aAAA,qBAhCJ,kCAoCI,iBAAA,6OApCJ,0BAwCI,MAAA,qBAxCJ,4BA0CM,MAAA,K3B9QJ,kCAAA,kC2BiRM,MAAA,KC7RR,MACE,SAAA,SACA,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OACA,UAAA,EACA,UAAA,WACA,iBAAA,KACA,gBAAA,WACA,OAAA,IAAA,MAAA,iBvBPE,cAAA,OuBDJ,SAYI,aAAA,EACA,YAAA,EAbJ,2DvBUI,uBAAA,OACA,wBAAA,OuBXJ,yDvBwBI,2BAAA,OACA,0BAAA,OuBIJ,WAGE,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,QAAA,QAIF,YACE,cAAA,OAGF,eACE,WAAA,SACA,cAAA,EAGF,sBACE,cAAA,E5BvCA,iB4B4CE,gBAAA,KAFJ,sBAMI,YAAA,QAQJ,aACE,QAAA,OAAA,QACA,cAAA,EAEA,iBAAA,gBACA,cAAA,IAAA,MAAA,iBALF,yBvB/DI,cAAA,mBAAA,mBAAA,EAAA,EuB+DJ,sDAaM,WAAA,EAKN,aACE,QAAA,OAAA,QACA,iBAAA,gBACA,WAAA,IAAA,MAAA,iBAHF,wBvBjFI,cAAA,EAAA,EAAA,mBAAA,mBuBgGJ,kBACE,aAAA,SACA,cAAA,QACA,YAAA,SACA,cAAA,EAGF,mBACE,aAAA,SACA,YAAA,SAIF,kBACE,SAAA,SACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,QAGF,UACE,MAAA,KvBvHE,cAAA,mBuB4HJ,cACE,MAAA,KvBpHE,uBAAA,mBACA,wBAAA,mBuBuHJ,iBACE,MAAA,KvB3GE,2BAAA,mBACA,0BAAA,mBuBiHJ,WACE,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OAFF,iBAKI,cAAA,KnBvFA,yBmBkFJ,WASI,cAAA,IAAA,KAAA,UAAA,IAAA,KACA,aAAA,MACA,YAAA,MAXJ,iBAcM,QAAA,YAAA,QAAA,KAEA,SAAA,EAAA,EAAA,GAAA,KAAA,EAAA,EAAA,GACA,mBAAA,OAAA,eAAA,OACA,aAAA,KACA,cAAA,EACA,YAAA,MAUN,YACE,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OAFF,kBAOI,cAAA,KnBvHA,yBmBgHJ,YAWI,cAAA,IAAA,KAAA,UAAA,IAAA,KAXJ,kBAgBM,SAAA,EAAA,EAAA,GAAA,KAAA,EAAA,EAAA,GACA,cAAA,EAjBN,wBAoBQ,YAAA,EACA,YAAA,EArBR,mCvBvJI,wBAAA,EACA,2BAAA,ERqmJF,gD+B/8IF,iDAgCY,wBAAA,E/Bm7IV,gD+Bn9IF,oDAqCY,2BAAA,EArCZ,oCvBzII,uBAAA,EACA,0BAAA,ERmmJF,iD+B39IF,kDA+CY,uBAAA,E/Bg7IV,iD+B/9IF,qDAoDY,0BAAA,GAaZ,oBAEI,cAAA,OnBnLA,yBmBiLJ,cAMI,qBAAA,EAAA,kBAAA,EAAA,aAAA,EACA,mBAAA,QAAA,gBAAA,QAAA,WAAA,QACA,QAAA,EACA,OAAA,EATJ,oBAYM,QAAA,aACA,MAAA,MAUN,iBAEI,SAAA,OAFJ,8DvB/PI,cAAA,EuB+PJ,wDAUQ,cAAA,EvBzQJ,cAAA,EuB+PJ,+BAgBM,cAAA,EvBxPF,2BAAA,EACA,0BAAA,EuBuOJ,8BvBtPI,uBAAA,EACA,wBAAA,EuBqPJ,8BAyBM,cAAA,KC7RN,YACE,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,QAAA,OAAA,KACA,cAAA,KACA,WAAA,KACA,iBAAA,QxBDE,cAAA,OwBKJ,kCAGI,aAAA,MAHJ,0CAMM,QAAA,aACA,cAAA,MACA,MAAA,QACA,QAAA,IATN,gDAoBI,gBAAA,UApBJ,gDAwBI,gBAAA,KAxBJ,wBA4BI,MAAA,QCtCJ,YACE,QAAA,YAAA,QAAA,K5BGA,aAAA,EACA,WAAA,KGAE,cAAA,OyBCJ,WACE,SAAA,SACA,QAAA,MACA,QAAA,MAAA,OACA,YAAA,KACA,YAAA,KACA,MAAA,QACA,iBAAA,KACA,OAAA,IAAA,MAAA,QARF,iBAWI,QAAA,EACA,MAAA,QACA,gBAAA,KACA,iBAAA,QACA,aAAA,QAfJ,iBAmBI,QAAA,EACA,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBAIJ,kCAGM,YAAA,EzBCF,uBAAA,OACA,0BAAA,OyBLJ,iCzBVI,wBAAA,OACA,2BAAA,OyBSJ,6BAcI,QAAA,EACA,MAAA,KACA,iBAAA,QACA,aAAA,QAjBJ,+BAqBI,MAAA,QACA,eAAA,KAEA,OAAA,KACA,iBAAA,KACA,aAAA,QCtDF,0BACE,QAAA,OAAA,OjC2HE,UAAA,QiCzHF,YAAA,IAKE,iD1BwBF,uBAAA,MACA,0BAAA,M0BpBE,gD1BKF,wBAAA,MACA,2BAAA,M0BnBF,0BACE,QAAA,OAAA,MjC2HE,UAAA,QiCzHF,YAAA,IAKE,iD1BwBF,uBAAA,MACA,0BAAA,M0BpBE,gD1BKF,wBAAA,MACA,2BAAA,M2BjBJ,OACE,QAAA,aACA,QAAA,MAAA,KlCiEE,UAAA,IkC/DF,YAAA,IACA,YAAA,EACA,WAAA,OACA,YAAA,OACA,eAAA,S3BRE,cAAA,OSCE,WAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAKF,uCkBNJ,OlBOM,WAAA,MdIJ,cAAA,cgCGI,gBAAA,KAdN,aAoBI,QAAA,KAKJ,YACE,SAAA,SACA,IAAA,KAOF,YACE,cAAA,KACA,aAAA,K3BpCE,cAAA,M2B6CF,eCjDA,MAAA,KACA,iBAAA,QjCcA,sBAAA,sBiCVI,MAAA,KACA,iBAAA,QAHI,sBAAA,sBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,mBDqCJ,iBCjDA,MAAA,KACA,iBAAA,QjCcA,wBAAA,wBiCVI,MAAA,KACA,iBAAA,QAHI,wBAAA,wBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,qBDqCJ,eCjDA,MAAA,KACA,iBAAA,QjCcA,sBAAA,sBiCVI,MAAA,KACA,iBAAA,QAHI,sBAAA,sBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,mBDqCJ,YCjDA,MAAA,KACA,iBAAA,QjCcA,mBAAA,mBiCVI,MAAA,KACA,iBAAA,QAHI,mBAAA,mBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBDqCJ,eCjDA,MAAA,QACA,iBAAA,QjCcA,sBAAA,sBiCVI,MAAA,QACA,iBAAA,QAHI,sBAAA,sBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,mBDqCJ,cCjDA,MAAA,KACA,iBAAA,QjCcA,qBAAA,qBiCVI,MAAA,KACA,iBAAA,QAHI,qBAAA,qBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,mBDqCJ,aCjDA,MAAA,QACA,iBAAA,QjCcA,oBAAA,oBiCVI,MAAA,QACA,iBAAA,QAHI,oBAAA,oBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,qBDqCJ,YCjDA,MAAA,KACA,iBAAA,QjCcA,mBAAA,mBiCVI,MAAA,KACA,iBAAA,QAHI,mBAAA,mBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,kBCbN,WACE,QAAA,KAAA,KACA,cAAA,KAEA,iBAAA,Q7BCE,cAAA,MIuDA,yByB5DJ,WAQI,QAAA,KAAA,MAIJ,iBACE,cAAA,EACA,aAAA,E7BTE,cAAA,E8BDJ,OACE,SAAA,SACA,QAAA,OAAA,QACA,cAAA,KACA,OAAA,IAAA,MAAA,Y9BHE,cAAA,O8BQJ,eAEE,MAAA,QAIF,YACE,YAAA,IAQF,mBACE,cAAA,KADF,0BAKI,SAAA,SACA,IAAA,EACA,MAAA,EACA,QAAA,OAAA,QACA,MAAA,QAUF,eC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,kBACE,iBAAA,QAGF,2BACE,MAAA,QDqCF,iBC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,oBACE,iBAAA,QAGF,6BACE,MAAA,QDqCF,eC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,kBACE,iBAAA,QAGF,2BACE,MAAA,QDqCF,YC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,eACE,iBAAA,QAGF,wBACE,MAAA,QDqCF,eC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,kBACE,iBAAA,QAGF,2BACE,MAAA,QDqCF,cC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,iBACE,iBAAA,QAGF,0BACE,MAAA,QDqCF,aC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,gBACE,iBAAA,QAGF,yBACE,MAAA,QDqCF,YC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,eACE,iBAAA,QAGF,wBACE,MAAA,QCRF,wCACE,KAAO,oBAAA,KAAA,EACP,GAAK,oBAAA,EAAA,GAFP,gCACE,KAAO,oBAAA,KAAA,EACP,GAAK,oBAAA,EAAA,GAIT,UACE,QAAA,YAAA,QAAA,KACA,OAAA,KACA,SAAA,OvCoHI,UAAA,OuClHJ,iBAAA,QhCRE,cAAA,OgCaJ,cACE,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OACA,cAAA,OAAA,gBAAA,OACA,MAAA,KACA,WAAA,OACA,YAAA,OACA,iBAAA,QvBnBI,WAAA,MAAA,IAAA,KAKF,uCuBOJ,cvBNM,WAAA,MuBiBN,sBrBcE,iBAAA,iKqBZA,gBAAA,KAAA,KAIA,uBACE,kBAAA,qBAAA,GAAA,OAAA,SAAA,UAAA,qBAAA,GAAA,OAAA,SAEA,uCAHF,uBAII,kBAAA,KAAA,UAAA,MCvCN,OACE,QAAA,YAAA,QAAA,KACA,eAAA,MAAA,YAAA,WAGF,YACE,SAAA,EAAA,KAAA,ECFF,YACE,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OAGA,aAAA,EACA,cAAA,EASF,wBACE,MAAA,KACA,MAAA,QACA,WAAA,QvCNA,8BAAA,8BuCUE,QAAA,EACA,MAAA,QACA,gBAAA,KACA,iBAAA,QAVJ,+BAcI,MAAA,QACA,iBAAA,QASJ,iBACE,SAAA,SACA,QAAA,MACA,QAAA,OAAA,QAEA,cAAA,KAEA,iBAAA,KACA,OAAA,IAAA,MAAA,iBARF,6BlC7BI,uBAAA,OACA,wBAAA,OkC4BJ,4BAeI,cAAA,ElC9BA,2BAAA,OACA,0BAAA,OkCcJ,0BAAA,0BAqBI,MAAA,QACA,eAAA,KACA,iBAAA,KAvBJ,wBA4BI,QAAA,EACA,MAAA,KACA,iBAAA,QACA,aAAA,QAaA,uBACE,mBAAA,IAAA,eAAA,IADF,wCAII,aAAA,KACA,cAAA,EALJ,oDlCpDA,uBAAA,OACA,0BAAA,OAYA,wBAAA,EkCuCA,mDAaM,aAAA,ElC/EN,wBAAA,OACA,2BAAA,OAsCA,0BAAA,EIAA,yB8B2BA,0BACE,mBAAA,IAAA,eAAA,IADF,2CAII,aAAA,KACA,cAAA,EALJ,uDlCpDA,uBAAA,OACA,0BAAA,OAYA,wBAAA,EkCuCA,sDAaM,aAAA,ElC/EN,wBAAA,OACA,2BAAA,OAsCA,0BAAA,GIAA,yB8B2BA,0BACE,mBAAA,IAAA,eAAA,IADF,2CAII,aAAA,KACA,cAAA,EALJ,uDlCpDA,uBAAA,OACA,0BAAA,OAYA,wBAAA,EkCuCA,sDAaM,aAAA,ElC/EN,wBAAA,OACA,2BAAA,OAsCA,0BAAA,GIAA,yB8B2BA,0BACE,mBAAA,IAAA,eAAA,IADF,2CAII,aAAA,KACA,cAAA,EALJ,uDlCpDA,uBAAA,OACA,0BAAA,OAYA,wBAAA,EkCuCA,sDAaM,aAAA,ElC/EN,wBAAA,OACA,2BAAA,OAsCA,0BAAA,GIAA,0B8B2BA,0BACE,mBAAA,IAAA,eAAA,IADF,2CAII,aAAA,KACA,cAAA,EALJ,uDlCpDA,uBAAA,OACA,0BAAA,OAYA,wBAAA,EkCuCA,sDAaM,aAAA,ElC/EN,wBAAA,OACA,2BAAA,OAsCA,0BAAA,GkCuDJ,mCAEI,aAAA,EACA,YAAA,ElCjHA,cAAA,EkC8GJ,8CAOM,cAAA,KAPN,2DAaM,WAAA,EAbN,yDAmBM,cAAA,EACA,cAAA,ECpIJ,yBACE,MAAA,QACA,iBAAA,QxCWF,sDAAA,sDwCPM,MAAA,QACA,iBAAA,QAPN,uDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,2BACE,MAAA,QACA,iBAAA,QxCWF,wDAAA,wDwCPM,MAAA,QACA,iBAAA,QAPN,yDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,yBACE,MAAA,QACA,iBAAA,QxCWF,sDAAA,sDwCPM,MAAA,QACA,iBAAA,QAPN,uDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,sBACE,MAAA,QACA,iBAAA,QxCWF,mDAAA,mDwCPM,MAAA,QACA,iBAAA,QAPN,oDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,yBACE,MAAA,QACA,iBAAA,QxCWF,sDAAA,sDwCPM,MAAA,QACA,iBAAA,QAPN,uDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,wBACE,MAAA,QACA,iBAAA,QxCWF,qDAAA,qDwCPM,MAAA,QACA,iBAAA,QAPN,sDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,uBACE,MAAA,QACA,iBAAA,QxCWF,oDAAA,oDwCPM,MAAA,QACA,iBAAA,QAPN,qDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,sBACE,MAAA,QACA,iBAAA,QxCWF,mDAAA,mDwCPM,MAAA,QACA,iBAAA,QAPN,oDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QChBR,OACE,MAAA,M3C8HI,UAAA,O2C5HJ,YAAA,IACA,YAAA,EACA,MAAA,KACA,YAAA,EAAA,IAAA,EAAA,KACA,QAAA,GzCKA,ayCDE,MAAA,KACA,gBAAA,KzCIF,2CAAA,2CyCCI,QAAA,IAWN,aACE,QAAA,EACA,iBAAA,YACA,OAAA,EACA,mBAAA,KAAA,gBAAA,KAAA,WAAA,KAMF,iBACE,eAAA,KCvCF,OACE,UAAA,MACA,SAAA,O5C6HI,UAAA,Q4C1HJ,iBAAA,sBACA,gBAAA,YACA,OAAA,IAAA,MAAA,eACA,WAAA,EAAA,OAAA,OAAA,eACA,wBAAA,WAAA,gBAAA,WACA,QAAA,ErCLE,cAAA,OqCLJ,wBAcI,cAAA,OAdJ,eAkBI,QAAA,EAlBJ,YAsBI,QAAA,MACA,QAAA,EAvBJ,YA2BI,QAAA,KAIJ,cACE,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,QAAA,OAAA,OACA,MAAA,QACA,iBAAA,sBACA,gBAAA,YACA,cAAA,IAAA,MAAA,gBAGF,YACE,QAAA,OCpCF,YAEE,SAAA,OAFF,mBAKI,WAAA,OACA,WAAA,KAKJ,OACE,SAAA,MACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,MAAA,KACA,OAAA,KACA,SAAA,OAGA,QAAA,EAOF,cACE,SAAA,SACA,MAAA,KACA,OAAA,MAEA,eAAA,KAGA,0B7BrCI,WAAA,kBAAA,IAAA,SAAA,WAAA,UAAA,IAAA,SAAA,WAAA,UAAA,IAAA,QAAA,CAAA,kBAAA,IAAA,S6BuCF,kBAAA,mBAAA,UAAA,mB7BlCA,uC6BgCF,0B7B/BI,WAAA,M6BmCJ,0BACE,kBAAA,KAAA,UAAA,KAIJ,yBACE,QAAA,YAAA,QAAA,KACA,WAAA,kBAFF,wCAKI,WAAA,mBACA,SAAA,O9CulLJ,uC8C7lLA,uCAWI,kBAAA,EAAA,YAAA,EAXJ,qCAeI,WAAA,KAIJ,uBACE,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,WAAA,kBAHF,+BAOI,QAAA,MACA,OAAA,mBACA,QAAA,GATJ,+CAcI,mBAAA,OAAA,eAAA,OACA,cAAA,OAAA,gBAAA,OACA,OAAA,KAhBJ,8DAmBM,WAAA,KAnBN,uDAuBM,QAAA,KAMN,eACE,SAAA,SACA,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OACA,MAAA,KAGA,eAAA,KACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,etCzGE,cAAA,MsC6GF,QAAA,EAIF,gBACE,SAAA,MACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,MAAA,MACA,OAAA,MACA,iBAAA,KAPF,qBAUW,QAAA,EAVX,qBAWW,QAAA,GAKX,cACE,QAAA,YAAA,QAAA,KACA,eAAA,MAAA,YAAA,WACA,cAAA,QAAA,gBAAA,cACA,QAAA,KAAA,KACA,cAAA,IAAA,MAAA,QtC7HE,uBAAA,MACA,wBAAA,MsCuHJ,qBASI,QAAA,KAAA,KAEA,OAAA,MAAA,MAAA,MAAA,KAKJ,aACE,cAAA,EACA,YAAA,IAKF,YACE,SAAA,SAGA,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,QAAA,KAIF,cACE,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,IAAA,gBAAA,SACA,QAAA,KACA,WAAA,IAAA,MAAA,QtC/IE,2BAAA,MACA,0BAAA,MsCyIJ,iCASyB,YAAA,OATzB,gCAUwB,aAAA,OAIxB,yBACE,SAAA,SACA,IAAA,QACA,MAAA,KACA,OAAA,KACA,SAAA,OlC7HE,yBkCzBJ,cA6JI,UAAA,MACA,OAAA,QAAA,KA7IJ,yBAiJI,WAAA,oBAjJJ,wCAoJM,WAAA,qBAjIN,uBAsII,WAAA,oBAtIJ,+BAyIM,OAAA,qBAQJ,UAAY,UAAA,OlC5JV,yBkCgKF,U9CglLA,U8C9kLE,UAAA,OlClKA,0BkCuKF,UAAY,UAAA,QClOd,SACE,SAAA,SACA,QAAA,KACA,QAAA,MACA,OAAA,ECJA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBAEA,WAAA,OACA,YAAA,IACA,YAAA,IACA,WAAA,KACA,WAAA,MACA,gBAAA,KACA,YAAA,KACA,eAAA,KACA,eAAA,OACA,WAAA,OACA,aAAA,OACA,YAAA,OACA,WAAA,K/CgHI,UAAA,Q8CpHJ,UAAA,WACA,QAAA,EAXF,cAaW,QAAA,GAbX,gBAgBI,SAAA,SACA,QAAA,MACA,MAAA,MACA,OAAA,MAnBJ,wBAsBM,SAAA,SACA,QAAA,GACA,aAAA,YACA,aAAA,MAKN,mCAAA,gBACE,QAAA,MAAA,EADF,0CAAA,uBAII,OAAA,EAJJ,kDAAA,+BAOM,IAAA,EACA,aAAA,MAAA,MAAA,EACA,iBAAA,KAKN,qCAAA,kBACE,QAAA,EAAA,MADF,4CAAA,yBAII,KAAA,EACA,MAAA,MACA,OAAA,MANJ,oDAAA,iCASM,MAAA,EACA,aAAA,MAAA,MAAA,MAAA,EACA,mBAAA,KAKN,sCAAA,mBACE,QAAA,MAAA,EADF,6CAAA,0BAII,IAAA,EAJJ,qDAAA,kCAOM,OAAA,EACA,aAAA,EAAA,MAAA,MACA,oBAAA,KAKN,oCAAA,iBACE,QAAA,EAAA,MADF,2CAAA,wBAII,MAAA,EACA,MAAA,MACA,OAAA,MANJ,mDAAA,gCASM,KAAA,EACA,aAAA,MAAA,EAAA,MAAA,MACA,kBAAA,KAqBN,eACE,UAAA,MACA,QAAA,OAAA,MACA,MAAA,KACA,WAAA,OACA,iBAAA,KvC3GE,cAAA,OyCLJ,SACE,SAAA,SACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,QAAA,MACA,UAAA,MDLA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBAEA,WAAA,OACA,YAAA,IACA,YAAA,IACA,WAAA,KACA,WAAA,MACA,gBAAA,KACA,YAAA,KACA,eAAA,KACA,eAAA,OACA,WAAA,OACA,aAAA,OACA,YAAA,OACA,WAAA,K/CgHI,UAAA,QgDnHJ,UAAA,WACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,ezCVE,cAAA,MyCLJ,gBAoBI,SAAA,SACA,QAAA,MACA,MAAA,KACA,OAAA,MACA,OAAA,EAAA,MAxBJ,uBAAA,wBA4BM,SAAA,SACA,QAAA,MACA,QAAA,GACA,aAAA,YACA,aAAA,MAKN,mCAAA,gBACE,cAAA,MADF,0CAAA,uBAII,OAAA,yBAJJ,kDAAA,+BAOM,OAAA,EACA,aAAA,MAAA,MAAA,EACA,iBAAA,gBATN,iDAAA,8BAaM,OAAA,IACA,aAAA,MAAA,MAAA,EACA,iBAAA,KAKN,qCAAA,kBACE,YAAA,MADF,4CAAA,yBAII,KAAA,yBACA,MAAA,MACA,OAAA,KACA,OAAA,MAAA,EAPJ,oDAAA,iCAUM,KAAA,EACA,aAAA,MAAA,MAAA,MAAA,EACA,mBAAA,gBAZN,mDAAA,gCAgBM,KAAA,IACA,aAAA,MAAA,MAAA,MAAA,EACA,mBAAA,KAKN,sCAAA,mBACE,WAAA,MADF,6CAAA,0BAII,IAAA,yBAJJ,qDAAA,kCAOM,IAAA,EACA,aAAA,EAAA,MAAA,MAAA,MACA,oBAAA,gBATN,oDAAA,iCAaM,IAAA,IACA,aAAA,EAAA,MAAA,MAAA,MACA,oBAAA,KAfN,8DAAA,2CAqBI,SAAA,SACA,IAAA,EACA,KAAA,IACA,QAAA,MACA,MAAA,KACA,YAAA,OACA,QAAA,GACA,cAAA,IAAA,MAAA,QAIJ,oCAAA,iBACE,aAAA,MADF,2CAAA,wBAII,MAAA,yBACA,MAAA,MACA,OAAA,KACA,OAAA,MAAA,EAPJ,mDAAA,gCAUM,MAAA,EACA,aAAA,MAAA,EAAA,MAAA,MACA,kBAAA,gBAZN,kDAAA,+BAgBM,MAAA,IACA,aAAA,MAAA,EAAA,MAAA,MACA,kBAAA,KAsBN,gBACE,QAAA,MAAA,OACA,cAAA,EhD3BI,UAAA,KgD8BJ,iBAAA,QACA,cAAA,IAAA,MAAA,QzChJE,uBAAA,kBACA,wBAAA,kByCyIJ,sBAWI,QAAA,KAIJ,cACE,QAAA,MAAA,OACA,MAAA,QC5JF,UACE,SAAA,SAGF,wBACE,iBAAA,MAAA,aAAA,MAGF,gBACE,SAAA,SACA,MAAA,KACA,SAAA,OCvBA,uBACE,QAAA,MACA,MAAA,KACA,QAAA,GDwBJ,eACE,SAAA,SACA,QAAA,KACA,MAAA,KACA,MAAA,KACA,aAAA,MACA,4BAAA,OAAA,oBAAA,OjC5BI,WAAA,kBAAA,IAAA,YAAA,WAAA,UAAA,IAAA,YAAA,WAAA,UAAA,IAAA,WAAA,CAAA,kBAAA,IAAA,YAKF,uCiCiBJ,ejChBM,WAAA,MjBomMN,oBACA,oBkD3kMA,sBAGE,QAAA,MlD6kMF,4BkD1kMA,6CAEE,kBAAA,iBAAA,UAAA,iBlD8kMF,2BkD3kMA,8CAEE,kBAAA,kBAAA,UAAA,kBAQF,8BAEI,QAAA,EACA,oBAAA,QACA,kBAAA,KAAA,UAAA,KlD0kMJ,sDACA,uDkD/kMA,qCAUI,QAAA,EACA,QAAA,EAXJ,0ClDqlMA,2CkDrkMI,QAAA,EACA,QAAA,EjCtEE,WAAA,GAAA,IAAA,QAKF,uCiCgDJ,0ClD6lME,2CiB5oMI,WAAA,MjBkpMN,uBkDxkMA,uBAEE,SAAA,SACA,IAAA,EACA,OAAA,EACA,QAAA,EAEA,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,OAAA,gBAAA,OACA,MAAA,IACA,MAAA,KACA,WAAA,OACA,QAAA,GjC7FI,WAAA,QAAA,KAAA,KAKF,uCjBuqMF,uBkD5lMF,uBjC1EM,WAAA,MjB6qMN,6BADA,6BGxqME,6BAAA,6B+CwFE,MAAA,KACA,gBAAA,KACA,QAAA,EACA,QAAA,GAGJ,uBACE,KAAA,EAKF,uBACE,MAAA,ElDolMF,4BkD7kMA,4BAEE,QAAA,aACA,MAAA,KACA,OAAA,KACA,WAAA,UAAA,GAAA,CAAA,KAAA,KAEF,4BACE,iBAAA,kLAEF,4BACE,iBAAA,kLASF,qBACE,SAAA,SACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,GACA,QAAA,YAAA,QAAA,KACA,cAAA,OAAA,gBAAA,OACA,aAAA,EAEA,aAAA,IACA,YAAA,IACA,WAAA,KAZF,wBAeI,WAAA,YACA,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,OAAA,IACA,aAAA,IACA,YAAA,IACA,YAAA,OACA,OAAA,QACA,iBAAA,KACA,gBAAA,YAEA,WAAA,KAAA,MAAA,YACA,cAAA,KAAA,MAAA,YACA,QAAA,GjCtKE,WAAA,QAAA,IAAA,KAKF,uCiCqIJ,wBjCpIM,WAAA,MiCoIN,6BAiCI,QAAA,EASJ,kBACE,SAAA,SACA,MAAA,IACA,OAAA,KACA,KAAA,IACA,QAAA,GACA,YAAA,KACA,eAAA,KACA,MAAA,KACA,WAAA,OE/LF,kCACE,GAAK,kBAAA,eAAA,UAAA,gBADP,0BACE,GAAK,kBAAA,eAAA,UAAA,gBAGP,gBACE,QAAA,aACA,MAAA,KACA,OAAA,KACA,eAAA,YACA,OAAA,MAAA,MAAA,aACA,mBAAA,YAEA,cAAA,IACA,kBAAA,eAAA,KAAA,OAAA,SAAA,UAAA,eAAA,KAAA,OAAA,SAGF,mBACE,MAAA,KACA,OAAA,KACA,aAAA,KAOF,gCACE,GACE,kBAAA,SAAA,UAAA,SAEF,IACE,QAAA,GALJ,wBACE,GACE,kBAAA,SAAA,UAAA,SAEF,IACE,QAAA,GAIJ,cACE,QAAA,aACA,MAAA,KACA,OAAA,KACA,eAAA,YACA,iBAAA,aAEA,cAAA,IACA,QAAA,EACA,kBAAA,aAAA,KAAA,OAAA,SAAA,UAAA,aAAA,KAAA,OAAA,SAGF,iBACE,MAAA,KACA,OAAA,KCnDF,gBAAqB,eAAA,mBACrB,WAAqB,eAAA,cACrB,cAAqB,eAAA,iBACrB,cAAqB,eAAA,iBACrB,mBAAqB,eAAA,sBACrB,gBAAqB,eAAA,mBCFnB,YACE,iBAAA,kBnDUF,mBAAA,mBHm2MF,wBADA,wBsDv2MM,iBAAA,kBANJ,cACE,iBAAA,kBnDUF,qBAAA,qBH62MF,0BADA,0BsDj3MM,iBAAA,kBANJ,YACE,iBAAA,kBnDUF,mBAAA,mBHu3MF,wBADA,wBsD33MM,iBAAA,kBANJ,SACE,iBAAA,kBnDUF,gBAAA,gBHi4MF,qBADA,qBsDr4MM,iBAAA,kBANJ,YACE,iBAAA,kBnDUF,mBAAA,mBH24MF,wBADA,wBsD/4MM,iBAAA,kBANJ,WACE,iBAAA,kBnDUF,kBAAA,kBHq5MF,uBADA,uBsDz5MM,iBAAA,kBANJ,UACE,iBAAA,kBnDUF,iBAAA,iBH+5MF,sBADA,sBsDn6MM,iBAAA,kBANJ,SACE,iBAAA,kBnDUF,gBAAA,gBHy6MF,qBADA,qBsD76MM,iBAAA,kBCCN,UACE,iBAAA,eAGF,gBACE,iBAAA,sBCXF,QAAkB,OAAA,IAAA,MAAA,kBAClB,YAAkB,WAAA,IAAA,MAAA,kBAClB,cAAkB,aAAA,IAAA,MAAA,kBAClB,eAAkB,cAAA,IAAA,MAAA,kBAClB,aAAkB,YAAA,IAAA,MAAA,kBAElB,UAAmB,OAAA,YACnB,cAAmB,WAAA,YACnB,gBAAmB,aAAA,YACnB,iBAAmB,cAAA,YACnB,eAAmB,YAAA,YAGjB,gBACE,aAAA,kBADF,kBACE,aAAA,kBADF,gBACE,aAAA,kBADF,aACE,aAAA,kBADF,gBACE,aAAA,kBADF,eACE,aAAA,kBADF,cACE,aAAA,kBADF,aACE,aAAA,kBAIJ,cACE,aAAA,eAOF,YACE,cAAA,gBAGF,SACE,cAAA,iBAGF,aACE,uBAAA,iBACA,wBAAA,iBAGF,eACE,wBAAA,iBACA,2BAAA,iBAGF,gBACE,2BAAA,iBACA,0BAAA,iBAGF,cACE,uBAAA,iBACA,0BAAA,iBAGF,YACE,cAAA,gBAGF,gBACE,cAAA,cAGF,cACE,cAAA,gBAGF,WACE,cAAA,YLxEA,iBACE,QAAA,MACA,MAAA,KACA,QAAA,GMOE,QAAwB,QAAA,eAAxB,UAAwB,QAAA,iBAAxB,gBAAwB,QAAA,uBAAxB,SAAwB,QAAA,gBAAxB,SAAwB,QAAA,gBAAxB,aAAwB,QAAA,oBAAxB,cAAwB,QAAA,qBAAxB,QAAwB,QAAA,sBAAA,QAAA,eAAxB,eAAwB,QAAA,6BAAA,QAAA,sB7CiD1B,yB6CjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uB7CiD1B,yB6CjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uB7CiD1B,yB6CjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uB7CiD1B,0B6CjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBAU9B,aAEI,cAAqB,QAAA,eAArB,gBAAqB,QAAA,iBAArB,sBAAqB,QAAA,uBAArB,eAAqB,QAAA,gBAArB,eAAqB,QAAA,gBAArB,mBAAqB,QAAA,oBAArB,oBAAqB,QAAA,qBAArB,cAAqB,QAAA,sBAAA,QAAA,eAArB,qBAAqB,QAAA,6BAAA,QAAA,uBCrBzB,kBACE,SAAA,SACA,QAAA,MACA,MAAA,KACA,QAAA,EACA,SAAA,OALF,0BAQI,QAAA,MACA,QAAA,GATJ,yC1DsxNA,wBADA,yBAEA,yBACA,wB0DvwNI,SAAA,SACA,IAAA,EACA,OAAA,EACA,KAAA,EACA,MAAA,KACA,OAAA,KACA,OAAA,EAQF,gCAEI,YAAA,WAFJ,gCAEI,YAAA,OAFJ,+BAEI,YAAA,IAFJ,+BAEI,YAAA,KCzBF,UAAgC,mBAAA,cAAA,eAAA,cAChC,aAAgC,mBAAA,iBAAA,eAAA,iBAChC,kBAAgC,mBAAA,sBAAA,eAAA,sBAChC,qBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,WAA8B,cAAA,eAAA,UAAA,eAC9B,aAA8B,cAAA,iBAAA,UAAA,iBAC9B,mBAA8B,cAAA,uBAAA,UAAA,uBAC9B,WAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,aAA8B,kBAAA,YAAA,UAAA,YAC9B,aAA8B,kBAAA,YAAA,UAAA,YAC9B,eAA8B,kBAAA,YAAA,YAAA,YAC9B,eAA8B,kBAAA,YAAA,YAAA,YAE9B,uBAAoC,cAAA,gBAAA,gBAAA,qBACpC,qBAAoC,cAAA,cAAA,gBAAA,mBACpC,wBAAoC,cAAA,iBAAA,gBAAA,iBACpC,yBAAoC,cAAA,kBAAA,gBAAA,wBACpC,wBAAoC,cAAA,qBAAA,gBAAA,uBAEpC,mBAAiC,eAAA,gBAAA,YAAA,qBACjC,iBAAiC,eAAA,cAAA,YAAA,mBACjC,oBAAiC,eAAA,iBAAA,YAAA,iBACjC,sBAAiC,eAAA,mBAAA,YAAA,mBACjC,qBAAiC,eAAA,kBAAA,YAAA,kBAEjC,qBAAkC,mBAAA,gBAAA,cAAA,qBAClC,mBAAkC,mBAAA,cAAA,cAAA,mBAClC,sBAAkC,mBAAA,iBAAA,cAAA,iBAClC,uBAAkC,mBAAA,kBAAA,cAAA,wBAClC,sBAAkC,mBAAA,qBAAA,cAAA,uBAClC,uBAAkC,mBAAA,kBAAA,cAAA,kBAElC,iBAAgC,oBAAA,eAAA,WAAA,eAChC,kBAAgC,oBAAA,gBAAA,WAAA,qBAChC,gBAAgC,oBAAA,cAAA,WAAA,mBAChC,mBAAgC,oBAAA,iBAAA,WAAA,iBAChC,qBAAgC,oBAAA,mBAAA,WAAA,mBAChC,oBAAgC,oBAAA,kBAAA,WAAA,kB/CYhC,yB+ClDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mB/CYhC,yB+ClDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mB/CYhC,yB+ClDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mB/CYhC,0B+ClDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBC1ChC,YAAwB,MAAA,eACxB,aAAwB,MAAA,gBACxB,YAAwB,MAAA,ehDoDxB,yBgDtDA,eAAwB,MAAA,eACxB,gBAAwB,MAAA,gBACxB,eAAwB,MAAA,gBhDoDxB,yBgDtDA,eAAwB,MAAA,eACxB,gBAAwB,MAAA,gBACxB,eAAwB,MAAA,gBhDoDxB,yBgDtDA,eAAwB,MAAA,eACxB,gBAAwB,MAAA,gBACxB,eAAwB,MAAA,gBhDoDxB,0BgDtDA,eAAwB,MAAA,eACxB,gBAAwB,MAAA,gBACxB,eAAwB,MAAA,gBCL1B,eAAsB,SAAA,eAAtB,iBAAsB,SAAA,iBCCtB,iBAAyB,SAAA,iBAAzB,mBAAyB,SAAA,mBAAzB,mBAAyB,SAAA,mBAAzB,gBAAyB,SAAA,gBAAzB,iBAAyB,SAAA,yBAAA,SAAA,iBAK3B,WACE,SAAA,MACA,IAAA,EACA,MAAA,EACA,KAAA,EACA,QAAA,KAGF,cACE,SAAA,MACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,KAI4B,2DAD9B,YAEI,SAAA,eAAA,SAAA,OACA,IAAA,EACA,QAAA,MCzBJ,SCEE,SAAA,SACA,MAAA,IACA,OAAA,IACA,QAAA,EACA,SAAA,OACA,KAAA,cACA,YAAA,OACA,OAAA,EAUA,0BAAA,yBAEE,SAAA,OACA,MAAA,KACA,OAAA,KACA,SAAA,QACA,KAAA,KACA,YAAA,OC5BJ,WAAa,WAAA,EAAA,QAAA,OAAA,2BACb,QAAU,WAAA,EAAA,MAAA,KAAA,0BACV,WAAa,WAAA,EAAA,KAAA,KAAA,2BACb,aAAe,WAAA,eCCX,MAAuB,MAAA,cAAvB,MAAuB,MAAA,cAAvB,MAAuB,MAAA,cAAvB,OAAuB,MAAA,eAAvB,QAAuB,MAAA,eAAvB,MAAuB,OAAA,cAAvB,MAAuB,OAAA,cAAvB,MAAuB,OAAA,cAAvB,OAAuB,OAAA,eAAvB,QAAuB,OAAA,eAI3B,QAAU,UAAA,eACV,QAAU,WAAA,eAIV,YAAc,UAAA,gBACd,YAAc,WAAA,gBAEd,QAAU,MAAA,gBACV,QAAU,OAAA,gBCfV,uBAEI,SAAA,SACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,EAEA,eAAA,KACA,QAAA,GAEA,iBAAA,cCNI,KAAgC,OAAA,YAChC,MpEsuPR,MoEpuPU,WAAA,YAEF,MpEuuPR,MoEruPU,aAAA,YAEF,MpEwuPR,MoEtuPU,cAAA,YAEF,MpEyuPR,MoEvuPU,YAAA,YAfF,KAAgC,OAAA,iBAChC,MpE8vPR,MoE5vPU,WAAA,iBAEF,MpE+vPR,MoE7vPU,aAAA,iBAEF,MpEgwPR,MoE9vPU,cAAA,iBAEF,MpEiwPR,MoE/vPU,YAAA,iBAfF,KAAgC,OAAA,gBAChC,MpEsxPR,MoEpxPU,WAAA,gBAEF,MpEuxPR,MoErxPU,aAAA,gBAEF,MpEwxPR,MoEtxPU,cAAA,gBAEF,MpEyxPR,MoEvxPU,YAAA,gBAfF,KAAgC,OAAA,eAChC,MpE8yPR,MoE5yPU,WAAA,eAEF,MpE+yPR,MoE7yPU,aAAA,eAEF,MpEgzPR,MoE9yPU,cAAA,eAEF,MpEizPR,MoE/yPU,YAAA,eAfF,KAAgC,OAAA,iBAChC,MpEs0PR,MoEp0PU,WAAA,iBAEF,MpEu0PR,MoEr0PU,aAAA,iBAEF,MpEw0PR,MoEt0PU,cAAA,iBAEF,MpEy0PR,MoEv0PU,YAAA,iBAfF,KAAgC,OAAA,eAChC,MpE81PR,MoE51PU,WAAA,eAEF,MpE+1PR,MoE71PU,aAAA,eAEF,MpEg2PR,MoE91PU,cAAA,eAEF,MpEi2PR,MoE/1PU,YAAA,eAfF,KAAgC,QAAA,YAChC,MpEs3PR,MoEp3PU,YAAA,YAEF,MpEu3PR,MoEr3PU,cAAA,YAEF,MpEw3PR,MoEt3PU,eAAA,YAEF,MpEy3PR,MoEv3PU,aAAA,YAfF,KAAgC,QAAA,iBAChC,MpE84PR,MoE54PU,YAAA,iBAEF,MpE+4PR,MoE74PU,cAAA,iBAEF,MpEg5PR,MoE94PU,eAAA,iBAEF,MpEi5PR,MoE/4PU,aAAA,iBAfF,KAAgC,QAAA,gBAChC,MpEs6PR,MoEp6PU,YAAA,gBAEF,MpEu6PR,MoEr6PU,cAAA,gBAEF,MpEw6PR,MoEt6PU,eAAA,gBAEF,MpEy6PR,MoEv6PU,aAAA,gBAfF,KAAgC,QAAA,eAChC,MpE87PR,MoE57PU,YAAA,eAEF,MpE+7PR,MoE77PU,cAAA,eAEF,MpEg8PR,MoE97PU,eAAA,eAEF,MpEi8PR,MoE/7PU,aAAA,eAfF,KAAgC,QAAA,iBAChC,MpEs9PR,MoEp9PU,YAAA,iBAEF,MpEu9PR,MoEr9PU,cAAA,iBAEF,MpEw9PR,MoEt9PU,eAAA,iBAEF,MpEy9PR,MoEv9PU,aAAA,iBAfF,KAAgC,QAAA,eAChC,MpE8+PR,MoE5+PU,YAAA,eAEF,MpE++PR,MoE7+PU,cAAA,eAEF,MpEg/PR,MoE9+PU,eAAA,eAEF,MpEi/PR,MoE/+PU,aAAA,eAQF,MAAwB,OAAA,kBACxB,OpE++PR,OoE7+PU,WAAA,kBAEF,OpEg/PR,OoE9+PU,aAAA,kBAEF,OpEi/PR,OoE/+PU,cAAA,kBAEF,OpEk/PR,OoEh/PU,YAAA,kBAfF,MAAwB,OAAA,iBACxB,OpEugQR,OoErgQU,WAAA,iBAEF,OpEwgQR,OoEtgQU,aAAA,iBAEF,OpEygQR,OoEvgQU,cAAA,iBAEF,OpE0gQR,OoExgQU,YAAA,iBAfF,MAAwB,OAAA,gBACxB,OpE+hQR,OoE7hQU,WAAA,gBAEF,OpEgiQR,OoE9hQU,aAAA,gBAEF,OpEiiQR,OoE/hQU,cAAA,gBAEF,OpEkiQR,OoEhiQU,YAAA,gBAfF,MAAwB,OAAA,kBACxB,OpEujQR,OoErjQU,WAAA,kBAEF,OpEwjQR,OoEtjQU,aAAA,kBAEF,OpEyjQR,OoEvjQU,cAAA,kBAEF,OpE0jQR,OoExjQU,YAAA,kBAfF,MAAwB,OAAA,gBACxB,OpE+kQR,OoE7kQU,WAAA,gBAEF,OpEglQR,OoE9kQU,aAAA,gBAEF,OpEilQR,OoE/kQU,cAAA,gBAEF,OpEklQR,OoEhlQU,YAAA,gBAMN,QAAmB,OAAA,eACnB,SpEklQJ,SoEhlQM,WAAA,eAEF,SpEmlQJ,SoEjlQM,aAAA,eAEF,SpEolQJ,SoEllQM,cAAA,eAEF,SpEqlQJ,SoEnlQM,YAAA,exDTF,yBwDlDI,QAAgC,OAAA,YAChC,SpEspQN,SoEppQQ,WAAA,YAEF,SpEspQN,SoEppQQ,aAAA,YAEF,SpEspQN,SoEppQQ,cAAA,YAEF,SpEspQN,SoEppQQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SpEyqQN,SoEvqQQ,WAAA,iBAEF,SpEyqQN,SoEvqQQ,aAAA,iBAEF,SpEyqQN,SoEvqQQ,cAAA,iBAEF,SpEyqQN,SoEvqQQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SpE4rQN,SoE1rQQ,WAAA,gBAEF,SpE4rQN,SoE1rQQ,aAAA,gBAEF,SpE4rQN,SoE1rQQ,cAAA,gBAEF,SpE4rQN,SoE1rQQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SpE+sQN,SoE7sQQ,WAAA,eAEF,SpE+sQN,SoE7sQQ,aAAA,eAEF,SpE+sQN,SoE7sQQ,cAAA,eAEF,SpE+sQN,SoE7sQQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SpEkuQN,SoEhuQQ,WAAA,iBAEF,SpEkuQN,SoEhuQQ,aAAA,iBAEF,SpEkuQN,SoEhuQQ,cAAA,iBAEF,SpEkuQN,SoEhuQQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SpEqvQN,SoEnvQQ,WAAA,eAEF,SpEqvQN,SoEnvQQ,aAAA,eAEF,SpEqvQN,SoEnvQQ,cAAA,eAEF,SpEqvQN,SoEnvQQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SpEwwQN,SoEtwQQ,YAAA,YAEF,SpEwwQN,SoEtwQQ,cAAA,YAEF,SpEwwQN,SoEtwQQ,eAAA,YAEF,SpEwwQN,SoEtwQQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SpE2xQN,SoEzxQQ,YAAA,iBAEF,SpE2xQN,SoEzxQQ,cAAA,iBAEF,SpE2xQN,SoEzxQQ,eAAA,iBAEF,SpE2xQN,SoEzxQQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SpE8yQN,SoE5yQQ,YAAA,gBAEF,SpE8yQN,SoE5yQQ,cAAA,gBAEF,SpE8yQN,SoE5yQQ,eAAA,gBAEF,SpE8yQN,SoE5yQQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SpEi0QN,SoE/zQQ,YAAA,eAEF,SpEi0QN,SoE/zQQ,cAAA,eAEF,SpEi0QN,SoE/zQQ,eAAA,eAEF,SpEi0QN,SoE/zQQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SpEo1QN,SoEl1QQ,YAAA,iBAEF,SpEo1QN,SoEl1QQ,cAAA,iBAEF,SpEo1QN,SoEl1QQ,eAAA,iBAEF,SpEo1QN,SoEl1QQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SpEu2QN,SoEr2QQ,YAAA,eAEF,SpEu2QN,SoEr2QQ,cAAA,eAEF,SpEu2QN,SoEr2QQ,eAAA,eAEF,SpEu2QN,SoEr2QQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UpEm2QN,UoEj2QQ,WAAA,kBAEF,UpEm2QN,UoEj2QQ,aAAA,kBAEF,UpEm2QN,UoEj2QQ,cAAA,kBAEF,UpEm2QN,UoEj2QQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UpEs3QN,UoEp3QQ,WAAA,iBAEF,UpEs3QN,UoEp3QQ,aAAA,iBAEF,UpEs3QN,UoEp3QQ,cAAA,iBAEF,UpEs3QN,UoEp3QQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UpEy4QN,UoEv4QQ,WAAA,gBAEF,UpEy4QN,UoEv4QQ,aAAA,gBAEF,UpEy4QN,UoEv4QQ,cAAA,gBAEF,UpEy4QN,UoEv4QQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UpE45QN,UoE15QQ,WAAA,kBAEF,UpE45QN,UoE15QQ,aAAA,kBAEF,UpE45QN,UoE15QQ,cAAA,kBAEF,UpE45QN,UoE15QQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UpE+6QN,UoE76QQ,WAAA,gBAEF,UpE+6QN,UoE76QQ,aAAA,gBAEF,UpE+6QN,UoE76QQ,cAAA,gBAEF,UpE+6QN,UoE76QQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YpE66QF,YoE36QI,WAAA,eAEF,YpE66QF,YoE36QI,aAAA,eAEF,YpE66QF,YoE36QI,cAAA,eAEF,YpE66QF,YoE36QI,YAAA,gBxDTF,yBwDlDI,QAAgC,OAAA,YAChC,SpE++QN,SoE7+QQ,WAAA,YAEF,SpE++QN,SoE7+QQ,aAAA,YAEF,SpE++QN,SoE7+QQ,cAAA,YAEF,SpE++QN,SoE7+QQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SpEkgRN,SoEhgRQ,WAAA,iBAEF,SpEkgRN,SoEhgRQ,aAAA,iBAEF,SpEkgRN,SoEhgRQ,cAAA,iBAEF,SpEkgRN,SoEhgRQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SpEqhRN,SoEnhRQ,WAAA,gBAEF,SpEqhRN,SoEnhRQ,aAAA,gBAEF,SpEqhRN,SoEnhRQ,cAAA,gBAEF,SpEqhRN,SoEnhRQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SpEwiRN,SoEtiRQ,WAAA,eAEF,SpEwiRN,SoEtiRQ,aAAA,eAEF,SpEwiRN,SoEtiRQ,cAAA,eAEF,SpEwiRN,SoEtiRQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SpE2jRN,SoEzjRQ,WAAA,iBAEF,SpE2jRN,SoEzjRQ,aAAA,iBAEF,SpE2jRN,SoEzjRQ,cAAA,iBAEF,SpE2jRN,SoEzjRQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SpE8kRN,SoE5kRQ,WAAA,eAEF,SpE8kRN,SoE5kRQ,aAAA,eAEF,SpE8kRN,SoE5kRQ,cAAA,eAEF,SpE8kRN,SoE5kRQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SpEimRN,SoE/lRQ,YAAA,YAEF,SpEimRN,SoE/lRQ,cAAA,YAEF,SpEimRN,SoE/lRQ,eAAA,YAEF,SpEimRN,SoE/lRQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SpEonRN,SoElnRQ,YAAA,iBAEF,SpEonRN,SoElnRQ,cAAA,iBAEF,SpEonRN,SoElnRQ,eAAA,iBAEF,SpEonRN,SoElnRQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SpEuoRN,SoEroRQ,YAAA,gBAEF,SpEuoRN,SoEroRQ,cAAA,gBAEF,SpEuoRN,SoEroRQ,eAAA,gBAEF,SpEuoRN,SoEroRQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SpE0pRN,SoExpRQ,YAAA,eAEF,SpE0pRN,SoExpRQ,cAAA,eAEF,SpE0pRN,SoExpRQ,eAAA,eAEF,SpE0pRN,SoExpRQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SpE6qRN,SoE3qRQ,YAAA,iBAEF,SpE6qRN,SoE3qRQ,cAAA,iBAEF,SpE6qRN,SoE3qRQ,eAAA,iBAEF,SpE6qRN,SoE3qRQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SpEgsRN,SoE9rRQ,YAAA,eAEF,SpEgsRN,SoE9rRQ,cAAA,eAEF,SpEgsRN,SoE9rRQ,eAAA,eAEF,SpEgsRN,SoE9rRQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UpE4rRN,UoE1rRQ,WAAA,kBAEF,UpE4rRN,UoE1rRQ,aAAA,kBAEF,UpE4rRN,UoE1rRQ,cAAA,kBAEF,UpE4rRN,UoE1rRQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UpE+sRN,UoE7sRQ,WAAA,iBAEF,UpE+sRN,UoE7sRQ,aAAA,iBAEF,UpE+sRN,UoE7sRQ,cAAA,iBAEF,UpE+sRN,UoE7sRQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UpEkuRN,UoEhuRQ,WAAA,gBAEF,UpEkuRN,UoEhuRQ,aAAA,gBAEF,UpEkuRN,UoEhuRQ,cAAA,gBAEF,UpEkuRN,UoEhuRQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UpEqvRN,UoEnvRQ,WAAA,kBAEF,UpEqvRN,UoEnvRQ,aAAA,kBAEF,UpEqvRN,UoEnvRQ,cAAA,kBAEF,UpEqvRN,UoEnvRQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UpEwwRN,UoEtwRQ,WAAA,gBAEF,UpEwwRN,UoEtwRQ,aAAA,gBAEF,UpEwwRN,UoEtwRQ,cAAA,gBAEF,UpEwwRN,UoEtwRQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YpEswRF,YoEpwRI,WAAA,eAEF,YpEswRF,YoEpwRI,aAAA,eAEF,YpEswRF,YoEpwRI,cAAA,eAEF,YpEswRF,YoEpwRI,YAAA,gBxDTF,yBwDlDI,QAAgC,OAAA,YAChC,SpEw0RN,SoEt0RQ,WAAA,YAEF,SpEw0RN,SoEt0RQ,aAAA,YAEF,SpEw0RN,SoEt0RQ,cAAA,YAEF,SpEw0RN,SoEt0RQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SpE21RN,SoEz1RQ,WAAA,iBAEF,SpE21RN,SoEz1RQ,aAAA,iBAEF,SpE21RN,SoEz1RQ,cAAA,iBAEF,SpE21RN,SoEz1RQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SpE82RN,SoE52RQ,WAAA,gBAEF,SpE82RN,SoE52RQ,aAAA,gBAEF,SpE82RN,SoE52RQ,cAAA,gBAEF,SpE82RN,SoE52RQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SpEi4RN,SoE/3RQ,WAAA,eAEF,SpEi4RN,SoE/3RQ,aAAA,eAEF,SpEi4RN,SoE/3RQ,cAAA,eAEF,SpEi4RN,SoE/3RQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SpEo5RN,SoEl5RQ,WAAA,iBAEF,SpEo5RN,SoEl5RQ,aAAA,iBAEF,SpEo5RN,SoEl5RQ,cAAA,iBAEF,SpEo5RN,SoEl5RQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SpEu6RN,SoEr6RQ,WAAA,eAEF,SpEu6RN,SoEr6RQ,aAAA,eAEF,SpEu6RN,SoEr6RQ,cAAA,eAEF,SpEu6RN,SoEr6RQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SpE07RN,SoEx7RQ,YAAA,YAEF,SpE07RN,SoEx7RQ,cAAA,YAEF,SpE07RN,SoEx7RQ,eAAA,YAEF,SpE07RN,SoEx7RQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SpE68RN,SoE38RQ,YAAA,iBAEF,SpE68RN,SoE38RQ,cAAA,iBAEF,SpE68RN,SoE38RQ,eAAA,iBAEF,SpE68RN,SoE38RQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SpEg+RN,SoE99RQ,YAAA,gBAEF,SpEg+RN,SoE99RQ,cAAA,gBAEF,SpEg+RN,SoE99RQ,eAAA,gBAEF,SpEg+RN,SoE99RQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SpEm/RN,SoEj/RQ,YAAA,eAEF,SpEm/RN,SoEj/RQ,cAAA,eAEF,SpEm/RN,SoEj/RQ,eAAA,eAEF,SpEm/RN,SoEj/RQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SpEsgSN,SoEpgSQ,YAAA,iBAEF,SpEsgSN,SoEpgSQ,cAAA,iBAEF,SpEsgSN,SoEpgSQ,eAAA,iBAEF,SpEsgSN,SoEpgSQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SpEyhSN,SoEvhSQ,YAAA,eAEF,SpEyhSN,SoEvhSQ,cAAA,eAEF,SpEyhSN,SoEvhSQ,eAAA,eAEF,SpEyhSN,SoEvhSQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UpEqhSN,UoEnhSQ,WAAA,kBAEF,UpEqhSN,UoEnhSQ,aAAA,kBAEF,UpEqhSN,UoEnhSQ,cAAA,kBAEF,UpEqhSN,UoEnhSQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UpEwiSN,UoEtiSQ,WAAA,iBAEF,UpEwiSN,UoEtiSQ,aAAA,iBAEF,UpEwiSN,UoEtiSQ,cAAA,iBAEF,UpEwiSN,UoEtiSQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UpE2jSN,UoEzjSQ,WAAA,gBAEF,UpE2jSN,UoEzjSQ,aAAA,gBAEF,UpE2jSN,UoEzjSQ,cAAA,gBAEF,UpE2jSN,UoEzjSQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UpE8kSN,UoE5kSQ,WAAA,kBAEF,UpE8kSN,UoE5kSQ,aAAA,kBAEF,UpE8kSN,UoE5kSQ,cAAA,kBAEF,UpE8kSN,UoE5kSQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UpEimSN,UoE/lSQ,WAAA,gBAEF,UpEimSN,UoE/lSQ,aAAA,gBAEF,UpEimSN,UoE/lSQ,cAAA,gBAEF,UpEimSN,UoE/lSQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YpE+lSF,YoE7lSI,WAAA,eAEF,YpE+lSF,YoE7lSI,aAAA,eAEF,YpE+lSF,YoE7lSI,cAAA,eAEF,YpE+lSF,YoE7lSI,YAAA,gBxDTF,0BwDlDI,QAAgC,OAAA,YAChC,SpEiqSN,SoE/pSQ,WAAA,YAEF,SpEiqSN,SoE/pSQ,aAAA,YAEF,SpEiqSN,SoE/pSQ,cAAA,YAEF,SpEiqSN,SoE/pSQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SpEorSN,SoElrSQ,WAAA,iBAEF,SpEorSN,SoElrSQ,aAAA,iBAEF,SpEorSN,SoElrSQ,cAAA,iBAEF,SpEorSN,SoElrSQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SpEusSN,SoErsSQ,WAAA,gBAEF,SpEusSN,SoErsSQ,aAAA,gBAEF,SpEusSN,SoErsSQ,cAAA,gBAEF,SpEusSN,SoErsSQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SpE0tSN,SoExtSQ,WAAA,eAEF,SpE0tSN,SoExtSQ,aAAA,eAEF,SpE0tSN,SoExtSQ,cAAA,eAEF,SpE0tSN,SoExtSQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SpE6uSN,SoE3uSQ,WAAA,iBAEF,SpE6uSN,SoE3uSQ,aAAA,iBAEF,SpE6uSN,SoE3uSQ,cAAA,iBAEF,SpE6uSN,SoE3uSQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SpEgwSN,SoE9vSQ,WAAA,eAEF,SpEgwSN,SoE9vSQ,aAAA,eAEF,SpEgwSN,SoE9vSQ,cAAA,eAEF,SpEgwSN,SoE9vSQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SpEmxSN,SoEjxSQ,YAAA,YAEF,SpEmxSN,SoEjxSQ,cAAA,YAEF,SpEmxSN,SoEjxSQ,eAAA,YAEF,SpEmxSN,SoEjxSQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SpEsySN,SoEpySQ,YAAA,iBAEF,SpEsySN,SoEpySQ,cAAA,iBAEF,SpEsySN,SoEpySQ,eAAA,iBAEF,SpEsySN,SoEpySQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SpEyzSN,SoEvzSQ,YAAA,gBAEF,SpEyzSN,SoEvzSQ,cAAA,gBAEF,SpEyzSN,SoEvzSQ,eAAA,gBAEF,SpEyzSN,SoEvzSQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SpE40SN,SoE10SQ,YAAA,eAEF,SpE40SN,SoE10SQ,cAAA,eAEF,SpE40SN,SoE10SQ,eAAA,eAEF,SpE40SN,SoE10SQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SpE+1SN,SoE71SQ,YAAA,iBAEF,SpE+1SN,SoE71SQ,cAAA,iBAEF,SpE+1SN,SoE71SQ,eAAA,iBAEF,SpE+1SN,SoE71SQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SpEk3SN,SoEh3SQ,YAAA,eAEF,SpEk3SN,SoEh3SQ,cAAA,eAEF,SpEk3SN,SoEh3SQ,eAAA,eAEF,SpEk3SN,SoEh3SQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UpE82SN,UoE52SQ,WAAA,kBAEF,UpE82SN,UoE52SQ,aAAA,kBAEF,UpE82SN,UoE52SQ,cAAA,kBAEF,UpE82SN,UoE52SQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UpEi4SN,UoE/3SQ,WAAA,iBAEF,UpEi4SN,UoE/3SQ,aAAA,iBAEF,UpEi4SN,UoE/3SQ,cAAA,iBAEF,UpEi4SN,UoE/3SQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UpEo5SN,UoEl5SQ,WAAA,gBAEF,UpEo5SN,UoEl5SQ,aAAA,gBAEF,UpEo5SN,UoEl5SQ,cAAA,gBAEF,UpEo5SN,UoEl5SQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UpEu6SN,UoEr6SQ,WAAA,kBAEF,UpEu6SN,UoEr6SQ,aAAA,kBAEF,UpEu6SN,UoEr6SQ,cAAA,kBAEF,UpEu6SN,UoEr6SQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UpE07SN,UoEx7SQ,WAAA,gBAEF,UpE07SN,UoEx7SQ,aAAA,gBAEF,UpE07SN,UoEx7SQ,cAAA,gBAEF,UpE07SN,UoEx7SQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YpEw7SF,YoEt7SI,WAAA,eAEF,YpEw7SF,YoEt7SI,aAAA,eAEF,YpEw7SF,YoEt7SI,cAAA,eAEF,YpEw7SF,YoEt7SI,YAAA,gBC/DN,gBAAkB,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,oBAIlB,cAAiB,WAAA,kBACjB,WAAiB,YAAA,iBACjB,aAAiB,YAAA,iBACjB,eCTE,SAAA,OACA,cAAA,SACA,YAAA,ODeE,WAAwB,WAAA,eACxB,YAAwB,WAAA,gBACxB,aAAwB,WAAA,iBzDqCxB,yByDvCA,cAAwB,WAAA,eACxB,eAAwB,WAAA,gBACxB,gBAAwB,WAAA,kBzDqCxB,yByDvCA,cAAwB,WAAA,eACxB,eAAwB,WAAA,gBACxB,gBAAwB,WAAA,kBzDqCxB,yByDvCA,cAAwB,WAAA,eACxB,eAAwB,WAAA,gBACxB,gBAAwB,WAAA,kBzDqCxB,0ByDvCA,cAAwB,WAAA,eACxB,eAAwB,WAAA,gBACxB,gBAAwB,WAAA,kBAM5B,gBAAmB,eAAA,oBACnB,gBAAmB,eAAA,oBACnB,iBAAmB,eAAA,qBAInB,mBAAuB,YAAA,cACvB,qBAAuB,YAAA,kBACvB,oBAAuB,YAAA,cACvB,kBAAuB,YAAA,cACvB,oBAAuB,YAAA,iBACvB,aAAuB,WAAA,iBAIvB,YAAc,MAAA,eEvCZ,cACE,MAAA,kBpEUF,qBAAA,qBoELM,MAAA,kBANN,gBACE,MAAA,kBpEUF,uBAAA,uBoELM,MAAA,kBANN,cACE,MAAA,kBpEUF,qBAAA,qBoELM,MAAA,kBANN,WACE,MAAA,kBpEUF,kBAAA,kBoELM,MAAA,kBANN,cACE,MAAA,kBpEUF,qBAAA,qBoELM,MAAA,kBANN,aACE,MAAA,kBpEUF,oBAAA,oBoELM,MAAA,kBANN,YACE,MAAA,kBpEUF,mBAAA,mBoELM,MAAA,kBANN,WACE,MAAA,kBpEUF,kBAAA,kBoELM,MAAA,kBFuCR,WAAa,MAAA,kBACb,YAAc,MAAA,kBAEd,eAAiB,MAAA,yBACjB,eAAiB,MAAA,+BAIjB,WGvDE,KAAA,CAAA,CAAA,EAAA,EACA,MAAA,YACA,YAAA,KACA,iBAAA,YACA,OAAA,EHuDF,sBAAwB,gBAAA,eAExB,YACE,WAAA,qBACA,cAAA,qBAKF,YAAc,MAAA,kBIjEd,SACE,WAAA,kBAGF,WACE,WAAA,iBCAA,a3EOF,ECwtTE,QADA,S0ExtTI,YAAA,eAEA,WAAA,eAGF,YAEI,gBAAA,UASJ,mBACE,QAAA,KAAA,YAAA,I3E+LN,I2EhLM,YAAA,mB1EusTJ,W0ErsTE,IAEE,OAAA,IAAA,MAAA,QACA,kBAAA,MAQF,MACE,QAAA,mB1EisTJ,I0E9rTE,GAEE,kBAAA,M1EgsTJ,GACA,G0E9rTE,EAGE,QAAA,EACA,OAAA,EAGF,G1E4rTF,G0E1rTI,iBAAA,MAQF,MACE,KAAA,G3E5CN,K2E+CM,UAAA,gBhEvFJ,WgE0FI,UAAA,gB5C9EN,Q4CmFM,QAAA,KvC/FN,OuCkGM,OAAA,IAAA,MAAA,K5DnGN,O4DuGM,gBAAA,mBADF,U1EsrTF,U0EjrTM,iBAAA,e1EqrTN,mBcxvTF,mB4D0EQ,OAAA,IAAA,MAAA,kB5DWR,Y4DNM,MAAA,Q1EkrTJ,wBAFA,eetyTA,efuyTA,qB0E3qTM,aAAA,Q5DlBR,sB4DuBM,MAAA,QACA,aAAA","sourcesContent":["/*!\n * Bootstrap v4.3.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"code\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"input-group\";\n@import \"custom-forms\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"jumbotron\";\n@import \"alert\";\n@import \"progress\";\n@import \"media\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"utilities\";\n@import \"print\";\n",":root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$color}: #{$value};\n }\n\n @each $bp, $value in $grid-breakpoints {\n --breakpoint-#{$bp}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --font-family-sans-serif: #{inspect($font-family-sans-serif)};\n --font-family-monospace: #{inspect($font-family-monospace)};\n}\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Suppress the focus outline on elements that cannot be accessed via keyboard.\n// This prevents an unwanted focus outline from appearing around elements that\n// might still respond to pointer events.\n//\n// Credit: https://github.com/suitcss/base\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","/*!\n * Bootstrap v4.3.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n -webkit-text-decoration-skip-ink: none;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014\\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-break: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n.row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.col-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n -ms-flex-order: -1;\n order: -1;\n}\n\n.order-last {\n -ms-flex-order: 13;\n order: 13;\n}\n\n.order-0 {\n -ms-flex-order: 0;\n order: 0;\n}\n\n.order-1 {\n -ms-flex-order: 1;\n order: 1;\n}\n\n.order-2 {\n -ms-flex-order: 2;\n order: 2;\n}\n\n.order-3 {\n -ms-flex-order: 3;\n order: 3;\n}\n\n.order-4 {\n -ms-flex-order: 4;\n order: 4;\n}\n\n.order-5 {\n -ms-flex-order: 5;\n order: 5;\n}\n\n.order-6 {\n -ms-flex-order: 6;\n order: 6;\n}\n\n.order-7 {\n -ms-flex-order: 7;\n order: 7;\n}\n\n.order-8 {\n -ms-flex-order: 8;\n order: 8;\n}\n\n.order-9 {\n -ms-flex-order: 9;\n order: 9;\n}\n\n.order-10 {\n -ms-flex-order: 10;\n order: 10;\n}\n\n.order-11 {\n -ms-flex-order: 11;\n order: 11;\n}\n\n.order-12 {\n -ms-flex-order: 12;\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-sm-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-sm-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-sm-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-sm-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-sm-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-sm-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-sm-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-sm-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-sm-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-sm-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-sm-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-sm-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-sm-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-sm-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-sm-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-md-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-md-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-md-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-md-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-md-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-md-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-md-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-md-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-md-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-md-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-md-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-md-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-md-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-md-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-md-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-lg-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-lg-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-lg-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-lg-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-lg-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-lg-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-lg-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-lg-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-lg-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-lg-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-lg-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-lg-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-lg-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-lg-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-lg-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-xl-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-xl-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-xl-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-xl-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-xl-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-xl-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-xl-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-xl-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-xl-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-xl-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-xl-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-xl-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-xl-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-xl-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-xl-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n margin-bottom: 1rem;\n color: #212529;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n color: #212529;\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-primary th,\n.table-primary td,\n.table-primary thead th,\n.table-primary tbody + tbody {\n border-color: #7abaff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-secondary th,\n.table-secondary td,\n.table-secondary thead th,\n.table-secondary tbody + tbody {\n border-color: #b3b7bb;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-success th,\n.table-success td,\n.table-success thead th,\n.table-success tbody + tbody {\n border-color: #8fd19e;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-info th,\n.table-info td,\n.table-info thead th,\n.table-info tbody + tbody {\n border-color: #86cfda;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-warning th,\n.table-warning td,\n.table-warning thead th,\n.table-warning tbody + tbody {\n border-color: #ffdf7e;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-danger th,\n.table-danger td,\n.table-danger thead th,\n.table-danger tbody + tbody {\n border-color: #ed969e;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-light th,\n.table-light td,\n.table-light thead th,\n.table-light tbody + tbody {\n border-color: #fbfcfc;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th,\n.table-dark tbody + tbody {\n border-color: #95999c;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #343a40;\n border-color: #454d55;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #343a40;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #454d55;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n color: #fff;\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::-webkit-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::-moz-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:-ms-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::-ms-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n margin-bottom: 0;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.form-control-lg {\n height: calc(1.5em + 1rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control[size], select.form-control[multiple] {\n height: auto;\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: -ms-inline-flexbox;\n display: inline-flex;\n -ms-flex-align: center;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #28a745;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: center right calc(0.375em + 0.1875rem);\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-control:valid ~ .valid-feedback,\n.was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback,\n.form-control.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:valid, .custom-select.is-valid {\n border-color: #28a745;\n padding-right: calc((1em + 0.75rem) * 3 / 4 + 1.75rem);\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-select:valid ~ .valid-feedback,\n.was-validated .custom-select:valid ~ .valid-tooltip, .custom-select.is-valid ~ .valid-feedback,\n.custom-select.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:valid ~ .valid-feedback,\n.was-validated .form-control-file:valid ~ .valid-tooltip, .form-control-file.is-valid ~ .valid-feedback,\n.form-control-file.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .valid-feedback,\n.was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback,\n.custom-control-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n border-color: #34ce57;\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .valid-feedback,\n.was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback,\n.custom-file-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E\");\n background-repeat: no-repeat;\n background-position: center right calc(0.375em + 0.1875rem);\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-control:invalid ~ .invalid-feedback,\n.was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback,\n.form-control.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:invalid, .custom-select.is-invalid {\n border-color: #dc3545;\n padding-right: calc((1em + 0.75rem) * 3 / 4 + 1.75rem);\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-select:invalid ~ .invalid-feedback,\n.was-validated .custom-select:invalid ~ .invalid-tooltip, .custom-select.is-invalid ~ .invalid-feedback,\n.custom-select.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:invalid ~ .invalid-feedback,\n.was-validated .form-control-file:invalid ~ .invalid-tooltip, .form-control-file.is-invalid ~ .invalid-feedback,\n.form-control-file.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .invalid-feedback,\n.was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback,\n.custom-control-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n border-color: #e4606d;\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .invalid-feedback,\n.was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback,\n.custom-file-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n -ms-flex-align: center;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n -ms-flex-negative: 0;\n flex-shrink: 0;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #212529;\n text-align: center;\n vertical-align: middle;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover {\n color: #212529;\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n text-decoration: none;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n box-shadow: none;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-sm-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-md-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-lg-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xl-right {\n right: 0;\n left: auto;\n }\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: -ms-inline-flexbox;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n -ms-flex-direction: column;\n flex-direction: column;\n -ms-flex-align: start;\n align-items: flex-start;\n -ms-flex-pack: center;\n justify-content: center;\n}\n\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: stretch;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .form-control-plaintext,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n width: 1%;\n margin-bottom: 0;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .form-control-plaintext + .form-control,\n.input-group > .form-control-plaintext + .custom-select,\n.input-group > .form-control-plaintext + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n}\n\n.input-group > .custom-file .custom-file-input:focus {\n z-index: 4;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: -ms-flexbox;\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn:focus,\n.input-group-append .btn:focus {\n z-index: 3;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: calc(1.5em + 1rem + 2px);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: calc(1.5em + 0.5rem + 2px);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: 1.75rem;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n display: -ms-inline-flexbox;\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n z-index: -1;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #80bdff;\n}\n\n.custom-control-input:not(:disabled):active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n border-color: #b3d7ff;\n}\n\n.custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n vertical-align: top;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n background-color: #fff;\n border: #adb5bd solid 1px;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background: no-repeat 50% / 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-switch {\n padding-left: 2.25rem;\n}\n\n.custom-switch .custom-control-label::before {\n left: -2.25rem;\n width: 1.75rem;\n pointer-events: all;\n border-radius: 0.5rem;\n}\n\n.custom-switch .custom-control-label::after {\n top: calc(0.25rem + 2px);\n left: calc(-2.25rem + 2px);\n width: calc(1rem - 4px);\n height: calc(1rem - 4px);\n background-color: #adb5bd;\n border-radius: 0.5rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out;\n transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-switch .custom-control-label::after {\n transition: none;\n }\n}\n\n.custom-switch .custom-control-input:checked ~ .custom-control-label::after {\n background-color: #fff;\n -webkit-transform: translateX(0.75rem);\n transform: translateX(0.75rem);\n}\n\n.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n display: none;\n}\n\n.custom-select-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.custom-select-lg {\n height: calc(1.5em + 1rem + 2px);\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input:disabled ~ .custom-file-label {\n background-color: #e9ecef;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-input ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: calc(1.5em + 0.75rem);\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: inherit;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n height: calc(1rem + 0.4rem);\n padding: 0;\n background-color: transparent;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-ms-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -webkit-appearance: none;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -moz-appearance: none;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-moz-range-thumb {\n transition: none;\n }\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: 0;\n margin-right: 0.2rem;\n margin-left: 0.2rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-ms-thumb {\n transition: none;\n }\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-webkit-slider-runnable-track {\n cursor: default;\n}\n\n.custom-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-moz-range-track {\n cursor: default;\n}\n\n.custom-range:disabled::-ms-thumb {\n background-color: #adb5bd;\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-control-label::before,\n .custom-file-label,\n .custom-select {\n transition: none;\n }\n}\n\n.nav {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill .nav-item {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified .nav-item {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: justify;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar > .container,\n.navbar > .container-fluid {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: justify;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n -ms-flex-preferred-size: 100%;\n flex-basis: 100%;\n -ms-flex-positive: 1;\n flex-grow: 1;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group:first-child .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.card > .list-group:last-child .list-group-item:last-child {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.card-body {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n border-top: 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n}\n\n.card-img {\n width: 100%;\n border-radius: calc(0.25rem - 1px);\n}\n\n.card-img-top {\n width: 100%;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img-bottom {\n width: 100%;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 1 0 0%;\n flex: 1 0 0%;\n -ms-flex-direction: column;\n flex-direction: column;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n }\n .card-group > .card {\n -ms-flex: 1 0 0%;\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n .card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n .card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n .card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n .card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n -webkit-column-count: 3;\n -moz-column-count: 3;\n column-count: 3;\n -webkit-column-gap: 1.25rem;\n -moz-column-gap: 1.25rem;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion > .card {\n overflow: hidden;\n}\n\n.accordion > .card:not(:first-of-type) .card-header:first-child {\n border-radius: 0;\n}\n\n.accordion > .card:not(:first-of-type):not(:last-of-type) {\n border-bottom: 0;\n border-radius: 0;\n}\n\n.accordion > .card:first-of-type {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion > .card:last-of-type {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.accordion > .card .card-header {\n margin-bottom: -1px;\n}\n\n.breadcrumb {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: -ms-flexbox;\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 2;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 1;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .badge {\n transition: none;\n }\n}\n\na.badge:hover, a.badge:focus {\n text-decoration: none;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\na.badge-primary:hover, a.badge-primary:focus {\n color: #fff;\n background-color: #0062cc;\n}\n\na.badge-primary:focus, a.badge-primary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\na.badge-secondary:hover, a.badge-secondary:focus {\n color: #fff;\n background-color: #545b62;\n}\n\na.badge-secondary:focus, a.badge-secondary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\na.badge-success:hover, a.badge-success:focus {\n color: #fff;\n background-color: #1e7e34;\n}\n\na.badge-success:focus, a.badge-success.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\na.badge-info:hover, a.badge-info:focus {\n color: #fff;\n background-color: #117a8b;\n}\n\na.badge-info:focus, a.badge-info.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\na.badge-warning:hover, a.badge-warning:focus {\n color: #212529;\n background-color: #d39e00;\n}\n\na.badge-warning:focus, a.badge-warning.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\na.badge-danger:hover, a.badge-danger:focus {\n color: #fff;\n background-color: #bd2130;\n}\n\na.badge-danger:focus, a.badge-danger.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\na.badge-light:hover, a.badge-light:focus {\n color: #212529;\n background-color: #dae0e5;\n}\n\na.badge-light:focus, a.badge-light.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\na.badge-dark:hover, a.badge-dark:focus {\n color: #fff;\n background-color: #1d2124;\n}\n\na.badge-dark:focus, a.badge-dark.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: -ms-flexbox;\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n -ms-flex-pack: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n -webkit-animation: progress-bar-stripes 1s linear infinite;\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n -webkit-animation: none;\n animation: none;\n }\n}\n\n.media {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: start;\n align-items: flex-start;\n}\n\n.media-body {\n -ms-flex: 1;\n flex: 1;\n}\n\n.list-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-horizontal {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n\n.list-group-horizontal .list-group-item {\n margin-right: -1px;\n margin-bottom: 0;\n}\n\n.list-group-horizontal .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n}\n\n.list-group-horizontal .list-group-item:last-child {\n margin-right: 0;\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .list-group-horizontal-sm .list-group-item {\n margin-right: -1px;\n margin-bottom: 0;\n }\n .list-group-horizontal-sm .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm .list-group-item:last-child {\n margin-right: 0;\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n}\n\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .list-group-horizontal-md .list-group-item {\n margin-right: -1px;\n margin-bottom: 0;\n }\n .list-group-horizontal-md .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md .list-group-item:last-child {\n margin-right: 0;\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n}\n\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .list-group-horizontal-lg .list-group-item {\n margin-right: -1px;\n margin-bottom: 0;\n }\n .list-group-horizontal-lg .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg .list-group-item:last-child {\n margin-right: 0;\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .list-group-horizontal-xl .list-group-item {\n margin-right: -1px;\n margin-bottom: 0;\n }\n .list-group-horizontal-xl .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl .list-group-item:last-child {\n margin-right: 0;\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n}\n\n.list-group-flush .list-group-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n\n.list-group-flush .list-group-item:last-child {\n margin-bottom: -1px;\n}\n\n.list-group-flush:first-child .list-group-item:first-child {\n border-top: 0;\n}\n\n.list-group-flush:last-child .list-group-item:last-child {\n margin-bottom: 0;\n border-bottom: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover {\n color: #000;\n text-decoration: none;\n}\n\n.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {\n opacity: .75;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\na.close.disabled {\n pointer-events: none;\n}\n\n.toast {\n max-width: 350px;\n overflow: hidden;\n font-size: 0.875rem;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n opacity: 0;\n border-radius: 0.25rem;\n}\n\n.toast:not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast.showing {\n opacity: 1;\n}\n\n.toast.show {\n display: block;\n opacity: 1;\n}\n\n.toast.hide {\n display: none;\n}\n\n.toast-header {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n padding: 0.25rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n.toast-body {\n padding: 0.75rem;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: -webkit-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out;\n -webkit-transform: translate(0, -50px);\n transform: translate(0, -50px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n -webkit-transform: none;\n transform: none;\n}\n\n.modal-dialog-scrollable {\n display: -ms-flexbox;\n display: flex;\n max-height: calc(100% - 1rem);\n}\n\n.modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 1rem);\n overflow: hidden;\n}\n\n.modal-dialog-scrollable .modal-header,\n.modal-dialog-scrollable .modal-footer {\n -ms-flex-negative: 0;\n flex-shrink: 0;\n}\n\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n min-height: calc(100% - 1rem);\n}\n\n.modal-dialog-centered::before {\n display: block;\n height: calc(100vh - 1rem);\n content: \"\";\n}\n\n.modal-dialog-centered.modal-dialog-scrollable {\n -ms-flex-direction: column;\n flex-direction: column;\n -ms-flex-pack: center;\n justify-content: center;\n height: 100%;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable .modal-content {\n max-height: none;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable::before {\n content: none;\n}\n\n.modal-content {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: start;\n align-items: flex-start;\n -ms-flex-pack: justify;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #dee2e6;\n border-top-left-radius: 0.3rem;\n border-top-right-radius: 0.3rem;\n}\n\n.modal-header .close {\n padding: 1rem 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: end;\n justify-content: flex-end;\n padding: 1rem;\n border-top: 1px solid #dee2e6;\n border-bottom-right-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.modal-footer > :not(:first-child) {\n margin-left: .25rem;\n}\n\n.modal-footer > :not(:last-child) {\n margin-right: .25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-scrollable {\n max-height: calc(100% - 3.5rem);\n }\n .modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 3.5rem);\n }\n .modal-dialog-centered {\n min-height: calc(100% - 3.5rem);\n }\n .modal-dialog-centered::before {\n height: calc(100vh - 3.5rem);\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n max-width: 800px;\n }\n}\n\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top > .arrow, .bs-popover-auto[x-placement^=\"top\"] > .arrow {\n bottom: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^=\"top\"] > .arrow::before {\n bottom: 0;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^=\"top\"] > .arrow::after {\n bottom: 1px;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right > .arrow, .bs-popover-auto[x-placement^=\"right\"] > .arrow {\n left: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^=\"right\"] > .arrow::before {\n left: 0;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^=\"right\"] > .arrow::after {\n left: 1px;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow {\n top: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::before {\n top: 0;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::after {\n top: 1px;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left > .arrow, .bs-popover-auto[x-placement^=\"left\"] > .arrow {\n right: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^=\"left\"] > .arrow::before {\n right: 0;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^=\"left\"] > .arrow::after {\n right: 1px;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n -ms-touch-action: pan-y;\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n transition: -webkit-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n -webkit-transform: translateX(100%);\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n -webkit-transform: translateX(-100%);\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n -webkit-transform: none;\n transform: none;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n transition: 0s 0.6s opacity;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-right {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: no-repeat 50% / 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-pack: center;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n box-sizing: content-box;\n -ms-flex: 0 1 auto;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: .5;\n transition: opacity 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators li {\n transition: none;\n }\n}\n\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n@-webkit-keyframes spinner-border {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n@keyframes spinner-border {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n -webkit-animation: spinner-border .75s linear infinite;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@-webkit-keyframes spinner-grow {\n 0% {\n -webkit-transform: scale(0);\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n }\n}\n\n@keyframes spinner-grow {\n 0% {\n -webkit-transform: scale(0);\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n -webkit-animation: spinner-grow .75s linear infinite;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded-sm {\n border-radius: 0.2rem !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-lg {\n border-radius: 0.3rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n}\n\n.d-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-md-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-print-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-4by3::before {\n padding-top: 75%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n}\n\n.flex-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n}\n\n.justify-content-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n}\n\n.align-items-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n}\n\n.align-items-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n}\n\n.align-items-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n}\n\n.align-items-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n}\n\n.align-content-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n}\n\n.align-content-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n}\n\n.align-content-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n}\n\n.align-content-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n}\n\n.align-content-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n}\n\n.align-self-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n}\n\n.align-self-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n}\n\n.align-self-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n}\n\n.align-self-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n}\n\n.align-self-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-sm-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-sm-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-sm-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-sm-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-sm-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-sm-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-sm-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-sm-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-md-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-md-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-md-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-md-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-md-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-md-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-md-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-md-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-md-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-md-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-md-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-md-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-md-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-md-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-md-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-md-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-lg-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-lg-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-lg-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-lg-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-lg-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-lg-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-lg-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-lg-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-xl-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-xl-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-xl-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-xl-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-xl-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-xl-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-xl-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-xl-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: -webkit-sticky !important;\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports ((position: -webkit-sticky) or (position: sticky)) {\n .sticky-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n pointer-events: auto;\n content: \"\";\n background-color: rgba(0, 0, 0, 0);\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !important;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-lighter {\n font-weight: lighter !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-weight-bolder {\n font-weight: bolder !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0056b3 !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #494f54 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #19692c !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #0f6674 !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #ba8b00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #a71d2a !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #cbd3da !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #121416 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-break {\n word-break: break-word !important;\n overflow-wrap: break-word !important;\n}\n\n.text-reset {\n color: inherit !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","/*!\n * Bootstrap v4.3.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014\\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-break: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n margin-bottom: 1rem;\n color: #212529;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n color: #212529;\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-primary th,\n.table-primary td,\n.table-primary thead th,\n.table-primary tbody + tbody {\n border-color: #7abaff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-secondary th,\n.table-secondary td,\n.table-secondary thead th,\n.table-secondary tbody + tbody {\n border-color: #b3b7bb;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-success th,\n.table-success td,\n.table-success thead th,\n.table-success tbody + tbody {\n border-color: #8fd19e;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-info th,\n.table-info td,\n.table-info thead th,\n.table-info tbody + tbody {\n border-color: #86cfda;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-warning th,\n.table-warning td,\n.table-warning thead th,\n.table-warning tbody + tbody {\n border-color: #ffdf7e;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-danger th,\n.table-danger td,\n.table-danger thead th,\n.table-danger tbody + tbody {\n border-color: #ed969e;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-light th,\n.table-light td,\n.table-light thead th,\n.table-light tbody + tbody {\n border-color: #fbfcfc;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th,\n.table-dark tbody + tbody {\n border-color: #95999c;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #343a40;\n border-color: #454d55;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #343a40;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #454d55;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n color: #fff;\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n margin-bottom: 0;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.form-control-lg {\n height: calc(1.5em + 1rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control[size], select.form-control[multiple] {\n height: auto;\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: inline-flex;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #28a745;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: center right calc(0.375em + 0.1875rem);\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-control:valid ~ .valid-feedback,\n.was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback,\n.form-control.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:valid, .custom-select.is-valid {\n border-color: #28a745;\n padding-right: calc((1em + 0.75rem) * 3 / 4 + 1.75rem);\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-select:valid ~ .valid-feedback,\n.was-validated .custom-select:valid ~ .valid-tooltip, .custom-select.is-valid ~ .valid-feedback,\n.custom-select.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:valid ~ .valid-feedback,\n.was-validated .form-control-file:valid ~ .valid-tooltip, .form-control-file.is-valid ~ .valid-feedback,\n.form-control-file.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .valid-feedback,\n.was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback,\n.custom-control-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n border-color: #34ce57;\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .valid-feedback,\n.was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback,\n.custom-file-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E\");\n background-repeat: no-repeat;\n background-position: center right calc(0.375em + 0.1875rem);\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-control:invalid ~ .invalid-feedback,\n.was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback,\n.form-control.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:invalid, .custom-select.is-invalid {\n border-color: #dc3545;\n padding-right: calc((1em + 0.75rem) * 3 / 4 + 1.75rem);\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-select:invalid ~ .invalid-feedback,\n.was-validated .custom-select:invalid ~ .invalid-tooltip, .custom-select.is-invalid ~ .invalid-feedback,\n.custom-select.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:invalid ~ .invalid-feedback,\n.was-validated .form-control-file:invalid ~ .invalid-tooltip, .form-control-file.is-invalid ~ .invalid-feedback,\n.form-control-file.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .invalid-feedback,\n.was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback,\n.custom-control-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n border-color: #e4606d;\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .invalid-feedback,\n.was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback,\n.custom-file-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: flex;\n flex-flow: row wrap;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: flex;\n flex: 0 0 auto;\n flex-flow: row wrap;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n flex-shrink: 0;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n align-items: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #212529;\n text-align: center;\n vertical-align: middle;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover {\n color: #212529;\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n text-decoration: none;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n box-shadow: none;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-sm-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-md-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-lg-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xl-right {\n right: 0;\n left: auto;\n }\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .form-control-plaintext,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n margin-bottom: 0;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .form-control-plaintext + .form-control,\n.input-group > .form-control-plaintext + .custom-select,\n.input-group > .form-control-plaintext + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n}\n\n.input-group > .custom-file .custom-file-input:focus {\n z-index: 4;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: flex;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn:focus,\n.input-group-append .btn:focus {\n z-index: 3;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: calc(1.5em + 1rem + 2px);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: calc(1.5em + 0.5rem + 2px);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: 1.75rem;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n z-index: -1;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #80bdff;\n}\n\n.custom-control-input:not(:disabled):active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n border-color: #b3d7ff;\n}\n\n.custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n vertical-align: top;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n background-color: #fff;\n border: #adb5bd solid 1px;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background: no-repeat 50% / 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-switch {\n padding-left: 2.25rem;\n}\n\n.custom-switch .custom-control-label::before {\n left: -2.25rem;\n width: 1.75rem;\n pointer-events: all;\n border-radius: 0.5rem;\n}\n\n.custom-switch .custom-control-label::after {\n top: calc(0.25rem + 2px);\n left: calc(-2.25rem + 2px);\n width: calc(1rem - 4px);\n height: calc(1rem - 4px);\n background-color: #adb5bd;\n border-radius: 0.5rem;\n transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-switch .custom-control-label::after {\n transition: none;\n }\n}\n\n.custom-switch .custom-control-input:checked ~ .custom-control-label::after {\n background-color: #fff;\n transform: translateX(0.75rem);\n}\n\n.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n display: none;\n}\n\n.custom-select-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.custom-select-lg {\n height: calc(1.5em + 1rem + 2px);\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input:disabled ~ .custom-file-label {\n background-color: #e9ecef;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-input ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: calc(1.5em + 0.75rem);\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: inherit;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n height: calc(1rem + 0.4rem);\n padding: 0;\n background-color: transparent;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-ms-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-moz-range-thumb {\n transition: none;\n }\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: 0;\n margin-right: 0.2rem;\n margin-left: 0.2rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-ms-thumb {\n transition: none;\n }\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-webkit-slider-runnable-track {\n cursor: default;\n}\n\n.custom-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-moz-range-track {\n cursor: default;\n}\n\n.custom-range:disabled::-ms-thumb {\n background-color: #adb5bd;\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-control-label::before,\n .custom-file-label,\n .custom-select {\n transition: none;\n }\n}\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar > .container,\n.navbar > .container-fluid {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n flex-flow: row nowrap;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group:first-child .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.card > .list-group:last-child .list-group-item:last-child {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.card-body {\n flex: 1 1 auto;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n border-top: 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n}\n\n.card-img {\n width: 100%;\n border-radius: calc(0.25rem - 1px);\n}\n\n.card-img-top {\n width: 100%;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img-bottom {\n width: 100%;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck {\n display: flex;\n flex-direction: column;\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n display: flex;\n flex: 1 0 0%;\n flex-direction: column;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group {\n display: flex;\n flex-direction: column;\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n .card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n .card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n .card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n .card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n column-count: 3;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion > .card {\n overflow: hidden;\n}\n\n.accordion > .card:not(:first-of-type) .card-header:first-child {\n border-radius: 0;\n}\n\n.accordion > .card:not(:first-of-type):not(:last-of-type) {\n border-bottom: 0;\n border-radius: 0;\n}\n\n.accordion > .card:first-of-type {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion > .card:last-of-type {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.accordion > .card .card-header {\n margin-bottom: -1px;\n}\n\n.breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 2;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 1;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .badge {\n transition: none;\n }\n}\n\na.badge:hover, a.badge:focus {\n text-decoration: none;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\na.badge-primary:hover, a.badge-primary:focus {\n color: #fff;\n background-color: #0062cc;\n}\n\na.badge-primary:focus, a.badge-primary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\na.badge-secondary:hover, a.badge-secondary:focus {\n color: #fff;\n background-color: #545b62;\n}\n\na.badge-secondary:focus, a.badge-secondary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\na.badge-success:hover, a.badge-success:focus {\n color: #fff;\n background-color: #1e7e34;\n}\n\na.badge-success:focus, a.badge-success.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\na.badge-info:hover, a.badge-info:focus {\n color: #fff;\n background-color: #117a8b;\n}\n\na.badge-info:focus, a.badge-info.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\na.badge-warning:hover, a.badge-warning:focus {\n color: #212529;\n background-color: #d39e00;\n}\n\na.badge-warning:focus, a.badge-warning.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\na.badge-danger:hover, a.badge-danger:focus {\n color: #fff;\n background-color: #bd2130;\n}\n\na.badge-danger:focus, a.badge-danger.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\na.badge-light:hover, a.badge-light:focus {\n color: #212529;\n background-color: #dae0e5;\n}\n\na.badge-light:focus, a.badge-light.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\na.badge-dark:hover, a.badge-dark:focus {\n color: #fff;\n background-color: #1d2124;\n}\n\na.badge-dark:focus, a.badge-dark.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n animation: none;\n }\n}\n\n.media {\n display: flex;\n align-items: flex-start;\n}\n\n.media-body {\n flex: 1;\n}\n\n.list-group {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n\n.list-group-horizontal .list-group-item {\n margin-right: -1px;\n margin-bottom: 0;\n}\n\n.list-group-horizontal .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n}\n\n.list-group-horizontal .list-group-item:last-child {\n margin-right: 0;\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm .list-group-item {\n margin-right: -1px;\n margin-bottom: 0;\n }\n .list-group-horizontal-sm .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm .list-group-item:last-child {\n margin-right: 0;\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n}\n\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md .list-group-item {\n margin-right: -1px;\n margin-bottom: 0;\n }\n .list-group-horizontal-md .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md .list-group-item:last-child {\n margin-right: 0;\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n}\n\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg .list-group-item {\n margin-right: -1px;\n margin-bottom: 0;\n }\n .list-group-horizontal-lg .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg .list-group-item:last-child {\n margin-right: 0;\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl .list-group-item {\n margin-right: -1px;\n margin-bottom: 0;\n }\n .list-group-horizontal-xl .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl .list-group-item:last-child {\n margin-right: 0;\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n}\n\n.list-group-flush .list-group-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n\n.list-group-flush .list-group-item:last-child {\n margin-bottom: -1px;\n}\n\n.list-group-flush:first-child .list-group-item:first-child {\n border-top: 0;\n}\n\n.list-group-flush:last-child .list-group-item:last-child {\n margin-bottom: 0;\n border-bottom: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover {\n color: #000;\n text-decoration: none;\n}\n\n.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {\n opacity: .75;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n appearance: none;\n}\n\na.close.disabled {\n pointer-events: none;\n}\n\n.toast {\n max-width: 350px;\n overflow: hidden;\n font-size: 0.875rem;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n opacity: 0;\n border-radius: 0.25rem;\n}\n\n.toast:not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast.showing {\n opacity: 1;\n}\n\n.toast.show {\n display: block;\n opacity: 1;\n}\n\n.toast.hide {\n display: none;\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: 0.25rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n.toast-body {\n padding: 0.75rem;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n transform: none;\n}\n\n.modal-dialog-scrollable {\n display: flex;\n max-height: calc(100% - 1rem);\n}\n\n.modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 1rem);\n overflow: hidden;\n}\n\n.modal-dialog-scrollable .modal-header,\n.modal-dialog-scrollable .modal-footer {\n flex-shrink: 0;\n}\n\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - 1rem);\n}\n\n.modal-dialog-centered::before {\n display: block;\n height: calc(100vh - 1rem);\n content: \"\";\n}\n\n.modal-dialog-centered.modal-dialog-scrollable {\n flex-direction: column;\n justify-content: center;\n height: 100%;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable .modal-content {\n max-height: none;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable::before {\n content: none;\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #dee2e6;\n border-top-left-radius: 0.3rem;\n border-top-right-radius: 0.3rem;\n}\n\n.modal-header .close {\n padding: 1rem 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n padding: 1rem;\n border-top: 1px solid #dee2e6;\n border-bottom-right-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.modal-footer > :not(:first-child) {\n margin-left: .25rem;\n}\n\n.modal-footer > :not(:last-child) {\n margin-right: .25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-scrollable {\n max-height: calc(100% - 3.5rem);\n }\n .modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 3.5rem);\n }\n .modal-dialog-centered {\n min-height: calc(100% - 3.5rem);\n }\n .modal-dialog-centered::before {\n height: calc(100vh - 3.5rem);\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n max-width: 800px;\n }\n}\n\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top > .arrow, .bs-popover-auto[x-placement^=\"top\"] > .arrow {\n bottom: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^=\"top\"] > .arrow::before {\n bottom: 0;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^=\"top\"] > .arrow::after {\n bottom: 1px;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right > .arrow, .bs-popover-auto[x-placement^=\"right\"] > .arrow {\n left: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^=\"right\"] > .arrow::before {\n left: 0;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^=\"right\"] > .arrow::after {\n left: 1px;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow {\n top: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::before {\n top: 0;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::after {\n top: 1px;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left > .arrow, .bs-popover-auto[x-placement^=\"left\"] > .arrow {\n right: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^=\"left\"] > .arrow::before {\n right: 0;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^=\"left\"] > .arrow::after {\n right: 1px;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n transition: 0s 0.6s opacity;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-right {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: no-repeat 50% / 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: flex;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: .5;\n transition: opacity 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators li {\n transition: none;\n }\n}\n\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg);\n }\n}\n\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded-sm {\n border-radius: 0.2rem !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-lg {\n border-radius: 0.3rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-4by3::before {\n padding-top: 75%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports (position: sticky) {\n .sticky-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n pointer-events: auto;\n content: \"\";\n background-color: rgba(0, 0, 0, 0);\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !important;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-lighter {\n font-weight: lighter !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-weight-bolder {\n font-weight: bolder !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0056b3 !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #494f54 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #19692c !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #0f6674 !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #ba8b00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #a71d2a !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #cbd3da !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #121416 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-break {\n word-break: break-word !important;\n overflow-wrap: break-word !important;\n}\n\n.text-reset {\n color: inherit !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover {\n &:hover { @content; }\n}\n\n@mixin hover-focus {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n","// stylelint-disable declaration-no-important, selector-list-comma-newline-after\n\n//\n// Headings\n//\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1, .h1 { @include font-size($h1-font-size); }\nh2, .h2 { @include font-size($h2-font-size); }\nh3, .h3 { @include font-size($h3-font-size); }\nh4, .h4 { @include font-size($h4-font-size); }\nh5, .h5 { @include font-size($h5-font-size); }\nh6, .h6 { @include font-size($h6-font-size); }\n\n.lead {\n @include font-size($lead-font-size);\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n.display-1 {\n @include font-size($display1-size);\n font-weight: $display1-weight;\n line-height: $display-line-height;\n}\n.display-2 {\n @include font-size($display2-size);\n font-weight: $display2-weight;\n line-height: $display-line-height;\n}\n.display-3 {\n @include font-size($display3-size);\n font-weight: $display3-weight;\n line-height: $display-line-height;\n}\n.display-4 {\n @include font-size($display4-size);\n font-weight: $display4-weight;\n line-height: $display-line-height;\n}\n\n\n//\n// Horizontal rules\n//\n\nhr {\n margin-top: $hr-margin-y;\n margin-bottom: $hr-margin-y;\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n}\n\n\n//\n// Emphasis\n//\n\nsmall,\n.small {\n @include font-size($small-font-size);\n font-weight: $font-weight-normal;\n}\n\nmark,\n.mark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled;\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n @include font-size(90%);\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $spacer;\n @include font-size($blockquote-font-size);\n}\n\n.blockquote-footer {\n display: block;\n @include font-size($blockquote-small-font-size);\n color: $blockquote-small-color;\n\n &::before {\n content: \"\\2014\\00A0\"; // em dash, nbsp\n }\n}\n","// Lists\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n@mixin list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n","// Responsive images (ensure images don't scale beyond their parents)\n//\n// This is purposefully opt-in via an explicit class rather than being the default for all ``s.\n// We previously tried the \"images are responsive by default\" approach in Bootstrap v2,\n// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)\n// which weren't expecting the images within themselves to be involuntarily resized.\n// See also https://github.com/twbs/bootstrap/issues/18178\n.img-fluid {\n @include img-fluid;\n}\n\n\n// Image thumbnails\n.img-thumbnail {\n padding: $thumbnail-padding;\n background-color: $thumbnail-bg;\n border: $thumbnail-border-width solid $thumbnail-border-color;\n @include border-radius($thumbnail-border-radius);\n @include box-shadow($thumbnail-box-shadow);\n\n // Keep them at most 100% wide\n @include img-fluid;\n}\n\n//\n// Figures\n//\n\n.figure {\n // Ensures the caption's text aligns with the image.\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: $spacer / 2;\n line-height: 1;\n}\n\n.figure-caption {\n @include font-size($figure-caption-font-size);\n color: $figure-caption-color;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n@mixin img-fluid {\n // Part 1: Set a maximum relative to the parent\n max-width: 100%;\n // Part 2: Override the height to auto, otherwise images will be stretched\n // when setting a width and height attribute on the img element.\n height: auto;\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size.\n\n@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {\n background-image: url($file-1x);\n\n // Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,\n // but doesn't convert dppx=>dpi.\n // There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.\n // Compatibility info: https://caniuse.com/#feat=css-media-resolution\n @media only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx\n only screen and (min-resolution: 2dppx) { // Standardized\n background-image: url($file-2x);\n background-size: $width-1x $height-1x;\n }\n @include deprecate(\"`img-retina()`\", \"v4.3.0\", \"v5\");\n}\n","// stylelint-disable property-blacklist\n// Single side border-radius\n\n@mixin border-radius($radius: $border-radius, $fallback-border-radius: false) {\n @if $enable-rounded {\n border-radius: $radius;\n }\n @else if $fallback-border-radius != false {\n border-radius: $fallback-border-radius;\n }\n}\n\n@mixin border-top-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-top-right-radius: $radius;\n }\n}\n\n@mixin border-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: $radius;\n border-bottom-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n\n@mixin border-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n\n@mixin border-top-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n }\n}\n\n@mixin border-top-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-right-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-left-radius($radius) {\n @if $enable-rounded {\n border-bottom-left-radius: $radius;\n }\n}\n","// Inline code\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-break: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n @include box-shadow($kbd-box-shadow);\n\n kbd {\n padding: 0;\n @include font-size(100%);\n font-weight: $nested-kbd-font-weight;\n @include box-shadow(none);\n }\n}\n\n// Blocks of code\npre {\n display: block;\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: $pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n}\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but with 100% width for\n// fluid, full width layouts.\n\n@if $enable-grid-classes {\n .container-fluid {\n @include make-container();\n }\n}\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col#{$infix}-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n }\n\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n}\n","//\n// Basic Bootstrap table\n//\n\n.table {\n width: 100%;\n margin-bottom: $spacer;\n color: $table-color;\n background-color: $table-bg; // Reset for nesting within parents with `background-color`.\n\n th,\n td {\n padding: $table-cell-padding;\n vertical-align: top;\n border-top: $table-border-width solid $table-border-color;\n }\n\n thead th {\n vertical-align: bottom;\n border-bottom: (2 * $table-border-width) solid $table-border-color;\n }\n\n tbody + tbody {\n border-top: (2 * $table-border-width) solid $table-border-color;\n }\n}\n\n\n//\n// Condensed table w/ half padding\n//\n\n.table-sm {\n th,\n td {\n padding: $table-cell-padding-sm;\n }\n}\n\n\n// Border versions\n//\n// Add or remove borders all around the table and between all the columns.\n\n.table-bordered {\n border: $table-border-width solid $table-border-color;\n\n th,\n td {\n border: $table-border-width solid $table-border-color;\n }\n\n thead {\n th,\n td {\n border-bottom-width: 2 * $table-border-width;\n }\n }\n}\n\n.table-borderless {\n th,\n td,\n thead th,\n tbody + tbody {\n border: 0;\n }\n}\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-accent-bg;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n tbody tr {\n @include hover {\n color: $table-hover-color;\n background-color: $table-hover-bg;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n@each $color, $value in $theme-colors {\n @include table-row-variant($color, theme-color-level($color, $table-bg-level), theme-color-level($color, $table-border-level));\n}\n\n@include table-row-variant(active, $table-active-bg);\n\n\n// Dark styles\n//\n// Same table markup, but inverted color scheme: dark background and light text.\n\n// stylelint-disable-next-line no-duplicate-selectors\n.table {\n .thead-dark {\n th {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n border-color: $table-dark-border-color;\n }\n }\n\n .thead-light {\n th {\n color: $table-head-color;\n background-color: $table-head-bg;\n border-color: $table-border-color;\n }\n }\n}\n\n.table-dark {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n\n th,\n td,\n thead th {\n border-color: $table-dark-border-color;\n }\n\n &.table-bordered {\n border: 0;\n }\n\n &.table-striped {\n tbody tr:nth-of-type(odd) {\n background-color: $table-dark-accent-bg;\n }\n }\n\n &.table-hover {\n tbody tr {\n @include hover {\n color: $table-dark-hover-color;\n background-color: $table-dark-hover-bg;\n }\n }\n }\n}\n\n\n// Responsive tables\n//\n// Generate series of `.table-responsive-*` classes for configuring the screen\n// size of where your table will overflow.\n\n.table-responsive {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $next: breakpoint-next($breakpoint, $grid-breakpoints);\n $infix: breakpoint-infix($next, $grid-breakpoints);\n\n &#{$infix} {\n @include media-breakpoint-down($breakpoint) {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n\n // Prevent double border on horizontal scroll due to use of `display: block;`\n > .table-bordered {\n border: 0;\n }\n }\n }\n }\n}\n","// Tables\n\n@mixin table-row-variant($state, $background, $border: null) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table-#{$state} {\n &,\n > th,\n > td {\n background-color: $background;\n }\n\n @if $border != null {\n th,\n td,\n thead th,\n tbody + tbody {\n border-color: $border;\n }\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover {\n $hover-background: darken($background, 5%);\n\n .table-#{$state} {\n @include hover {\n background-color: $hover-background;\n\n > td,\n > th {\n background-color: $hover-background;\n }\n }\n }\n }\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Textual form controls\n//\n\n.form-control {\n display: block;\n width: 100%;\n height: $input-height;\n padding: $input-padding-y $input-padding-x;\n font-family: $input-font-family;\n @include font-size($input-font-size);\n font-weight: $input-font-weight;\n line-height: $input-line-height;\n color: $input-color;\n background-color: $input-bg;\n background-clip: padding-box;\n border: $input-border-width solid $input-border-color;\n\n // Note: This has no effect on `s in CSS.\n @include border-radius($input-border-radius, 0);\n\n @include box-shadow($input-box-shadow);\n @include transition($input-transition);\n\n // Unstyle the caret on ` receives focus\n // in IE and (under certain conditions) Edge, as it looks bad and cannot be made to\n // match the appearance of the native widget.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n}\n\n// Make file inputs better match text inputs by forcing them to new lines.\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n\n//\n// Labels\n//\n\n// For use with horizontal and inline forms, when you need the label (or legend)\n// text to align with the form controls.\n.col-form-label {\n padding-top: calc(#{$input-padding-y} + #{$input-border-width});\n padding-bottom: calc(#{$input-padding-y} + #{$input-border-width});\n margin-bottom: 0; // Override the `