diff --git a/.circleci/config.yml b/.circleci/config.yml index 073ed50bb5..9bac0b7e42 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,8 +5,7 @@ orbs: executors: standard-node: docker: - - image: "cimg/node:14.17.6" - - image: "circleci/redis:6.2.1-alpine" + - image: "cimg/node:18.14.2" - image: "circleci/postgres:12.3-postgis" environment: POSTGRES_USER: bloom-ci @@ -15,7 +14,7 @@ executors: POSTGRES_DB: bloom cypress-node: docker: - - image: "cypress/base:14.17.0" + - image: "cypress/base:18.14.1" - image: "circleci/redis:6.2.1-alpine" - image: "circleci/postgres:12.3-postgis" environment: @@ -26,21 +25,12 @@ executors: environment: PORT: "3100" EMAIL_API_KEY: "SG.SOME-LONG-SECRET-KEY" - EMAIL_FROM_ADDRESS: "Bloom Dev Housing Portal " APP_SECRET: "CI-LONG-SECRET-KEY" # DB URL for migration and seeds: DATABASE_URL: "postgres://bloom-ci@localhost:5432/bloom" # DB URL for the jest tests per ormconfig.test.ts TEST_DATABASE_URL: "postgres://bloom-ci@localhost:5432/bloom" - REDIS_TLS_URL: "rediss://localhost:6379/0" - REDIS_URL: "redis://localhost:6379/0" - REDIS_USE_TLS: "0" - CLOUDINARY_SECRET: "fake_secret" - CLOUDINARY_KEY: "fake_key" PARTNERS_PORTAL_URL: "http://localhost:3001" - puppeteer-node: - docker: - - image: "cimg/node:14.17.6-browsers" jobs: setup: @@ -70,18 +60,6 @@ jobs: - restore_cache: key: build-cache-{{ .Environment.CIRCLE_SHA1 }} - run: yarn test:shared:helpers - jest-ui-components: - executor: standard-node - steps: - - restore_cache: - key: build-cache-{{ .Environment.CIRCLE_SHA1 }} - - run: yarn test:shared:ui - jest-ui-components-a11y: - executor: puppeteer-node - steps: - - restore_cache: - key: build-cache-{{ .Environment.CIRCLE_SHA1 }} - - run: yarn test:shared:ui:a11y jest-backend: executor: standard-node steps: @@ -96,17 +74,15 @@ jobs: environment: PORT: "3100" EMAIL_API_KEY: "SG.SOME-LONG-SECRET-KEY" - EMAIL_FROM_ADDRESS: "Bloom Dev Housing Portal " APP_SECRET: "CI-LONG-SECRET-KEY" # DB URL for migration and seeds: DATABASE_URL: "postgres://bloom-ci@localhost:5432/bloom" # DB URL for the jest tests per ormconfig.test.ts TEST_DATABASE_URL: "postgres://bloom-ci@localhost:5432/bloom" - REDIS_TLS_URL: "rediss://localhost:6379/0" - REDIS_URL: "redis://localhost:6379/0" - REDIS_USE_TLS: "0" - CLOUDINARY_SECRET: "fake_secret" + CLOUDINARY_SIGNED_PRESET: "fake_secret" CLOUDINARY_KEY: "fake_key" + CLOUDINARY_CLOUD_NAME: "exygy" + CLOUDINARY_SECRET: "fake_secret" PARTNERS_PORTAL_URL: "http://localhost:3001" build-public: executor: standard-node @@ -120,6 +96,18 @@ jobs: - restore_cache: key: build-cache-{{ .Environment.CIRCLE_SHA1 }} - run: yarn build:app:partners + unit-test-partners: + executor: standard-node + steps: + - restore_cache: + key: build-cache-{{ .Environment.CIRCLE_SHA1 }} + - run: yarn test:app:partners:unit + unit-test-public: + executor: standard-node + steps: + - restore_cache: + key: build-cache-{{ .Environment.CIRCLE_SHA1 }} + - run: yarn test:app:public:unit workflows: version: 2 @@ -132,22 +120,23 @@ workflows: - jest-shared-helpers: requires: - setup - - jest-ui-components: + - jest-backend: requires: - setup - - jest-ui-components-a11y: + - build-public: requires: - setup - - jest-backend: + - unit-test-public: requires: - setup - - build-public: + - build-partners: requires: - setup - - build-partners: + - unit-test-partners: requires: - setup - cypress/run: + name: "cypress-public" requires: - setup executor: cypress-node @@ -158,3 +147,15 @@ workflows: start: yarn dev:all-cypress wait-on: "http://0.0.0.0:3000" store_artifacts: true + - cypress/run: + name: "cypress-partners" + requires: + - setup + executor: cypress-node + working_directory: sites/partners + yarn: true + build: | + yarn test:backend:core:dbsetup + start: yarn dev:all-cypress + wait-on: "http://0.0.0.0:3001" + store_artifacts: true diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..a7c62d42d6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.circleci +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.next +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/charts +**/docker-compose* +**/compose* +**/cypress +**/docs +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +backend/core/dist +backend/core/test +README.md \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index ac87940c47..54df1d3b92 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,10 +41,10 @@ module.exports = { "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-argument": "off", }, ignorePatterns: [ "node_modules", - "storybook-static", ".next", "dist", "migration/", diff --git a/.github/ISSUE_TEMPLATE/basic-story-issue.md b/.github/ISSUE_TEMPLATE/basic-story-issue.md new file mode 100644 index 0000000000..33dc6edb4b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/basic-story-issue.md @@ -0,0 +1,18 @@ +--- +name: Basic Story Issue +about: For general tasks as part of a feature +title: "[Issue Title]" +labels: '' +assignees: '' + +--- + +What feature is this part of? + +Is there any context that will help with completing this task? + +Are there any potential sub-steps necessary to complete this task? + +Are there any potential solutions or alternatives? + +What is the definition of done? diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000000..7923706782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,19 @@ +--- +name: Bug Report +about: For bugs +title: "[Bug] " +labels: 'bug' +assignees: '' + +--- + +[REMEMBER: a bug should be understandable years later by someone who will never talk to the reporter or assignee.] + +STEPS TO REPRODUCE: +1. + +EXPECTED RESULTS: + +OBSERVED RESULTS: + +ADDITIONAL INFORMATION: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..f3d5c415e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..3ba13e0cec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/general-issue.md b/.github/ISSUE_TEMPLATE/general-issue.md new file mode 100644 index 0000000000..6465c969cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/general-issue.md @@ -0,0 +1,26 @@ +--- +name: General Issue +about: Issues for a feature +title: "[Issue Title]" +labels: '' +assignees: '' + +--- + +**What is this feature or what feature is this part of?** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. + +**What is the acceptance criteria/definition of done?** +A clear and concise description of the acceptance criteria required to close this issue. + +**QA Review Instructions** +This is to be filled out by the developer who completes this issue, before passing to QA. diff --git a/.github/card-labeler.yml b/.github/card-labeler.yml new file mode 100644 index 0000000000..d7da79f75a --- /dev/null +++ b/.github/card-labeler.yml @@ -0,0 +1,15 @@ +Milestones: + 'Epics': + - 'tracking' + 'M7': + - 'M7' + 'M8': + - 'M8' + 'M9': + - 'M9' + 'M10': + - 'M10' + 'M11': + - 'M11' + 'M12': + - 'M12' diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..cd51d2c60c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every weekday + interval: "daily" diff --git a/.github/workflows/auto-assign-project.yml b/.github/workflows/auto-assign-project.yml new file mode 100644 index 0000000000..da6ef6eac0 --- /dev/null +++ b/.github/workflows/auto-assign-project.yml @@ -0,0 +1,25 @@ +name: Assign to One Project + +on: + issues: + types: [opened, labeled] +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + assign_one_project: + runs-on: ubuntu-latest + name: Assign to One Project + steps: + - name: Assign NEW issues to the Backlog project + uses: srggrs/assign-one-project-github-action@1.3.1 + if: github.event.action == 'opened' && github.event.issue != null + with: + project: 'https://github.com/CityOfDetroit/bloom/projects/1' + column_name: 'Needs triage' + - name: Assign NEW issues to the Milestones project + uses: srggrs/assign-one-project-github-action@1.3.1 + if: github.event.action == 'opened' && github.event.issue != null + with: + project: 'https://github.com/CityOfDetroit/bloom/projects/2' + column_name: 'Triage' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..4416e95e89 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ dev, master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ dev ] + schedule: + - cron: '40 8 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f1508cb3b4..1e859694d3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,14 +1,14 @@ name: Lint on: - # Trigger the workflow on push or pull request, + # Trigger the workflow on pull request, # but only for the dev branch push: branches: - dev pull_request: branches: - - dev + - main jobs: run-linters: @@ -17,12 +17,12 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v5 with: - node-version: 14 + node-version: 18 # ESLint and Prettier must be in `package.json` - name: Install Node.js dependencies @@ -31,5 +31,9 @@ jobs: - name: Run linters uses: wearerequired/lint-action@v1 with: + eslint: true prettier: true - auto_fix: true + auto_fix: false + + - name: Run separate lint command + run: yarn lint diff --git a/.github/workflows/pre-release_components.yml b/.github/workflows/pre-release_components.yml new file mode 100644 index 0000000000..b1d4b337a5 --- /dev/null +++ b/.github/workflows/pre-release_components.yml @@ -0,0 +1,40 @@ +name: Pre-release ui-components + +on: + # Triggers the workflow on push only for the dev branch + push: + branches: [dev] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Config user name + run: git config --global user.name github.context.workflow + + - name: Config user email + run: git config --global user.email "github-actions@github.com" + + - name: Check out git repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.ADMIN_TOKEN }} + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: 18 + registry-url: "https://registry.npmjs.org" + + - name: Version ui-components + run: yarn version:prerelease:ui-components + env: + GITHUB_TOKEN: ${{ secrets.ADMIN_TOKEN }} + + - name: Publish ui-components + run: yarn publish:ui-components + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/project-card-moved.yml b/.github/workflows/project-card-moved.yml new file mode 100644 index 0000000000..b59965f9cc --- /dev/null +++ b/.github/workflows/project-card-moved.yml @@ -0,0 +1,10 @@ +on: + project_card: + types: [moved] +name: Project Card Event +jobs: + triage: + name: Auto card labeler + runs-on: ubuntu-latest + steps: + - uses: technote-space/auto-card-labeler@v2 diff --git a/.gitignore b/.gitignore index fdeda5805d..ba3dcda84a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,12 +9,10 @@ yarn-error.log* .next/ **/out/_next* -# Storybook build output -storybook-static - # Cypress test output videos **/cypress/videos **/cypress/screenshots +**/cypress/downloads # Complied Typescript dist @@ -81,6 +79,7 @@ yarn-error.log # IDE configs .idea .vscode +*.code-workspace # VS code debugger config launch.json @@ -90,3 +89,10 @@ test-coverage/ # redis dumps dump.rdb + +# csv import files +backend/core/detroit-listings.csv +backend/core/detroit-listings-units.csv + +# DB backups +.dump diff --git a/.node-version b/.node-version index 5595ae1aa9..72e4a483c0 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.17.6 +18.14.2 diff --git a/.prettierignore b/.prettierignore index 155f52594b..01cee720ef 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,8 @@ **/*.hbs .github -ui-components/src/locales +.travis.yml +sites/public/CHANGELOG.md +sites/public/src/md_content/* +sites/partners/CHANGELOG.md +shared-helpers/CHANGELOG.md +backend/core/CHANGELOG.md \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..ab0bcc9373 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,63 @@ +language: node_js +node_js: + - 18 +before_install: + - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf + - sudo systemctl restart postgresql@12-main + - sleep 1 +before_script: + - cp sites/public/.env.template sites/public/.env + - cp sites/partners/.env.template sites/partners/.env + - cp backend/core/.env.template backend/core/.env +jobs: + include: + - script: yarn build:app:public + name: Build public site + - script: yarn build:app:partners + name: Build partners site + - script: yarn test:backend:core:testdbsetup && yarn test:backend:core + name: Backend unit tests + - script: yarn test:e2e:backend:core + name: Backend e2e tests + - script: yarn test:app:public:unit + name: Public site unit tests + - script: yarn test:app:partners:unit + name: Partners site unit tests + - stage: longer tests + name: Partners site Cypress tests + script: + - yarn cypress install + - cd backend/core + - yarn db:reseed:detroit + - yarn nest start & + - cd ../../sites/partners + - yarn build + - yarn start -p 3001 & + - yarn wait-on "http-get://localhost:3001" && yarn cypress run + - kill $(jobs -p) || true + - stage: longer tests + name: Public site Cypress tests + script: + - yarn cypress install + - yarn db:reseed + - cd backend/core + - yarn nest start & + - cd ../../sites/public + - yarn build + - yarn start -p 3000 & + - yarn wait-on "http-get://localhost:3000" && yarn cypress run + - kill $(jobs -p) || true +dist: focal +addons: + postgresql: "12" + apt: + packages: + - postgresql-12 + - postgresql-client-12 + - libgconf-2-4 +env: + global: PGPORT=5433 + PGUSER=travis + TEST_DATABASE_URL=postgres://localhost:5433/bloom_test + NEW_RELIC_ENABLED=false + NEW_RELIC_LOG_ENABLED=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 86d6a392bb..c4cc8874a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,115 @@ All notable changes to this project will be documented in this file. The format (_Note:_ it our intention to improve our release process going forward by using [Semantic Versioning](https://semver.org/spec/v2.0.0.html).) +## Detroit Team M12 + +- Fixed: + + - Removed units and application methods from seed listings + ([#590](https://github.com/CityOfDetroit/bloom/pull/590)) + +- Added: + + - Partially Senior reserved community type ([#572](https://github.com/CityOfDetroit/bloom/pull/572)) + - "About" page ([#589](https://github.com/CityOfDetroit/bloom/pull/589)) + - RTL support ([#627](https://github.com/CityOfDetroit/bloom/pull/627)) + - Add senior housing filtering to the filter modal ([#631](https://github.com/CityOfDetroit/bloom/pull/631)) + - Add AMI filter to frontend ([\$645](https://github.com/CityOfDetroit/bloom/pull/645)) + - Add independent living filtering([#639](https://github.com/CityOfDetroit/bloom/pull/639)) + +## Detroit Team M11 + +- Added: + + - Reset filter button + ([#489](https://github.com/CityOfDetroit/bloom/pull/489)) + - Number of filters in filter button + ([#489](https://github.com/CityOfDetroit/bloom/pull/489)) + - Filtering by availability ([#501](https://github.com/CityOfDetroit/bloom/pull/501)) + - Filtering by rent price ([#531](https://github.com/CityOfDetroit/bloom/pull/531)) + - UI for adding / editing units summaries ([#475](https://github.com/CityOfDetroit/bloom/pull/475)) + - Validate listing edit form ([#535](https://github.com/CityOfDetroit/bloom/pull/535)) + - Backend Filtering by AMI ([#532](https://github.com/CityOfDetroit/bloom/pull/532)) + - Add backend listing sorting ([#542](https://github.com/CityOfDetroit/bloom/pull/542) and [#548](https://github.com/CityOfDetroit/bloom/pull/548)) + +- Removed: + + - Removed minimum income from frontend forms and tables ([#578](https://github.com/CityOfDetroit/bloom/pull/578)) + +## Detroit Team M10 + +- Fixed: + + - Display a message when there are no results after applying filters + ([#456](https://github.com/CityOfDetroit/bloom/pull/456)) + +- Added: + + - Zipcode filtering to backend ([#399](https://github.com/CityOfDetroit/bloom/pull/399)) + - CSV import script ([#404](https://github.com/CityOfDetroit/bloom/pull/404)) + - Zipcode filtering to frontend ([#417](https://github.com/CityOfDetroit/bloom/pull/417)) + - Detroit AMI data and import script + ([#443](https://github.com/CityOfDetroit/bloom/pull/443)) + - Fetch UnitsSummary data in listing query ([#441](https://github.com/CityOfDetroit/bloom/pull/441)) + - Senior housing filtering for the eligibility questionnaire ([#446](https://github.com/CityOfDetroit/bloom/issues/446)] + +- Removed: + - Eligibility section of detailed listing view + ([#422](https://github.com/CityOfDetroit/bloom/pull/422)) + - Application section of partner portal + ([#438](https://github.com/CityOfDetroit/bloom/pull/438)) + +## Detroit Team M9 + +- Fixed: + + - Cypress tests for `sites/public` ([#171](https://github.com/CityOfDetroit/bloom/issues/171)) + - Change the COUNTY_CODE to Detroit ([#351](https://github.com/CityOfDetroit/bloom/pull/351)) + +- Added: + - Added fields to Listing and Property to accommodate Detroit listing data ([#311](https://github.com/CityOfDetroit/bloom/pull/311)) + - Add eligibility questionnaire validation ([#327](https://github.com/CityOfDetroit/bloom/pull/327)) + - Add support for comma-separated lists to filters ([#356](https://github.com/CityOfDetroit/bloom/pull/356)) + - Add eligibility questionnaire state management and back buttons ([#371](https://github.com/CityOfDetroit/bloom/pull/371)) + - Detroit seed properties ([#362](https://github.com/CityOfDetroit/bloom/pull/362)) + - Add bedrooms/unit size filter to backend ([#368](https://github.com/CityOfDetroit/bloom/pull/368)) + - Add bedrooms/unit size filter to frontend ([#391](https://github.com/CityOfDetroit/bloom/pull/391)) + +## Detroit Team M8 + +- Added: + - Made `addFilters()` more generic ([#257](https://github.com/CityOfDetroit/bloom/pull/257)) + - Fixed lowercaseing issue in query built by `addFilters()` ([#264](https://github.com/CityOfDetroit/bloom/pull/264)) + - Move where clauses and pagination to subquery, so filtered results return all listing data ([#271](https://github.com/CityOfDetroit/bloom/pull/271)) + +## Detroit Team M7 + +- Added: + - Debug flags for public and partner site ([#195](https://github.com/CityOfDetroit/bloom/pull/195)) + - Upstream filter param parsing, with changes to support pagination params and filters that aren't on the listings table ([#180](https://github.com/CityOfDetroit/bloom/pull/180)) + - Eligibility questionnaire ([#154](https://github.com/CityOfDetroit/bloom/pull/154), [#198](https://github.com/CityOfDetroit/bloom/pull/198), [#208](https://github.com/CityOfDetroit/bloom/pull/208)) + ## Unreleased +## Frontend + +- Fixed: + + - Language typo in the paper applications table ([#1965](https://github.com/bloom-housing/bloom/pull/1965)) (Jared White) + - Improved UX for the Building Selection Criteria drawer ([#1994](https://github.com/bloom-housing/bloom/pull/1994)) (Jared White) + - alternate contact email is validated ([#2035](https://github.com/bloom-housing/bloom/pull/2035)) (Yazeed) + - Incorrect last name ([#2107](https://github.com/bloom-housing/bloom/pull/2107)) (Dominik Barcikowski) + +## Backend + +- Fixed: + + - Incorrect listing status ([#2015](https://github.com/bloom-housing/bloom/pull/2015)) (Dominik Barcikowski) + +## v2.0.0-pre-tailwind 09/16/2021 + +## Frontend + - Added: - Support PDF uploads or webpage links for building selection criteria ([#1893](https://github.com/bloom-housing/bloom/pull/1893)) (Jared White) @@ -40,9 +147,12 @@ All notable changes to this project will be documented in this file. The format - Listings management AMI charts populate after Save and New on units ([#1952](https://github.com/bloom-housing/bloom/pull/1952)) (Emily Jablonski) - Brings in updates from Alameda which fixes some issues with preference handling and lisitngs getStaticProps in production ([#1958](https://github.com/bloom-housing/bloom/pull/1958)) - Preview can load without building address ([#1960](https://github.com/bloom-housing/bloom/pull/1960)) (Emily Jablonski) + - Page now scrolls after closing modal ([#1962](https://github.com/bloom-housing/bloom/pull/1962)) (Emily Jablonski) + - Copy & New and Save & New in LM will no longer create duplicate units ([#1963](https://github.com/bloom-housing/bloom/pull/1963)) (Emily Jablonski) - Changed: + - Update text for preferred unit types and terms ([#1934](https://github.com/bloom-housing/bloom/pull/1934)) (Jared White) - Upgrade the public and partners sites to Next v11 and React v17 ([#1793](https://github.com/bloom-housing/bloom/pull/1793)) (Jared White) - **Breaking Change** - The main changes are around removing the try catch blocks so errors prevent the build from finishing (should cover #1618) and the export script was removed, since it isn't valid with [fallback: true](https://nextjs.org/docs/advanced-features/static-html-export#caveats). So we'll have to change the build command to replace `export` with `start`. ([#1861](https://github.com/bloom-housing/bloom/pull/1861)) @@ -65,6 +175,8 @@ All notable changes to this project will be documented in this file. The format - StandardTable styling bug ([#1632](https://github.com/bloom-housing/bloom/pull/1632)) (Emily Jablonski) - More robust Features section for public listing view ([#1688](https://github.com/bloom-housing/bloom/pull/1688)) - A11Y issues with the image tint in ImageCard ([#1964](https://github.com/bloom-housing/bloom/pull/1964)) (Emily Jablonski) + - Visual bugs with SiteHeader ([#2010](https://github.com/bloom-housing/bloom/pull/2010)) (Emily Jablonski) + - HouseholdSizeField bug when householdSizeMax is 0 ([#1991](https://github.com/bloom-housing/bloom/pull/1991)) (Yazeed) - Changed: @@ -95,6 +207,7 @@ All notable changes to this project will be documented in this file. The format - **Breaking Change**: Moved tableHeader prop into new tableHeaderProps object - Re-wrote SiteHeader to remove Bulma dependency and bugs ([#1885](https://github.com/bloom-housing/bloom/pull/1885)) (Emily Jablonski) - **Breaking Change**: SiteHeader has a new prop set, including some props to toggle new visual features + - Set a max width for hero buttons when there are secondary buttons ([#2002](https://github.com/bloom-housing/bloom/pull/2002)) (Andrea Egan) ### Backend @@ -118,6 +231,9 @@ All notable changes to this project will be documented in this file. The format - Added the optional jurisdiction setting notificationsSignUpURL, which now appears on the home page if set ([#1802](https://github.com/bloom-housing/bloom/pull/1802)) (Emily Jablonski) - Adds Listings managment validations required for publishing a Listing [#1850](https://github.com/bloom-housing/bloom/pull/1850) (Michał Plebański & Emily Jablonski) - Add UnitCreateDto model changes to prevent form submission from creating UnitType, UnitRentType and AccessibilityType from creating a new DB row on each submission. ([#1956](https://github.com/bloom-housing/bloom/pull/1956)) + - Adds Program entity to Listing (Many to Many) and to Jurisdiction (Many to many) and seed programs ([1968](https://github.com/bloom-housing/bloom/pull/1968)) + - Add Language to Jurisidiction entity ([#1998](https://github.com/bloom-housing/bloom/pull/1998)) + - Add `DELETE /user/:id` and `GET /user/:id` endpoints and add leasingAgentInListings to UserUpdateDto - Changed: @@ -139,6 +255,9 @@ All notable changes to this project will be documented in this file. The format - `amiPercentage` field on UnitsSummary is migrated to an integer instead of a string. ((#1797)[https://github.com/bloom-housing/bloom/pull/1797]) - Change preferredUnit property to store unitType ids ([#1787](https://github.com/bloom-housing/bloom/pull/1787)) (Sean Albert) - Trying to confirm already confirmed user now throws account already confirmed error instead of tokenMissing ([#1971](https://github.com/bloom-housing/bloom/pull/1971)) + - Updates CSV Builder service to work with any data set, predefined or not. ([#1955](https://github.com/bloom-housing/bloom/pull/1955)) + - Remove field applicationAddress ([#2009](https://github.com/bloom-housing/bloom/pull/2009)) (Emily Jablonski) + - Introduce N-M Listing-Preference relation through a self managed (not TypeORM managed) intermediate entity ListingPreference, which now holds ordinal and page. Remove Preference entity entirely with an appropriate DB migration. ([1947](https://github.com/bloom-housing/bloom/pull/1947)) - Fixed: - Added checks for property in listing.dto transforms @@ -151,6 +270,8 @@ All notable changes to this project will be documented in this file. The format - updated DTOs to omit entities and use DTOs for application-method, user-roles, user, listing and units-summary ([#1679](https://github.com/bloom-housing/bloom/pull/1679)) - makes application flagged sets module take applications edits into account (e.g. a leasing agent changes something in the application) ([#1810](https://github.com/bloom-housing/bloom/pull/1810)) - Listings with multiple AMI charts show a max value instead of a range ([#1925](https://github.com/bloom-housing/bloom/pull/1925)) (Emily Jablonski) + - fix AFS totalFlagged missing in swagger documentation + - lower cases email during user creation, across saved users, and where that now lower cased email is compared to a possibly non-lower cased email ([#1972](https://github.com/bloom-housing/bloom/pull/1972)) (Yazeed) ### General diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..2b59cf6b4d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +dev-services@exygy.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Dockerfile.sites-partners b/Dockerfile.sites-partners new file mode 100644 index 0000000000..b08ea65157 --- /dev/null +++ b/Dockerfile.sites-partners @@ -0,0 +1,67 @@ +FROM node:14.17-alpine AS development +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat + +WORKDIR /usr/src/app/ + +# Copy all the source code from the root of the repo into the container +COPY . ./ + +WORKDIR /usr/src/app/sites/partners + +COPY sites/partners/package.json ./ +COPY sites/partners/yarn*.lock ./ +COPY sites/partners/tsconfig*.json ./ +COPY sites/partners/*config.js ./ + +RUN yarn install --frozen-lockfile + +COPY sites/partners . + +# If not defined, use our production backend site for build-time rendering. +# In order to override it for local development when building the image on +# its own, use --build-arg BACKEND_API_BASE=. +ARG BACKEND_API_BASE=https://backend-core-tj3gg4i5eq-uc.a.run.app +ARG PUBLIC_BASE_URL=https://sites-public-tj3gg4i5eq-uc.a.run.app +ARG SHOW_LM_LINKS=TRUE +ARG LANGUAGES=en,es,ar,bn + +RUN yarn build + +FROM node:14.17-alpine AS production + +# Use the NEXTJS_PORT variable as the PORT if defined, otherwise use PORT. +ENV NEXTJS_PORT=3001 +ENV PORT=${NEXTJS_PORT} +ARG NODE_ENV=production +ENV NODE_ENV=${NODE_ENV} + +WORKDIR /usr/src/app/ + +# We need to copy the source code for backend/core and ui-components as well in order +# to ensure that breaking changes in local dependencies from those packages are included +# instead of being pulled from npm. +COPY backend/core ./backend/core + +COPY --from=development /usr/src/app/package.json ./ +COPY --from=development /usr/src/app/yarn*.lock ./ +COPY --from=development /usr/src/app/tsconfig*.json ./ +COPY --from=development /usr/src/app/node_modules ./node_modules + +WORKDIR /usr/src/app/sites/partners + +COPY sites/partners . + +COPY --from=development /usr/src/app/sites/partners/next.config.js ./next.config.js +COPY --from=development /usr/src/app/sites/partners/public ./public +COPY --from=development /usr/src/app/sites/partners/.next ./.next +COPY --from=development /usr/src/app/sites/partners/node_modules ./node_modules + +EXPOSE ${PORT} + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry. +# ENV NEXT_TELEMETRY_DISABLED 1 + +CMD yarn next start -p ${PORT} diff --git a/Dockerfile.sites-public b/Dockerfile.sites-public new file mode 100644 index 0000000000..92296b8bb6 --- /dev/null +++ b/Dockerfile.sites-public @@ -0,0 +1,65 @@ +FROM node:14.17-alpine AS development +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat + +WORKDIR /usr/src/app/ + +# Copy all the source code from the root of the repo into the container +COPY . ./ + +WORKDIR /usr/src/app/sites/public + +COPY sites/public/package.json ./ +COPY sites/public/yarn*.lock ./ +COPY sites/public/tsconfig*.json ./ +COPY sites/public/*config.js ./ + +RUN yarn install --frozen-lockfile + +COPY sites/public . + +# If not defined, use our production backend site for build-time rendering. +# In order to override it for local development when building the image on +# its own, use --build-arg BACKEND_API_BASE=. +ARG BACKEND_API_BASE=https://backend-core-tj3gg4i5eq-uc.a.run.app +ARG LANGUAGES=en,es,ar,bn + +RUN yarn build + +FROM node:14.17-alpine AS production + +# Use the NEXTJS_PORT variable as the PORT if defined, otherwise use PORT. +ENV NEXTJS_PORT=3000 +ENV PORT=${NEXTJS_PORT} +ARG NODE_ENV=production +ENV NODE_ENV=${NODE_ENV} + +WORKDIR /usr/src/app/ + +# We need to copy the source code for backend/core and ui-components as well in order +# to ensure that breaking changes in local dependencies from those packages are included +# instead of being pulled from npm. +COPY backend/core ./backend/core + +COPY --from=development /usr/src/app/package.json ./package.json +COPY --from=development /usr/src/app/yarn*.lock ./ +COPY --from=development /usr/src/app/tsconfig*.json ./ +COPY --from=development /usr/src/app/node_modules ./node_modules + +WORKDIR /usr/src/app/sites/public + +COPY sites/public . + +COPY --from=development /usr/src/app/sites/public/next.config.js ./next.config.js +COPY --from=development /usr/src/app/sites/public/public ./public +COPY --from=development /usr/src/app/sites/public/.next ./.next +COPY --from=development /usr/src/app/sites/public/node_modules ./node_modules + +EXPOSE ${PORT} + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry. +# ENV NEXT_TELEMETRY_DISABLED 1 + +CMD yarn next start -p ${PORT} diff --git a/README.md b/README.md index a6771bf408..45751de88a 100644 --- a/README.md +++ b/README.md @@ -6,41 +6,36 @@ This is the repository for the Bloom affordable housing system. Bloom consists of a client/server architecture using [Next.js](https://nextjs.org) (a React-based site framework) for the frontend applications and [NestJS](https://nestjs.com) for the backend API. -The frontend apps can easily be deployed to any Jamstack-friendly web host such as Netlify or Vercel. The frontend build process performs a static rendering of as much of the React page component trees as possible based on API data available at the time of the build. Additional real-time interactivity is made possible by React components at run-time. +The frontend apps can easily be deployed to any Jamstack-friendly web host such as [Netlify](https://www.netlify.com/) or Vercel. The frontend build process performs a static rendering of as much of the React page component trees as possible based on API data available at the time of the build. Additional real-time interactivity is made possible by React components at run-time. The backend can be simultaenously deployed to PaaS-style hosts such as Heroku. Its primary architectural dependency is a PostgreSQL database. ### Structure -Bloom uses a monorepo-style repository, containing multiple user-facing applications and back-end services. The three main high-level packages are `backend/core`, `sites`, and `ui-components`. +Bloom uses a monorepo-style repository containing multiple user-facing applications and backend services. The three main high-level packages are `backend/core`, `sites`, `ui-components`, and `shared-helpers`. -The sites package contains reference implementations for each of the two main user-facing applications in the system: +The `sites` package contains reference implementations for the two user-facing applications in the system: --- -- `sites/public` is the applicant-facing site available to the general public. It provides the ability to browse available listings and to apply for listings either using the Common Application (which we built and maintain) or an external link to an online or paper PDF application. +- `sites/public` is the applicant-facing site available to the general public. It provides the ability to browse available listings and to apply for listings either using the Common Application (which we build and maintain) or an external link to a third-party online or paper application. - Visit [sites/public/README](https://github.com/bloom-housing/bloom/blob/dev/sites/public/README.md) for more details. -- `sites/partners` is the site designed for housing developers, property managers, and city/county (jurisdiction) employees. At the moment it offers the ability to view, edit, and export applications for listings and other administrative tasks. In the near future it will offer the ability to create, edit, and publish listings (which at the moment is done internally by our team). A login is required to use the Partners Portal. +- `sites/partners` is the site designed for housing developers, property managers, and city/county (jurisdiction) employees. For application management, it offers the ability to view, edit, and export applications for listings and other administrative tasks. For listing management, it offers the ability to create, edit, and publish listings. A login is required to use the Partners Portal. - Visit [sites/partners/README](https://github.com/bloom-housing/bloom/blob/dev/sites/partners/README.md) for more details. -Currently across our jurisdictions, our backend and partners portal implementations are shared, and the public site diverges slightly to accomodate jurisdictional customizations. The [housingbayarea Bloom fork](https://github.com/housingbayarea/bloom) is an example with customized public sites. In this fork of Bloom, our jurisdictions are each a separate branch. +In some cases the sites diverge slightly to accomodate jurisdictional customizations. The [housingbayarea Bloom fork](https://github.com/housingbayarea/bloom) is a fork of Bloom core for Bay Area jurisdictions which is loosely customized for that location. In this fork, our jurisdictions are each a separate branch. --- -- `backend/core` is the container for the key backend services (e.g. listings, applications, users). Information is stored in a Postgres database and served over HTTPS to the front-end (either at build time for things that can be server-rendered, or at run time). Most services are part of a NestJS application which allows for consolidated operation in one runtime environment. Services expose a REST API, and aren't expected to have any UI other than for debugging. You can read more about our backend in the README in that package. +- `backend/core` is the container for the key backend services (e.g. listings, applications, users). Information is stored in a Postgres database and served over HTTPS to the front-end (either at build time for things that can be server-rendered, or at run time). Most services are part of a NestJS application which allows for consolidated operation in one runtime environment. Services expose a REST API, and aren't expected to have any UI other than for debugging. - Visit [backend/core/README](https://github.com/bloom-housing/bloom/blob/dev/backend/core/README.md) for more details. --- -- `shared-helpers` contains types and functions intended for shared use between the Next.js sites, and in certain instances the frontend plus the backend (not currently but perhaps in the future). +- `shared-helpers` contains types and functions intended for shared use between the public and partners sites. - Visit [shared-helpers/README](https://github.com/bloom-housing/bloom/blob/dev/shared-helpers/README.md) for more details. ---- - -- `ui-components` contains React components that are either shared between our applications or pulled out to be more customizable for our consumers. We use [Storybook](https://storybook.js.org/), an environment for easily browing the UI components independent of their implementation. Contributions to component stories are encouraged. -- Visit [ui-components/README](https://github.com/bloom-housing/bloom/blob/dev/ui-components/README.md) for more details and view our [published Storybook](https://storybook.bloom.exygy.dev/). - ## Getting Started for Developers If this is your first time working with Bloom, please be sure to check out the `sites/public`, `sites/partners` and `backend/core` README files for important configuration information specific to those pieces. @@ -57,7 +52,19 @@ yarn install Configuration of each app and service is read from environment variables. There is an `.env.template` file in each app or service directory that must be copied to `.env` (or equivalent). Some keys are purposefully missing for security concerns and are internally available. -### Running a local test server +### Installing Dependencies and Seeding the Database + +This alias does a `yarn:install` in the root of the repo and `yarn install` and `yarn db:reseed` in the `backend/core` dir. + +``` +yarn setup +``` + +### Setting up a test Database + +The new `backend/core` uses a postgres database, which is accessed via TypeORM. Once postgres is set up and a blank database is initialized, yarn scripts are available within that package to create/migrate the schema, and to seed the database for development and testing. See [backend/core/README.md](https://github.com/bloom-housing/bloom/blob/master/backend/core/README.md) for more details. + +### Running a Local Test Server ``` yarn dev:all @@ -69,34 +76,34 @@ This runs 3 processes for both apps and the backend services on 3 different port - 3001 for the partners app - 3100 for backend/core -### Versioning +## Contributing + +Contributions to the core Bloom applications and services are welcomed. To help us meet the project's goals around quality and maintainability, we ask that all contributors read, understand, and agree to our guidelines. -We are using [lerna](https://lerna.js.org/) as a package versioning tool. It helps with keeping multiple package versions in sync for the entire monorepo. In conjunction with Lerna we are also using [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), a specification for commit messages that helps tools like Lerna understand what level of change the commit is so that you can automate things like versioning, releases, and changelogs. On commit, three steps run: (1) linting, (2) a conventional commit CLI, and (3) a verification of the conventional commit standard. If you have trouble with the CLI you may need to install the tool globally with `npm install -g commitizen`. +### Issue tracking -## Releasing +Our development tasks are managed through GitHub issues and any development (in the vast majority of cases) should be tied to an issue. Please feel free to submit issues even if you don't plan on implementing it yourself. Before creating an issue, check first to see if one already exists. When creating an issue, give it a descriptive title and include screenshots if relevant. Please don't start work on an issue without checking in with the Bloom team first as it may already be in development! You can tag us (@seanmalbert, @emilyjablonski, @yazeedloonat) to get started on an issue or ask any questions. -PRs are opened to our dev branch. Netlify deploy previews are generated and automatically posted to all PRs. We have an application in Netlify for our dev environment that is published on every push to dev. +### Committing, Versioning, and Releasing -Approximately weekly or as our roadmap requires us to, we will merge dev to master and then update our jurisdictional branches to get our changeset on a staging environment. Once that has been QA-ed we will publish to our production environment. +We are using [lerna](https://lerna.js.org/) as a monorepo management tool. It automatically versions, releases, and generates a changelog across our packages. In conjunction with lerna we are also using [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), a specification for commit messages that helps lerna understand what level of change each commit is in order to automate our processes. -`ui-components` is currently released on an ad-hoc basis, but we will soon be implementing a more frequent automatic release. +On commit, two steps automatically run: (1) linting and (2) a verification of the conventional commit standard. We recommend not running `git commit` and instead globally installing commitizen (`npm install -g commitizen`) and then committing with `git cz` which will run a commit message CLI. The CLI asks a series of questions about your changeset and builds the commit message for you in the conventional commit format. You can also `git commit` with your own message if you are confident it follows the conventional standard. -## Contributing +In addition to commits needing to be formatted as conventional commits, if you are making different levels of version change across multiple packages, your commits must also be separated by package in order to avoid improperly versioning a package. -Contributions to the core Bloom applications and services are welcomed. To help us meet the project's goals around quality and maintainability, we ask that all contributors read, understand, and agree to these guidelines. +On every merge to dev, our Netlify `development` environment is updated and a pre-release of the ui-components package is automatically published to npm. -### Issue tracking +On every merge to master (roughly bi-weekly), a release of the backend/core and ui-components packages are automatically published to npm and our Netlify `staging` environment is updated. -Our development tasks are managed through GitHub issues and any development (in the vast majority of cases) should be tied to an issue. Even if you don't plan on implementing an issue yourself, please feel free to submit them if you run into issues. Before creating an issue, check first to see if one already exists. When creating an issue, give it a descriptive title and include screenshots if relevant. Please don't start work on an issue without checking in with the Bloom team first as it may already be in development! If you have questions, feel free to tag us on issues (@seanmalbert, @emilyjablonski) and note that we are also using GitHub discussions. +Once staging has been QAed, we manually update `production`. ### Pull Requests -Pull requests are opened to the dev branch, not to master. When opening a pull request please fill out the entire pull request template which includes tagging the issue your PR is related to, a description of your PR, indicating the type of change, including details for the reviewer about how to test your PR, and a testing checklist. Additionally, officially link the issue in GitHub's right-hand panel. - -Every PR needs to manually update our changelog. Find the relevant section (General, Frontend, Backend, UI Components) and subsection (Added, Changed, Fixed) and add a short description of your change followed by a link to the PR and your name (- Description Here ([#1234](https://github.com/bloom-housing/bloom/pull/1234)) (Your Name)). If it is a breaking change, please include **Breaking Change** and some notes below it about how to migrate. +Pull requests are opened to the dev branch, not to master. When opening a pull request please fill out the entire pull request template which includes tagging the issue your PR is related to, a description of your PR, indicating the type of change, including details for the reviewer about how to test your PR, and a testing checklist. Additionally, officially link the issue to the PR using GitHub's linking UI. -When your PR is ready for review, add the `ready for review` label to help surface it to our internal team. If there are specific team members working frequently on pieces you're changing, assign them as reviewers. If you put up a PR that is not yet ready, add the `wip` label. +When your PR is ready for review, add the `needs review(s)` label to help surface it to our internal team. You can assign people as reviewers to surface the work further. If you put up a PR that is not yet ready for eyes, add the `wip` label. -Once the PR has been approved, you either squash and merge if your changes are in one package, or rebase and merge if your changes are across packages to allow the versions based off of your commit messages to propagate appropriately. +Once the PR has been approved, you either (1) squash and merge the commits if your changes are just in one package, or (2) rebase and merge your commits if your commits are cleanly separated across multiple packages to allow the versions to propagate appropriately. -As a review on a PR, try not to leave only comments. If the PR requires further discussion or changes, mark it with Requested Changes. If a PR looks good to you or even if there are smaller changes requested that won't require an additional review, please mark it with Approved and comment on the last few changes needed. This helps other reviewers better understand the state of PRs at the list view and prevents an additionl unnecessary review cycle. +As a reviewer on a PR, try not to leave only comments, but a clear next step action. If the PR requires further discussion or changes, mark it with Requested Changes. If a PR looks good to you (or even if there are small changes requested that won't require an additional review), please mark it with Approved and comment on the last few changes needed. This helps other reviewers better understand the state of PRs at the list view and prevents an additional unnecessary review cycle. diff --git a/backend/core/.dockerignore b/backend/core/.dockerignore new file mode 100644 index 0000000000..4c55262482 --- /dev/null +++ b/backend/core/.dockerignore @@ -0,0 +1,24 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +README.md \ No newline at end of file diff --git a/backend/core/.env.template b/backend/core/.env.template index 7bc470c304..0e8c3cf46e 100644 --- a/backend/core/.env.template +++ b/backend/core/.env.template @@ -1,21 +1,22 @@ PORT=3100 NODE_ENV=development -DATABASE_URL=postgres://localhost/bloom +DATABASE_URL=postgres://localhost/bloom_detroit TEST_DATABASE_URL=postgres://localhost/bloom_test -REDIS_TLS_URL= -REDIS_URL=redis://127.0.0.1:6379/0 -REDIS_USE_TLS=0 THROTTLE_TTL=60 THROTTLE_LIMIT=2 EMAIL_API_KEY='SOME-LONG-SECRET-KEY' -EMAIL_FROM_ADDRESS='Bloom Dev Housing Portal ' APP_SECRET='SOME-LONG-SECRET-KEY' CLOUDINARY_SECRET=CLOUDINARY_SECRET CLOUDINARY_KEY=CLOUDINARY_KEY PARTNERS_BASE_URL=http://localhost:3001 NEW_RELIC_APP_NAME=Bloom Backend Local NEW_RELIC_LICENSE_KEY= +NEW_RELIC_ENABLED=false +NEW_RELIC_LOG_ENABLED=false GOOGLE_API_ID= GOOGLE_API_KEY= GOOGLE_API_EMAIL= PARTNERS_PORTAL_URL=http://localhost:3001 +TWILIO_ACCOUNT_SID='AC.THE-TWILIO-ACCOUNT-SID' +TWILIO_AUTH_TOKEN='THE-TWILIO-AUTH-TOKEN' +TWILIO_FROM_NUMBER='THE-TWILIO-FROM-NUMBER' diff --git a/backend/core/Aptfile b/backend/core/Aptfile new file mode 100644 index 0000000000..939ef80033 --- /dev/null +++ b/backend/core/Aptfile @@ -0,0 +1,2 @@ +# list packages +lsof diff --git a/backend/core/CHANGELOG.md b/backend/core/CHANGELOG.md index d8e770b940..2e83644bab 100644 --- a/backend/core/CHANGELOG.md +++ b/backend/core/CHANGELOG.md @@ -3,10 +3,2466 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [0.2.5](https://github.com/bloom-housing/bloom/compare/v0.2.3...v0.2.5) (2020-06-30) +# [4.4.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@4.2.3...@bloom-housing/backend-core@4.4.0) (2022-05-24) + + +* 2022-05-24 release (#2753) ([3beb6b7](https://github.com/seanmalbert/bloom/commit/3beb6b77f74e51ec37457d4676a1fd01d1304a65)), closes [#2753](https://github.com/seanmalbert/bloom/issues/2753) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.3.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.3.1-alpha.0...@bloom-housing/backend-core@4.3.1-alpha.1) (2022-05-24) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [4.3.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.2-alpha.9...@bloom-housing/backend-core@4.3.1-alpha.0) (2022-05-16) + + +### Bug Fixes + +* adds region to user phone # validation ([#2652](https://github.com/bloom-housing/bloom/issues/2652)) ([d3e0c40](https://github.com/bloom-housing/bloom/commit/d3e0c4041d9aba4d703da85596d59ec765ad179a)) +* ami charts without all households ([#2430](https://github.com/bloom-housing/bloom/issues/2430)) ([5e18eba](https://github.com/bloom-housing/bloom/commit/5e18eba1d24bff038b192477b72d9d3f1f05a39d)) +* app submission w/ no due date ([8e5a81c](https://github.com/bloom-housing/bloom/commit/8e5a81c37c4efc3404e5536bd54c10cd2962bca3)) +* authservice.token data null issue ([#2703](https://github.com/bloom-housing/bloom/issues/2703)) ([5430fa2](https://github.com/bloom-housing/bloom/commit/5430fa2802b2590c7514f228c200ae040eccf403)) +* await casbin enforcer ([d7eb196](https://github.com/bloom-housing/bloom/commit/d7eb196be0b05732325938e2db7b583d66cbc9cf)) +* cannot save custom mailing, dropoff, or pickup address ([edcb068](https://github.com/bloom-housing/bloom/commit/edcb068ca23411e0a34f1dc2ff4c77ab489ac0fc)) +* csv export auth check ([#2488](https://github.com/bloom-housing/bloom/issues/2488)) ([6faf8f5](https://github.com/bloom-housing/bloom/commit/6faf8f59b115adf73e70d56c855ba5b6d325d22a)) +* fix for csv dempgraphics and preference patch ([0ffc090](https://github.com/bloom-housing/bloom/commit/0ffc0900fee73b34fd953e5355552e2e763c239c)) +* listings management keep empty strings, remove empty objects ([3aba274](https://github.com/bloom-housing/bloom/commit/3aba274a751cdb2db55b65ade1cda5d1689ca681)) +* patches translations for preferences ([#2410](https://github.com/bloom-housing/bloom/issues/2410)) ([21f517e](https://github.com/bloom-housing/bloom/commit/21f517e3f62dc5fefc8b4031d8915c8d7690677d)) +* recalculate units available on listing update ([9c3967f](https://github.com/bloom-housing/bloom/commit/9c3967f0b74526db39df4f5dbc7ad9a52741a6ea)) +* units with invalid ami chart ([621ff02](https://github.com/bloom-housing/bloom/commit/621ff0227270861047e885467f9ddd77459adec1)) +* updates household member count ([f822713](https://github.com/bloom-housing/bloom/commit/f82271397d02025629d7ea039b40cdac95877c45)) +* updates partner check for listing perm ([#2484](https://github.com/bloom-housing/bloom/issues/2484)) ([c2ab01f](https://github.com/bloom-housing/bloom/commit/c2ab01f6520b138bead01dec7352618b90635432)) + + +* 2022-04-08 release (#2646) ([aa9de52](https://github.com/bloom-housing/bloom/commit/aa9de524d5e849ffded475070abf529de77c9a92)), closes [#2646](https://github.com/bloom-housing/bloom/issues/2646) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-04-05 release (#2627) ([485fb48](https://github.com/bloom-housing/bloom/commit/485fb48cfbad48bcabfef5e2e704025f608aee89)), closes [#2627](https://github.com/bloom-housing/bloom/issues/2627) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-04-04 release (#2614) ([fecab85](https://github.com/bloom-housing/bloom/commit/fecab85c748a55ab4aff5d591c8e0ac702254559)), closes [#2614](https://github.com/bloom-housing/bloom/issues/2614) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-03-01 release (#2550) ([2f2264c](https://github.com/bloom-housing/bloom/commit/2f2264cffe41d0cc1ebb79ef5c894458694d9340)), closes [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-01-27 release (#2439) ([860f6af](https://github.com/bloom-housing/bloom/commit/860f6af6204903e4dcddf671d7ba54f3ec04f121)), closes [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) +* Release 11 11 21 (#2162) ([4847469](https://github.com/bloom-housing/bloom/commit/484746982e440c1c1c87c85089d86cd5968f1cae)), closes [#2162](https://github.com/bloom-housing/bloom/issues/2162) + + +### Features + +* add a phone number column to the user_accounts table ([44881da](https://github.com/bloom-housing/bloom/commit/44881da1a7ccc17b7d4db1fcf79513632c18066d)) +* Add San Jose email translations ([#2519](https://github.com/bloom-housing/bloom/issues/2519)) ([d1db032](https://github.com/bloom-housing/bloom/commit/d1db032672f40d325eba9e4a833d24f8b02464cc)) +* add SRO unit type ([a4c1403](https://github.com/bloom-housing/bloom/commit/a4c140350a84a5bacfa65fb6714aa594e406945d)) +* adding alameda prog and prefs ([#2696](https://github.com/bloom-housing/bloom/issues/2696)) ([85c0bf5](https://github.com/bloom-housing/bloom/commit/85c0bf5b41c86c4dddb0bfb99d65a652f8cad1a0)) +* adds jurisdictions to pref seeds ([8a70b68](https://github.com/bloom-housing/bloom/commit/8a70b688ec8c6eb785543d5ce91ae182f62af168)) +* adds new preferences, reserved community type ([90c0673](https://github.com/bloom-housing/bloom/commit/90c0673779eeb028041717d0b1e0e69fb0766c71)) +* adds whatToExpect to GTrans ([461961a](https://github.com/bloom-housing/bloom/commit/461961a4dd48d7a1c935e4dc03e9a62d2f455088)) +* adds whatToExpect to GTrans ([#2303](https://github.com/bloom-housing/bloom/issues/2303)) ([38e672a](https://github.com/bloom-housing/bloom/commit/38e672a4dbd6c39a7a01b04698f2096a62eed8a1)) +* ami chart jurisdictionalized ([b2e2537](https://github.com/bloom-housing/bloom/commit/b2e2537818d92ff41ea51fbbeb23d9d7e8c1cf52)) +* **backend:** add storing listing translations ([#2215](https://github.com/bloom-housing/bloom/issues/2215)) ([d6a1337](https://github.com/bloom-housing/bloom/commit/d6a1337fbe3da8a159e2b60638fc527aa65aaef0)) +* **backend:** all programs to csv export ([#2302](https://github.com/bloom-housing/bloom/issues/2302)) ([48b50f9](https://github.com/bloom-housing/bloom/commit/48b50f95be794773cc68ebee3144c1f44db26f04)) +* **backend:** fix translations table relation to jurisdiction ([#2506](https://github.com/bloom-housing/bloom/issues/2506)) ([22b9f23](https://github.com/bloom-housing/bloom/commit/22b9f23eb405f701796193515dff35058cc4f7dc)) +* better seed data for ami-charts ([24eb7e4](https://github.com/bloom-housing/bloom/commit/24eb7e41512963f8dc716b74e8a8684e1272e1b7)) +* feat(backend): make use of new application confirmation codes ([8f386e8](https://github.com/bloom-housing/bloom/commit/8f386e8e656c8d498d41de947f2e5246d3c16b19)) +* new demographics sub-race questions ([910df6a](https://github.com/bloom-housing/bloom/commit/910df6ad3985980becdc2798076ed5dfeeb310b5)) +* one month rent ([319743d](https://github.com/bloom-housing/bloom/commit/319743d23268f5b55e129c0878510edb4204b668)) +* overrides fallback to english, tagalog support ([b79fd10](https://github.com/bloom-housing/bloom/commit/b79fd1018619f618bd9be8e870d35c1180b81dfb)) +* temp disable terms and set mfa enabled to false ([#2595](https://github.com/bloom-housing/bloom/issues/2595)) ([6de2dcd](https://github.com/bloom-housing/bloom/commit/6de2dcd8baeb28166d7a6c383846a7ab9a84b0e2)) +* updates email confirmation for lottery ([768064a](https://github.com/bloom-housing/bloom/commit/768064a985ed858fae681caebcbcdb561319eaf9)) + + +### Reverts + +* Revert "chore: removes application program partners" ([91e22d8](https://github.com/bloom-housing/bloom/commit/91e22d891104e8d4fc024d709a6a14cec1400733)) +* Revert "chore: removes application program display" ([740cf00](https://github.com/bloom-housing/bloom/commit/740cf00dc3a729eed037d56a8dfc5988decd2651)) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + + + + + +## [4.2.3](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@4.2.2...@bloom-housing/backend-core@4.2.3) (2022-04-28) + +**Note:** Version bump only for package @bloom-housing/backend-core + +### Features +* adding alameda prog and prefs ([#2696](https://github.com/seanmalbert/bloom/issues/2696)) ([85c0bf5](https://github.com/seanmalbert/bloom/commit/85c0bf5b41c86c4dddb0bfb99d65a652f8cad1a0)) + +## [4.2.2-alpha.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.2-alpha.8...@bloom-housing/backend-core@4.2.2-alpha.9) (2022-05-11) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [4.2.2-alpha.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.2-alpha.7...@bloom-housing/backend-core@4.2.2-alpha.8) (2022-05-11) + + +### Features + +* **backend:** add search param to GET /user/list endpoint ([#2714](https://github.com/bloom-housing/bloom/issues/2714)) ([95c9a68](https://github.com/bloom-housing/bloom/commit/95c9a6838f534450c0da6919064f4a799898ed8f)) + + + + + +## [4.2.2-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.2-alpha.6...@bloom-housing/backend-core@4.2.2-alpha.7) (2022-05-03) + + +### Features + +* **backend:** improve ami chart dto definitions ([#2677](https://github.com/bloom-housing/bloom/issues/2677)) ([ca3890e](https://github.com/bloom-housing/bloom/commit/ca3890e2759f230824e31e6bd985300f40b0a0ed)) + + + + + +## [4.2.2-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.2-alpha.5...@bloom-housing/backend-core@4.2.2-alpha.6) (2022-04-29) + + +### Bug Fixes + +* check for empty translations before sending to google translate service ([#2700](https://github.com/bloom-housing/bloom/issues/2700)) ([d116fdb](https://github.com/bloom-housing/bloom/commit/d116fdbdab3c874679abc8e3dba8e23179fc78e2)) + + + + + +## [4.2.2-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.2-alpha.4...@bloom-housing/backend-core@4.2.2-alpha.5) (2022-04-28) + + +### Bug Fixes + +* authservice.token data null issue ([#2703](https://github.com/bloom-housing/bloom/issues/2703)) ([3b1b931](https://github.com/bloom-housing/bloom/commit/3b1b9316a6dd42adc22249b8e8dd836de2258406)) + + + + + +## [4.2.2-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.2-alpha.3...@bloom-housing/backend-core@4.2.2-alpha.4) (2022-04-27) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [4.2.2-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.2-alpha.2...@bloom-housing/backend-core@4.2.2-alpha.3) (2022-04-21) + + +### Features + +* **backend:** improve user queries ([#2676](https://github.com/bloom-housing/bloom/issues/2676)) ([4733e8a](https://github.com/bloom-housing/bloom/commit/4733e8a9909e47bb2522f9b319f45fe25923cdb5)) + + + + + +## [4.2.2-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.2-alpha.1...@bloom-housing/backend-core@4.2.2-alpha.2) (2022-04-20) + + +### Features + +* **backend:** add jurisdiction default rental assistance text ([#2604](https://github.com/bloom-housing/bloom/issues/2604)) ([00b684c](https://github.com/bloom-housing/bloom/commit/00b684cd8b8b1f9ef201b8aec78c13572a4125a5)) + + + + + +## [4.2.2-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.2-alpha.0...@bloom-housing/backend-core@4.2.2-alpha.1) (2022-04-14) + + +### Features + +* **backend:** add order param to listings GET endpoint ([#2630](https://github.com/bloom-housing/bloom/issues/2630)) ([2a915f2](https://github.com/bloom-housing/bloom/commit/2a915f2bb0d07fb20e2c829896fa22a13e4da1bf)) + + + + + +## [4.2.2-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.1-alpha.2...@bloom-housing/backend-core@4.2.2-alpha.0) (2022-04-13) + + +* 2022-04-11 sync master (#2649) ([9d30acf](https://github.com/bloom-housing/bloom/commit/9d30acf7b53fca50a87fc8bd2658c11d3ed37427)), closes [#2649](https://github.com/bloom-housing/bloom/issues/2649) [#2037](https://github.com/bloom-housing/bloom/issues/2037) [#2095](https://github.com/bloom-housing/bloom/issues/2095) [#2162](https://github.com/bloom-housing/bloom/issues/2162) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2519](https://github.com/bloom-housing/bloom/issues/2519) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2534](https://github.com/bloom-housing/bloom/issues/2534) [#2544](https://github.com/bloom-housing/bloom/issues/2544) [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) + + +### BREAKING CHANGES + +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + +* fix: adds jurisdictionId to useSWR path + +* fix: recalculate units available on listing update + +picked form dev f1a3dbce6478b16542ed61ab20de5dfb9b797262 + +* feat: feat(backend): make use of new application confirmation codes + +picked from dev 3c45c2904818200eed4568931d4cc352fd2f449e + +* revert: revert "chore(deps): bump axios from 0.21.1 to 0.21.2 + +picked from dev 2b83bc0393afc42eed542e326d5ef75502ce119c + +* fix: app submission w/ no due date + +picked from dev 4af1f5a8448f16d347b4a65ecb85fda4d6ed71fc + +* feat: adds new preferences, reserved community type + +* feat: adds bottom border to preferences + +* feat: updates preference string + +* fix: preference cleanup for avance + +* refactor: remove applicationAddress + +picked from dev bf10632a62bf2f14922948c046ea3352ed010f4f + +* feat: refactor and add public site application flow cypress tests + +picked from dev 9ec0e8d05f9570773110754e7fdaf49254d1eab8 + +* feat: better seed data for ami-charts + +picked from dev d8b1d4d185731a589c563a32bd592d01537785f3 + +* feat: adds listing management cypress tests to partner portal + +* fix: listings management keep empty strings, remove empty objects + +picked from dev c4b1e833ec128f457015ac7ffa421ee6047083d9 + +* feat: one month rent + +picked from dev 883b0d53030e1c4d54f2f75bd5e188bb1d255f64 + +* test: view.spec.ts test + +picked from dev 324446c90138d8fac50aba445f515009b5a58bfb + +* refactor: removes jsonpath + +picked from dev deb39acc005607ce3076942b1f49590d08afc10c + +* feat: adds jurisdictions to pref seeds + +picked from dev 9e47cec3b1acfe769207ccbb33c07019cd742e33 + +* feat: new demographics sub-race questions + +picked from dev 9ab892694c1ad2fa8890b411b3b32af68ade1fc3 + +* feat: updates email confirmation for lottery + +picked from dev 1a5e824c96d8e23674c32ea92688b9f7255528d3 + +* fix: add ariaHidden to Icon component + +picked from dev c7bb86aec6fd5ad386c7ca50087d0113b14503be + +* fix: add ariaLabel prop to Button component + +picked from dev 509ddc898ba44c05e26f8ed8c777f1ba456eeee5 + +* fix: change the yes/no radio text to be more descriptive + +picked from dev 0c46054574535523d6f217bb0677bbe732b8945f + +* fix: remove alameda reference in demographics + +picked from dev 7d5991cbf6dbe0b61f2b14d265e87ce3687f743d + +* chore: release version + +picked from dev fe82f25dc349877d974ae62d228fea0354978fb7 + +* feat: ami chart jurisdictionalized + +picked from dev 0a5cbc88a9d9e3c2ff716fe0f44ca6c48f5dcc50 + +* refactor: make backend a peer dependency in ui-components + +picked from dev 952aaa14a77e0960312ff0eeee51399d1d6af9f3 + +* feat: add a phone number column to the user_accounts table + +picked from dev 2647df9ab9888a525cc8a164d091dda6482c502a + +* chore: removes application program partners + +* chore: removes application program display + +* Revert "chore: removes application program display" + +This reverts commit 14825b4a6c9cd1a7235e32074e32af18a71b5c26. + +* Revert "chore: removes application program partners" + +This reverts commit d7aa38c777972a2e21d9f816441caa27f98d3f86. + +* chore: yarn.lock and backend-swagger + +* fix: removes Duplicate identifier fieldGroupObjectToArray + +* feat: skip preferences if not on listing + +* chore(release): version + +* fix: cannot save custom mailing, dropoff, or pickup address + +* chore(release): version + +* chore: converge on one axios version, remove peer dependency + +* chore(release): version + +* feat: simplify Waitlist component and use more flexible schema + +* chore(release): version + +* fix: lottery results uploads now save + +* chore(release): version + +* feat: add SRO unit type + +* chore(release): version + +* fix: paper application submission + +* chore(release): version + +* fix: choose-language context + +* chore(release): version + +* fix: applications/view hide prefs + +* chore(release): version + +* feat: overrides fallback to english, tagalog support + +* chore(release): version + +* fix: account translations + +* chore(release): version + +* fix: units with invalid ami chart + +* chore(release): version + +* fix: remove description for the partners programs + +* fix: fix modal styles on mobile + +* fix: visual improvement to programs form display + +* fix: submission tests not running +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.2.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@4.2.0...@bloom-housing/backend-core@4.2.1) (2022-04-11) + + +* 2022-04-08 release (#2646) ([aa9de52](https://github.com/seanmalbert/bloom/commit/aa9de524d5e849ffded475070abf529de77c9a92)), closes [#2646](https://github.com/seanmalbert/bloom/issues/2646) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +## [4.2.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.1-alpha.1...@bloom-housing/backend-core@4.2.1-alpha.2) (2022-04-13) + + +### Bug Fixes + +* adds region to user phone # validation ([#2652](https://github.com/bloom-housing/bloom/issues/2652)) ([f4ab660](https://github.com/bloom-housing/bloom/commit/f4ab660912a4c675073558d407880c8a98687530)) + + + + + +## [4.2.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.2.1-alpha.0...@bloom-housing/backend-core@4.2.1-alpha.1) (2022-04-07) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [4.2.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.1.3-alpha.1...@bloom-housing/backend-core@4.2.1-alpha.0) (2022-04-06) + + +* 2022-04-06 sync master (#2628) ([bc31833](https://github.com/bloom-housing/bloom/commit/bc31833f7ea5720a242d93a01bb1b539181fbad4)), closes [#2628](https://github.com/bloom-housing/bloom/issues/2628) [#2037](https://github.com/bloom-housing/bloom/issues/2037) [#2095](https://github.com/bloom-housing/bloom/issues/2095) [#2162](https://github.com/bloom-housing/bloom/issues/2162) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2519](https://github.com/bloom-housing/bloom/issues/2519) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2534](https://github.com/bloom-housing/bloom/issues/2534) [#2544](https://github.com/bloom-housing/bloom/issues/2544) [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) + + +### BREAKING CHANGES + +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + +* fix: adds jurisdictionId to useSWR path + +* fix: recalculate units available on listing update + +picked form dev f1a3dbce6478b16542ed61ab20de5dfb9b797262 + +* feat: feat(backend): make use of new application confirmation codes + +picked from dev 3c45c2904818200eed4568931d4cc352fd2f449e + +* revert: revert "chore(deps): bump axios from 0.21.1 to 0.21.2 + +picked from dev 2b83bc0393afc42eed542e326d5ef75502ce119c + +* fix: app submission w/ no due date + +picked from dev 4af1f5a8448f16d347b4a65ecb85fda4d6ed71fc + +* feat: adds new preferences, reserved community type + +* feat: adds bottom border to preferences + +* feat: updates preference string + +* fix: preference cleanup for avance + +* refactor: remove applicationAddress + +picked from dev bf10632a62bf2f14922948c046ea3352ed010f4f + +* feat: refactor and add public site application flow cypress tests + +picked from dev 9ec0e8d05f9570773110754e7fdaf49254d1eab8 + +* feat: better seed data for ami-charts + +picked from dev d8b1d4d185731a589c563a32bd592d01537785f3 + +* feat: adds listing management cypress tests to partner portal + +* fix: listings management keep empty strings, remove empty objects + +picked from dev c4b1e833ec128f457015ac7ffa421ee6047083d9 + +* feat: one month rent + +picked from dev 883b0d53030e1c4d54f2f75bd5e188bb1d255f64 + +* test: view.spec.ts test + +picked from dev 324446c90138d8fac50aba445f515009b5a58bfb + +* refactor: removes jsonpath + +picked from dev deb39acc005607ce3076942b1f49590d08afc10c + +* feat: adds jurisdictions to pref seeds + +picked from dev 9e47cec3b1acfe769207ccbb33c07019cd742e33 + +* feat: new demographics sub-race questions + +picked from dev 9ab892694c1ad2fa8890b411b3b32af68ade1fc3 + +* feat: updates email confirmation for lottery + +picked from dev 1a5e824c96d8e23674c32ea92688b9f7255528d3 + +* fix: add ariaHidden to Icon component + +picked from dev c7bb86aec6fd5ad386c7ca50087d0113b14503be + +* fix: add ariaLabel prop to Button component + +picked from dev 509ddc898ba44c05e26f8ed8c777f1ba456eeee5 + +* fix: change the yes/no radio text to be more descriptive + +picked from dev 0c46054574535523d6f217bb0677bbe732b8945f + +* fix: remove alameda reference in demographics + +picked from dev 7d5991cbf6dbe0b61f2b14d265e87ce3687f743d + +* chore: release version + +picked from dev fe82f25dc349877d974ae62d228fea0354978fb7 + +* feat: ami chart jurisdictionalized + +picked from dev 0a5cbc88a9d9e3c2ff716fe0f44ca6c48f5dcc50 + +* refactor: make backend a peer dependency in ui-components + +picked from dev 952aaa14a77e0960312ff0eeee51399d1d6af9f3 + +* feat: add a phone number column to the user_accounts table + +picked from dev 2647df9ab9888a525cc8a164d091dda6482c502a + +* chore: removes application program partners + +* chore: removes application program display + +* Revert "chore: removes application program display" + +This reverts commit 14825b4a6c9cd1a7235e32074e32af18a71b5c26. + +* Revert "chore: removes application program partners" + +This reverts commit d7aa38c777972a2e21d9f816441caa27f98d3f86. + +* chore: yarn.lock and backend-swagger + +* fix: removes Duplicate identifier fieldGroupObjectToArray + +* feat: skip preferences if not on listing + +* chore(release): version + +* fix: cannot save custom mailing, dropoff, or pickup address + +* chore(release): version + +* chore: converge on one axios version, remove peer dependency + +* chore(release): version + +* feat: simplify Waitlist component and use more flexible schema + +* chore(release): version + +* fix: lottery results uploads now save + +* chore(release): version + +* feat: add SRO unit type + +* chore(release): version + +* fix: paper application submission + +* chore(release): version + +* fix: choose-language context + +* chore(release): version + +* fix: applications/view hide prefs + +* chore(release): version + +* feat: overrides fallback to english, tagalog support + +* chore(release): version + +* fix: account translations + +* chore(release): version + +* fix: units with invalid ami chart + +* chore(release): version + +* fix: remove description for the partners programs + +* fix: fix modal styles on mobile + +* fix: visual improvement to programs form display + +* fix: submission tests not running +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +# [4.2.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@4.1.2...@bloom-housing/backend-core@4.2.0) (2022-04-06) + + +* 2022-04-05 release (#2627) ([485fb48](https://github.com/seanmalbert/bloom/commit/485fb48cfbad48bcabfef5e2e704025f608aee89)), closes [#2627](https://github.com/seanmalbert/bloom/issues/2627) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) +* 2022-04-04 release (#2614) ([fecab85](https://github.com/seanmalbert/bloom/commit/fecab85c748a55ab4aff5d591c8e0ac702254559)), closes [#2614](https://github.com/seanmalbert/bloom/issues/2614) [#2349](https://github.com/seanmalbert/bloom/issues/2349) [#2350](https://github.com/seanmalbert/bloom/issues/2350) [#2351](https://github.com/seanmalbert/bloom/issues/2351) [#2348](https://github.com/seanmalbert/bloom/issues/2348) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.1.3-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.1.3-alpha.0...@bloom-housing/backend-core@4.1.3-alpha.1) (2022-04-04) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [4.1.3-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.1.1-alpha.3...@bloom-housing/backend-core@4.1.3-alpha.0) (2022-03-30) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [4.1.2](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@4.1.0...@bloom-housing/backend-core@4.1.2) (2022-03-29) + +### Features + +* temp disable terms and set mfa enabled to false ([#2595](https://github.com/seanmalbert/bloom/issues/2595)) ([6de2dcd](https://github.com/seanmalbert/bloom/commit/6de2dcd8baeb28166d7a6c383846a7ab9a84b0e2)) + + + +## [4.1.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.1.1-alpha.2...@bloom-housing/backend-core@4.1.1-alpha.3) (2022-03-29) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [4.1.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.1.1-alpha.1...@bloom-housing/backend-core@4.1.1-alpha.2) (2022-03-28) + + +### Features + +* adds partners re-request confirmation ([#2574](https://github.com/bloom-housing/bloom/issues/2574)) ([235af78](https://github.com/bloom-housing/bloom/commit/235af781914e5c36104bb3862dd55152a16e6750)), closes [#2577](https://github.com/bloom-housing/bloom/issues/2577) + + + + + +## [4.1.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@4.1.1-alpha.0...@bloom-housing/backend-core@4.1.1-alpha.1) (2022-03-25) + + +### Bug Fixes + +* update for subject line ([#2578](https://github.com/bloom-housing/bloom/issues/2578)) ([dace763](https://github.com/bloom-housing/bloom/commit/dace76332bbdb3ad104638f32a07e71fd85edc0c)) +* update to mfa text's text ([#2579](https://github.com/bloom-housing/bloom/issues/2579)) ([ac5b812](https://github.com/bloom-housing/bloom/commit/ac5b81242f3177de09ed176a60f06be871906178)) + + + + + +## [4.1.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2...@bloom-housing/backend-core@4.1.1-alpha.0) (2022-03-02) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.45...@bloom-housing/backend-core@3.0.2) (2022-03-02) + +**Note:** Version bump only for package @bloom-housing/backend-core +# [4.1.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@4.0.3...@bloom-housing/backend-core@4.1.0) (2022-03-02) + + +* 2022-03-01 release (#2550) ([2f2264c](https://github.com/seanmalbert/bloom/commit/2f2264cffe41d0cc1ebb79ef5c894458694d9340)), closes [#2550](https://github.com/seanmalbert/bloom/issues/2550) [#2288](https://github.com/seanmalbert/bloom/issues/2288) [#2317](https://github.com/seanmalbert/bloom/issues/2317) [#2319](https://github.com/seanmalbert/bloom/issues/2319) [#2108](https://github.com/seanmalbert/bloom/issues/2108) [#2326](https://github.com/seanmalbert/bloom/issues/2326) [#2349](https://github.com/seanmalbert/bloom/issues/2349) [#2350](https://github.com/seanmalbert/bloom/issues/2350) [#2351](https://github.com/seanmalbert/bloom/issues/2351) [#2348](https://github.com/seanmalbert/bloom/issues/2348) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [3.0.2-alpha.45](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.44...@bloom-housing/backend-core@3.0.2-alpha.45) (2022-02-28) + + +### Features + +* updates to mfa styling ([#2532](https://github.com/bloom-housing/bloom/issues/2532)) ([7654efc](https://github.com/bloom-housing/bloom/commit/7654efc8a7c5cba0f7436fda62b886f646fe8a03)) + + + + + +## [4.0.3](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@4.0.2...@bloom-housing/backend-core@4.0.3) (2022-02-25) + + +### Bug Fixes + +* csv export auth check ([#2488](https://github.com/seanmalbert/bloom/issues/2488)) ([6faf8f5](https://github.com/seanmalbert/bloom/commit/6faf8f59b115adf73e70d56c855ba5b6d325d22a)) + +### Features + +* Add San Jose email translations ([#2519](https://github.com/seanmalbert/bloom/issues/2519)) ([d1db032](https://github.com/seanmalbert/bloom/commit/d1db032672f40d325eba9e4a833d24f8b02464cc)) +* **backend:** fix translations table relation to jurisdiction ([#2506](https://github.com/seanmalbert/bloom/issues/2506)) ([22b9f23](https://github.com/seanmalbert/bloom/commit/22b9f23eb405f701796193515dff35058cc4f7dc)) + + + + + +## [3.0.2-alpha.44](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.43...@bloom-housing/backend-core@3.0.2-alpha.44) (2022-02-22) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.43](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.42...@bloom-housing/backend-core@3.0.2-alpha.43) (2022-02-17) + + +### Features + +* adds NULLS LAST to mostRecentlyClosed ([#2521](https://github.com/bloom-housing/bloom/issues/2521)) ([39737a3](https://github.com/bloom-housing/bloom/commit/39737a3207e22815d184fc19cb2eaf6b6390dda8)) + + + + + +## [3.0.2-alpha.42](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.41...@bloom-housing/backend-core@3.0.2-alpha.42) (2022-02-17) + + +### Features + +* **backend:** add listing order by mostRecentlyClosed param ([#2478](https://github.com/bloom-housing/bloom/issues/2478)) ([0f177c1](https://github.com/bloom-housing/bloom/commit/0f177c1847ac254f63837b0aca7fa8a705e3632c)) + + + + + +## [3.0.2-alpha.41](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.40...@bloom-housing/backend-core@3.0.2-alpha.41) (2022-02-16) + + +### Features + +* **backend:** fix translations table relation to jurisdiction (make … ([#2506](https://github.com/bloom-housing/bloom/issues/2506)) ([8e1e3a9](https://github.com/bloom-housing/bloom/commit/8e1e3a9eb0ff76412831e122390ac25ad7754645)) + + + + + +## [3.0.2-alpha.40](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.39...@bloom-housing/backend-core@3.0.2-alpha.40) (2022-02-16) + + +### Bug Fixes + +* checks for existance of image_id ([#2505](https://github.com/bloom-housing/bloom/issues/2505)) ([d2051af](https://github.com/bloom-housing/bloom/commit/d2051afa188ce62c42f3d6bf737fd2059f9b7599)) + + + + + +## [3.0.2-alpha.39](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.38...@bloom-housing/backend-core@3.0.2-alpha.39) (2022-02-15) + + +### Features + +* **backend:** make listing image an array ([#2477](https://github.com/bloom-housing/bloom/issues/2477)) ([cab9800](https://github.com/bloom-housing/bloom/commit/cab98003e640c880be2218fa42321eadeec35e9c)) + + + + + +## [3.0.2-alpha.38](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.37...@bloom-housing/backend-core@3.0.2-alpha.38) (2022-02-15) + + +### Code Refactoring + +* remove backend dependencies from events components, consolidate ([#2495](https://github.com/bloom-housing/bloom/issues/2495)) ([d884689](https://github.com/bloom-housing/bloom/commit/d88468965bc67c74b8b3eaced20c77472e90331f)) + + +### BREAKING CHANGES + +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + + + + + +## [3.0.2-alpha.37](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.36...@bloom-housing/backend-core@3.0.2-alpha.37) (2022-02-15) + + +### Bug Fixes + +* **backend:** mfa_enabled migration fix ([#2503](https://github.com/bloom-housing/bloom/issues/2503)) ([a5b9a60](https://github.com/bloom-housing/bloom/commit/a5b9a604faccef55775dbbc54441251e29999fa4)) + + + + + +## [3.0.2-alpha.36](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.35...@bloom-housing/backend-core@3.0.2-alpha.36) (2022-02-15) + + +### Features + +* **backend:** add partners portal users multi factor authentication ([#2291](https://github.com/bloom-housing/bloom/issues/2291)) ([5b10098](https://github.com/bloom-housing/bloom/commit/5b10098d8668f9f42c60e90236db16d6cc517793)), closes [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) + + + + + +## [3.0.2-alpha.35](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.34...@bloom-housing/backend-core@3.0.2-alpha.35) (2022-02-10) + + +### Bug Fixes + +* csv export auth check ([#2488](https://github.com/bloom-housing/bloom/issues/2488)) ([2471d4a](https://github.com/bloom-housing/bloom/commit/2471d4afdd747843f58c0c154d6e94a9c76d733d)) + + + + + +## [3.0.2-alpha.34](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.33...@bloom-housing/backend-core@3.0.2-alpha.34) (2022-02-10) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + +## [4.0.2](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@4.0.1...@bloom-housing/backend-core@4.0.2) (2022-02-09) + + +### Bug Fixes + +* updates partner check for listing perm ([#2484](https://github.com/seanmalbert/bloom/issues/2484)) ([c2ab01f](https://github.com/seanmalbert/bloom/commit/c2ab01f6520b138bead01dec7352618b90635432)) + + + + + +## [3.0.2-alpha.33](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.32...@bloom-housing/backend-core@3.0.2-alpha.33) (2022-02-09) + + +### Features + +* **backend:** remove assigning partner user as an application owner ([#2476](https://github.com/bloom-housing/bloom/issues/2476)) ([4f6edf7](https://github.com/bloom-housing/bloom/commit/4f6edf7ed882ae926e363e4db4e40e6f19ed4746)) + + + + + +## [3.0.2-alpha.32](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.31...@bloom-housing/backend-core@3.0.2-alpha.32) (2022-02-09) + + +### Bug Fixes + +* updates partner check for listing perm ([#2484](https://github.com/bloom-housing/bloom/issues/2484)) ([9b0a6f5](https://github.com/bloom-housing/bloom/commit/9b0a6f560ec5dd95f846b330afb71eed40cbfa1b)) + + + + + +## [3.0.2-alpha.31](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.30...@bloom-housing/backend-core@3.0.2-alpha.31) (2022-02-09) + + +### Bug Fixes + +* cannot remove some fields in listings management ([#2455](https://github.com/bloom-housing/bloom/issues/2455)) ([acd9b51](https://github.com/bloom-housing/bloom/commit/acd9b51bb49581b4728b445d56c5c0a3c43e2777)) + + + + + +## [3.0.2-alpha.30](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.29...@bloom-housing/backend-core@3.0.2-alpha.30) (2022-02-07) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + +## [4.0.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.23...@bloom-housing/backend-core@4.0.1) (2022-02-03) + +### Bug Fixes + +* ami charts without all households ([#2430](https://github.com/seanmalbert/bloom/issues/2430)) ([5e18eba](https://github.com/seanmalbert/bloom/commit/5e18eba1d24bff038b192477b72d9d3f1f05a39d)) +* app submission w/ no due date ([8e5a81c](https://github.com/seanmalbert/bloom/commit/8e5a81c37c4efc3404e5536bd54c10cd2962bca3)) +* await casbin enforcer ([d7eb196](https://github.com/seanmalbert/bloom/commit/d7eb196be0b05732325938e2db7b583d66cbc9cf)) +* cannot save custom mailing, dropoff, or pickup address ([edcb068](https://github.com/seanmalbert/bloom/commit/edcb068ca23411e0a34f1dc2ff4c77ab489ac0fc)) +* fix for csv dempgraphics and preference patch ([0ffc090](https://github.com/seanmalbert/bloom/commit/0ffc0900fee73b34fd953e5355552e2e763c239c)) +* listings management keep empty strings, remove empty objects ([3aba274](https://github.com/seanmalbert/bloom/commit/3aba274a751cdb2db55b65ade1cda5d1689ca681)) +* patches translations for preferences ([#2410](https://github.com/seanmalbert/bloom/issues/2410)) ([21f517e](https://github.com/seanmalbert/bloom/commit/21f517e3f62dc5fefc8b4031d8915c8d7690677d)) +* recalculate units available on listing update ([9c3967f](https://github.com/seanmalbert/bloom/commit/9c3967f0b74526db39df4f5dbc7ad9a52741a6ea)) +* units with invalid ami chart ([621ff02](https://github.com/seanmalbert/bloom/commit/621ff0227270861047e885467f9ddd77459adec1)) +* updates household member count ([f822713](https://github.com/seanmalbert/bloom/commit/f82271397d02025629d7ea039b40cdac95877c45)) + + +* 2022-01-27 release (#2439) ([860f6af](https://github.com/seanmalbert/bloom/commit/860f6af6204903e4dcddf671d7ba54f3ec04f121)), closes [#2439](https://github.com/seanmalbert/bloom/issues/2439) [#2196](https://github.com/seanmalbert/bloom/issues/2196) [#2238](https://github.com/seanmalbert/bloom/issues/2238) [#2226](https://github.com/seanmalbert/bloom/issues/2226) [#2230](https://github.com/seanmalbert/bloom/issues/2230) [#2243](https://github.com/seanmalbert/bloom/issues/2243) [#2195](https://github.com/seanmalbert/bloom/issues/2195) [#2215](https://github.com/seanmalbert/bloom/issues/2215) [#2266](https://github.com/seanmalbert/bloom/issues/2266) [#2188](https://github.com/seanmalbert/bloom/issues/2188) [#2270](https://github.com/seanmalbert/bloom/issues/2270) [#2188](https://github.com/seanmalbert/bloom/issues/2188) [#2213](https://github.com/seanmalbert/bloom/issues/2213) [#2234](https://github.com/seanmalbert/bloom/issues/2234) [#1901](https://github.com/seanmalbert/bloom/issues/1901) [#2260](https://github.com/seanmalbert/bloom/issues/2260) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#2280](https://github.com/seanmalbert/bloom/issues/2280) [#2253](https://github.com/seanmalbert/bloom/issues/2253) [#2276](https://github.com/seanmalbert/bloom/issues/2276) [#2282](https://github.com/seanmalbert/bloom/issues/2282) [#2262](https://github.com/seanmalbert/bloom/issues/2262) [#2278](https://github.com/seanmalbert/bloom/issues/2278) [#2293](https://github.com/seanmalbert/bloom/issues/2293) [#2295](https://github.com/seanmalbert/bloom/issues/2295) [#2296](https://github.com/seanmalbert/bloom/issues/2296) [#2294](https://github.com/seanmalbert/bloom/issues/2294) [#2277](https://github.com/seanmalbert/bloom/issues/2277) [#2290](https://github.com/seanmalbert/bloom/issues/2290) [#2299](https://github.com/seanmalbert/bloom/issues/2299) [#2292](https://github.com/seanmalbert/bloom/issues/2292) [#2303](https://github.com/seanmalbert/bloom/issues/2303) [#2305](https://github.com/seanmalbert/bloom/issues/2305) [#2306](https://github.com/seanmalbert/bloom/issues/2306) [#2308](https://github.com/seanmalbert/bloom/issues/2308) [#2190](https://github.com/seanmalbert/bloom/issues/2190) [#2239](https://github.com/seanmalbert/bloom/issues/2239) [#2311](https://github.com/seanmalbert/bloom/issues/2311) [#2302](https://github.com/seanmalbert/bloom/issues/2302) [#2301](https://github.com/seanmalbert/bloom/issues/2301) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#2313](https://github.com/seanmalbert/bloom/issues/2313) [#2289](https://github.com/seanmalbert/bloom/issues/2289) [#2279](https://github.com/seanmalbert/bloom/issues/2279) [#2288](https://github.com/seanmalbert/bloom/issues/2288) [#2317](https://github.com/seanmalbert/bloom/issues/2317) [#2319](https://github.com/seanmalbert/bloom/issues/2319) [#2108](https://github.com/seanmalbert/bloom/issues/2108) [#2326](https://github.com/seanmalbert/bloom/issues/2326) [#2349](https://github.com/seanmalbert/bloom/issues/2349) [#2350](https://github.com/seanmalbert/bloom/issues/2350) [#2351](https://github.com/seanmalbert/bloom/issues/2351) [#2348](https://github.com/seanmalbert/bloom/issues/2348) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2434](https://github.com/seanmalbert/bloom/issues/2434) +* Release 11 11 21 (#2162) ([4847469](https://github.com/seanmalbert/bloom/commit/484746982e440c1c1c87c85089d86cd5968f1cae)), closes [#2162](https://github.com/seanmalbert/bloom/issues/2162) + +### Features + +* add a phone number column to the user_accounts table ([44881da](https://github.com/seanmalbert/bloom/commit/44881da1a7ccc17b7d4db1fcf79513632c18066d)) +* add SRO unit type ([a4c1403](https://github.com/seanmalbert/bloom/commit/a4c140350a84a5bacfa65fb6714aa594e406945d)) +* adds jurisdictions to pref seeds ([8a70b68](https://github.com/seanmalbert/bloom/commit/8a70b688ec8c6eb785543d5ce91ae182f62af168)) +* adds new preferences, reserved community type ([90c0673](https://github.com/seanmalbert/bloom/commit/90c0673779eeb028041717d0b1e0e69fb0766c71)) +* adds whatToExpect to GTrans ([461961a](https://github.com/seanmalbert/bloom/commit/461961a4dd48d7a1c935e4dc03e9a62d2f455088)) +* adds whatToExpect to GTrans ([#2303](https://github.com/seanmalbert/bloom/issues/2303)) ([38e672a](https://github.com/seanmalbert/bloom/commit/38e672a4dbd6c39a7a01b04698f2096a62eed8a1)) +* ami chart jurisdictionalized ([b2e2537](https://github.com/seanmalbert/bloom/commit/b2e2537818d92ff41ea51fbbeb23d9d7e8c1cf52)) +* **backend:** add storing listing translations ([#2215](https://github.com/seanmalbert/bloom/issues/2215)) ([d6a1337](https://github.com/seanmalbert/bloom/commit/d6a1337fbe3da8a159e2b60638fc527aa65aaef0)) +* **backend:** all programs to csv export ([#2302](https://github.com/seanmalbert/bloom/issues/2302)) ([48b50f9](https://github.com/seanmalbert/bloom/commit/48b50f95be794773cc68ebee3144c1f44db26f04)) +* better seed data for ami-charts ([24eb7e4](https://github.com/seanmalbert/bloom/commit/24eb7e41512963f8dc716b74e8a8684e1272e1b7)) +* feat(backend): make use of new application confirmation codes ([8f386e8](https://github.com/seanmalbert/bloom/commit/8f386e8e656c8d498d41de947f2e5246d3c16b19)) +* new demographics sub-race questions ([910df6a](https://github.com/seanmalbert/bloom/commit/910df6ad3985980becdc2798076ed5dfeeb310b5)) +* one month rent ([319743d](https://github.com/seanmalbert/bloom/commit/319743d23268f5b55e129c0878510edb4204b668)) +* overrides fallback to english, tagalog support ([b79fd10](https://github.com/seanmalbert/bloom/commit/b79fd1018619f618bd9be8e870d35c1180b81dfb)) +* updates email confirmation for lottery ([768064a](https://github.com/seanmalbert/bloom/commit/768064a985ed858fae681caebcbcdb561319eaf9)) + + +### Reverts + +* Revert "chore: removes application program partners" ([91e22d8](https://github.com/seanmalbert/bloom/commit/91e22d891104e8d4fc024d709a6a14cec1400733)) +* Revert "chore: removes application program display" ([740cf00](https://github.com/seanmalbert/bloom/commit/740cf00dc3a729eed037d56a8dfc5988decd2651)) + +### BREAKING CHANGES + +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + + + + + +## [3.0.2-alpha.29](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.28...@bloom-housing/backend-core@3.0.2-alpha.29) (2022-02-02) + + +### Bug Fixes + +* unit accordion radio button not showing default value ([#2451](https://github.com/bloom-housing/bloom/issues/2451)) ([4ed8103](https://github.com/bloom-housing/bloom/commit/4ed81039b9130d0433b11df2bdabc495ce2b9f24)) + + + + + +## [3.0.2-alpha.28](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.27...@bloom-housing/backend-core@3.0.2-alpha.28) (2022-02-02) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.27](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.26...@bloom-housing/backend-core@3.0.2-alpha.27) (2022-02-02) + + +### Bug Fixes + +* **backend:** translations input validator ([#2466](https://github.com/bloom-housing/bloom/issues/2466)) ([603c3dc](https://github.com/bloom-housing/bloom/commit/603c3dc52a400db815c4d81552a5aa74f397fe0f)) + + + + + +## [3.0.2-alpha.26](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.25...@bloom-housing/backend-core@3.0.2-alpha.26) (2022-02-02) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.25](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.24...@bloom-housing/backend-core@3.0.2-alpha.25) (2022-02-01) + + +### Bug Fixes + +* date validation issue ([#2464](https://github.com/bloom-housing/bloom/issues/2464)) ([158f7bf](https://github.com/bloom-housing/bloom/commit/158f7bf7fdc59954aebfebbd1ad3741239ed1a35)) + + + + + +## [3.0.2-alpha.24](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.23...@bloom-housing/backend-core@3.0.2-alpha.24) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.23](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.22...@bloom-housing/backend-core@3.0.2-alpha.23) (2022-02-01) + + +### Bug Fixes + +* await casbin enforcer ([4feacec](https://github.com/bloom-housing/bloom/commit/4feacec44635135bc5469c0edd02a3424a2697cc)) + + + + + +## [3.0.2-alpha.22](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.21...@bloom-housing/backend-core@3.0.2-alpha.22) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.21](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.20...@bloom-housing/backend-core@3.0.2-alpha.21) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.20](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.19...@bloom-housing/backend-core@3.0.2-alpha.20) (2022-02-01) + + +### Features + +* **backend:** add publishedAt and closedAt to listing entity ([#2432](https://github.com/bloom-housing/bloom/issues/2432)) ([f3b0f86](https://github.com/bloom-housing/bloom/commit/f3b0f864a6d5d2ad3d886e828743454c3e8fca71)) + + + + + +## [3.0.2-alpha.19](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.18...@bloom-housing/backend-core@3.0.2-alpha.19) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.18](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.17...@bloom-housing/backend-core@3.0.2-alpha.18) (2022-01-31) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.17](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.16...@bloom-housing/backend-core@3.0.2-alpha.17) (2022-01-27) + + +### Features + +* outdated password messaging updates ([b14e19d](https://github.com/bloom-housing/bloom/commit/b14e19d43099af2ba721d8aaaeeb2be886d05111)) + + + + + +## [3.0.2-alpha.16](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.15...@bloom-housing/backend-core@3.0.2-alpha.16) (2022-01-24) + + +### Bug Fixes + +* ami charts without all households ([#2430](https://github.com/bloom-housing/bloom/issues/2430)) ([92dfbad](https://github.com/bloom-housing/bloom/commit/92dfbad32c90d84ee1ec3a3468c084cb110aa8be)) + + + + + +## [3.0.2-alpha.15](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.14...@bloom-housing/backend-core@3.0.2-alpha.15) (2022-01-14) + + +### Bug Fixes + +* patches translations for preferences ([#2410](https://github.com/bloom-housing/bloom/issues/2410)) ([7906e6b](https://github.com/bloom-housing/bloom/commit/7906e6bc035fab4deea79ea51833a0ef29926d45)) + + + + +## [3.0.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.23...@bloom-housing/backend-core@3.0.1) (2022-01-13) + +### Bug Fixes + +* app submission w/ no due date ([8e5a81c](https://github.com/seanmalbert/bloom/commit/8e5a81c37c4efc3404e5536bd54c10cd2962bca3)) +* cannot save custom mailing, dropoff, or pickup address ([edcb068](https://github.com/seanmalbert/bloom/commit/edcb068ca23411e0a34f1dc2ff4c77ab489ac0fc)) +* fix for csv dempgraphics and preference patch ([0ffc090](https://github.com/seanmalbert/bloom/commit/0ffc0900fee73b34fd953e5355552e2e763c239c)) +* listings management keep empty strings, remove empty objects ([3aba274](https://github.com/seanmalbert/bloom/commit/3aba274a751cdb2db55b65ade1cda5d1689ca681)) +* recalculate units available on listing update ([9c3967f](https://github.com/seanmalbert/bloom/commit/9c3967f0b74526db39df4f5dbc7ad9a52741a6ea)) +* units with invalid ami chart ([621ff02](https://github.com/seanmalbert/bloom/commit/621ff0227270861047e885467f9ddd77459adec1)) +* updates household member count ([f822713](https://github.com/seanmalbert/bloom/commit/f82271397d02025629d7ea039b40cdac95877c45)) + + +* Release 11 11 21 (#2162) ([4847469](https://github.com/seanmalbert/bloom/commit/484746982e440c1c1c87c85089d86cd5968f1cae)), closes [#2162](https://github.com/seanmalbert/bloom/issues/2162) + +### Features + +* add a phone number column to the user_accounts table ([44881da](https://github.com/seanmalbert/bloom/commit/44881da1a7ccc17b7d4db1fcf79513632c18066d)) +* add SRO unit type ([a4c1403](https://github.com/seanmalbert/bloom/commit/a4c140350a84a5bacfa65fb6714aa594e406945d)) +* adds jurisdictions to pref seeds ([8a70b68](https://github.com/seanmalbert/bloom/commit/8a70b688ec8c6eb785543d5ce91ae182f62af168)) +* adds new preferences, reserved community type ([90c0673](https://github.com/seanmalbert/bloom/commit/90c0673779eeb028041717d0b1e0e69fb0766c71)) +* adds whatToExpect to GTrans ([461961a](https://github.com/seanmalbert/bloom/commit/461961a4dd48d7a1c935e4dc03e9a62d2f455088)) +* ami chart jurisdictionalized ([b2e2537](https://github.com/seanmalbert/bloom/commit/b2e2537818d92ff41ea51fbbeb23d9d7e8c1cf52)) +* **backend:** all programs to csv export ([#2302](https://github.com/seanmalbert/bloom/issues/2302)) ([48b50f9](https://github.com/seanmalbert/bloom/commit/48b50f95be794773cc68ebee3144c1f44db26f04)) +* better seed data for ami-charts ([24eb7e4](https://github.com/seanmalbert/bloom/commit/24eb7e41512963f8dc716b74e8a8684e1272e1b7)) +* feat(backend): make use of new application confirmation codes ([8f386e8](https://github.com/seanmalbert/bloom/commit/8f386e8e656c8d498d41de947f2e5246d3c16b19)) +* new demographics sub-race questions ([910df6a](https://github.com/seanmalbert/bloom/commit/910df6ad3985980becdc2798076ed5dfeeb310b5)) +* one month rent ([319743d](https://github.com/seanmalbert/bloom/commit/319743d23268f5b55e129c0878510edb4204b668)) +* overrides fallback to english, tagalog support ([b79fd10](https://github.com/seanmalbert/bloom/commit/b79fd1018619f618bd9be8e870d35c1180b81dfb)) +* updates email confirmation for lottery ([768064a](https://github.com/seanmalbert/bloom/commit/768064a985ed858fae681caebcbcdb561319eaf9)) + + +### Reverts + +* Revert "chore: removes application program partners" ([91e22d8](https://github.com/seanmalbert/bloom/commit/91e22d891104e8d4fc024d709a6a14cec1400733)) +* Revert "chore: removes application program display" ([740cf00](https://github.com/seanmalbert/bloom/commit/740cf00dc3a729eed037d56a8dfc5988decd2651)) + + +### BREAKING CHANGES + +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + + + + + +## [3.0.2-alpha.14](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.13...@bloom-housing/backend-core@3.0.2-alpha.14) (2022-01-13) + + +### Bug Fixes + +* partners render issue ([#2395](https://github.com/bloom-housing/bloom/issues/2395)) ([7fb108d](https://github.com/bloom-housing/bloom/commit/7fb108d744fcafd6b9df42706d2a2f58fbc30f0a)) + + + + + +## [3.0.2-alpha.13](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.12...@bloom-housing/backend-core@3.0.2-alpha.13) (2022-01-13) + + +### Bug Fixes + +* dates showing as invalid in send by mail section ([#2362](https://github.com/bloom-housing/bloom/issues/2362)) ([3567388](https://github.com/bloom-housing/bloom/commit/35673882d87e2b524b2c94d1fb7b40c9d777f0a3)) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + + + + + +## [3.0.2-alpha.12](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.11...@bloom-housing/backend-core@3.0.2-alpha.12) (2022-01-07) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.11](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.10...@bloom-housing/backend-core@3.0.2-alpha.11) (2022-01-07) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.10](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.9...@bloom-housing/backend-core@3.0.2-alpha.10) (2022-01-04) + + +### Bug Fixes + +* fix sortig on applications partner grid ([f097037](https://github.com/bloom-housing/bloom/commit/f097037afd896eec8bb90cc5e2de07f222907870)) +* fixes linting error ([aaaf858](https://github.com/bloom-housing/bloom/commit/aaaf85822e3b03224fb336bae66209a2b6b88d1d)) +* fixes some issues with the deployment ([a0042ba](https://github.com/bloom-housing/bloom/commit/a0042badc5474dde413e41a7f4f84c8ee7b2f8f1)) +* fixes tests and also issue with user grid ([da07ba4](https://github.com/bloom-housing/bloom/commit/da07ba49459f77fe77e3f72555eb50a0cbaab095)) + + + + + +## [3.0.2-alpha.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.8...@bloom-housing/backend-core@3.0.2-alpha.9) (2022-01-04) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.7...@bloom-housing/backend-core@3.0.2-alpha.8) (2022-01-03) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1...@bloom-housing/backend-core@3.0.2-alpha.7) (2022-01-03) + + +### Bug Fixes + +* bump version ([#2349](https://github.com/bloom-housing/bloom/issues/2349)) ([b9e3ba1](https://github.com/bloom-housing/bloom/commit/b9e3ba10aebd6534090f8be231a9ea77b3c929b6)) +* bump version ([#2350](https://github.com/bloom-housing/bloom/issues/2350)) ([05863f5](https://github.com/bloom-housing/bloom/commit/05863f55f3939bea4387bd7cf4eb1f34df106124)) +* check for user lastLoginAt ([d78745a](https://github.com/bloom-housing/bloom/commit/d78745a4c8b770864c4f5e6140ee602e745b8bec)) + + +### Features + +* **backend:** add appropriate http exception for password outdated login failure ([e5df66e](https://github.com/bloom-housing/bloom/commit/e5df66e4fe0f937f507d014f3b25c6c9b4b5deff)) +* **backend:** add password outdating only to users which are either admins or partners ([754546d](https://github.com/bloom-housing/bloom/commit/754546dfd5194f8c30e12963031791818566d22d)) +* **backend:** add user password expiration ([107c2f0](https://github.com/bloom-housing/bloom/commit/107c2f06e2f8367b52cb7cc8f00e6d9aef751fe0)) +* **backend:** lock failed login attempts ([a8370ce](https://github.com/bloom-housing/bloom/commit/a8370ce1516f75180796d190a9a9f2697723e181)) +* **backend:** remove activity log interceptor from update-password ([2e56b98](https://github.com/bloom-housing/bloom/commit/2e56b9878969604bec2f7694a83dbf7061af9df2)) + + + + + +## [3.0.2-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1...@bloom-housing/backend-core@3.0.2-alpha.6) (2022-01-03) + + +### Bug Fixes + +* bump version ([#2349](https://github.com/bloom-housing/bloom/issues/2349)) ([b9e3ba1](https://github.com/bloom-housing/bloom/commit/b9e3ba10aebd6534090f8be231a9ea77b3c929b6)) +* bump version ([#2350](https://github.com/bloom-housing/bloom/issues/2350)) ([05863f5](https://github.com/bloom-housing/bloom/commit/05863f55f3939bea4387bd7cf4eb1f34df106124)) +* check for user lastLoginAt ([d78745a](https://github.com/bloom-housing/bloom/commit/d78745a4c8b770864c4f5e6140ee602e745b8bec)) + + +### Features + +* **backend:** add appropriate http exception for password outdated login failure ([e5df66e](https://github.com/bloom-housing/bloom/commit/e5df66e4fe0f937f507d014f3b25c6c9b4b5deff)) +* **backend:** add password outdating only to users which are either admins or partners ([754546d](https://github.com/bloom-housing/bloom/commit/754546dfd5194f8c30e12963031791818566d22d)) +* **backend:** add user password expiration ([107c2f0](https://github.com/bloom-housing/bloom/commit/107c2f06e2f8367b52cb7cc8f00e6d9aef751fe0)) +* **backend:** lock failed login attempts ([a8370ce](https://github.com/bloom-housing/bloom/commit/a8370ce1516f75180796d190a9a9f2697723e181)) +* **backend:** remove activity log interceptor from update-password ([2e56b98](https://github.com/bloom-housing/bloom/commit/2e56b9878969604bec2f7694a83dbf7061af9df2)) + + + + + +## [3.0.2-alpha.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@3.0.2-alpha.0...@bloom-housing/backend-core@3.0.2-alpha.1) (2021-12-23) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.2-alpha.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@3.0.1...@bloom-housing/backend-core@3.0.2-alpha.0) (2021-12-23) + + +### Features + +* **backend:** lock failed login attempts ([a8370ce](https://github.com/seanmalbert/bloom/commit/a8370ce1516f75180796d190a9a9f2697723e181)) + + + + + +## [3.0.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.40...@bloom-housing/backend-core@3.0.1) (2021-12-22) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.40](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.39...@bloom-housing/backend-core@3.0.1-alpha.40) (2021-12-15) + + +### Features + +* **backend:** refactor applications module ([#2279](https://github.com/bloom-housing/bloom/issues/2279)) ([e0b4523](https://github.com/bloom-housing/bloom/commit/e0b4523817c7d3863c3802d8a9f61d1a1c8685d4)) + + + + + +## [3.0.1-alpha.39](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.38...@bloom-housing/backend-core@3.0.1-alpha.39) (2021-12-14) + + +### Features + +* removes ListingLangCacheInterceptor from get by id ([7acbd82](https://github.com/bloom-housing/bloom/commit/7acbd82485edfa9a8aa5a82473d5bbe5cee571e7)) + + + + + +## [3.0.1-alpha.38](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.37...@bloom-housing/backend-core@3.0.1-alpha.38) (2021-12-14) + + +### Features + +* **backend:** add partnerTerms to jurisdiction entity ([#2301](https://github.com/bloom-housing/bloom/issues/2301)) ([7ecf3ef](https://github.com/bloom-housing/bloom/commit/7ecf3ef24f261bf6b42fc38cf0080251a3c60e89)) + + + + + +## [3.0.1-alpha.37](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.36...@bloom-housing/backend-core@3.0.1-alpha.37) (2021-12-13) + + +### Features + +* **backend:** all programs to csv export ([#2302](https://github.com/bloom-housing/bloom/issues/2302)) ([f4d6a62](https://github.com/bloom-housing/bloom/commit/f4d6a62920e3b859310898e3a040f8116b43cab3)) + + + + + +## [3.0.1-alpha.36](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.35...@bloom-housing/backend-core@3.0.1-alpha.36) (2021-12-13) + + +### Features + +* **backend:** add activity logging to listings module ([#2190](https://github.com/bloom-housing/bloom/issues/2190)) ([88d60e3](https://github.com/bloom-housing/bloom/commit/88d60e32d77381d6e830158ce77c058b1cfcc022)) + + + + + +## [3.0.1-alpha.35](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.34...@bloom-housing/backend-core@3.0.1-alpha.35) (2021-12-10) + + +### Features + +* adds whatToExpect to GTrans ([#2303](https://github.com/bloom-housing/bloom/issues/2303)) ([6d7305b](https://github.com/bloom-housing/bloom/commit/6d7305b8e3b7e1c3a9776123e8e6d370ab803af0)) + + + + + +## [3.0.1-alpha.34](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.33...@bloom-housing/backend-core@3.0.1-alpha.34) (2021-12-09) + + +### Bug Fixes + +* units with invalid ami chart ([#2290](https://github.com/bloom-housing/bloom/issues/2290)) ([a6516e1](https://github.com/bloom-housing/bloom/commit/a6516e142ec13db5c3c8d2bb4f726be681e172e3)) + + + + + +## [3.0.1-alpha.33](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.32...@bloom-housing/backend-core@3.0.1-alpha.33) (2021-12-07) + + +### Features + +* overrides fallback to english, tagalog support ([#2262](https://github.com/bloom-housing/bloom/issues/2262)) ([679ab9b](https://github.com/bloom-housing/bloom/commit/679ab9b1816d5934f48f02ca5f5696952ef88ae7)) + + + + + +## [3.0.1-alpha.32](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.31...@bloom-housing/backend-core@3.0.1-alpha.32) (2021-12-06) + + +### Features + +* **backend:** add listings closing routine ([#2213](https://github.com/bloom-housing/bloom/issues/2213)) ([a747806](https://github.com/bloom-housing/bloom/commit/a747806282f80c92bd9a171a2b4d5c9b74d3b49a)) + + + + + +## [3.0.1-alpha.31](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.30...@bloom-housing/backend-core@3.0.1-alpha.31) (2021-12-03) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.30](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.29...@bloom-housing/backend-core@3.0.1-alpha.30) (2021-12-03) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.29](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.28...@bloom-housing/backend-core@3.0.1-alpha.29) (2021-12-03) + + +### Features + +* **backend:** add storing listing translations ([#2215](https://github.com/bloom-housing/bloom/issues/2215)) ([6ac63ea](https://github.com/bloom-housing/bloom/commit/6ac63eae82e14ab32d541b907c7e5dc800c1971f)) + + + + + +## [3.0.1-alpha.28](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.27...@bloom-housing/backend-core@3.0.1-alpha.28) (2021-12-01) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.27](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.26...@bloom-housing/backend-core@3.0.1-alpha.27) (2021-12-01) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.26](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.25...@bloom-housing/backend-core@3.0.1-alpha.26) (2021-11-30) + + +### Bug Fixes + +* **backend:** nginx with heroku configuration ([#2196](https://github.com/bloom-housing/bloom/issues/2196)) ([a1e2630](https://github.com/bloom-housing/bloom/commit/a1e26303bdd660b9ac267da55dc8d09661216f1c)) + + + + + +## [3.0.1-alpha.25](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.24...@bloom-housing/backend-core@3.0.1-alpha.25) (2021-11-29) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.24](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.23...@bloom-housing/backend-core@3.0.1-alpha.24) (2021-11-29) + + +### Bug Fixes + +* cannot save custom mailing, dropoff, or pickup address ([#2207](https://github.com/bloom-housing/bloom/issues/2207)) ([96484b5](https://github.com/bloom-housing/bloom/commit/96484b5676ecb000e492851ee12766ba9e6cd86f)) + + + + + +## [3.0.1-alpha.23](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.22...@bloom-housing/backend-core@3.0.1-alpha.23) (2021-11-23) + + +### Features + +* updates email confirmation for lottery ([#2200](https://github.com/bloom-housing/bloom/issues/2200)) ([1a5e824](https://github.com/bloom-housing/bloom/commit/1a5e824c96d8e23674c32ea92688b9f7255528d3)) + + + + + +## [3.0.1-alpha.22](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.21...@bloom-housing/backend-core@3.0.1-alpha.22) (2021-11-23) + + +### Features + +* new demographics sub-race questions ([#2109](https://github.com/bloom-housing/bloom/issues/2109)) ([9ab8926](https://github.com/bloom-housing/bloom/commit/9ab892694c1ad2fa8890b411b3b32af68ade1fc3)) + + + + + +## [3.0.1-alpha.21](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.20...@bloom-housing/backend-core@3.0.1-alpha.21) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.20](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.19...@bloom-housing/backend-core@3.0.1-alpha.20) (2021-11-22) + + +### Features + +* adds jurisdictions to pref seeds ([#2199](https://github.com/bloom-housing/bloom/issues/2199)) ([9e47cec](https://github.com/bloom-housing/bloom/commit/9e47cec3b1acfe769207ccbb33c07019cd742e33)) + + + + + +## [3.0.1-alpha.19](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.18...@bloom-housing/backend-core@3.0.1-alpha.19) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.18](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.17...@bloom-housing/backend-core@3.0.1-alpha.18) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.17](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.16...@bloom-housing/backend-core@3.0.1-alpha.17) (2021-11-17) + + +### Bug Fixes + +* **backend:** fix view.spec.ts test ([#2175](https://github.com/bloom-housing/bloom/issues/2175)) ([324446c](https://github.com/bloom-housing/bloom/commit/324446c90138d8fac50aba445f515009b5a58bfb)) + + + + + +## [3.0.1-alpha.16](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.15...@bloom-housing/backend-core@3.0.1-alpha.16) (2021-11-16) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.15](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.14...@bloom-housing/backend-core@3.0.1-alpha.15) (2021-11-15) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.14](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.13...@bloom-housing/backend-core@3.0.1-alpha.14) (2021-11-15) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.13](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.12...@bloom-housing/backend-core@3.0.1-alpha.13) (2021-11-15) + + +### Reverts + +* Revert "feat(backend): add nginx proxy-cache configuration (#2119)" ([d7a8951](https://github.com/bloom-housing/bloom/commit/d7a8951bc6686d4361f7c1100f09a45b29058fd0)), closes [#2119](https://github.com/bloom-housing/bloom/issues/2119) + + + + + +## [3.0.1-alpha.12](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.11...@bloom-housing/backend-core@3.0.1-alpha.12) (2021-11-12) + + +### Bug Fixes + +* sapp submission w/ no due date ([4af1f5a](https://github.com/bloom-housing/bloom/commit/4af1f5a8448f16d347b4a65ecb85fda4d6ed71fc)) + + + + + +## [3.0.1-alpha.11](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.10...@bloom-housing/backend-core@3.0.1-alpha.11) (2021-11-12) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.10](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.9...@bloom-housing/backend-core@3.0.1-alpha.10) (2021-11-11) + + +### Bug Fixes + +* recalculate units available on listing update ([#2150](https://github.com/bloom-housing/bloom/issues/2150)) ([f1a3dbc](https://github.com/bloom-housing/bloom/commit/f1a3dbce6478b16542ed61ab20de5dfb9b797262)) + + + + + +## [3.0.1-alpha.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.8...@bloom-housing/backend-core@3.0.1-alpha.9) (2021-11-10) + + +### Features + +* **backend:** add nginx proxy-cache configuration ([#2119](https://github.com/bloom-housing/bloom/issues/2119)) ([34d32e7](https://github.com/bloom-housing/bloom/commit/34d32e75ceae378a26c57f4c9b7feec8c88339e0)) + + + + + +## [3.0.1-alpha.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.7...@bloom-housing/backend-core@3.0.1-alpha.8) (2021-11-09) + + +### Bug Fixes + +* updates address order ([#2151](https://github.com/bloom-housing/bloom/issues/2151)) ([252e014](https://github.com/bloom-housing/bloom/commit/252e014dcbd2e4c305384ed552135f5a8e4e4767)) + + + + + +## [3.0.1-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.6...@bloom-housing/backend-core@3.0.1-alpha.7) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.5...@bloom-housing/backend-core@3.0.1-alpha.6) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.4...@bloom-housing/backend-core@3.0.1-alpha.5) (2021-11-09) + + +### Features + +* **backend:** improve application flagged set saving efficiency ([#2147](https://github.com/bloom-housing/bloom/issues/2147)) ([08a064c](https://github.com/bloom-housing/bloom/commit/08a064c319adabb5385e474f5751246d92dba9a2)) + + + + + +## [3.0.1-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.3...@bloom-housing/backend-core@3.0.1-alpha.4) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.2...@bloom-housing/backend-core@3.0.1-alpha.3) (2021-11-08) + + +### Features + +* add Programs section to listings management ([#2093](https://github.com/bloom-housing/bloom/issues/2093)) ([9bd1fe1](https://github.com/bloom-housing/bloom/commit/9bd1fe1033dee0fb7e73756254474471bc304f5e)) + + + + + +## [3.0.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.1...@bloom-housing/backend-core@3.0.1-alpha.2) (2021-11-08) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [3.0.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.1-alpha.0...@bloom-housing/backend-core@3.0.1-alpha.1) (2021-11-08) + + +### Features + +* **backend:** extend UserUpdateDto to support email change with confirmation ([#2120](https://github.com/bloom-housing/bloom/issues/2120)) ([3e1fdbd](https://github.com/bloom-housing/bloom/commit/3e1fdbd0ea91d4773973d5c485a5ba61303db90a)) + + + + + +## [3.0.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@3.0.0...@bloom-housing/backend-core@3.0.1-alpha.0) (2021-11-05) + + +* 1837/preferences cleanup 3 (#2144) ([3ce6d5e](https://github.com/bloom-housing/bloom/commit/3ce6d5eb5aac49431ec5bf4912dbfcbe9077d84e)), closes [#2144](https://github.com/bloom-housing/bloom/issues/2144) + + +### BREAKING CHANGES + +* Preferences are now M-N relation with a listing and have an intermediate table with ordinal number + +* refactor(backend): preferences deduplication + +So far each listing referenced it's own unique Preferences. This change introduces Many to Many +relationship between Preference and Listing entity and forces sharing Preferences between listings. + +* feat(backend): extend preferences migration with moving existing relations to a new intermediate tab + +* feat(backend): add Preference - Jurisdiction ManyToMany relation + +* feat: adapt frontend to backend changes + +* fix(backend): typeORM preferences select statement + +* fix(backend): connect preferences with jurisdictions in seeds, fix pref filter validator + +* fix(backend): fix missing import in preferences-filter-params.ts + +* refactor: rebase issue + +* feat: uptake jurisdictional preferences + +* fix: fixup tests + +* fix: application preferences ignore page, always separate + +* Remove page from src/migration/1633359409242-add-listing-preferences-intermediate-relation.ts + +* fix: preference fetching and ordering/pages + +* Fix code style issues with Prettier + +* fix(backend): query User__leasingAgentInListings__jurisdiction_User__leasingAgentIn specified more + +* fix: perferences cypress tests + +Co-authored-by: Michal Plebanski +Co-authored-by: Emily Jablonski +Co-authored-by: Lint Action + + + + + +# [3.0.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@2.0.1-alpha.7...@bloom-housing/backend-core@3.0.0) (2021-11-05) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [2.0.1-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.1-alpha.6...@bloom-housing/backend-core@2.0.1-alpha.7) (2021-11-05) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [2.0.1-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.1-alpha.5...@bloom-housing/backend-core@2.0.1-alpha.6) (2021-11-04) + + +### Reverts + +* Revert "refactor: listing preferences and adds jurisdictional filtering" ([41f72c0](https://github.com/bloom-housing/bloom/commit/41f72c0db49cf94d7930f5cfc88f6ee9d6040986)) + + + + + +## [2.0.1-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.1-alpha.4...@bloom-housing/backend-core@2.0.1-alpha.5) (2021-11-04) + + +### Bug Fixes + +* **backend:** make it possible to filter portal users in /users endpoint ([#2078](https://github.com/bloom-housing/bloom/issues/2078)) ([29bf714](https://github.com/bloom-housing/bloom/commit/29bf714d28755916ec8ec896366c8c32c3a227c4)) + + + + + +## [2.0.1-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.1-alpha.3...@bloom-housing/backend-core@2.0.1-alpha.4) (2021-11-04) + + +### Features + +* Updates application confirmation numbers ([#2072](https://github.com/bloom-housing/bloom/issues/2072)) ([75cd67b](https://github.com/bloom-housing/bloom/commit/75cd67bcb62280936bdeeaee8c9b7b2583a1339d)) + + + + + +## [2.0.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.1-alpha.2...@bloom-housing/backend-core@2.0.1-alpha.3) (2021-11-03) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +## [2.0.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.1-alpha.1...@bloom-housing/backend-core@2.0.1-alpha.2) (2021-11-03) + + +### Bug Fixes + +* don't send email confirmation on paper app submission ([#2110](https://github.com/bloom-housing/bloom/issues/2110)) ([7f83b70](https://github.com/bloom-housing/bloom/commit/7f83b70327049245ecfba04ae3aea4e967929b2a)) + + + + + +## [2.0.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.1-alpha.0...@bloom-housing/backend-core@2.0.1-alpha.1) (2021-11-03) + + +### Features + +* jurisdictional email signatures ([#2111](https://github.com/bloom-housing/bloom/issues/2111)) ([7a146ff](https://github.com/bloom-housing/bloom/commit/7a146ffb5de88cfa2950e2a469a99e38d71b33c8)) + + + + + +## [2.0.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0...@bloom-housing/backend-core@2.0.1-alpha.0) (2021-11-02) + + +### Features + +* two new common app questions - Household Changes and Household Student ([#2070](https://github.com/bloom-housing/bloom/issues/2070)) ([42a752e](https://github.com/bloom-housing/bloom/commit/42a752ec073c0f5b65374c7a68da1e34b0b1c949)) + + + + + +# [2.0.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.16...@bloom-housing/backend-core@2.0.0) (2021-11-02) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +# [2.0.0-pre-tailwind.16](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.15...@bloom-housing/backend-core@2.0.0-pre-tailwind.16) (2021-11-02) + + +### Code Refactoring + +* listing preferences and adds jurisdictional filtering ([9f661b4](https://github.com/bloom-housing/bloom/commit/9f661b43921ec939bd1bf5709c934ad6f56dd859)) + + +### BREAKING CHANGES + +* updates preference relationship with listings + + + + + +# [2.0.0-pre-tailwind.15](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.14...@bloom-housing/backend-core@2.0.0-pre-tailwind.15) (2021-11-01) + + +### Bug Fixes + +* reverts preferences to re-add as breaking/major bump ([508078e](https://github.com/bloom-housing/bloom/commit/508078e16649e4d5f669273c50ef62407aab995f)) +* reverts preferences to re-add as breaking/major bump ([4f7d893](https://github.com/bloom-housing/bloom/commit/4f7d89327361b3b28b368c23cfd24e6e8123a0a8)) + + + + + +# [2.0.0-pre-tailwind.14](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.13...@bloom-housing/backend-core@2.0.0-pre-tailwind.14) (2021-10-30) + + +### Bug Fixes + +* updates household member count ([#2112](https://github.com/bloom-housing/bloom/issues/2112)) ([3dee0f7](https://github.com/bloom-housing/bloom/commit/3dee0f7d676ff42d546ecf83a17659cd69d7e1bc)) + + + + + +# [2.0.0-pre-tailwind.13](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.12...@bloom-housing/backend-core@2.0.0-pre-tailwind.13) (2021-10-30) + + +* Preferences cleanup (#1947) ([7329a58](https://github.com/bloom-housing/bloom/commit/7329a58cc9242faf647459e46de1e3cff3fe9c9d)), closes [#1947](https://github.com/bloom-housing/bloom/issues/1947) + + +### BREAKING CHANGES + +* Preferences are now M-N relation with a listing and have an intermediate table with ordinal number + +* refactor(backend): preferences deduplication + +So far each listing referenced it's own unique Preferences. This change introduces Many to Many +relationship between Preference and Listing entity and forces sharing Preferences between listings. + +* feat(backend): extend preferences migration with moving existing relations to a new intermediate tab + +* feat(backend): add Preference - Jurisdiction ManyToMany relation + +* feat: adapt frontend to backend changes + +* fix(backend): typeORM preferences select statement + +* fix(backend): connect preferences with jurisdictions in seeds, fix pref filter validator + +* fix(backend): fix missing import in preferences-filter-params.ts + +* refactor: rebase issue + +* feat: uptake jurisdictional preferences + +* fix: fixup tests + +* fix: application preferences ignore page, always separate + +* Remove page from src/migration/1633359409242-add-listing-preferences-intermediate-relation.ts + +* fix: preference fetching and ordering/pages + +* Fix code style issues with Prettier + +* fix(backend): query User__leasingAgentInListings__jurisdiction_User__leasingAgentIn specified more + +* fix: perferences cypress tests + +Co-authored-by: Emily Jablonski +Co-authored-by: Sean Albert +Co-authored-by: Lint Action + + + + + +# [2.0.0-pre-tailwind.12](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.11...@bloom-housing/backend-core@2.0.0-pre-tailwind.12) (2021-10-29) + + +### Bug Fixes + +* fix for csv demographics and preference patch ([4768fb0](https://github.com/bloom-housing/bloom/commit/4768fb00be55957b3b1b197d149187c79374b48d)) + + + + + +# [2.0.0-pre-tailwind.11](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.10...@bloom-housing/backend-core@2.0.0-pre-tailwind.11) (2021-10-28) + + +### Bug Fixes + +* in listings management keep empty strings, remove empty objects ([#2064](https://github.com/bloom-housing/bloom/issues/2064)) ([c4b1e83](https://github.com/bloom-housing/bloom/commit/c4b1e833ec128f457015ac7ffa421ee6047083d9)) + + + + + +# [2.0.0-pre-tailwind.10](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.9...@bloom-housing/backend-core@2.0.0-pre-tailwind.10) (2021-10-27) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +# [2.0.0-pre-tailwind.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.8...@bloom-housing/backend-core@2.0.0-pre-tailwind.9) (2021-10-26) + + +### Bug Fixes + +* Incorrect listing status ([#2015](https://github.com/bloom-housing/bloom/issues/2015)) ([48aa14e](https://github.com/bloom-housing/bloom/commit/48aa14eb522cb8e4d0a25fdeadcc392b30d7f1a9)) + + + + + +# [2.0.0-pre-tailwind.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.7...@bloom-housing/backend-core@2.0.0-pre-tailwind.8) (2021-10-22) + + +### Bug Fixes + +* alternate contact email now validated ([#2035](https://github.com/bloom-housing/bloom/issues/2035)) ([b411695](https://github.com/bloom-housing/bloom/commit/b411695350f8f8de39c6994f2fac2fcb4678f678)) + + + + + +# [2.0.0-pre-tailwind.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.6...@bloom-housing/backend-core@2.0.0-pre-tailwind.7) (2021-10-22) + + +### Bug Fixes + +* makes listing programs optional ([fbe7134](https://github.com/bloom-housing/bloom/commit/fbe7134348e59e3fdb86663cfdca7648655e7b4b)) + + + + + +# [2.0.0-pre-tailwind.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.5...@bloom-housing/backend-core@2.0.0-pre-tailwind.6) (2021-10-22) + + +### Features + +* **backend:** add Program entity ([#1968](https://github.com/bloom-housing/bloom/issues/1968)) ([492ec4d](https://github.com/bloom-housing/bloom/commit/492ec4d333cf9b73af772a1aceed29813f405ba0)), closes [#2034](https://github.com/bloom-housing/bloom/issues/2034) + + + + + +# [2.0.0-pre-tailwind.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.4...@bloom-housing/backend-core@2.0.0-pre-tailwind.5) (2021-10-22) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +# [2.0.0-pre-tailwind.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.3...@bloom-housing/backend-core@2.0.0-pre-tailwind.4) (2021-10-22) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +# [2.0.0-pre-tailwind.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.2...@bloom-housing/backend-core@2.0.0-pre-tailwind.3) (2021-10-21) + +**Note:** Version bump only for package @bloom-housing/backend-core + + + + + +# [2.0.0-pre-tailwind.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.1...@bloom-housing/backend-core@2.0.0-pre-tailwind.2) (2021-10-21) + + +### Bug Fixes + +* **backend:** enforces lower casing of emails ([#1972](https://github.com/bloom-housing/bloom/issues/1972)) ([2608e82](https://github.com/bloom-housing/bloom/commit/2608e8228830a2fc7e6b522c73cb587adbb5803b)) +* migration fix ([#2043](https://github.com/bloom-housing/bloom/issues/2043)) ([ffa4d45](https://github.com/bloom-housing/bloom/commit/ffa4d45e0f53ce071fc4dcf8079c06cf5e836ed3)) + + +### Features + +* adds jurisdiction filtering to listings ([#2027](https://github.com/bloom-housing/bloom/issues/2027)) ([219696b](https://github.com/bloom-housing/bloom/commit/219696ba784cfc079dd5aec74b24c3a8479160b6)) +* **backend:** add languages (Language[]) to Jurisdiction entity ([#1998](https://github.com/bloom-housing/bloom/issues/1998)) ([9ceed24](https://github.com/bloom-housing/bloom/commit/9ceed24d48b14888e6ea59b421b409f875d12b01)) +* **backend:** Add user delete endpoint and expose leasingAgentInList… ([#1996](https://github.com/bloom-housing/bloom/issues/1996)) ([a13f735](https://github.com/bloom-housing/bloom/commit/a13f73574b470beff2f8948abb226a6786856480)) +* **backend:** make use of new application confirmation codes ([#2014](https://github.com/bloom-housing/bloom/issues/2014)) ([3c45c29](https://github.com/bloom-housing/bloom/commit/3c45c2904818200eed4568931d4cc352fd2f449e)) +* **backend:** try fixing SETEX redis e2e tests flakiness ([#2044](https://github.com/bloom-housing/bloom/issues/2044)) ([4087c53](https://github.com/bloom-housing/bloom/commit/4087c532ddba672a415a048f4362e509aba7fd7f)) + + + + + +# [2.0.0-pre-tailwind.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/backend-core@2.0.0-pre-tailwind.0...@bloom-housing/backend-core@2.0.0-pre-tailwind.1) (2021-10-19) **Note:** Version bump only for package @bloom-housing/backend-core -## [0.0.9](https://github.com/bloom-housing/bloom/compare/v0.0.2...v0.0.9) (2020-04-21) +# 2.0.0-pre-tailwind.0 (2021-10-19) + +### Bug Fixes + +- **backend:** Change tokenMissing to account already confirmed error … ([#1971](https://github.com/bloom-housing/bloom/issues/1971)) ([bc6ec92](https://github.com/bloom-housing/bloom/commit/bc6ec9243fb5be62ca8e240d96b828d418a9ee5b)) +- **backend:** totalFlagged from AFS missing in swagger documentation ([#1997](https://github.com/bloom-housing/bloom/issues/1997)) ([0abf5dd](https://github.com/bloom-housing/bloom/commit/0abf5ddefe8d4f33a895fe3faf59d43316f56003)) +- **backend:** unitCreate and UnitUpdateDto now require only IdDto for… ([#1956](https://github.com/bloom-housing/bloom/issues/1956)) ([43dcfbe](https://github.com/bloom-housing/bloom/commit/43dcfbe7493bdd654d7b898ed9650804a016065c)), closes [#1897](https://github.com/bloom-housing/bloom/issues/1897) +- Fix dev seeds with new priority types ([#1920](https://github.com/bloom-housing/bloom/issues/1920)) ([b01bd7c](https://github.com/bloom-housing/bloom/commit/b01bd7ca2c1ba3ba7948ad8213a0939375003d90)) +- Fix maps unit max occupancy to household size ([d1fefcf](https://github.com/bloom-housing/bloom/commit/d1fefcf2ea20cccf90375881c2a19d51bf986678)) +- Fixes reserved community type import ([e5b0e25](https://github.com/bloom-housing/bloom/commit/e5b0e25f556af6cdcdf05d79825736dddcd1e105)) +- Fixes unit types for max income ([87f018a](https://github.com/bloom-housing/bloom/commit/87f018a410657037a7c9a74a93ec6dbac6b42dec)) +- Fixes unit types for max income ([#2013](https://github.com/bloom-housing/bloom/issues/2013)) ([b8966a1](https://github.com/bloom-housing/bloom/commit/b8966a19ea79012456f7f28d01c34b32d6f207bb)) +- Multiple ami charts should show a max not a range ([#1925](https://github.com/bloom-housing/bloom/issues/1925)) ([142f436](https://github.com/bloom-housing/bloom/commit/142f43697bff23d2f59c7897d51ced83a2003308)) +- Plus one to maxHouseholdSize for bmr ([401c956](https://github.com/bloom-housing/bloom/commit/401c956b0e885d3485b427622b82b85fd9a5f8b1)) +- Removes 150 char limit on textarea fields ([6eb7036](https://github.com/bloom-housing/bloom/commit/6eb70364409c5910aa9b8277b37a8214c2a94358)) +- Removes nested validation from applicationAddress ([747fd83](https://github.com/bloom-housing/bloom/commit/747fd836a9b5b8333a6586727b00c5674ef87a86)) +- Update alameda's notification sign up URL ([#1874](https://github.com/bloom-housing/bloom/issues/1874)) ([3eb85fc](https://github.com/bloom-housing/bloom/commit/3eb85fccf7521e32f3d1f369e706cec0c078b536)) + +### Features + +- **backend:** Add jurisdiction relation to ami charts entity ([#1905](https://github.com/bloom-housing/bloom/issues/1905)) ([1f13985](https://github.com/bloom-housing/bloom/commit/1f13985142c7908b4c37eaf0fbbbad0ad660f014)) +- **backend:** Add jurisidction relation to ReservedCommunittType Entity ([#1889](https://github.com/bloom-housing/bloom/issues/1889)) ([9b0fe73](https://github.com/bloom-housing/bloom/commit/9b0fe73fe9ed1349584e119f235cb66f6e68785f)) +- Listings management draft and publish validation backend & frontend ([#1850](https://github.com/bloom-housing/bloom/issues/1850)) ([ef67997](https://github.com/bloom-housing/bloom/commit/ef67997a056c6f1f758d2fa67bf877d4a3d897ab)) +- Support PDF uploads or webpage links for building selection criteria ([#1893](https://github.com/bloom-housing/bloom/issues/1893)) ([8514b43](https://github.com/bloom-housing/bloom/commit/8514b43ba337d33cb877ff468bf780ff47fdc772)) + +### Performance Improvements + +- **applications and flagged sets:** Adds indexes and updates listWit… ([#2003](https://github.com/bloom-housing/bloom/issues/2003)) ([f9efb15](https://github.com/bloom-housing/bloom/commit/f9efb15b930865b517249d5dc525c11d68dc251d)) + +### Reverts + +- Revert "latest dev (#1999)" ([73a2789](https://github.com/bloom-housing/bloom/commit/73a2789d8f133f2d788e2399faa42b374d74ab15)), closes [#1999](https://github.com/bloom-housing/bloom/issues/1999) +- **backend:** Revert some listing filters ([#1984](https://github.com/bloom-housing/bloom/issues/1984)) ([14847e1](https://github.com/bloom-housing/bloom/commit/14847e1a797930f3e30bd945a2617dec2e3d679f)) + +### BREAKING CHANGES -**Note:** Version bump only for package @bloom-housing/listings-service +- POST/PUT /listings interface change +- Manually add totalFlagged until fixed diff --git a/backend/core/Dockerfile b/backend/core/Dockerfile new file mode 100644 index 0000000000..6d98d84c96 --- /dev/null +++ b/backend/core/Dockerfile @@ -0,0 +1,35 @@ +FROM node:14.17-alpine AS development + +WORKDIR /usr/src/app + +# Supports an optional yarn.lock +COPY package.json yarn*.lock tsconfig*.json ./ + +RUN yarn install + +COPY . . + +RUN yarn build + +FROM node:14.17-alpine AS production + +ARG NODE_ENV=production +ENV NODE_ENV=${NODE_ENV} + +WORKDIR /usr/src/app + +# Supports an optional yarn.lock +COPY package.json yarn*.lock tsconfig*.json ./ + +RUN yarn install --only=production + +COPY . . + +COPY --from=development /usr/src/app/dist ./dist + +EXPOSE ${PORT} + +# If you attempt to run this by itself, you'll need to pass in: +# PORT, DATABASE_URL +# Typically via: docker run --env-file=".env" -t my-app +CMD yarn start diff --git a/backend/core/README.md b/backend/core/README.md index dd79746674..2a1c19a2e1 100644 --- a/backend/core/README.md +++ b/backend/core/README.md @@ -10,20 +10,18 @@ OpenAPI (fka Swagger) documentation is automatically generated by the server at - Install Node.js 14.x `brew install node@14.` - Install Postgres 12 `brew install postgresql` -- Install Redis `brew install redis` - Copy the `.env.template` within `backend/core` to `.env` and edit variables appropriate to your local environment. Ensure sure the Database URL and Test Database URL match your Postgres configuration. - Install dependencies `yarn install` within `backend/core` -### Redis +### Using Docker containers -To start Redis: -`redis-server`. +If you don't want to install Postgres on your local machine and instead want to use Docker containers, run: -To launch Redis as background service and restart at login: -`brew services start redis`. +```shell script +docker-compose up postgres +``` -Test if Redis is working: -`redis-cli ping` +All `psql` and `yarn` commands related to databases will then be connecting to the database in the docker container `postgres`. ### Seeding the Database @@ -34,7 +32,7 @@ If you are just starting to work with the projects it's best to simply run: yarn && yarn db:reseed ``` -which will create the `bloom` DB for you, migrate it to the latest schema, and seed with appropriate dev data. If running the reseed command requires that you input a password for Postgres, set the following environment variables: `PGUSER` to postgres and `PGPASSWORD` to the default password you inputted for the postgres user during Postgres installation. +which will create the `bloom` DB for you, migrate it to the latest schema, and seed with appropriate dev data. If running the reseed command requires that you input a password for Postgres, set the following environment variables: `PGUSER` to postgres and `PGPASSWORD` to the default password you inputted for the postgres user during Postgres installation. If you get the `FATAL: database "" does not exist` error please run: `createdb ` first. Dropping the DB: @@ -93,18 +91,14 @@ translation pair: ### Environment Variables -| Name | Description | Default | Type | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | ----------------------------- | -| PORT | Port number the server will listen to for incoming connections | 3100 | number | -| NODE_ENV | Controls build optimization and enables some additional logging when set to `development` | development | "development" \| "production" | -| DATABASE_URL | Database connection | postgres://localhost/bloom | string | -| TEST_DATABASE_URL | Test database connection | postgres://localhost/bloom_test | string | string | -| REDIS_TLS_URL | Secure Redis connection | rediss://127.0.0.1:6379/ | string | -| REDIS_URL | TCP Redis connection string | redis://127.0.0.1:6379/0 | string | -| REDIS_USE_TLS | Flag controlling the use of TLS or unsecure transport for Redis | 0 | 0 \| 1 | -| THROTTLE_TTL | Rate limit TTL in seconds (currently used only for application submission endpoint) | 60 | number | -| THROTTLE_LIMIT | Max number of operations in given time window THROTTLE_TTL after which HTTP 429 Too Many Requests will be returned by the server | 2 | number | -| EMAIL_API_KEY | Sendgrid API key (see [sendgrid docs for creating API keys](https://sendgrid.com/docs/ui/account-and-settings/api-keys/#managing-api-keys) | Available internally | string | -| EMAIL_FROM_ADDRESS | Controls "from" field of all the emails sent by the process | 'Bloom Dev Housing Portal ' | string | -| APP_SECRET | Secret used for signing JWT tokens (generate it with e.g. `openssl rand -hex 48`) | Available internally | string | -| PARTNERS_PORTAL_URL | URL for partners site | http://localhost:3001 | string | +| Name | Description | Default | Type | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | ----------------------------- | +| PORT | Port number the server will listen to for incoming connections | 3100 | number | +| NODE_ENV | Controls build optimization and enables some additional logging when set to `development` | development | "development" \| "production" | +| DATABASE_URL | Database connection | postgres://localhost/bloom | string | +| TEST_DATABASE_URL | Test database connection | postgres://localhost/bloom_test | string | +| THROTTLE_TTL | Rate limit TTL in seconds (currently used only for application submission endpoint) | 60 | number | +| THROTTLE_LIMIT | Max number of operations in given time window THROTTLE_TTL after which HTTP 429 Too Many Requests will be returned by the server | 2 | number | +| EMAIL_API_KEY | Sendgrid API key (see [sendgrid docs for creating API keys](https://sendgrid.com/docs/ui/account-and-settings/api-keys/#managing-api-keys) | Available internally | string | +| APP_SECRET | Secret used for signing JWT tokens (generate it with e.g. `openssl rand -hex 48`) | Available internally | string | +| PARTNERS_PORTAL_URL | URL for partners site | http://localhost:3001 | string | diff --git a/backend/core/archer.ts b/backend/core/archer.ts index a6b76bd76c..9cff070f2d 100644 --- a/backend/core/archer.ts +++ b/backend/core/archer.ts @@ -1,7 +1,7 @@ import { AmiChart, - CSVFormattingType, Listing, + ListingMarketingTypeEnum, ListingReviewOrder, ListingStatus, UnitStatus, @@ -16,9 +16,6 @@ export const SanMateoHUD2019: AmiChart = { name: "SanMateoHUD2019", jurisdiction: { id: "jurisdictiion_id", - createdAt: new Date(), - updatedAt: new Date(), - name: "Alameda", }, items: [ { @@ -253,7 +250,6 @@ export const ArcherListing: Listing = { id: "Uvbk5qurpB2WI9V6WnNdH", applicationConfig: undefined, applicationOpenDate: new Date("2019-12-31T15:22:57.000-07:00"), - applicationDueTime: new Date(), applicationPickUpAddress: undefined, applicationPickUpAddressOfficeHours: "", applicationDropOffAddress: null, @@ -263,6 +259,7 @@ export const ArcherListing: Listing = { jurisdiction: { id: "id", name: "San Jose", + publicUrl: "", }, depositMax: "", disableUnitsAccordion: false, @@ -273,17 +270,6 @@ export const ArcherListing: Listing = { whatToExpect: "Applicant will be contacted. All info will be verified. Be prepared if chosen.", status: ListingStatus.active, postmarkedApplicationsReceivedByDate: new Date("2019-12-05"), - applicationAddress: { - id: "id", - createdAt: new Date(), - updatedAt: new Date(), - city: "San Jose", - street: "98 Archer Street", - zipCode: "95112", - state: "CA", - latitude: 37.36537, - longitude: -121.91071, - }, applicationDueDate: new Date("2019-12-31T15:22:57.000-07:00"), applicationMethods: [], applicationOrganization: "98 Archer Street", @@ -307,6 +293,7 @@ export const ArcherListing: Listing = { creditHistory: "Applications will be rated on a score system for housing. An applicant's score may be impacted by negative tenant peformance information provided to the credit reporting agency. All applicants are expected have a passing acore of 70 points out of 100 to be considered for housing. Applicants with no credit history will receive a maximum of 80 points to fairly outweigh positive and/or negative trades as would an applicant with established credit history. Refer to Tenant Selection Criteria or Qualification Criteria for details related to the qualification process. ", depositMin: "1140.0", + listingPrograms: [], programRules: "Applicants must adhere to minimum & maximum income limits. Tenant Selection Criteria applies.", // TODO confirm not used anywhere @@ -336,7 +323,6 @@ export const ArcherListing: Listing = { applicationFee: "30.0", criminalBackground: "A criminal background investigation will be obtained on each applicant. As criminal background checks are done county by county and will be ran for all counties in which the applicant lived, Applicants will be disqualified for tenancy if they have been convicted of a felony or misdemeanor. Refer to Tenant Selection Criteria or Qualification Criteria for details related to the qualification process. ", - CSVFormattingType: CSVFormattingType.basic, leasingAgentAddress: { id: "id", createdAt: new Date(), @@ -353,16 +339,16 @@ export const ArcherListing: Listing = { leasingAgentOfficeHours: "Monday, Tuesday & Friday, 9:00AM - 5:00PM", leasingAgentPhone: "(408) 217-8562", leasingAgentTitle: "", + listingPreferences: [], rentalAssistance: "Custom rental assistance", rentalHistory: "Two years of rental history will be verified with all applicable landlords. Household family members and/or personal friends are not acceptable landlord references. Two professional character references may be used in lieu of rental history for applicants with no prior rental history. An unlawful detainer report will be processed thourhg the U.D. Registry, Inc. Applicants will be disqualified if they have any evictions filing within the last 7 years. Refer to Tenant Selection Criteria or Qualification Criteria for details related to the qualification process.", - preferences: [], householdSizeMin: 2, householdSizeMax: 3, smokingPolicy: "Non-smoking building", unitsAvailable: 0, - unitsSummary: [], - unitsSummarized: undefined, + unitSummaries: undefined, + unitGroups: [], unitAmenities: "Dishwasher", developer: "Charities Housing ", yearBuilt: 2012, @@ -1535,6 +1521,7 @@ export const ArcherListing: Listing = { monthlyRentAsPercentOfIncome: null, }, ], + marketingType: ListingMarketingTypeEnum.marketing, // TODO confirm not used anywhere // totalUnits: 2, } diff --git a/backend/core/nest-cli.json b/backend/core/nest-cli.json index 8faf66e583..70bb77d7be 100644 --- a/backend/core/nest-cli.json +++ b/backend/core/nest-cli.json @@ -15,7 +15,7 @@ ], "assets": [ { "include": "auth/*.{conf,csv}", "outDir": "dist/src" }, - { "include": "views/**/*.hbs", "outDir": "dist/src" }, + { "include": "shared/views/**/*.hbs", "outDir": "dist/src" }, { "include": "locals/*.json", "outDir": "dist/src" } ] } diff --git a/backend/core/ormconfig.test.ts b/backend/core/ormconfig.test.ts index a459a433fc..bb2837064d 100644 --- a/backend/core/ormconfig.test.ts +++ b/backend/core/ormconfig.test.ts @@ -10,7 +10,7 @@ try { // Pass } -export = { +export default { type: "postgres", url: process.env.TEST_DATABASE_URL || "postgres://localhost:5432/bloom_test", synchronize: true, diff --git a/backend/core/ormconfig.ts b/backend/core/ormconfig.ts index 3381af9ad7..a6e811da9d 100644 --- a/backend/core/ormconfig.ts +++ b/backend/core/ormconfig.ts @@ -31,7 +31,7 @@ if (process.env.NODE_ENV === "production") { // Unfortunately, we need to use CommonJS/AMD style exports rather than ES6-style modules for this due to how // TypeORM expects the config to be available. -export = { +export default { type: "postgres", ...connectionInfo, synchronize: false, diff --git a/backend/core/package.json b/backend/core/package.json index 50d66219fb..0e0b51957b 100644 --- a/backend/core/package.json +++ b/backend/core/package.json @@ -1,9 +1,13 @@ { "name": "@bloom-housing/backend-core", - "version": "1.0.5", + "version": "4.4.0", "description": "Listings service reference implementation for the Bloom affordable housing system", - "author": "Marcin Jedras ", + "author": "Sean Albert ", "private": false, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "license": "Apache-2.0", "main": "index.js", "scripts": { @@ -12,88 +16,107 @@ "start": "node dist/src/main", "dev": "NODE_ENV=development nest start --watch --preserveWatchOutput", "debug": "nest start --debug --watch", - "db:drop": "psql -c 'DROP DATABASE IF EXISTS bloom;'", - "db:create": "psql -c 'CREATE DATABASE bloom;'", - "db:add-uuid-extension": "psql -d bloom -c 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";'", - "db:seed": "ts-node src/seed.ts", + "db:drop": "psql -c 'DROP DATABASE IF EXISTS bloom_detroit;'", + "db:create": "psql -c 'CREATE DATABASE bloom_detroit;'", + "db:add-uuid-extension": "psql -d bloom_detroit -c 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";'", + "db:seed": "ts-node src/seeder/seed.ts", "db:migration:run": "yarn typeorm migration:run", "db:migration:generate": "yarn typeorm migration:generate", "db:reseed": "yarn db:drop && yarn db:create && yarn db:add-uuid-extension && yarn db:migration:run && yarn db:seed", + "db:reseed:detroit": "yarn db:drop && yarn db:create && yarn db:add-uuid-extension && yarn db:migration:run && yarn db:seed:detroit", + "db:seed:detroit": "ts-node src/seeder/detroit-seed.ts", + "db:seed:detroit-arcgis": "yarn ts-node scripts/import-listings-from-detroit-arcgis.ts http://localhost:3100 test@example.com:abcdef https://services2.arcgis.com/qvkbeam7Wirps6zC/ArcGIS/rest/services/Affordable_Housing_Website_data_12_20/FeatureServer/0//query", "test": "jest --config ./jest.json --runInBand --detectOpenHandles", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./jest-e2e.json --runInBand --forceExit", - "test:e2e:local": "psql -c 'DROP DATABASE IF EXISTS bloom_test' && psql -c 'CREATE DATABASE bloom_test' && psql -d bloom_test -c 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";' && ts-node ./node_modules/.bin/typeorm --config ./ormconfig.test.ts migration:run && ts-node src/seed.ts --test && yarn run test:e2e", - "typeorm": "ts-node ./node_modules/.bin/typeorm", + "test:e2e:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --config ./jest-e2e.json --forceExit", + "test:e2e:local": "yarn run test:db:setup && yarn run test:e2e", + "typeorm": "ts-node -P ./tsconfig.json -O '{\"module\":\"commonjs\"}' ./node_modules/.bin/typeorm", + "typeorm:test": "ts-node -P ./tsconfig.json -O '{\"module\":\"commonjs\"}' ./node_modules/.bin/typeorm --config ./ormconfig.test.ts", + "test:db:setup": "psql -c 'DROP DATABASE IF EXISTS bloom_test' && psql -c 'CREATE DATABASE bloom_test' && psql -d bloom_test -c 'CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";' && yarn typeorm:test migration:run && ts-node src/seeder/seed.ts --test", "herokusetup": "node heroku.setup.js", "heroku-postbuild": "rimraf dist && nest build && yarn run db:migration:run", "generate:client": "ts-node scripts/generate-axios-client.ts && prettier -w types/src/backend-swagger.ts", - "migrator": "ts-node migrator.ts" + "migrator": "ts-node migrator.ts", + "import:unit-groups": "ts-node ./scripts/import-unit-groups.ts", + "import:listings": "ts-node scripts/import-listings-basic.ts" }, "dependencies": { "@anchan828/nest-sendgrid": "^0.3.25", "@google-cloud/translate": "^6.2.6", - "@nestjs/cli": "^7.5.1", - "@nestjs/common": "^7.6.18", - "@nestjs/config": "^0.5.0", - "@nestjs/core": "^7.6.18", - "@nestjs/jwt": "^7.1.0", - "@nestjs/passport": "^7.1.0", - "@nestjs/platform-express": "^7.6.18", - "@nestjs/swagger": "4.7.3", - "@nestjs/throttler": "^1.1.2", - "@nestjs/typeorm": "^7.1.0", + "@types/mapbox": "^1.6.42", + "@nestjs/cli": "^8.2.1", + "@nestjs/common": "^8.3.1", + "@nestjs/config": "^1.2.0", + "@nestjs/core": "^8.3.1", + "@nestjs/jwt": "^8.0.0", + "@nestjs/passport": "^8.2.1", + "@nestjs/platform-express": "^8.3.1", + "@nestjs/schedule": "^1.0.2", + "@nestjs/swagger": "5.2.0", + "@nestjs/throttler": "^2.0.0", + "@nestjs/typeorm": "~8.0.3", "@types/cache-manager": "^3.4.0", + "@types/cron": "^1.7.3", "async-retry": "^1.3.1", - "axios": "^0.21.0", + "axios": "0.21.2", "cache-manager": "^3.4.0", - "cache-manager-redis-store": "^2.0.0", - "casbin": "^5.1.6", + "casbin": "5.13.0", "class-transformer": "0.3.1", "class-validator": "^0.12.2", "cloudinary": "^1.25.2", + "csv-parser": "^3.0.0", + "csv-reader": "^1.0.8", + "dayjs": "^1.10.7", "dotenv": "^8.2.0", "express": "^4.17.1", + "fast-xml-parser": "^4.0.0-beta.2", "handlebars": "^4.7.6", - "ioredis": "^4.24.4", "joi": "^17.3.0", - "jsonpath": "^1.0.2", "jwt-simple": "^0.5.6", - "moment": "^2.29.1", + "jszip": "^3.10.1", + "lodash": "^4.17.21", + "mapbox": "^1.0.0-beta10", "nanoid": "^3.1.12", - "nestjs-throttler-storage-redis": "^0.1.11", - "nestjs-typeorm-paginate": "^2.2.1", + "nestjs-twilio": "^2.1.0", + "nestjs-typeorm-paginate": "^3.1.3", "newrelic": "7.5.1", "node-polyglot": "^2.4.0", "passport": "^0.4.1", + "passport-custom": "^1.1.1", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "pg": "^8.4.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", - "rxjs": "^6.6.3", + "rxjs": "^7.5.4", "swagger-ui-express": "^4.1.4", - "ts-node": "^9.0.0", - "typeorm": "0.2.34", + "ts-node": "10.8.0", + "twilio": "^3.71.3", + "typeorm": "0.2.41", "typeorm-naming-strategies": "^1.1.0", - "typescript": "^3.9.7" + "xlsx": "^0.17.4", + "typescript": "4.7.4", + "uuid": "^8.3.2" }, "devDependencies": { - "@babel/core": "^7.11.6", - "@babel/plugin-proposal-decorators": "^7.10.5", - "@nestjs/schematics": "^7.1.2", - "@nestjs/testing": "^7.4.4", + "@babel/core": "^7.21.3", + "@babel/plugin-proposal-decorators": "^7.21.0", + "@nestjs/schematics": "^8.0.7", + "@nestjs/testing": "^8.3.1", "@types/axios": "^0.14.0", + "@types/cron": "^1.7.3", "@types/express": "^4.17.8", "@types/node": "^12.12.67", "@types/passport-jwt": "^3.0.3", "@types/passport-local": "^1.0.33", "@types/supertest": "^2.0.10", - "axios": "^0.21.0", "dotenv": "^8.2.0", "fishery": "^0.3.0", "jest": "^26.5.3", + "mapbox": "^1.0.0-beta10", "supertest": "^4.0.2", "swagger-axios-codegen": "0.11.16", "ts-jest": "26.4.1", @@ -116,8 +139,7 @@ "testEnvironment": "node" }, "engines": { - "node": "14", + "node": "18", "yarn": "^1.22" - }, - "gitHead": "5f7694e3f2baff2a1a7a22e0676e487dda781a3e" + } } diff --git a/backend/core/scripts/detroit-helpers.ts b/backend/core/scripts/detroit-helpers.ts new file mode 100644 index 0000000000..34343fd0d1 --- /dev/null +++ b/backend/core/scripts/detroit-helpers.ts @@ -0,0 +1,12 @@ +import { UnitStatus } from "../src/units/types/unit-status-enum" + +export function createUnitsArray(type: string, number: number) { + const units = [] + for (let unit_index = 0; unit_index < number; unit_index++) { + units.push({ + unitType: type, + status: UnitStatus.unknown, + }) + } + return units +} diff --git a/backend/core/scripts/generate-ami-chart.sh b/backend/core/scripts/generate-ami-chart.sh new file mode 100755 index 0000000000..9ffc3adb0a --- /dev/null +++ b/backend/core/scripts/generate-ami-chart.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +if [ -z "$1" ]; then + cat << EOF +Usage: generate-ami-chart.sh path/to/FILE + +WARNING: overwrites the file path/to/FILE.ts + +This script takes a formatted text file and writes a .ts file containing the JSON representation +of the AMI chart data. It is expecting 9 columns, the first one being the AMI percentage and then +8 columns representing the income for a household with 1..8 people corresponding to that AMI. Ex: +20% 10,000 11,000 12,000 13,000 14,000 15,000 16,000 17,000 +30% 15,000 16,000 17,000 18,000 19,000 20,000 21,000 22,000 + +This format is based on the PDF format for published MSHDA charts. Noutput ote: there must be a newline +at the end of the file or the last row will not be read in. +EOF + exit +fi + +# Get the file name and the path separately. +DIRECTORY=$(dirname "$1") +FILE=$(basename "$1") +FILENAME=${FILE%.*} +OUTPUT_FILE="$DIRECTORY/$FILENAME.ts" + + +echo "Generating $OUTPUT_FILE" + +cat << EOF > $OUTPUT_FILE +import { AmiChartCreateDto } from "../../src/ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM $FILE. +export const $FILENAME: Omit = { + name: "$FILENAME", + items: [ +EOF + +# For each line, generate a set of JSON values +sed -e "s/%//g" -e "s/,//g" $1 | + while read -ra INCOME; do + # AMI is the first column + AMI=${INCOME[0]} + for i in $(seq 8); do + # print this AMI table value to the OUTPUT + cat << EOF >> $OUTPUT_FILE + { + percentOfAmi: $AMI, + householdSize: $i, + income: ${INCOME[$i]}, + }, +EOF + done + done + +# Finish the JSON +cat << EOF >> $OUTPUT_FILE + ], +} +EOF diff --git a/backend/core/scripts/import-amc-waitlist-report.ts b/backend/core/scripts/import-amc-waitlist-report.ts new file mode 100644 index 0000000000..19ed53e395 --- /dev/null +++ b/backend/core/scripts/import-amc-waitlist-report.ts @@ -0,0 +1,103 @@ +import * as client from "../types/src/backend-swagger" +import yargs from "yargs" +import axios from "axios" +import { serviceOptions } from "../types/src/backend-swagger" +import { readFile, WorkBook } from "xlsx" + +// To view usage: +// $ yarn ts-node scripts/import-amc-waitlist-report.ts --help + +const args = yargs.options({ + email: { + type: "string", + demandOption: true, + describe: + "The email of the user updating the listings. Must have admin or listing agent permissions.", + }, + password: { + type: "string", + demandOption: true, + describe: "The password of the user updating the listings.", + }, + backendUrl: { type: "string", demandOption: true, describe: "The URL of the backend service." }, + reportFilePath: { + type: "string", + demandOption: true, + describe: "The file path of the AMC waitlist report (in XLSX format).", + }, + listingId: { + type: "string", + demandOption: true, + describe: "The database ID of the listing being updated.", + }, +}).argv + +async function main(): Promise { + const workBook: WorkBook = readFile(args.reportFilePath) + + // First, find the waitlist info we're looking for. + let waitlistSize = -1 + for (const name of workBook.SheetNames) { + const sheet = workBook.Sheets[name] + for (const key in sheet) { + if (Object.prototype.hasOwnProperty.call(sheet[key], "v")) { + try { + const match = sheet[key].v.match(/Total on Waitlist:\s*(\d*)/) + if (match?.length > 0) { + waitlistSize = Number.parseInt(match[1]) + break + } + } catch (err) { + continue + } + } + } + if (waitlistSize >= 0) { + break + } + } + + if (waitlistSize < 0) { + console.log("Couldn't find waitlist size. Stopping.") + return + } + + console.log(`Found waitlist size: ${waitlistSize}`) + + serviceOptions.axios = axios.create({ + baseURL: args.backendUrl, + timeout: 10000, + }) + + const { accessToken } = await new client.AuthService().login({ + body: { + email: args.email, + password: args.password, + }, + }) + + // Update the axios config so future requests include the access token in the header. + serviceOptions.axios = axios.create({ + baseURL: args.backendUrl, + timeout: 10000, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + const listingsService = new client.ListingsService() + const listing = await listingsService.retrieve({ id: args.listingId }) + if (!listing.waitlistMaxSize) { + // If there's no specified maximum waitlist size, we assume it's "unbounded". + // Max int in SQL is 2 ** 31 - 1, so we use that to represent "unbounded". + listing.waitlistMaxSize = 2 ** 31 - 1 + } + listing.waitlistCurrentSize = waitlistSize + console.log( + `Updating "${listing.name}" listing with new waitlist size ${listing.waitlistCurrentSize} (out of ${listing.waitlistMaxSize})` + ) + // TODO: Update with new unit groups model + // await listingsService.update({ id: args.listingId, body: listing }) +} + +void main() diff --git a/backend/core/scripts/import-helpers.ts b/backend/core/scripts/import-helpers.ts index ffe0f89b68..ca38b9e616 100644 --- a/backend/core/scripts/import-helpers.ts +++ b/backend/core/scripts/import-helpers.ts @@ -4,7 +4,7 @@ import { ListingCreate, ListingStatus, serviceOptions, - UnitsSummaryCreate, + UnitGroupCreate, UnitCreate, } from "../types/src/backend-swagger" @@ -31,7 +31,7 @@ export interface ListingImport reservedCommunityTypeName?: string jurisdictionName?: string } -export interface UnitsSummaryImport extends Omit { +export interface UnitsSummaryImport extends Omit { unitType?: string } export interface UnitImport extends Omit { @@ -72,7 +72,10 @@ async function uploadListing(listing: ListingCreate) { async function uploadReservedCommunityType(name: string, jurisdictions: client.Jurisdiction[]) { try { return await reservedCommunityTypesService.create({ - body: { name, jurisdiction: jurisdictions[0] }, + body: { + name, + jurisdiction: jurisdictions.find((jurisdiction) => jurisdiction.name == "Detroit"), + }, }) } catch (e) { console.log(e.response) @@ -162,6 +165,25 @@ export async function importListing( const relationsKeys = [] listing = reformatListing(listing, relationsKeys) + // If a managementWebsite is provided, make sure it is a well-formed URL. + if (listing.managementWebsite) { + if (!listing.managementWebsite.startsWith("http")) { + listing.managementWebsite = "http://" + listing.managementWebsite + } + + // This next line will throw an error if managementWebsite is a malformed URL. + try { + new URL(listing.managementWebsite) + } catch (e) { + console.log( + `Error: ${listing.name} has a malformed managementWebsite (${listing.managementWebsite});` + + ` this website will be discarded and the listing will be uploaded without it.` + ) + console.log(e) + listing.managementWebsite = null + } + } + // Upload new entities. listing = await uploadEntity("preferences", preferencesService, listing) listing = await uploadEntity("applicationMethods", applicationMethodsService, listing) @@ -186,13 +208,13 @@ export async function importListing( const unitType = findByName(unitTypes, unit.unitType) unitsCreate.push({ ...unit, priorityType: priorityType, unitType: unitType }) }) - const unitsSummaryCreate: UnitsSummaryCreate[] = [] - if (listing.unitsSummary) { + const unitGroupsCreate: UnitGroupCreate[] = [] + /* if (listing.unitsSummary) { listing.unitsSummary.forEach((summary) => { - const unitType = findByName(unitTypes, summary.unitType) - unitsSummaryCreate.push({ ...summary, unitType: unitType }) + // const unitType = findByName(unitTypes, summary.unitType) + // unitGroupsCreate.push({ ...summary }) }) - } + } */ let jurisdiction: client.Jurisdiction = null if (listing.jurisdictionName) { @@ -204,7 +226,7 @@ export async function importListing( // types. const listingCreate: ListingCreate = { ...listing, - unitsSummary: unitsSummaryCreate, + unitGroups: unitGroupsCreate, units: unitsCreate, reservedCommunityType: reservedCommunityType, jurisdiction: jurisdiction, diff --git a/backend/core/scripts/import-listing-from-json-file.ts b/backend/core/scripts/import-listing-from-json-file.ts index 5383c3fca7..cef6199aa9 100644 --- a/backend/core/scripts/import-listing-from-json-file.ts +++ b/backend/core/scripts/import-listing-from-json-file.ts @@ -3,7 +3,7 @@ import fs from "fs" import { Listing } from "../types/src/backend-swagger" // Example usage (from within /backend/core): -// $ yarn ts-node scripts/import-listing-from-json-file.ts http://localhost:3100 admin@example.com:abcdef scripts/minimal-listing.json +// $ yarn ts-node scripts/import-listing-from-json-file.ts http://localhost:3100 admin@example.com:abcdef scripts/script-data/minimal-listing.json async function main() { if (process.argv.length < 5) { diff --git a/backend/core/scripts/import-listings-basic.ts b/backend/core/scripts/import-listings-basic.ts new file mode 100644 index 0000000000..4e5a08835b --- /dev/null +++ b/backend/core/scripts/import-listings-basic.ts @@ -0,0 +1,269 @@ +import * as fs from "fs" +import CsvReadableStream from "csv-reader" +import { Connection, DeepPartial } from "typeorm" +import { Listing } from "../src/listings/entities/listing.entity" +import { Jurisdiction } from "../src/jurisdictions/entities/jurisdiction.entity" +import dbOptions from "../ormconfig" +import { Program } from "../src/program/entities/program.entity" +import { AddressCreateDto } from "../src/shared/dto/address.dto" + +/* eslint-disable-next-line @typescript-eslint/no-var-requires */ +const getStream = require("get-stream") + +/* eslint-disable-next-line @typescript-eslint/no-var-requires */ +const MapboxClient = require("mapbox") + +if (!process.env["MAPBOX_TOKEN"]) { + throw new Error("environment variable MAPBOX_TOKEN is undefined") +} +const args = process.argv.slice(2) + +const client = new MapboxClient(process.env["MAPBOX_TOKEN"]) + +const filePath = args[0] +if (typeof filePath !== "string" && !fs.existsSync(filePath)) { + throw new Error(`usage: ts-node import-unit-groups.ts csv-file-path`) +} + +export class HeaderConstants { + public static readonly TemporaryId: string = "ID" + public static readonly Name: string = "Building Name" + public static readonly Developer: string = "Developer" + public static readonly BuildingAddressStreet: string = "Building Street Address" + public static readonly BuildingAddressCity: string = "City" + public static readonly BuildingAddressState: string = "State" + public static readonly BuildingAddressZipCode: string = "Zip Code" + public static readonly Neighborhood: string = "Neighborhood 2" + public static readonly YearBuilt: string = "Year Built" + public static readonly CommunityTypePrograms: string = "Community Type" + public static readonly LeasingAgentName: string = "Leasing Agent Name (Property Mgmt Company)" + public static readonly LeasingAgentEmail: string = "Leasing Agent Email" + public static readonly LeasingAgentPhone: string = "Leasing Agent Phone" + public static readonly ManagementWebsite: string = "Management Website" + public static readonly LeasingAgentAddress: string = "Leasing Agent Address" + public static readonly ApplicationFee: string = "Application Fee" + public static readonly DepositMin: string = "Deposit Min" + public static readonly DepositMax: string = "Deposit Max" + public static readonly DepositHelperText: string = "Deposit HelperText" + public static readonly CostsNotIncluded: string = "Costs not included" + public static readonly PropertyAmenities: string = "Property Amenities" + public static readonly HeatingInUnit: string = "Heating in Unit" + public static readonly AcInUnit: string = "AC in Unit" + public static readonly LaundryInBuilding: string = "Laundry in Building" + public static readonly ParkingOnSiteElevator: string = "Parking On Site Elevator" + public static readonly ServiceAnimalsAllowed: string = "Service Animals Allowed" + public static readonly RollInShower: string = "Roll in Shower" + public static readonly WheelchairRamp: string = "Wheelchair Ramp" + public static readonly AccessibleParking: string = "Accessible Parking" + public static readonly InUnitWasherDryer: string = "In Unit Washer Dryer" + public static readonly BarrierFreeEntrance: string = "Barrier Free Entrance" + public static readonly GrabBars: string = "Grab Bars" + public static readonly Hearing: string = "Hearing" + public static readonly Visual: string = "Visual" + public static readonly Mobility: string = "Mobility" + public static readonly AdditionalAccessibility: string = "Additional Accessibility" + public static readonly RentalAssistance: string = "RentalAssistance" + public static readonly SmokingPolicy: string = "Smoking Policy" + public static readonly PetPolicy: string = "Pet Policy" + public static readonly RequiredDocuments: string = "Required Documents" + public static readonly ImportantProgramRules: string = "Important Program Rules" + public static readonly SpecialNotes: string = "Special Notes" +} + +async function fetchDetroitJurisdiction(connection: Connection): Promise { + const jurisdictionsRepository = connection.getRepository(Jurisdiction) + return await jurisdictionsRepository.findOneOrFail({ + where: { + name: "Detroit", + }, + }) +} + +async function fetchProgramsOrFail( + connection: Connection, + programsString: string +): Promise { + if (!programsString) { + return [] + } + + const programsRepository = connection.getRepository(Program) + const programTitles = programsString.split(",").map((p) => p.trim()) + + return Promise.all( + programTitles.map((programTitle) => { + return programsRepository.findOneOrFail({ where: { title: programTitle } }) + }) + ) +} + +function destructureYearBuilt(yearBuilt: string): number { + if (!yearBuilt) { + return null + } + + if (typeof yearBuilt === "number") { + return yearBuilt + } + + if (yearBuilt.includes("/")) { + const [_year1, year2] = yearBuilt.split("/") + return Number.parseInt(year2) + } + + return Number.parseInt(yearBuilt) +} + +async function getLatitudeAndLongitude( + address: string +): Promise<{ latitude: number; longitude: number }> { + const res = await client.geocodeForward(address) + let latitude + let longitude + if (res.entity?.features?.length) { + latitude = res.entity.features[0].center[0] + longitude = res.entity.features[0].center[1] + } + return { latitude, longitude } +} + +async function destructureAddressString(addressString: string): Promise { + if (!addressString) { + return null + } + + const tokens = addressString.split(",").map((addressString) => addressString.trim()) + + const { latitude, longitude } = await getLatitudeAndLongitude(addressString) + + if (tokens.length === 1) { + return { + street: tokens[0], + city: undefined, + state: undefined, + zipCode: undefined, + latitude, + longitude, + } + } + + const [state, zipCode] = tokens[2].split(" ") + + return { + street: tokens[0], + city: tokens[1], + state, + zipCode, + latitude, + longitude, + } +} + +async function main() { + const connection = new Connection(dbOptions) + await connection.connect() + + const detroitJurisdiction = await fetchDetroitJurisdiction(connection) + + const listingsRepository = connection.getRepository(Listing) + + let rowsCount = 0 + let failedRowsCounts = 0 + const failedRowsIDs = [] + + const inputRows = await getStream.array( + fs.createReadStream(filePath, "utf8").pipe( + new CsvReadableStream({ + parseNumbers: true, + parseBooleans: true, + trim: true, + asObject: true, + }) + ) + ) + + for (const row of inputRows) { + rowsCount += 1 + try { + console.info(`Importing row ${row[HeaderConstants.TemporaryId]}`) + const programsString = row[HeaderConstants.CommunityTypePrograms] + const communityTypePrograms = await fetchProgramsOrFail(connection, programsString) + const newListing: DeepPartial = { + temporaryListingId: row[HeaderConstants.TemporaryId], + assets: [], + name: row[HeaderConstants.Name], + displayWaitlistSize: false, + property: { + developer: row[HeaderConstants.Developer], + accessibility: row[HeaderConstants.AdditionalAccessibility], + smokingPolicy: row[HeaderConstants.SmokingPolicy], + petPolicy: row[HeaderConstants.PetPolicy], + amenities: row[HeaderConstants.PropertyAmenities], + buildingAddress: { + street: row[HeaderConstants.BuildingAddressStreet], + city: row[HeaderConstants.BuildingAddressCity], + state: row[HeaderConstants.BuildingAddressState], + zipCode: row[HeaderConstants.BuildingAddressZipCode], + ...(await getLatitudeAndLongitude( + [ + row[HeaderConstants.BuildingAddressStreet], + row[HeaderConstants.BuildingAddressCity], + row[HeaderConstants.BuildingAddressState], + row[HeaderConstants.BuildingAddressZipCode], + ].join(" ") + )), + }, + neighborhood: row[HeaderConstants.Neighborhood], + yearBuilt: destructureYearBuilt(row[HeaderConstants.YearBuilt]), + }, + jurisdiction: detroitJurisdiction, + listingPrograms: communityTypePrograms.map((program) => { + return { + program: program, + ordinal: null, + } + }), + leasingAgentName: row[HeaderConstants.LeasingAgentName], + leasingAgentEmail: row[HeaderConstants.LeasingAgentEmail], + leasingAgentPhone: row[HeaderConstants.LeasingAgentPhone], + managementWebsite: row[HeaderConstants.ManagementWebsite], + leasingAgentAddress: await destructureAddressString( + row[HeaderConstants.LeasingAgentAddress] + ), + applicationFee: row[HeaderConstants.ApplicationFee], + depositMin: row[HeaderConstants.DepositMin], + depositMax: row[HeaderConstants.DepositMax], + depositHelperText: row[HeaderConstants.DepositHelperText], + costsNotIncluded: row[HeaderConstants.CostsNotIncluded], + features: { + heatingInUnit: row[HeaderConstants.HeatingInUnit] === "Yes", + acInUnit: row[HeaderConstants.AcInUnit] === "Yes", + laundryInBuilding: row[HeaderConstants.LaundryInBuilding] === "Yes", + parkingOnSite: row[HeaderConstants.ParkingOnSiteElevator] === "Yes", + serviceAnimalsAllowed: row[HeaderConstants.ServiceAnimalsAllowed] === "Yes", + rollInShower: row[HeaderConstants.RollInShower] === "Yes", + wheelchairRamp: row[HeaderConstants.WheelchairRamp] === "Yes", + accessibleParking: row[HeaderConstants.AccessibleParking] === "Yes", + inUnitWasherDryer: row[HeaderConstants.InUnitWasherDryer] === "Yes", + barrierFreeEntrance: row[HeaderConstants.BarrierFreeEntrance] === "Yes", + grabBars: row[HeaderConstants.GrabBars] === "Yes", + }, + requiredDocuments: row[HeaderConstants.RequiredDocuments], + programRules: row[HeaderConstants.ImportantProgramRules], + specialNotes: row[HeaderConstants.SpecialNotes], + rentalAssistance: row[HeaderConstants.RentalAssistance], + } + await listingsRepository.save(newListing) + } catch (e) { + console.error(`skipping row: ${row[HeaderConstants.TemporaryId]}`) + console.error(e) + failedRowsCounts += 1 + failedRowsIDs.push(row[HeaderConstants.TemporaryId]) + } + } + console.log(`${failedRowsCounts}/${rowsCount} rows failed`) + console.log("IDs:") + console.log(failedRowsIDs) +} + +void main() diff --git a/backend/core/scripts/import-listings-from-csv.ts b/backend/core/scripts/import-listings-from-csv.ts new file mode 100644 index 0000000000..e05c56f144 --- /dev/null +++ b/backend/core/scripts/import-listings-from-csv.ts @@ -0,0 +1,235 @@ +import csv from "csv-parser" +import fs from "fs" +import axios from "axios" +import { importListing, ListingImport, UnitsSummaryImport } from "./import-helpers" +import * as client from "../types/src/backend-swagger" +import { + AddressCreate, + ListingMarketingTypeEnum, + ListingStatus, + serviceOptions, +} from "../types/src/backend-swagger" +import { ListingReviewOrder } from "../src/listings/types/listing-review-order-enum" + +// This script reads in listing data from a CSV file and sends requests to the backend to create +// the corresponding Listings. A few notes: +// - This script does not delete or modify any existing listings. +// - If one listing fails to be uploaded, the script will still attempt all the rest. At the end, +// it will report how many failed (with error messages) and how many succeeded. +// - Each line in the CSV file is assumed to correspond to a distinct listing. +// - This script assumes particular heading names in the input CSV file (see listingFields["..."] +// below). + +// Sample usage: +// $ yarn ts-node scripts/import-listings-from-csv.ts http://localhost:3100 admin@example.com:abcdef path/to/file.csv + +async function main() { + if (process.argv.length < 5) { + console.log( + "usage: yarn ts-node scripts/import-listings-from-csv.ts import_api_url email:password csv_file_path" + ) + process.exit(1) + } + + const [importApiUrl, userAndPassword, csvFilePath] = process.argv.slice(2) + const [email, password] = userAndPassword.split(":") + + serviceOptions.axios = axios.create({ + baseURL: importApiUrl, + timeout: 10000, + }) + + const hrdIds: Set = new Set( + (await new client.ListingsService().list({ limit: "all" })).items.map( + (listing) => listing.hrdId + ) + ) + console.log(`Got ${hrdIds.size} HRD ids.`) + + // Regex used to parse the AMI from an AMI column name + const amiColumnRegex = /(\d+) Pct AMI/ // e.g. 30 Pct AMI + + // Read raw CSV data into memory. + // Note: createReadStream creates ReadStream's whose on("data", ...) methods are called + // asynchronously. To ensure that all CSV lines are read in before we start trying to upload + // listings from it, we wrap this step in a Promise. + const rawListingFields = [] + const promise = new Promise((resolve, reject) => { + fs.createReadStream(csvFilePath) + .pipe(csv()) + .on("data", (listingFields) => { + const listingName: string = listingFields["Project Name"].trim() + // Exclude listings that are not "regulated" affordable housing + const affordabilityStatus: string = listingFields["Affordability status"] + if (affordabilityStatus?.toLowerCase() !== "regulated") { + console.log( + `Skipping listing because it is not *regulated* affordable housing: ${listingName}` + ) + return + } + + // Exclude listings that are already present in the db, based on HRD id. + if (hrdIds.has(listingFields["HRDID"])) { + console.log(`Skipping ${listingName} because it's already in the database.`) + return + } + + // Exclude listings that are not at the stage of housing people. + // Some listings are in the "development pipeline" and should not yet be shown to + // housing seekers. The "Development Pipeline Bucket" below is a code that is meaningful + // within HRD. + const projectType: string = listingFields["Project Type"] + const developmentPipelineBucket: number = parseInt( + listingFields["Development Pipeline Bucket"] + ) + if (projectType?.toLowerCase() !== "existing occupied" && developmentPipelineBucket < 3) { + console.log( + `Skipping listing because it is not far enough along in the development pipeline: ${listingName}` + ) + return + } + + rawListingFields.push(listingFields) + }) + .on("end", resolve) + .on("error", reject) + }) + await promise + + console.log(`CSV file successfully read in; ${rawListingFields.length} listings to upload`) + + const uploadFailureMessages = [] + let numListingsSuccessfullyUploaded = 0 + for (const listingFields of rawListingFields) { + const address: AddressCreate = { + street: listingFields["Project Address"], + zipCode: listingFields["Zip Code"], + city: "Detroit", + state: "MI", + longitude: listingFields["Longitude"], + latitude: listingFields["Latitude"], + } + + // Add data about unitsSummaries + // const unitsSummaries: UnitsSummaryImport[] = [] + // TODO: Update with new unit groups model + // if (listingFields["Number 0BR"]) { + // unitsSummaries.push({ + // unitType: "studio", + // totalCount: Number(listingFields["Number 0BR"]), + // }) + // } + // if (listingFields["Number 1BR"]) { + // unitsSummaries.push({ + // unitType: "oneBdrm", + // totalCount: Number(listingFields["Number 1BR"]), + // }) + // } + // if (listingFields["Number 2BR"]) { + // unitsSummaries.push({ + // unitType: "twoBdrm", + // totalCount: Number(listingFields["Number 2BR"]), + // }) + // } + // if (listingFields["Number 3BR"]) { + // unitsSummaries.push({ + // unitType: "threeBdrm", + // totalCount: Number(listingFields["Number 3BR"]), + // }) + // } + // // Lump 4BR and 5BR together as "fourBdrm" + // const numberFourBdrm = listingFields["Number 4BR"] ? parseInt(listingFields["Number 4BR"]) : 0 + // const numberFiveBdrm = listingFields["Number 5BR"] ? parseInt(listingFields["Number 5BR"]) : 0 + // if (numberFourBdrm + numberFiveBdrm > 0) { + // unitsSummaries.push({ + // unitType: "fourBdrm", + // totalCount: numberFourBdrm + numberFiveBdrm, + // }) + // } + + // Listing affordability details + let amiPercentageMin, amiPercentageMax + const listingFieldsArray = Object.entries(listingFields) + const colStart = listingFieldsArray.findIndex((element) => element[0] === "15 Pct AMI") + const colEnd = listingFieldsArray.findIndex((element) => element[0] === "80 Pct AMI") + for (const [key, value] of listingFieldsArray.slice(colStart, colEnd + 1)) { + if (!value) continue + if (!amiPercentageMin) { + amiPercentageMin = parseInt(amiColumnRegex.exec(key)[1]) + } + amiPercentageMax = parseInt(amiColumnRegex.exec(key)[1]) + } + + let leasingAgentEmail = null + if (listingFields["Manager Email"]) { + leasingAgentEmail = listingFields["Manager Email"] + } + + let reservedCommunityTypeName: string = null + const hudClientGroup = listingFields["HUD Client group"].toLowerCase() + if (["wholly physically handicapped", "wholly physically disabled"].includes(hudClientGroup)) { + reservedCommunityTypeName = "specialNeeds" + } else if (hudClientGroup === "wholly elderly housekeeping") { + reservedCommunityTypeName = "senior62" + } + + const listing: ListingImport = { + name: listingFields["Project Name"], + hrdId: listingFields["HRDID"], + buildingAddress: address, + region: listingFields["Region"], + ownerCompany: listingFields["Owner Company"], + managementCompany: listingFields["Management Company"], + leasingAgentName: listingFields["Manager Contact"], + leasingAgentPhone: listingFields["Manager Phone"], + managementWebsite: listingFields["Management Website"], + leasingAgentEmail: leasingAgentEmail, + phoneNumber: listingFields["Property Phone"], + amiPercentageMin: amiPercentageMin, + amiPercentageMax: amiPercentageMax, + status: ListingStatus.active, + // unitsSummary: unitsSummaries, + jurisdictionName: "Detroit", + reservedCommunityTypeName: reservedCommunityTypeName, + neighborhood: listingFields["Neighborhood"], + + // The following fields are only set because they are required + units: [], + applicationMethods: [], + applicationDropOffAddress: null, + applicationMailingAddress: null, + events: [], + assets: [], + displayWaitlistSize: false, + depositMin: "", + depositMax: "", + developer: "", + digitalApplication: false, + images: [], + isWaitlistOpen: true, + paperApplication: false, + referralOpportunity: false, + rentalAssistance: "", + reviewOrderType: ListingReviewOrder.firstComeFirstServe, + listingPreferences: [], + marketingType: ListingMarketingTypeEnum.marketing, + } + + try { + const newListing = await importListing(importApiUrl, email, password, listing) + console.log(`New listing uploaded successfully: ${newListing.name}`) + numListingsSuccessfullyUploaded++ + } catch (e) { + console.log(e) + uploadFailureMessages.push(`Upload failed for ${listing.name}: ${e}`) + } + } + + console.log(`\nNumber of listings successfully uploaded: ${numListingsSuccessfullyUploaded}`) + console.log(`Number of failed listing uploads: ${uploadFailureMessages.length}\n`) + for (const failureMessage of uploadFailureMessages) { + console.log(failureMessage) + } +} + +void main() diff --git a/backend/core/scripts/import-realpages-availability-report.ts b/backend/core/scripts/import-realpages-availability-report.ts new file mode 100644 index 0000000000..f08ac2f003 --- /dev/null +++ b/backend/core/scripts/import-realpages-availability-report.ts @@ -0,0 +1,165 @@ +import * as client from "../types/src/backend-swagger" +import fs from "fs" +import yargs from "yargs" +import axios from "axios" +import { XMLParser } from "fast-xml-parser" +import { serviceOptions } from "../types/src/backend-swagger" + +// To view usage: +// $ yarn ts-node scripts/import-realpages-availability-report.ts --help + +const args = yargs.options({ + email: { + type: "string", + demandOption: true, + describe: + "The email of the user updating the listings. Must have admin or listing agent permissions.", + }, + password: { + type: "string", + demandOption: true, + describe: "The password of the user updating the listings.", + }, + backendUrl: { type: "string", demandOption: true, describe: "The URL of the backend service." }, + reportFilePath: { + type: "string", + demandOption: true, + describe: "The file path of the Realpages availability report (in XML format).", + }, + listingId: { + type: "string", + demandOption: true, + describe: "The database ID of the listing being updated.", + }, + mapping: { + type: "array", + describe: + 'The mapping from floorplan code to unit type. E.g. "1.5 B:oneBdrm". Must be repeated for every floorplan code value.', + }, +}).argv + +function attributeFetcher(unitXmlData, attribute: string): string { + return unitXmlData[`@_${attribute}`] +} + +function tagContentsFetcher(unitXmlData, tag: string): string { + return unitXmlData[tag] +} + +async function main(): Promise { + const fpCodeUnitTypeMap: Record = args.mapping.reduce((a, m: string) => { + return { ...a, [m.split(":")[0]]: m.split(":")[1] } + }, {}) + + const reportUnitData: any[] = new XMLParser({ ignoreAttributes: false }).parse( + fs.readFileSync(args.reportFilePath, "utf-8") + ).root.Response.FileContents.root.LeaseVariance.Row + + if (reportUnitData.length == 0) { + console.log("No unit data to process. Aborting.") + process.exit(0) + } + + // Some reports contain the data in attributes, and others contain it in sub-tags. + let infoFetcher: (unit: any, key: string) => string + if (reportUnitData.every((u) => "@_fpCode" in u && "@_UnitAvailableBit" in u)) { + infoFetcher = attributeFetcher + } else if (reportUnitData.every((u) => "fpCode" in u && "UnitAvailableBit" in u)) { + infoFetcher = tagContentsFetcher + } else { + throw "Missing fpCode or UnitAvailableBit information from unit data." + } + + // Make sure there's a mapping for every fpCode in the XML before proceeding. + const reportFpCodes = new Set( + reportUnitData + .map((u) => infoFetcher(u, "fpCode")) + .filter((fpCode: string) => fpCode.length > 0) + ) + const mappedFpCodes = new Set(Object.keys(fpCodeUnitTypeMap)) + for (const fpCode of reportFpCodes) { + if (!mappedFpCodes.has(fpCode)) { + throw `Missing fpCode "${fpCode}" from mapping.` + } + } + + serviceOptions.axios = axios.create({ + baseURL: args.backendUrl, + timeout: 10000, + }) + + const { accessToken } = await new client.AuthService().login({ + body: { + email: args.email, + password: args.password, + }, + }) + + // Update the axios config so future requests include the access token in the header. + serviceOptions.axios = axios.create({ + baseURL: args.backendUrl, + timeout: 10000, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + const unitTypes = await new client.UnitTypesService().list() + + // Make sure there's a unit type for every mapped unit type before proceeding. + const unitTypeNames = new Set(unitTypes.map((u) => u.name)) + const mappedUnitTypeNames = new Set(Object.values(fpCodeUnitTypeMap)) + for (const mappedUnitTypeName of mappedUnitTypeNames) { + if (!unitTypeNames.has(mappedUnitTypeName)) { + throw `Unknown unit type "${mappedUnitTypeName}"` + } + } + + // const listingsService = new client.ListingsService() + // const listing = await listingsService.retrieve({ id: args.listingId }) + // TODO: Update with new unit groups model + // const listingUnitTypeNameSummaryMap = listing.unitsSummary.reduce((a, s) => { + // return { ...a, [s.unitType.name]: s } + // }, {}) + // const listingUnitTypeNameSummaryMap = {} + + // Make sure that the listing has all specified mapped unit type names. + /* const listingUnitTypeNames = new Set(Object.keys(listingUnitTypeNameSummaryMap)) + for (const mappedUnitTypeName of mappedUnitTypeNames) { + if (!listingUnitTypeNames.has(mappedUnitTypeName)) { + throw `Listing "${listing.name}" is missing unit type ${mappedUnitTypeName} from unit summaries.` + } + } */ + + let newUnitTypeNameAvailabilityMap = {} + for (const mappedUnitTypeName of mappedUnitTypeNames) { + newUnitTypeNameAvailabilityMap = { + ...newUnitTypeNameAvailabilityMap, + [mappedUnitTypeName]: reportUnitData.filter( + (u) => + fpCodeUnitTypeMap[infoFetcher(u, "fpCode")] === mappedUnitTypeName && + infoFetcher(u, "UnitAvailableBit") === "1" + ).length, + } + } + + // Make sure that the availability count is < the total count. + /* for (const unitTypeName in newUnitTypeNameAvailabilityMap) { + if ( + newUnitTypeNameAvailabilityMap[unitTypeName] > + listingUnitTypeNameSummaryMap[unitTypeName].totalCount + ) { + throw `New availability (${newUnitTypeNameAvailabilityMap[unitTypeName]}) for unit type ${unitTypeName} is greater than total (${listingUnitTypeNameSummaryMap[unitTypeName].totalCount})` + } + } + + // TODO: Update with new unit groups model + // for (const unitSummary of listing.unitsSummary) { + // unitSummary.totalAvailable = newUnitTypeNameAvailabilityMap[unitSummary.unitType.name] || 0 + // } + console.log(`Updating listing "${listing.name}" with new availabilities:`) + console.log(newUnitTypeNameAvailabilityMap) + await listingsService.update({ id: args.listingId, body: listing }) */ +} + +void main() diff --git a/backend/core/scripts/import-regions.ts b/backend/core/scripts/import-regions.ts new file mode 100644 index 0000000000..28c61e6844 --- /dev/null +++ b/backend/core/scripts/import-regions.ts @@ -0,0 +1,63 @@ +import * as fs from "fs" +import * as path from "path" +import CsvReadableStream from "csv-reader" +import { Connection, QueryRunner } from "typeorm" +import dbOptions from "../ormconfig" + +// uses data from https://docs.google.com/spreadsheets/d/15KIX_wSuKmrtjYYrgRgb-LN1cdxIIJ9RSffPJ7SQ7j4/edit#gid=259554428 +// from backend/core run with ts-node ./scripts/import-regions.ts [path to file] + +/* eslint-disable-next-line @typescript-eslint/no-var-requires */ +const getStream = require("get-stream") + +const args = process.argv.slice(2) + +const filePath = args[0] +if (typeof filePath !== "string" && !fs.existsSync(filePath)) { + throw new Error(`usage: ts-node import-unit-groups.ts csv-file-path`) +} + +async function main() { + const connection = new Connection(dbOptions) + await connection.connect() + const queryRunner: QueryRunner = connection.createQueryRunner() + await queryRunner.connect() + + const inputRows = await getStream.array( + fs.createReadStream(path.resolve("", filePath), "utf8").pipe( + new CsvReadableStream({ + parseNumbers: true, + parseBooleans: true, + trim: true, + asObject: true, + }) + ) + ) + + const regions = ["Greater Downtown", "Eastside", "Southwest", "Westside"] + + for (const row of inputRows) { + try { + if ( + row.neighborhood_new === "#N/A" || + row.neighborhood_new === "" || + regions.includes(row.region_new) === false + ) + continue + + const listing = await queryRunner.query(`SELECT property_id FROM listings WHERE id = $1`, [ + row.id, + ]) + await queryRunner.query(`UPDATE property SET neighborhood = $1, region = $2 WHERE id = $3`, [ + row.neighborhood_new, + row.region_new, + listing[0].property_id, + ]) + } catch (e) { + console.log({ e }) + } + } + await queryRunner.release() +} + +void main() diff --git a/backend/core/scripts/import-section-8-info.ts b/backend/core/scripts/import-section-8-info.ts new file mode 100644 index 0000000000..52527109b6 --- /dev/null +++ b/backend/core/scripts/import-section-8-info.ts @@ -0,0 +1,46 @@ +import * as fs from "fs" +import * as path from "path" +import CsvReadableStream from "csv-reader" +import { Connection, QueryRunner } from "typeorm" +import dbOptions from "../ormconfig" + +// uses data from script-data/section-8-existing-info.csv +// from backend/core run with yarn ts-node ./scripts/import-section-8-info.ts [path to file] + +/* eslint-disable-next-line @typescript-eslint/no-var-requires */ +const getStream = require("get-stream") + +async function main() { + const connection = new Connection(dbOptions) + await connection.connect() + const queryRunner: QueryRunner = connection.createQueryRunner() + await queryRunner.connect() + + const inputRows = await getStream.array( + fs + .createReadStream(path.resolve("", "scripts/script-data/section-8-existing-info.csv"), "utf8") + .pipe( + new CsvReadableStream({ + parseNumbers: true, + parseBooleans: true, + trim: true, + asObject: true, + }) + ) + ) + const section8Rows = inputRows.reduce((acc, current) => { + if (current.section_8 === "yes") acc += `'${current.id}',` + return acc + }, "") + try { + await queryRunner.query( + //slice to remove trailing comma + `UPDATE listings set section8_acceptance = true WHERE id in (${section8Rows.slice(0, -1)})` + ) + } catch (e) { + console.log({ e }) + } + await queryRunner.release() +} + +void main() diff --git a/backend/core/scripts/import-unit-groups.ts b/backend/core/scripts/import-unit-groups.ts new file mode 100644 index 0000000000..074417ea12 --- /dev/null +++ b/backend/core/scripts/import-unit-groups.ts @@ -0,0 +1,285 @@ +import * as fs from "fs" +import CsvReadableStream from "csv-reader" +import { Connection, DeepPartial } from "typeorm" +import { Listing } from "../src/listings/entities/listing.entity" +import { UnitGroup } from "../src/units-summary/entities/unit-group.entity" +import { UnitGroupAmiLevel } from "../src/units-summary/entities/unit-group-ami-level.entity" +import { UnitType } from "../src/unit-types/entities/unit-type.entity" +import { AmiChart } from "../src/ami-charts/entities/ami-chart.entity" +import { HUD2021 } from "../src/seeder/seeds/ami-charts/HUD2021" +import { MSHDA2021 } from "../src/seeder/seeds/ami-charts/MSHDA2021" +import { MonthlyRentDeterminationType } from "../src/units-summary/types/monthly-rent-determination.enum" +import dbOptions from "../ormconfig" + +type AmiChartNameType = "MSHDA" | "HUD" + +const args = process.argv.slice(2) + +const filePath = args[0] +if (typeof filePath !== "string" && !fs.existsSync(filePath)) { + throw new Error(`usage: ts-node import-unit-groups.ts csv-file-path`) +} + +export class HeaderConstants { + public static readonly TemporaryListingId: string = "ID" + public static readonly UnitTypeName: string = "Unit Types" + public static readonly MinOccupancy: string = "Min Occupancy" + public static readonly MaxOccupancy: string = "Max Occupancy" + public static readonly TotalCount: string = "Unit Type Quantity (Affordable)" + public static readonly TotalAvailable: string = "Vacant Units" + public static readonly WaitlistClosed: string = "Waitlist Closed" + public static readonly WaitlistOpen: string = "Waitlist Open" + public static readonly AMIChart: string = "AMI Chart" + public static readonly AmiChartPercentage: string = "Percent AMIs" + public static readonly Type20: string = "20% (Flat / Percent)" + public static readonly Type25: string = "25% (Flat / Percent)" + public static readonly Type30: string = "30% (Flat / Percent)" + public static readonly Type35: string = "35% (Flat / Percent)" + public static readonly Type40: string = "40% (Flat / Percent)" + public static readonly Type45: string = "45% (Flat / Percent)" + public static readonly Type50: string = "50% (Flat / Percent)" + public static readonly Type55: string = "55% (Flat / Percent)" + public static readonly Type60: string = "60% (Flat / Percent)" + public static readonly Type70: string = "70% (Flat / Percent)" + public static readonly Type80: string = "80% (Flat / Percent)" + public static readonly Type100: string = "100% (Flat / Percent)" + public static readonly Type120: string = "120% (Flat / Percent)" + public static readonly Type125: string = "125% (Flat / Percent)" + public static readonly Type140: string = "140% (Flat / Percent)" + public static readonly Type150: string = "150% (Flat / Percent)" + public static readonly Value20: string = "20% (Value)" + public static readonly Value25: string = "25% (Value)" + public static readonly Value30: string = "30% (Value)" + public static readonly Value35: string = "35% (Value)" + public static readonly Value40: string = "40% (Value)" + public static readonly Value45: string = "45% (Value)" + public static readonly Value50: string = "50% (Value)" + public static readonly Value55: string = "55% (Value)" + public static readonly Value60: string = "60% (Value)" + public static readonly Value70: string = "70% (Value)" + public static readonly Value80: string = "80% (Value)" + public static readonly Value100: string = "100% (Value)" + public static readonly Value120: string = "120% (Value)" + public static readonly Value125: string = "125% (Value)" + public static readonly Value140: string = "140% (Value)" + public static readonly Value150: string = "150% (Value)" +} + +function findAmiChartByName( + amiCharts: Array, + spreadSheetAmiChartName: AmiChartNameType +): AmiChart { + const SpreadSheetAmiChartNameToDbChartNameMapping: Record = { + MSHDA: MSHDA2021.name, + HUD: HUD2021.name, + } + return amiCharts.find( + (amiChart) => + amiChart.name === SpreadSheetAmiChartNameToDbChartNameMapping[spreadSheetAmiChartName] + ) +} + +function getAmiValueFromColumn(row, amiPercentage: number, type: "percentage" | "flat") { + const mapAmiPercentageToColumnName = { + 20: HeaderConstants.Value20, + 25: HeaderConstants.Value25, + 30: HeaderConstants.Value30, + 35: HeaderConstants.Value35, + 40: HeaderConstants.Value40, + 45: HeaderConstants.Value45, + 50: HeaderConstants.Value50, + 55: HeaderConstants.Value55, + 60: HeaderConstants.Value60, + 70: HeaderConstants.Value70, + 80: HeaderConstants.Value80, + 100: HeaderConstants.Value100, + 120: HeaderConstants.Value120, + 125: HeaderConstants.Value125, + 140: HeaderConstants.Value140, + 150: HeaderConstants.Value150, + } + const value = row[mapAmiPercentageToColumnName[amiPercentage]] + + if (value) { + // This is case where $ is added by google spreadsheet because it's a single non % value + if (type === "flat" && value.toString().includes("$")) { + return Number.parseInt(value.replace(/\$/g, "").replace(/,/g, "")) + } + + const splitValues = value.toString().split(",") + + if (splitValues.length === 1) { + return Number.parseInt(value) + } else if (splitValues.length === 2) { + return type === "flat" ? Number.parseInt(splitValues[0]) : Number.parseInt(splitValues[1]) + } + + throw new Error("This part should not be reached") + } +} + +function getAmiTypeFromColumn(row, amiPercentage: number) { + const mapAmiPercentageToColumnName = { + 20: HeaderConstants.Type20, + 25: HeaderConstants.Type25, + 30: HeaderConstants.Type30, + 35: HeaderConstants.Type35, + 40: HeaderConstants.Type40, + 45: HeaderConstants.Type45, + 50: HeaderConstants.Type50, + 55: HeaderConstants.Type55, + 60: HeaderConstants.Type60, + 70: HeaderConstants.Type70, + 80: HeaderConstants.Type80, + 100: HeaderConstants.Type100, + 120: HeaderConstants.Type120, + 125: HeaderConstants.Type125, + 140: HeaderConstants.Type140, + 150: HeaderConstants.Type150, + } + const type = row[mapAmiPercentageToColumnName[amiPercentage]] + return type +} + +function generateUnitsSummaryAmiLevels( + row, + amiChartEntities: Array, + amiChartString: string, + amiChartPercentagesString: string +) { + const amiCharts = amiChartString.split("/") + + let amiPercentages: Array = [] + if (amiChartPercentagesString && typeof amiChartPercentagesString === "string") { + amiPercentages = amiChartPercentagesString + .split(",") + .map((s) => s.trim()) + .map((s) => Number.parseInt(s)) + } else if (amiChartPercentagesString && typeof amiChartPercentagesString === "number") { + amiPercentages = [amiChartPercentagesString] + } + + const amiChartLevels: Array> = [] + + for (const amiChartName of amiCharts) { + const amiChartEntity = findAmiChartByName(amiChartEntities, amiChartName as AmiChartNameType) + + for (const amiPercentage of amiPercentages) { + const type = getAmiTypeFromColumn(row, amiPercentage) + const splitTypes = type.split(", ") + + splitTypes.forEach((monthlyRentDeterminationType) => { + amiChartLevels.push({ + amiChart: amiChartEntity, + amiPercentage, + percentageOfIncomeValue: + monthlyRentDeterminationType === "Percent" + ? getAmiValueFromColumn(row, amiPercentage, "percentage") + : null, + monthlyRentDeterminationType: + monthlyRentDeterminationType === "Flat" + ? MonthlyRentDeterminationType.flatRent + : MonthlyRentDeterminationType.percentageOfIncome, + flatRentValue: + monthlyRentDeterminationType === "Flat" + ? getAmiValueFromColumn(row, amiPercentage, "flat") + : null, + }) + }) + } + } + + return amiChartLevels +} + +function getOpenWaitlistValue(row): boolean { + const waitlistClosedColumn = row[HeaderConstants.WaitlistClosed] + if (waitlistClosedColumn === "Closed") { + return false + } + + const waitlistOpenColumn = row[HeaderConstants.WaitlistOpen] + if (waitlistOpenColumn === "Open") { + return true + } + + return true +} + +async function main() { + const connection = new Connection(dbOptions) + await connection.connect() + + const listingsRepository = connection.getRepository(Listing) + const unitTypesRepository = connection.getRepository(UnitType) + const amiChartsRepository = connection.getRepository(AmiChart) + + const amiCharts = await amiChartsRepository.find() + + const inputStream = fs.createReadStream(filePath, "utf8") + inputStream + .pipe( + new CsvReadableStream({ parseNumbers: true, parseBooleans: true, trim: true, asObject: true }) + ) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .on("data", async (row) => { + try { + const listing: DeepPartial = await listingsRepository.findOne({ + where: { + temporaryListingId: row[HeaderConstants.TemporaryListingId], + }, + }) + if (!listing) { + throw new Error(`Listing with ID: ${row[HeaderConstants.TemporaryListingId]} not found.`) + } + + const unitTypes = [] + if (row[HeaderConstants.UnitTypeName]) { + const spreadsheetUnitTypeNameToDbUnitTypeName = { + "1BR": "oneBdrm", + "2BR": "twoBdrm", + "3BR": "threeBdrm", + "4+BR": "fourBdrm", + "4BR": "fourBdrm", + Studio: "studio", + } + + const unitType = await unitTypesRepository.findOneOrFail({ + where: { + name: spreadsheetUnitTypeNameToDbUnitTypeName[row[HeaderConstants.UnitTypeName]], + }, + }) + unitTypes.push(unitType) + } + + const newUnitsSummary: DeepPartial = { + minOccupancy: row[HeaderConstants.MinOccupancy] + ? row[HeaderConstants.MinOccupancy] + : null, + maxOccupancy: row[HeaderConstants.MaxOccupancy] + ? row[HeaderConstants.MaxOccupancy] + : null, + totalCount: row[HeaderConstants.TotalCount] ? row[HeaderConstants.TotalCount] : null, + totalAvailable: row[HeaderConstants.TotalAvailable] + ? row[HeaderConstants.TotalAvailable] + : null, + openWaitlist: getOpenWaitlistValue(row), + unitType: unitTypes, + amiLevels: generateUnitsSummaryAmiLevels( + row, + amiCharts, + row[HeaderConstants.AMIChart], + row[HeaderConstants.AmiChartPercentage] + ), + } + listing.unitGroups.push(newUnitsSummary) + + await listingsRepository.save(listing) + } catch (e) { + console.error(row) + console.error(e) + } + }) +} + +void main() diff --git a/backend/core/scripts/minimal-listing.json b/backend/core/scripts/minimal-listing.json deleted file mode 100644 index 338c0fafd1..0000000000 --- a/backend/core/scripts/minimal-listing.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "Minimal Listing", - "status": "active", - "CSVFormattingType": "basic", - "countyCode": "Alameda", - "preferences": [], - "buildingAddress": { - "city": "Oakland", - "state": "CA", - "street": "Main St", - "zipCode": "94501", - "latitude": "37.80", - "longitude": "122.27" - }, - "units": [ - { - "unitType": "oneBdrm", - "status": "available" - } - ], - "assets": [], - "applicationMethods": [], - "events": [], - "displayWaitlistSize": false, - "jurisdictionName": "Alameda" -} diff --git a/backend/core/scripts/script-data/HOME2024.ts b/backend/core/scripts/script-data/HOME2024.ts new file mode 100644 index 0000000000..8aec7f08b0 --- /dev/null +++ b/backend/core/scripts/script-data/HOME2024.ts @@ -0,0 +1,169 @@ +import { AmiChartCreateDto } from "../../src/ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM HOME2024.txt. +export const ami: Omit = { + name: "HOME 2024", + items: [ + { + percentOfAmi: 30, + householdSize: 1, + income: 20150, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 23000, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 25900, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 28750, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 31050, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 33350, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 35650, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 37950, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 33600, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 38400, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 43200, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 47950, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 51800, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 55650, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 59500, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 63300, + }, + { + percentOfAmi: 60, + householdSize: 1, + income: 40320, + }, + { + percentOfAmi: 60, + householdSize: 2, + income: 46080, + }, + { + percentOfAmi: 60, + householdSize: 3, + income: 51840, + }, + { + percentOfAmi: 60, + householdSize: 4, + income: 57540, + }, + { + percentOfAmi: 60, + householdSize: 5, + income: 62160, + }, + { + percentOfAmi: 60, + householdSize: 6, + income: 66780, + }, + { + percentOfAmi: 60, + householdSize: 7, + income: 71400, + }, + { + percentOfAmi: 60, + householdSize: 8, + income: 75960, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 53700, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 61400, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 69050, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 76700, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 82850, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 89000, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 95150, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 101250, + }, + ], +} diff --git a/backend/core/scripts/script-data/HOME2024.txt b/backend/core/scripts/script-data/HOME2024.txt new file mode 100644 index 0000000000..d2f93d1b9e --- /dev/null +++ b/backend/core/scripts/script-data/HOME2024.txt @@ -0,0 +1,4 @@ +30 20150 23000 25900 28750 31050 33350 35650 37950 +50 33600 38400 43200 47950 51800 55650 59500 63300 +60 40320 46080 51840 57540 62160 66780 71400 75960 +80 53700 61400 69050 76700 82850 89000 95150 101250 diff --git a/backend/core/scripts/script-data/HUD2025.ts b/backend/core/scripts/script-data/HUD2025.ts new file mode 100644 index 0000000000..8b7779de3b --- /dev/null +++ b/backend/core/scripts/script-data/HUD2025.ts @@ -0,0 +1,169 @@ +import { AmiChartCreateDto } from "../../src/ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM HOME2024.txt. +export const amiHUD2025: Omit = { + name: "HUD 2025", + items: [ + { + percentOfAmi: 30, + householdSize: 1, + income: 21250, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 24250, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 27300, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 30300, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 32750, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 35150, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 37600, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 40000, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 35350, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 40400, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 45450, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 50500, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 54550, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 58600, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 62650, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 66700, + }, + { + percentOfAmi: 60, + householdSize: 1, + income: 42420, + }, + { + percentOfAmi: 60, + householdSize: 2, + income: 48480, + }, + { + percentOfAmi: 60, + householdSize: 3, + income: 54540, + }, + { + percentOfAmi: 60, + householdSize: 4, + income: 60600, + }, + { + percentOfAmi: 60, + householdSize: 5, + income: 65460, + }, + { + percentOfAmi: 60, + householdSize: 6, + income: 70320, + }, + { + percentOfAmi: 60, + householdSize: 7, + income: 75180, + }, + { + percentOfAmi: 60, + householdSize: 8, + income: 80040, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 56600, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 64650, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 72750, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 80800, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 87300, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 93750, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 100200, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 106700, + }, + ], +} diff --git a/backend/core/scripts/script-data/HUD2025.txt b/backend/core/scripts/script-data/HUD2025.txt new file mode 100644 index 0000000000..3baed5deec --- /dev/null +++ b/backend/core/scripts/script-data/HUD2025.txt @@ -0,0 +1,4 @@ +30 21250 24250 27300 30300 32750 35150 37600 40000 +50 35350 40400 45450 50500 54550 58600 62650 66700 +60 42420 48480 54540 60600 65460 70320 75180 80040 +80 56600 64650 72750 80800 87300 93750 100200 106700 \ No newline at end of file diff --git a/backend/core/scripts/script-data/MSHDA2024.ts b/backend/core/scripts/script-data/MSHDA2024.ts new file mode 100644 index 0000000000..af01ae8fa4 --- /dev/null +++ b/backend/core/scripts/script-data/MSHDA2024.ts @@ -0,0 +1,649 @@ +import { AmiChartCreateDto } from "../../src/ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM MSHDA2024.txt. +export const ami: Omit = { + name: "MSHDA 2024", + items: [ + { + percentOfAmi: 20, + householdSize: 1, + income: 13440, + }, + { + percentOfAmi: 20, + householdSize: 2, + income: 15360, + }, + { + percentOfAmi: 20, + householdSize: 3, + income: 17280, + }, + { + percentOfAmi: 20, + householdSize: 4, + income: 19180, + }, + { + percentOfAmi: 20, + householdSize: 5, + income: 20720, + }, + { + percentOfAmi: 20, + householdSize: 6, + income: 22260, + }, + { + percentOfAmi: 20, + householdSize: 7, + income: 23800, + }, + { + percentOfAmi: 20, + householdSize: 8, + income: 25320, + }, + { + percentOfAmi: 25, + householdSize: 1, + income: 16800, + }, + { + percentOfAmi: 25, + householdSize: 2, + income: 19200, + }, + { + percentOfAmi: 25, + householdSize: 3, + income: 21600, + }, + { + percentOfAmi: 25, + householdSize: 4, + income: 23975, + }, + { + percentOfAmi: 25, + householdSize: 5, + income: 25900, + }, + { + percentOfAmi: 25, + householdSize: 6, + income: 27825, + }, + { + percentOfAmi: 25, + householdSize: 7, + income: 29750, + }, + { + percentOfAmi: 25, + householdSize: 8, + income: 31650, + }, + { + percentOfAmi: 30, + householdSize: 1, + income: 20160, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 23040, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 25920, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 28770, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 31808, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 33390, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 35700, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 37980, + }, + { + percentOfAmi: 35, + householdSize: 1, + income: 23520, + }, + { + percentOfAmi: 35, + householdSize: 2, + income: 26880, + }, + { + percentOfAmi: 35, + householdSize: 3, + income: 30240, + }, + { + percentOfAmi: 35, + householdSize: 4, + income: 33565, + }, + { + percentOfAmi: 35, + householdSize: 5, + income: 36260, + }, + { + percentOfAmi: 35, + householdSize: 6, + income: 38955, + }, + { + percentOfAmi: 35, + householdSize: 7, + income: 41650, + }, + { + percentOfAmi: 35, + householdSize: 8, + income: 44310, + }, + { + percentOfAmi: 40, + householdSize: 1, + income: 26880, + }, + { + percentOfAmi: 40, + householdSize: 2, + income: 30720, + }, + { + percentOfAmi: 40, + householdSize: 3, + income: 34560, + }, + { + percentOfAmi: 40, + householdSize: 4, + income: 38360, + }, + { + percentOfAmi: 40, + householdSize: 5, + income: 41440, + }, + { + percentOfAmi: 40, + householdSize: 6, + income: 44520, + }, + { + percentOfAmi: 40, + householdSize: 7, + income: 47600, + }, + { + percentOfAmi: 40, + householdSize: 8, + income: 50640, + }, + { + percentOfAmi: 45, + householdSize: 1, + income: 30240, + }, + { + percentOfAmi: 45, + householdSize: 2, + income: 34560, + }, + { + percentOfAmi: 45, + householdSize: 3, + income: 38880, + }, + { + percentOfAmi: 45, + householdSize: 4, + income: 43155, + }, + { + percentOfAmi: 45, + householdSize: 5, + income: 46620, + }, + { + percentOfAmi: 45, + householdSize: 6, + income: 50085, + }, + { + percentOfAmi: 45, + householdSize: 7, + income: 53550, + }, + { + percentOfAmi: 45, + householdSize: 8, + income: 56970, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 33600, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 38400, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 43200, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 47950, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 51800, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 55650, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 59500, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 63300, + }, + { + percentOfAmi: 55, + householdSize: 1, + income: 36960, + }, + { + percentOfAmi: 55, + householdSize: 2, + income: 42240, + }, + { + percentOfAmi: 55, + householdSize: 3, + income: 47520, + }, + { + percentOfAmi: 55, + householdSize: 4, + income: 52740, + }, + { + percentOfAmi: 55, + householdSize: 5, + income: 62160, + }, + { + percentOfAmi: 55, + householdSize: 6, + income: 66780, + }, + { + percentOfAmi: 55, + householdSize: 7, + income: 71400, + }, + { + percentOfAmi: 55, + householdSize: 8, + income: 75960, + }, + { + percentOfAmi: 60, + householdSize: 1, + income: 40320, + }, + { + percentOfAmi: 60, + householdSize: 2, + income: 46080, + }, + { + percentOfAmi: 60, + householdSize: 3, + income: 51840, + }, + { + percentOfAmi: 60, + householdSize: 4, + income: 57540, + }, + { + percentOfAmi: 60, + householdSize: 5, + income: 62160, + }, + { + percentOfAmi: 60, + householdSize: 6, + income: 66780, + }, + { + percentOfAmi: 60, + householdSize: 7, + income: 71400, + }, + { + percentOfAmi: 60, + householdSize: 8, + income: 75960, + }, + { + percentOfAmi: 70, + householdSize: 1, + income: 47040, + }, + { + percentOfAmi: 70, + householdSize: 2, + income: 53760, + }, + { + percentOfAmi: 70, + householdSize: 3, + income: 60480, + }, + { + percentOfAmi: 70, + householdSize: 4, + income: 67130, + }, + { + percentOfAmi: 70, + householdSize: 5, + income: 72520, + }, + { + percentOfAmi: 70, + householdSize: 6, + income: 77910, + }, + { + percentOfAmi: 70, + householdSize: 7, + income: 83300, + }, + { + percentOfAmi: 70, + householdSize: 8, + income: 88620, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 53760, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 61440, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 69120, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 76720, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 82880, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 89040, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 95200, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 101280, + }, + { + percentOfAmi: 100, + householdSize: 1, + income: 67200, + }, + { + percentOfAmi: 100, + householdSize: 2, + income: 76800, + }, + { + percentOfAmi: 100, + householdSize: 3, + income: 86400, + }, + { + percentOfAmi: 100, + householdSize: 4, + income: 95900, + }, + { + percentOfAmi: 100, + householdSize: 5, + income: 103600, + }, + { + percentOfAmi: 100, + householdSize: 6, + income: 111300, + }, + { + percentOfAmi: 100, + householdSize: 7, + income: 119000, + }, + { + percentOfAmi: 100, + householdSize: 8, + income: 126600, + }, + { + percentOfAmi: 120, + householdSize: 1, + income: 80640, + }, + { + percentOfAmi: 120, + householdSize: 2, + income: 92160, + }, + { + percentOfAmi: 120, + householdSize: 3, + income: 103680, + }, + { + percentOfAmi: 120, + householdSize: 4, + income: 115080, + }, + { + percentOfAmi: 120, + householdSize: 5, + income: 124320, + }, + { + percentOfAmi: 120, + householdSize: 6, + income: 133560, + }, + { + percentOfAmi: 120, + householdSize: 7, + income: 142800, + }, + { + percentOfAmi: 120, + householdSize: 8, + income: 151920, + }, + { + percentOfAmi: 125, + householdSize: 1, + income: 84000, + }, + { + percentOfAmi: 125, + householdSize: 2, + income: 96000, + }, + { + percentOfAmi: 125, + householdSize: 3, + income: 108000, + }, + { + percentOfAmi: 125, + householdSize: 4, + income: 119875, + }, + { + percentOfAmi: 125, + householdSize: 5, + income: 129500, + }, + { + percentOfAmi: 125, + householdSize: 6, + income: 139125, + }, + { + percentOfAmi: 125, + householdSize: 7, + income: 148750, + }, + { + percentOfAmi: 125, + householdSize: 8, + income: 158250, + }, + { + percentOfAmi: 140, + householdSize: 1, + income: 94080, + }, + { + percentOfAmi: 140, + householdSize: 2, + income: 107520, + }, + { + percentOfAmi: 140, + householdSize: 3, + income: 120960, + }, + { + percentOfAmi: 140, + householdSize: 4, + income: 134260, + }, + { + percentOfAmi: 140, + householdSize: 5, + income: 145040, + }, + { + percentOfAmi: 140, + householdSize: 6, + income: 155820, + }, + { + percentOfAmi: 140, + householdSize: 7, + income: 166600, + }, + { + percentOfAmi: 140, + householdSize: 8, + income: 177240, + }, + { + percentOfAmi: 150, + householdSize: 1, + income: 100800, + }, + { + percentOfAmi: 150, + householdSize: 2, + income: 115200, + }, + { + percentOfAmi: 150, + householdSize: 3, + income: 129600, + }, + { + percentOfAmi: 150, + householdSize: 4, + income: 143850, + }, + { + percentOfAmi: 150, + householdSize: 5, + income: 155400, + }, + { + percentOfAmi: 150, + householdSize: 6, + income: 166950, + }, + { + percentOfAmi: 150, + householdSize: 7, + income: 178500, + }, + { + percentOfAmi: 150, + householdSize: 8, + income: 189900, + }, + ], +} diff --git a/backend/core/scripts/script-data/MSHDA2024.txt b/backend/core/scripts/script-data/MSHDA2024.txt new file mode 100644 index 0000000000..bb4bd57717 --- /dev/null +++ b/backend/core/scripts/script-data/MSHDA2024.txt @@ -0,0 +1,16 @@ +20 13,440 15,360 17,280 19,180 20,720 22,260 23,800 25,320 +25 16,800 19,200 21,600 23,975 25,900 27,825 29,750 31,650 +30 20,160 23,040 25,920 28,770 31,808 33,390 35,700 37,980 +35 23,520 26,880 30,240 33,565 36,260 38,955 41,650 44,310 +40 26,880 30,720 34,560 38,360 41,440 44,520 47,600 50,640 +45 30,240 34,560 38,880 43,155 46,620 50,085 53,550 56,970 +50 33,600 38,400 43,200 47,950 51,800 55,650 59,500 63,300 +55 36,960 42,240 47,520 52,740 62,160 66,780 71,400 75,960 +60 40,320 46,080 51,840 57,540 62,160 66,780 71,400 75,960 +70 47,040 53,760 60,480 67,130 72,520 77,910 83,300 88,620 +80 53,760 61,440 69,120 76,720 82,880 89,040 95,200 101,280 +100 67,200 76,800 86,400 95,900 103,600 111,300 119,000 126,600 +120 80,640 92,160 103,680 115,080 124,320 133,560 142,800 151,920 +125 84,000 96,000 108,000 119,875 129,500 139,125 148,750 158,250 +140 94,080 107,520 120,960 134,260 145,040 155,820 166,600 177,240 +150 100,800 115,200 129,600 143,850 155,400 166,950 178,500 189,900 diff --git a/backend/core/scripts/script-data/MSHDA2025.ts b/backend/core/scripts/script-data/MSHDA2025.ts new file mode 100644 index 0000000000..38ae07fa36 --- /dev/null +++ b/backend/core/scripts/script-data/MSHDA2025.ts @@ -0,0 +1,649 @@ +import { AmiChartCreateDto } from "../../src/ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM HOME2024.txt. +export const amiMSHDA2025: Omit = { + name: "MSHDA 2025", + items: [ + { + percentOfAmi: 20, + householdSize: 1, + income: 14140, + }, + { + percentOfAmi: 20, + householdSize: 2, + income: 16160, + }, + { + percentOfAmi: 20, + householdSize: 3, + income: 18180, + }, + { + percentOfAmi: 20, + householdSize: 4, + income: 20200, + }, + { + percentOfAmi: 20, + householdSize: 5, + income: 21820, + }, + { + percentOfAmi: 20, + householdSize: 6, + income: 23440, + }, + { + percentOfAmi: 20, + householdSize: 7, + income: 25060, + }, + { + percentOfAmi: 20, + householdSize: 8, + income: 26680, + }, + { + percentOfAmi: 25, + householdSize: 1, + income: 17675, + }, + { + percentOfAmi: 25, + householdSize: 2, + income: 20200, + }, + { + percentOfAmi: 25, + householdSize: 3, + income: 22725, + }, + { + percentOfAmi: 25, + householdSize: 4, + income: 25250, + }, + { + percentOfAmi: 25, + householdSize: 5, + income: 27275, + }, + { + percentOfAmi: 25, + householdSize: 6, + income: 29300, + }, + { + percentOfAmi: 25, + householdSize: 7, + income: 31325, + }, + { + percentOfAmi: 25, + householdSize: 8, + income: 33350, + }, + { + percentOfAmi: 30, + householdSize: 1, + income: 21210, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 24240, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 27270, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 30300, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 32730, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 35160, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 37590, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 40020, + }, + { + percentOfAmi: 35, + householdSize: 1, + income: 24745, + }, + { + percentOfAmi: 35, + householdSize: 2, + income: 28280, + }, + { + percentOfAmi: 35, + householdSize: 3, + income: 31815, + }, + { + percentOfAmi: 35, + householdSize: 4, + income: 35350, + }, + { + percentOfAmi: 35, + householdSize: 5, + income: 38185, + }, + { + percentOfAmi: 35, + householdSize: 6, + income: 41020, + }, + { + percentOfAmi: 35, + householdSize: 7, + income: 43855, + }, + { + percentOfAmi: 35, + householdSize: 8, + income: 46690, + }, + { + percentOfAmi: 40, + householdSize: 1, + income: 28280, + }, + { + percentOfAmi: 40, + householdSize: 2, + income: 32320, + }, + { + percentOfAmi: 40, + householdSize: 3, + income: 36360, + }, + { + percentOfAmi: 40, + householdSize: 4, + income: 40400, + }, + { + percentOfAmi: 40, + householdSize: 5, + income: 43640, + }, + { + percentOfAmi: 40, + householdSize: 6, + income: 46880, + }, + { + percentOfAmi: 40, + householdSize: 7, + income: 50120, + }, + { + percentOfAmi: 40, + householdSize: 8, + income: 53360, + }, + { + percentOfAmi: 45, + householdSize: 1, + income: 31815, + }, + { + percentOfAmi: 45, + householdSize: 2, + income: 36360, + }, + { + percentOfAmi: 45, + householdSize: 3, + income: 40905, + }, + { + percentOfAmi: 45, + householdSize: 4, + income: 45450, + }, + { + percentOfAmi: 45, + householdSize: 5, + income: 49095, + }, + { + percentOfAmi: 45, + householdSize: 6, + income: 52740, + }, + { + percentOfAmi: 45, + householdSize: 7, + income: 56385, + }, + { + percentOfAmi: 45, + householdSize: 8, + income: 60030, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 35350, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 40400, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 45450, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 50500, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 54550, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 58600, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 62650, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 66700, + }, + { + percentOfAmi: 55, + householdSize: 1, + income: 38885, + }, + { + percentOfAmi: 55, + householdSize: 2, + income: 44440, + }, + { + percentOfAmi: 55, + householdSize: 3, + income: 49995, + }, + { + percentOfAmi: 55, + householdSize: 4, + income: 55550, + }, + { + percentOfAmi: 55, + householdSize: 5, + income: 60005, + }, + { + percentOfAmi: 55, + householdSize: 6, + income: 64460, + }, + { + percentOfAmi: 55, + householdSize: 7, + income: 68915, + }, + { + percentOfAmi: 55, + householdSize: 8, + income: 73370, + }, + { + percentOfAmi: 60, + householdSize: 1, + income: 42420, + }, + { + percentOfAmi: 60, + householdSize: 2, + income: 48480, + }, + { + percentOfAmi: 60, + householdSize: 3, + income: 54540, + }, + { + percentOfAmi: 60, + householdSize: 4, + income: 60600, + }, + { + percentOfAmi: 60, + householdSize: 5, + income: 65460, + }, + { + percentOfAmi: 60, + householdSize: 6, + income: 70320, + }, + { + percentOfAmi: 60, + householdSize: 7, + income: 75180, + }, + { + percentOfAmi: 60, + householdSize: 8, + income: 80040, + }, + { + percentOfAmi: 70, + householdSize: 1, + income: 49490, + }, + { + percentOfAmi: 70, + householdSize: 2, + income: 56560, + }, + { + percentOfAmi: 70, + householdSize: 3, + income: 63630, + }, + { + percentOfAmi: 70, + householdSize: 4, + income: 70700, + }, + { + percentOfAmi: 70, + householdSize: 5, + income: 76370, + }, + { + percentOfAmi: 70, + householdSize: 6, + income: 82040, + }, + { + percentOfAmi: 70, + householdSize: 7, + income: 87710, + }, + { + percentOfAmi: 70, + householdSize: 8, + income: 93380, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 56560, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 64640, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 72720, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 80800, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 87280, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 93760, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 100240, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 106720, + }, + { + percentOfAmi: 100, + householdSize: 1, + income: 70700, + }, + { + percentOfAmi: 100, + householdSize: 2, + income: 80800, + }, + { + percentOfAmi: 100, + householdSize: 3, + income: 90900, + }, + { + percentOfAmi: 100, + householdSize: 4, + income: 101000, + }, + { + percentOfAmi: 100, + householdSize: 5, + income: 109100, + }, + { + percentOfAmi: 100, + householdSize: 6, + income: 117200, + }, + { + percentOfAmi: 100, + householdSize: 7, + income: 125300, + }, + { + percentOfAmi: 100, + householdSize: 8, + income: 133400, + }, + { + percentOfAmi: 120, + householdSize: 1, + income: 84840, + }, + { + percentOfAmi: 120, + householdSize: 2, + income: 96960, + }, + { + percentOfAmi: 120, + householdSize: 3, + income: 109080, + }, + { + percentOfAmi: 120, + householdSize: 4, + income: 121200, + }, + { + percentOfAmi: 120, + householdSize: 5, + income: 130920, + }, + { + percentOfAmi: 120, + householdSize: 6, + income: 140640, + }, + { + percentOfAmi: 120, + householdSize: 7, + income: 150360, + }, + { + percentOfAmi: 120, + householdSize: 8, + income: 160080, + }, + { + percentOfAmi: 125, + householdSize: 1, + income: 88375, + }, + { + percentOfAmi: 125, + householdSize: 2, + income: 101000, + }, + { + percentOfAmi: 125, + householdSize: 3, + income: 113625, + }, + { + percentOfAmi: 125, + householdSize: 4, + income: 126250, + }, + { + percentOfAmi: 125, + householdSize: 5, + income: 136375, + }, + { + percentOfAmi: 125, + householdSize: 6, + income: 146500, + }, + { + percentOfAmi: 125, + householdSize: 7, + income: 156625, + }, + { + percentOfAmi: 125, + householdSize: 8, + income: 166750, + }, + { + percentOfAmi: 140, + householdSize: 1, + income: 98980, + }, + { + percentOfAmi: 140, + householdSize: 2, + income: 113120, + }, + { + percentOfAmi: 140, + householdSize: 3, + income: 127260, + }, + { + percentOfAmi: 140, + householdSize: 4, + income: 141400, + }, + { + percentOfAmi: 140, + householdSize: 5, + income: 152740, + }, + { + percentOfAmi: 140, + householdSize: 6, + income: 164080, + }, + { + percentOfAmi: 140, + householdSize: 7, + income: 175420, + }, + { + percentOfAmi: 140, + householdSize: 8, + income: 186760, + }, + { + percentOfAmi: 150, + householdSize: 1, + income: 106050, + }, + { + percentOfAmi: 150, + householdSize: 2, + income: 121200, + }, + { + percentOfAmi: 150, + householdSize: 3, + income: 136350, + }, + { + percentOfAmi: 150, + householdSize: 4, + income: 151500, + }, + { + percentOfAmi: 150, + householdSize: 5, + income: 163650, + }, + { + percentOfAmi: 150, + householdSize: 6, + income: 175800, + }, + { + percentOfAmi: 150, + householdSize: 7, + income: 187950, + }, + { + percentOfAmi: 150, + householdSize: 8, + income: 200100, + }, + ], +} diff --git a/backend/core/scripts/script-data/MSHDA2025.txt b/backend/core/scripts/script-data/MSHDA2025.txt new file mode 100644 index 0000000000..dabdcd699d --- /dev/null +++ b/backend/core/scripts/script-data/MSHDA2025.txt @@ -0,0 +1,16 @@ +20 14140 16160 18180 20200 21820 23440 25060 26680 +25 17675 20200 22725 25250 27275 29300 31325 33350 +30 21210 24240 27270 30300 32730 35160 37590 40020 +35 24745 28280 31815 35350 38185 41020 43855 46690 +40 28280 32320 36360 40400 43640 46880 50120 53360 +45 31815 36360 40905 45450 49095 52740 56385 60030 +50 35350 40400 45450 50500 54550 58600 62650 66700 +55 38885 44440 49995 55550 60005 64460 68915 73370 +60 42420 48480 54540 60600 65460 70320 75180 80040 +70 49490 56560 63630 70700 76370 82040 87710 93380 +80 56560 64640 72720 80800 87280 93760 100240 106720 +100 70700 80800 90900 101000 109100 117200 125300 133400 +120 84840 96960 109080 121200 130920 140640 150360 160080 +125 88375 101000 113625 126250 136375 146500 156625 166750 +140 98980 113120 127260 141400 152740 164080 175420 186760 +150 106050 121200 136350 151500 163650 175800 187950 200100 \ No newline at end of file diff --git a/backend/core/scripts/script-data/minimal-listing.json b/backend/core/scripts/script-data/minimal-listing.json new file mode 100644 index 0000000000..a79e8f246f --- /dev/null +++ b/backend/core/scripts/script-data/minimal-listing.json @@ -0,0 +1,41 @@ +{ + "name": "Minimal Listing", + "status": "active", + "countyCode": "Alameda", + "listingPreferences": [], + "buildingAddress": { + "city": "Oakland", + "state": "CA", + "street": "Main St", + "zipCode": "94501", + "latitude": "37.80", + "longitude": "122.27" + }, + "units": [ + { + "unitType": "oneBdrm", + "status": "available" + } + ], + "assets": [], + "applicationMethods": [], + "events": [], + "displayWaitlistSize": false, + "jurisdictionName": "Alameda", + "depositMin": "min", + "depositMax": "max", + "developer": "developer", + "digitalApplication": false, + "image": { + "fileId": "file_id", + "label": "label" + }, + "isWaitlistOpen": false, + "leasingAgentEmail": "leasing_agent@example.com", + "leasingAgentName": "leasing agent name", + "leasingAgentPhone": "(202) 555-0194", + "paperApplication": false, + "referralOpportunity": false, + "rentalAssistance": "rental_assistance", + "reviewOrderType": "lottery" +} diff --git a/backend/core/scripts/script-data/section-8-existing-info.csv b/backend/core/scripts/script-data/section-8-existing-info.csv new file mode 100644 index 0000000000..fc2418c67a --- /dev/null +++ b/backend/core/scripts/script-data/section-8-existing-info.csv @@ -0,0 +1,273 @@ +id,name,section_8 +86b31413-22c2-4aec-9c61-7e08f5381c8a,Bridgeview II, +9c50855b-a1a8-40c2-a520-9a529d82d542,Chapel Hill Townhouses,yes +2bb93127-c7f8-475b-9ede-9afcb0119571,Benjamin O Davis, +1a71b1b9-cb6c-4cac-92da-076708a04cf6,Transfiguration Place, +d7c05b24-ba57-4dee-9880-fa998f454bb0,150 Bagley, +4361ee68-1fd3-4347-bf4c-6cbe00a97518,Genesis Villas At Medbury Park, +fe6e0445-3f24-4f7b-9dfc-7f70a9bf0350,Lakewood Manor Apartments, +5ec672fb-37b1-4ff4-9427-53a5c7d4071a,The Village of University Meadows, +f0988852-1b30-4dd2-a38f-c51023896edd,City Club Apartments - Lafayette Park, +b82b8d5c-c8dd-447a-819f-ebebb6f2ce2e,Clement Kern Gardens, +8d52ead9-80f0-4e17-b4fd-fc501f183354,Bridgeview I, +4943b211-d0e7-462a-84f7-e41a7bbe9b77,Left Field, +495f89d1-0fa9-4be5-a8db-a5c860b5b839,The Stott,yes +cbbbc23b-95fa-4924-892a-863fdbb1dc93,The Residences at City Modern,yes +44ae69d5-178b-499f-b2de-39f1f7a22c7b,Elementa Watson, +6ed504d6-c1f3-4088-ae68-729f195920ea,Field Street I & II, +11b5a7a4-8b09-4cb3-9028-05927d1972f9,River Crest Apartments, +35cb771d-ca1d-478f-857a-6715fe59bbcf,Beaubien, +814f52c9-00f0-4507-81ad-53ed14825fa0,Wellington Square Apartments, +dca566eb-f6b0-466b-9e67-60baf8975be6,655 W. Willis, +dde8ab6b-6851-4230-a6ac-54cf6c6c4eab,Osi Art Apartments, +f3cb740c-dbac-4a59-b7e1-548854ebea71,Cabot Building, +43bf232f-7d33-4990-89b9-1b4b82b73da0,Carson Building, +78db8825-5d20-4e54-addc-b1d46f43ca00,Claradale Building, +ee384201-0615-421e-9fed-b6f4ae37d847,1000 Townsend, +f7ca5cf7-20e9-4b9b-a5be-2f3ef6b41d56,Dreamtroit, +a32cef44-867e-4ce3-b3ed-8c19ff3ac7ec,Islandview Duplexes (Phase I), +9d9cc27d-9bbb-4898-8284-14346588decd,Ferndale Building, +37ae53fa-7f6c-4a23-a4b1-03ca7646da26,Eddystone, +7b606467-c54f-4a44-9f44-3b1dbf2574a3,655 Hazelwood, +0bd9fa74-f1c4-486b-9625-f07f1fc9c1aa,Reverand Dr. Jim Holley Residences, +889bf615-c4a4-446e-b6e3-1d5506b4ae4e,Ruth Ellis Clairmount Center, +884da794-918b-4077-92d7-d083885910ec,Savannah Apartments, +a7ad2c50-db72-4dc4-86bd-f00533003050,Pitt/Carson Building, +f4f869ef-b5da-4136-9893-410a7596488f,Sawyer Art Apartments, +85d85729-e7cf-4d38-9934-eae6cfa1c8b7,BELNORD APARTMENTS, +d2ab9a65-4d7f-4777-aac7-9067b0bcda26,Book Building & Tower, +e5502cfe-f90f-4e58-8785-e6903681012b,BRAINARD APARTMENTS, +c3bdd348-652b-44d0-a67b-c716185e6047,Brush & Watson (Beaubien), +bfdbeb9e-3a35-4873-8e6a-f9c40a8ab9ab,Lolita Test, +c25c7ebf-60e8-4cf5-a85e-a0bae930a7af,Osborn Duplex Rehabs, +eaaeb88a-7e41-49b7-be8a-0cba529e42d9,Brightmoor Homes Ia, +e96b72e9-473c-40d6-8a8e-496da2917f66,Brush & Watson (Brush), +1090a962-d943-4148-9768-b063b318cdd5,Field Street 1 and 2, +771b0212-1925-4a95-8bc6-16c8ffd71c95,Morton Manor Apartments, +bfca0049-05b1-4da8-9ec5-190ceb0c24a5,Friendship Meadows I & III, +1561e47c-6bd3-4cd6-b3a7-d75382a88033,Baltimore Station II, +2149f0c6-5356-423c-92ae-ed7fc35b5ae2,Kercheval South Rehab, +86fd6f1c-edc5-4efd-8e69-dd2cadd7f220,Brush Park Apartments, +8c88218e-deef-4e41-91d5-12f82c064dd3,Miller-Grover Center, +74b32f50-805c-4d47-ab7d-95ecabc4729a,Mariners Inn, +089ad7ac-fdfc-4727-a0f1-df95b7ea440f,"Southwest Partners aka Hubbard Farms (Cole, Harwill, Harington)", +123be50f-caac-41a4-b463-a179fe746af0,Woodward Avenue Apartments, +24e82fbb-d554-4fd1-b36c-0e16b2c214c5,Lafayette West Rental, +3eec056e-848b-4e97-8458-11a0787859ac,Mack Alter Homes, +23090592-fe35-490c-9e30-24a8013a98b4,Woodward West, +80419247-5312-46e2-8449-d99c84676617,Merrill Place II, +1a3b339c-4d99-45cf-b27e-c3f86b4edea8,NSO Healthy Housing, +4e485e43-20f3-4a85-82fc-44ae7ad9894e,Friendship Meadows II, +679d1ca0-5446-464f-a6b9-33e9f7023940,Piety Hill 2, +8127006f-506c-4290-be8e-fddf52cbabc2,Sugar Hill, +67e57c79-aaad-424c-b958-ba1375c6a10d,The Exchange: Rental, +60e2f613-f35e-40c2-8a6b-e263b09a7a84,The Charlotte, +05c50f8a-f7a2-4b58-84bc-1a2ecac858e2,Midtown Square Apartments, +995b03d5-a713-43d5-8e28-81c432a393f1,PARKVIEW COURT APARTMENTS, +ec23dd15-58d4-477a-add3-b01dae083dad,Transfiguration School, +da96fe86-1ade-43b7-be88-3b506fb40fab,GenesisHOPE Phase I, +a3de5d7d-6b42-471a-b9a7-47941749ad83,Himelhoch Apartments, +ff2c31eb-ac62-4e9a-9ec9-cc4cce4a685f,Manchester Place Apartments & Townhomes,yes +cc518c12-41cc-4640-a45c-a663e720ae45,Woodbridge Estates,yes +f5fcd839-d645-42da-9370-8496e4adf7a5,Nortown Homes LDHALP,yes +100c16f8-eb97-4a2e-954b-b10dc9a7c83f,Cornerstone Estates,yes +d8c5f540-f777-484c-be45-ec38e9e3cb78,381 Holbrook,yes +9334aaa3-c4aa-4bcb-a5c0-cae9a228e6b6,City Club Apartments - CBD Detroit, +4f4a9cd7-e072-4264-8514-8cff157a2344,Marlborough/JEI Residential, +6da133f6-7fe0-434c-8b4d-e52620590546,Shelborne Square Apartments,yes +84c4e1a4-22b4-4d67-80cf-bf753a0903c0,Fitzgerald Phase 1, +78b61c17-aef4-4c3c-8748-ab6681b8a69b,7850 E Jefferson, +c7b00334-495d-4f5e-a78e-6716198c7d1b,Kercheval Place Townhomes, +04db8a97-ce7d-4a50-97ee-56560dfab548,Art Center Townhomes, +ec3fde6d-8f44-4f3d-837b-0d31ee55fff1,Arthur Antisdel Apartments, +f1f0feb5-136d-4df4-b59d-438b7b6b4519,Karley Square, +dc74afb0-872e-4b10-b563-27e34bbdb50d,Lawndale Apartments, +35971f5f-e417-444a-8777-6c2a6e450cd2,Pablo Davis, +370fcfee-6a33-47c0-9211-35694fa78fe5,Brightmoor Homes I,yes +f525da42-47e3-48c8-9a8b-8c07e23b61df,Delta Manor Apartments, +1b4421ef-726f-40a7-b336-d496b4354725,Bell Building, +dba99761-8382-49ff-8ec3-685b37986e03,Oakman Townhomes, +32e9d543-5481-489b-a7c3-b3179297c4f1,Antonella Test, +b0ce7611-a240-45f6-87fe-bfb4742b7fd1,The Corner, +981d197e-647d-4c0a-b954-8994c7d84f21,Wellington Square II, +7b00615f-91ce-43bc-808c-6dbce0b0c364,Woodbridge Senior Village, +07220700-86f1-4ff8-a3f0-da1613519514,Chene Park Commons, +b2832a7b-4d1d-436e-81ec-03f3a484bcee,Northlawn Apartments, +a493caad-5fef-4b1d-8ce7-478b98adba1f,Eden Manor, +6157d462-2040-49ad-baa6-cee69fcc0716,Jefferson Meadows II, +666c9924-dbb7-472c-834c-3300482c6d32,Jennings Senior Living, +a5d68d28-9bdb-420e-ad43-6fb8520a8f3d,Freedom Place Apartments, +abda5ce8-753d-4351-b1dc-9434b355d676,Robert Thomas Apartments,yes +5911d84f-f57b-4104-8a42-309118ad12ce,David Stott, +c6060216-1585-40eb-b82a-3bcff5c14251,San Juan Square Townhomes, +7d682abe-729f-4252-96dd-ece0d028ac5d,Woodbridge Senior Apartments, +fd210aa4-1ad8-4fce-b8d8-64454e294921,9100 ON Gratiot, +04969fa7-3ca2-411d-b6f0-3a4bb1a7be8b,Renaissance Village Apartments, +77fd3e65-cf25-4911-8bd2-e7425641ecdb,Warren Plaza Apartments, +1485f58d-c8c6-4705-a9c8-1554710cef9e,The Flats at 124 Alfred,yes +3a2684ef-e6fa-41b9-ad0c-6e73d75c40d8,Calumet Townhomes, +f8ba76f0-d26d-4ee1-973e-76c403e233fe,Conner Creek Apartments, +3e75d761-7e91-49fe-a56b-3b693c9ba864,LaVogue Square, +ba994450-29e8-4613-b268-6a5a01750b03,Parker Durand, +1e4ea6d5-bfc0-4fc2-8429-bfc4ee704652,Dickerson Manor Senior Apartments,yes +83e653c6-5f02-4d0f-8304-8fbf59ec48ba,Savannah Gardens, +c9b65d07-cf04-44d3-8735-cef53759c4bc,Milwaukee Junction Apartments, +691fac47-8b2e-4eaa-ba8f-4b5f8efb8741,Prince Hall Place, +f93bb23e-c126-49b2-8419-dfa329823310,Samaritas Affordable Living at Gateshead Crossing, +a2f8941f-3b3f-4072-8ef9-cea2af2bf594,Woodbridge Estates Phase IX, +38dcbc4a-ff99-41a7-883b-9f75c37211ed,Gardenview Estates,yes +148f2458-78f5-4ba4-84d2-32c1a277fede,Clinton House Apartments, +33daaf97-ab36-4058-bca1-7f4f1257f930,Meyers Plaza Co-op Senior Apartments, +4c07a335-8012-4190-8120-876b3f992b3e,River Crest Apartments,yes +923f1cc0-25c8-40e5-8447-af0a92ffe6c6,329 Holbrook, +9efea022-48e3-4d3a-9fac-852a065f4864,New Center Square, +4d2d8716-e5ab-4723-b2b0-95fee8cbad7c,Pilgrim Village, +8023159c-8764-4d36-8306-b3e0ecabc8a8,Cathedral Tower, +e40aaf7b-10ca-4086-8563-2a58e87c4ea5,Friendship Meadows Apartments,yes +5b50be22-fb68-424b-8e64-e10f163c0261,Saratoga Homes, +92e6e9c6-4089-4296-aeb7-5d4e5e2114e3,Joy West Manor, +e7e81c7b-4356-4224-812e-7b6c1e214e54,Chesterfield Apartments, +e607e294-1926-46a2-aa40-f0ee1943efc3,Palmer Court Townhomes, +a7452108-5c23-4c69-95ff-3755a12a73ff,Greenhouse Apartments, +557271f2-0fb1-4e58-aafc-ebb20f0137fe,Cass Plaza, +6e51ad21-4f5c-4584-aea1-700e127a38ad,Washington Boulevard Apartments,yes +d9253b83-b722-40dd-8009-1f0610605ec1,28 Grand Apartments,yes +972233b5-0785-41e7-9c86-ea922ed00b6d,Peterboro Place, +e99f93f0-33dd-4e43-b7e5-80c9c313ca28,Benjamin Manor Townhomes,yes +4f6af8df-1abf-4650-b548-850d94c85a22,Penrose Village, +c0f9f872-d0aa-4b9b-8489-a7d658d25612,The Wellesley, +479ae36d-9ab4-41c3-8ff9-a21341a0cbc9,Pablo Davis II Elder Living Center, +bff4b1b8-5fc3-4646-8e9d-c36dd6ce6d59,Selden Properties, +b4020df6-bb65-475c-b4c9-b0df39025773,Marketplace Court, +067bbeb3-d925-421b-a098-5382d7218046,Clay Apartments, +b15b9790-a057-451a-b047-f1fc904fef7f,The Corner wrong, +d53c5f30-5b21-4ae3-acc4-1e9b4020b76b,Kamper Stevens, +689df71a-2fd1-4e27-b525-bd1efb13f190,The Village of Oakman Manor, +8c73db04-d21e-4251-bbf8-0efcad3b064c,Cambridge Towers, +0a830d7a-1412-484c-8973-0bbad610600f,8330 On the River, +c19fc289-f39a-485e-a45f-1c9c44749dd3,Village Center, +9eba5f0b-5e93-4e67-bbca-bdd9a6212c4c,State Fair Apartment, +9ab11b6a-08dc-4cda-9e6c-2bc92182f6fa,Helisa Square, +62975e1f-02f9-47cc-bd1f-a01e10292bee,Plymouth Square Village, +6e6d7285-9f85-4b87-971f-fdce0c56ff2e,Forest Park Apartments, +c29ef79f-b6cf-4cdf-957e-16ab6e6f3a27,The Village of Harmony Manor, +f84781d7-5e71-40b7-802c-41edf5fb6f1d,MCDONALD SQUARE APARTMENTS, +56750ef6-6655-4a92-92b4-e4d400d11c35,Northwest Unity Homes, +7cfebad8-c6fa-4d6d-8d1d-28f35249dcce,McCoy Townhouses, +1d8e0c1d-c365-4691-9e64-69f50ce50efc,New Center Commons, +19beec77-adca-4de8-8ca4-5864ecd8183e,Fairview Manor Apartments, +28aa75f4-cde0-4af9-954a-c6368515c374,NEW CENTER PAVILION, +36c230ee-31ad-4e05-ae14-b06dc254ace5,City Modern Senior Affordable, +7d39601a-714f-413a-89d7-0d0f3b09bdc4,Kendall Homes, +2b90a7b6-51e7-46d9-ab46-2da4317e6ab2,Montana Gardens, +5caaa948-1026-4975-b3b6-beb8b3b93472,Cole Apartments, +95681b3f-ced3-4047-8dad-816cbeb2d9d0,Vernor Townhomes, +84784058-8e7a-4a22-87cf-5dbb5399d4a8,Restoration Towers, +2f1d14e9-2630-461f-885c-890c064af01d,Shirley Manor Apartment, +eedc856b-f2f2-4c9e-b539-f82f0e8f4dd0,Harriet Tubman, +89db0a5a-b63e-4853-8529-ab28b80b9ef5,St. Aubin Square, +8cec52b1-4af4-41e4-b920-e473cb39662b,Elmwood Towers, +2016ff43-0abb-47dc-a3a4-4ebb6ec1cff9,Orchestra Place Apartments, +09fbabc6-2dda-4131-ac95-476b552681fc,Galston Apartments, +baa56c22-7a4e-486b-8975-20183a0d3933,Mohican Regent/ Lifebuilders II, +c0dfa845-0114-4424-bfe4-2a63f47da9bc,Rivercrest Apartments, +d2d0c966-c3a9-43e6-a38e-073700d5d8db,The Village of St. Martha's, +bbb0869e-1e10-4d71-9cd2-47fd9ad828d3,Sojourner Truth Homes, +753c8e3c-eddb-45d9-bdd1-879c3bc35df2,Heather Hall,yes +0d1ca2a8-f891-427b-b58e-77d4a0148227,New Listing, +af440ad1-bf15-4ad8-994b-b080ef05f833,North End Single Family Infill, +d8715a58-e562-46ae-a4df-dc284fe314ad,River Towers Senior Apartments, +af0a582d-351f-4ced-a263-294018c6e80d,Palmer Park Square, +4e8ab348-5c6b-4278-8f91-3231437cb1c2,Casamira Apartments, +8ec2e459-678b-4817-b69c-0a60c9257527,Genesis Villas Townhomes, +ec18716e-dae6-4a4e-b1e3-915dc6365562,Obama Building, +461e3e71-af6d-408c-b5bd-0719e797d460,Park Square, +e049dab5-55f9-404d-847b-d9d9fdbfd0d5,Penrose Village ll, +af882c3a-7bc0-47d9-819c-ed63161892c0,Across The Park Apartments, +19cd8e54-dade-474b-8320-35a95f6544ff,Bellemere Senior Living, +462bc996-23ef-4a42-81ec-f834fcdb6273,Helen O'Dean Butler, +aa96fb83-2e07-4510-8eed-1d11365708f3,The Villages at Parkside, +a6985399-8c0e-4712-822b-1af5fee1d6ad,The Murray, +2eff48d9-a9a1-430b-a474-0a1e3f36afc4,Tiny Homes Phase Ib, +5706f837-47e6-4bf7-8a1e-95c52d87dd37,Marwood Apts, +ae9dbffc-28be-44c4-bfa7-560687c1c727,Hope Park Homes,yes +545d4538-02ce-4da7-ad12-9096c7c02553,Springwells Village Townhomes, +71d5fedd-34fe-441c-adaa-203f6cf5bdfb,Melie Apartments, +5c321a04-78bb-426d-aba1-55619fb1340e,Prak 202, +9d584ffa-b5d5-494b-b1f6-4efcde04d681,Village of Bethany Manor, +f9a3cc8d-3053-47ab-b5b4-145bb1e3e847,Central Park Apartments, +5559ed4f-1320-4e05-9cbf-cd4d862bf1c0,Orleans Landing, +471c3b4b-23dd-4fa6-a075-ae8aa03285c6,Harrington Apartments, +13f18705-ad5a-46ae-9ef3-ecb1f786ad8d,Grand Apartments, +5e1f2114-3576-4060-82cd-17f632000efb,Piquette Square Apartments, +7578c958-0c30-4cb1-ab2b-93047c363e3b,Mack Ashland Apartment, +3dbc1225-34b9-4225-80ba-c20d51821c0d,Alberta W. King Village, +08bca28e-b1fb-4c9e-8f7e-324d4b76da46,Buersmeyer Manor, +46bb2f4d-ba80-42ee-84c8-fe8512ca00f9,Charlotte Apartments, +7f653106-fce2-493a-8336-a1a855c71673,Hamtramck Square, +7182829e-6ede-41f1-801e-252c9da2e31c,Milwaukee Junction, +6db12d27-9002-4ef7-ae20-d0a1804eeed3,Lexington Village, +9998bf90-5f71-446e-ad4e-7e680d660837,The Peterboro Arms, +b5b1784a-4fb9-4857-8fc4-d6a66809974d,Mohican Regent/ Lifebuilders I, +d24503a1-7005-41af-bb18-c99386bdf379,Day Star Estates, +5056688f-867d-4926-b1ed-50d82acea107,Treymore Apartments, +4f0aa11c-c4a0-49c8-9b17-b99909078503,Trumbull Crossing, +8835867f-da35-48aa-88fa-dd8719a26d6a,Young Manor, +eb9bad73-46bf-4f82-979e-5187dc235f4f,Rio Vista Detroit Co-op, +214ccb4c-0d0b-4ff6-b420-ed05bae0e22e,Gratiot Woods Co-op Senior Apartments, +de0a0605-a235-4a18-9b6b-cc4752d135ed,Vernor Building, +19df7f2b-e021-41dc-8bac-64b281ee71a0,Central Towers, +5dfa63f0-587e-4995-9988-1ac62c7a912a,Eugene-Hogan Estates, +fbeaa83f-1069-46c1-b7f5-1eda557aedfe,Medical Center Village - Senior, +a7ad6cbf-9057-4910-b9a2-4571feda8231,Brightmoor Homes I, +aa7bf663-4f3b-4c96-adc2-ba2428d118c9,Gratiot Central Commons, +c103cbb8-70ce-4dd7-b49b-c440a44f7420,Melrose Square Homes, +dbf40f50-e978-4851-982d-668b88bb7e9c,Roberts III Apartments,yes +be45843e-f5cd-49c9-8d83-7ce12fa71ae7,Brightmoor Homes II,yes +9a886f7a-7ab2-4b6a-9401-284ae9d1f36c,Mt. Vernon Apartments II, +174ce426-eb69-4173-9895-234809584b0b,Chalmers Square Apartments, +19d40c0b-b3e3-48e3-a350-285577e6e14e,Gray Street, +19d6dbed-ae3f-48e8-a2f9-8aad2d22c33b,St. Paul / Kingston Arms Apartments, +7afa96ae-9ac9-4ec6-a98a-8597929ec192,Jefferson Square Apartments, +edb8eb68-cf8c-4cd9-bd27-635efdb35ec6,Hidden Pines Apartments, +321ce6ca-22c0-48ac-8149-028a1b9dcfa4,Village Park, +2d1ab153-a3c1-451b-8831-46cff1b24436,Evangelical Manor, +59c1e16a-13b8-4c7b-af8d-14e1b45093a8,Springwells Partner, +3477f59d-9bd8-436d-a337-7d6dd0a6af4a,Wilshire Apartments, +8ac7bbea-43c2-4c82-b751-89e1f2fd7794,Van Dyke Center, +bb6a7338-1b1e-48e3-a65d-9bb8e07d124e,Saint Rita Owen Apartments, +c5f3b7b3-0d96-4c81-9a33-54f6668cd8ae,Morningside Commons, +1c0ebd17-ad69-4615-8735-93f09606ae91,Agnes Street Apartments, +41360d0e-3019-4920-a75a-3990577a2359,Positive Images, +b6c7c809-7887-4fe7-9209-b1f2a232e132,The Peterboro Arms, +7e31951e-33dc-494d-902c-7b3083b64ada,The Boulevard, +a75df1cd-5cf1-427d-af85-3ec2a4837af1,Wellington Square, +da0f9745-980f-4bc8-8cce-b5a85e93794a,Selden Manor,yes +6819289c-f066-46c2-8381-2638e6e727da,Coronado Apartment II, +01692de1-4f2d-454d-bc28-06cd9e9a3d3e,Pilgrim Meadows, +c57137f4-5e5e-4643-8ebb-f001a57ca905,Harwill Manor Apartments, +9228c8b2-d70e-47f5-8fc9-48f46f955b25,Elton Park Checker Cab, +636ed2ec-1197-4448-b45f-c0f232564cb4,Clark Apartments, +e54790fd-ebb7-477a-ba62-f5b3e336a8fe,Martin Garden Apartments, +564020ff-f3b4-4fc8-a45e-3ad474888136,Whitdell Apartments, +926d8d7c-9eed-45b7-a970-19476b820f80,Parkview Tower and Square, +576d5544-6c2a-4891-9f79-6e24ed2800df,Bowin Place, +13797b46-7adc-4f76-9cda-d96b6536edcf,University Club Apartments, +fd0a990f-2760-4d74-96a1-fa5ee8016c83,Peterboro Place Apartments, +29ab3785-39ab-4925-ae83-5424d90718d2,"B. Siegel Building, 7 .Liv", +522e78f2-2d45-495a-9816-7656cd81f0af,St. Paul Manor, +3756a218-3ea0-401c-b34d-f4dd045c7e3b,Architects Building Apartments II, +223d6bc9-fa11-4c5b-b46e-77900782f980,Newberry Lofts, +20e6556c-e924-428b-94af-5c9f2ee9fbd7,Rouge Woods, +107e4629-9af8-47f9-a052-101c6dcf6c0d,KMG Prestige, +7df9780c-8480-4c09-912e-9d9b9779e092,KMG Prestige Test 2, +60148dd6-0a93-446d-9fd1-5d1240587da5,Woodward-Gladstone, +551806ad-15bf-4c05-8fc7-571af7677b4b,Wellington I & II, +99d54ba3-e542-43cf-89f2-12c611f3f1fc,Medical Center Village - Family, +020be6c8-79bf-4043-848a-995448bc1c70,Midtown Place,yes +9c123f30-7bbd-417a-88a5-a86214a649e7,Brightmoor Homes III,yes +a6975388-ee6d-4481-bf36-ecc97c205201,Brightmoor Homes IV,yes +1713b83f-908d-4073-a934-bdfb07dc265a,Coronado Square, +f8612d37-b94d-4295-a25c-58221bacdf32,McKinley Manor, +72457e4e-cca1-4f0f-9553-d6c4e50bb792,Parkview Place Apartments,yes +9cb3d63a-2b89-4b51-9c19-8f714fba05b5,West Boston Apartments, \ No newline at end of file diff --git a/backend/core/src/activity-log/activity-log.module.ts b/backend/core/src/activity-log/activity-log.module.ts new file mode 100644 index 0000000000..4fc610fad1 --- /dev/null +++ b/backend/core/src/activity-log/activity-log.module.ts @@ -0,0 +1,13 @@ +import { forwardRef, Module } from "@nestjs/common" +import { TypeOrmModule } from "@nestjs/typeorm" +import { AuthModule } from "../auth/auth.module" +import { ActivityLog } from "./entities/activity-log.entity" +import { ActivityLogService } from "./services/activity-log.service" + +@Module({ + imports: [TypeOrmModule.forFeature([ActivityLog]), forwardRef(() => AuthModule)], + controllers: [], + providers: [ActivityLogService], + exports: [ActivityLogService], +}) +export class ActivityLogModule {} diff --git a/backend/core/src/activity-log/decorators/activity-log-metadata.decorator.ts b/backend/core/src/activity-log/decorators/activity-log-metadata.decorator.ts new file mode 100644 index 0000000000..4d82260dcc --- /dev/null +++ b/backend/core/src/activity-log/decorators/activity-log-metadata.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from "@nestjs/common" +import { ActivityLogMetadataType } from "../types/activity-log-metadata-type" + +export const ActivityLogMetadata = (metadata: ActivityLogMetadataType) => + SetMetadata("activity_log_metadata", metadata) diff --git a/backend/core/src/activity-log/entities/activity-log.entity.ts b/backend/core/src/activity-log/entities/activity-log.entity.ts new file mode 100644 index 0000000000..878bfe1c0d --- /dev/null +++ b/backend/core/src/activity-log/entities/activity-log.entity.ts @@ -0,0 +1,29 @@ +import { Column, Entity, JoinColumn, ManyToOne } from "typeorm" +import { Expose, Type } from "class-transformer" +import { AbstractEntity } from "../../shared/entities/abstract.entity" +import { User } from "../../auth/entities/user.entity" + +@Entity({ name: "activity_logs" }) +export class ActivityLog extends AbstractEntity { + @Column() + @Expose() + module: string + + @Column("uuid") + @Expose() + recordId: string + + @Column() + @Expose() + action: string + + @ManyToOne(() => User, { nullable: true, onDelete: "SET NULL" }) + @JoinColumn() + @Expose() + @Type(() => User) + user: User + + @Column({ type: "jsonb", nullable: true }) + @Expose() + metadata?: any +} diff --git a/backend/core/src/activity-log/interceptors/activity-log.interceptor.ts b/backend/core/src/activity-log/interceptors/activity-log.interceptor.ts new file mode 100644 index 0000000000..e5cf4128e5 --- /dev/null +++ b/backend/core/src/activity-log/interceptors/activity-log.interceptor.ts @@ -0,0 +1,90 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common" +import { ActivityLogService } from "../services/activity-log.service" +import { Reflector } from "@nestjs/core" +import { httpMethodsToAction } from "../../shared/http-methods-to-actions" +import { User } from "../../auth/entities/user.entity" +import { authzActions } from "../../auth/enum/authz-actions.enum" +import { endWith, ignoreElements, mergeMap } from "rxjs/operators" +import { from } from "rxjs" +import { ActivityLogMetadataType } from "../types/activity-log-metadata-type" +import { deepFind } from "../../shared/utils/deep-find" + +@Injectable() +export class ActivityLogInterceptor implements NestInterceptor { + constructor( + protected readonly activityLogService: ActivityLogService, + protected reflector: Reflector + ) {} + + getBasicRequestInfo( + context: ExecutionContext + ): { + module?: string + action?: string + resourceId?: string + user?: User + activityLogMetadata: ActivityLogMetadataType + } { + const req = context.switchToHttp().getRequest() + const module = this.reflector.getAllAndOverride("authz_type", [ + context.getClass(), + context.getHandler(), + ]) + const action = + this.reflector.get("authz_action", context.getHandler()) || + httpMethodsToAction[req.method] + const user: User | null = req.user + const activityLogMetadata = this.reflector.getAllAndOverride( + "activity_log_metadata", + [context.getClass(), context.getHandler()] + ) + return { module, action, user, activityLogMetadata } + } + + extractMetadata(body: any, activityLogMetadata: ActivityLogMetadataType) { + let metadata + if (activityLogMetadata) { + metadata = {} + for (const trackPropertiesMetadata of activityLogMetadata) { + metadata[trackPropertiesMetadata.targetPropertyName] = deepFind( + body, + trackPropertiesMetadata.propertyPath + ) + } + } + return metadata + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + intercept(context: ExecutionContext, next: CallHandler) { + const { module, action, user, activityLogMetadata } = this.getBasicRequestInfo(context) + + if (action === authzActions.read) { + return next.handle() + } + + const metadata = this.extractMetadata( + context.switchToHttp().getRequest().body, + activityLogMetadata + ) + + return next.handle().pipe( + mergeMap((value) => + // NOTE: Resource ID is taken from the response value because it does not exist for e.g. create endpoints + { + const req = context.switchToHttp().getRequest() + let resourceId + if (req.method === "POST") { + resourceId = value?.id + } else { + resourceId = req.body.id + } + return from(this.activityLogService.log(module, action, resourceId, user, metadata)).pipe( + ignoreElements(), + endWith(value) + ) + } + ) + ) + } +} diff --git a/backend/core/src/activity-log/services/activity-log.service.ts b/backend/core/src/activity-log/services/activity-log.service.ts new file mode 100644 index 0000000000..2b8f0472db --- /dev/null +++ b/backend/core/src/activity-log/services/activity-log.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from "@nestjs/common" +import { User } from "../../auth/entities/user.entity" +import { ActivityLog } from "../entities/activity-log.entity" +import { InjectRepository } from "@nestjs/typeorm" +import { Repository } from "typeorm" + +@Injectable() +export class ActivityLogService { + constructor( + @InjectRepository(ActivityLog) + private readonly repository: Repository + ) {} + + public async log(module: string, action: string, recordId: string, user: User, metadata?: any) { + return await this.repository.save({ module, action, recordId, user, metadata }) + } +} diff --git a/backend/core/src/activity-log/types/activity-log-metadata-type.ts b/backend/core/src/activity-log/types/activity-log-metadata-type.ts new file mode 100644 index 0000000000..fbeb4e4d68 --- /dev/null +++ b/backend/core/src/activity-log/types/activity-log-metadata-type.ts @@ -0,0 +1 @@ +export type ActivityLogMetadataType = Array<{ targetPropertyName: string; propertyPath: string }> diff --git a/backend/core/src/ami-charts/ami-charts.service.ts b/backend/core/src/ami-charts/ami-charts.service.ts index f7b5bde22f..0875dec137 100644 --- a/backend/core/src/ami-charts/ami-charts.service.ts +++ b/backend/core/src/ami-charts/ami-charts.service.ts @@ -1,11 +1,10 @@ import { AmiChart } from "./entities/ami-chart.entity" import { AmiChartCreateDto, AmiChartUpdateDto } from "./dto/ami-chart.dto" import { InjectRepository } from "@nestjs/typeorm" -import { Repository } from "typeorm" -import { QueryOneOptions } from "../shared/services/abstract-service" +import { FindOneOptions, Repository } from "typeorm" import { NotFoundException } from "@nestjs/common" -import { assignDefined } from "../shared/assign-defined" import { AmiChartListQueryParams } from "./dto/ami-chart-list-query-params" +import { assignDefined } from "../shared/utils/assign-defined" export class AmiChartsService { constructor( @@ -22,6 +21,8 @@ export class AmiChartsService { where: (qb) => { if (queryParams.jurisdictionName) { qb.where("jurisdiction.name = :jurisdictionName", queryParams) + } else if (queryParams.jurisdictionId) { + qb.where("jurisdiction.id = :jurisdictionId", queryParams) } }, }) @@ -31,8 +32,8 @@ export class AmiChartsService { return await this.repository.save(dto) } - async findOne(queryOneOptions: QueryOneOptions): Promise { - const obj = await this.repository.findOne(queryOneOptions) + async findOne(findOneOptions: FindOneOptions): Promise { + const obj = await this.repository.findOne(findOneOptions) if (!obj) { throw new NotFoundException() } diff --git a/backend/core/src/ami-charts/dto/ami-chart-list-query-params.ts b/backend/core/src/ami-charts/dto/ami-chart-list-query-params.ts index 559ec00977..9c7faa020e 100644 --- a/backend/core/src/ami-charts/dto/ami-chart-list-query-params.ts +++ b/backend/core/src/ami-charts/dto/ami-chart-list-query-params.ts @@ -13,4 +13,14 @@ export class AmiChartListQueryParams { @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) jurisdictionName?: string + + @Expose() + @ApiProperty({ + name: "jurisdictionId", + required: false, + type: String, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + jurisdictionId?: string } diff --git a/backend/core/src/ami-charts/dto/ami-chart.dto.ts b/backend/core/src/ami-charts/dto/ami-chart.dto.ts index 87d34f06ce..6766070747 100644 --- a/backend/core/src/ami-charts/dto/ami-chart.dto.ts +++ b/backend/core/src/ami-charts/dto/ami-chart.dto.ts @@ -1,51 +1,54 @@ import { Expose, Type } from "class-transformer" -import { IsDate, IsDefined, IsOptional, IsUUID, ValidateNested } from "class-validator" -import { OmitType } from "@nestjs/swagger" +import { IsDate, IsDefined, IsOptional, IsString, IsUUID, ValidateNested } from "class-validator" import { AmiChart } from "../entities/ami-chart.entity" import { AmiChartItem } from "../entities/ami-chart-item.entity" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" -import { JurisdictionDto } from "../../jurisdictions/dto/jurisdiction.dto" import { IdDto } from "../../shared/dto/id.dto" +import { HasKeys } from "../../shared/types/has-keys" +import { AbstractEntity } from "../../shared/entities/abstract.entity" + +export class AmiChartDto implements HasKeys { + @Expose() + id: string + + @Expose() + @Type(() => Date) + createdAt: Date + + @Expose() + @Type(() => Date) + updatedAt: Date -export class AmiChartDto extends OmitType(AmiChart, ["items", "jurisdiction"] as const) { @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AmiChartItem) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) items: AmiChartItem[] @Expose() - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => JurisdictionDto) - jurisdiction: JurisdictionDto + name: string + + @Expose() + @Type(() => IdDto) + jurisdiction: IdDto } -export class AmiChartCreateDto extends OmitType(AmiChartDto, [ - "id", - "createdAt", - "updatedAt", - "items", - "jurisdiction", -] as const) { +export class AmiChartCreateDto implements Omit, keyof AbstractEntity> { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AmiChartItem) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => AmiChartItem) items: AmiChartItem[] + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + name: string + @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => IdDto) jurisdiction: IdDto } -export class AmiChartUpdateDto extends OmitType(AmiChartDto, [ - "id", - "createdAt", - "updatedAt", - "items", - "jurisdiction", -]) { +export class AmiChartUpdateDto extends AmiChartCreateDto { @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) @@ -62,15 +65,4 @@ export class AmiChartUpdateDto extends OmitType(AmiChartDto, [ @IsDate({ groups: [ValidationsGroupsEnum.default] }) @Type(() => Date) updatedAt?: Date - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AmiChartItem) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - items: AmiChartItem[] - - @Expose() - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => IdDto) - jurisdiction: IdDto } diff --git a/backend/core/src/ami-charts/entities/ami-chart.entity.ts b/backend/core/src/ami-charts/entities/ami-chart.entity.ts index d1ce1678a5..d1f2b3be08 100644 --- a/backend/core/src/ami-charts/entities/ami-chart.entity.ts +++ b/backend/core/src/ami-charts/entities/ami-chart.entity.ts @@ -6,47 +6,26 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from "typeorm" -import { Expose, Type } from "class-transformer" -import { IsDate, IsDefined, IsString, IsUUID, ValidateNested } from "class-validator" import { AmiChartItem } from "./ami-chart-item.entity" -import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" @Entity() export class AmiChart { @PrimaryGeneratedColumn("uuid") - @Expose() - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) id: string @CreateDateColumn() - @Expose() - @IsDate({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Date) createdAt: Date @UpdateDateColumn() - @Expose() - @IsDate({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Date) updatedAt: Date @Column("jsonb") - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => AmiChartItem) items: AmiChartItem[] @Column() - @Expose() - @IsString({ groups: [ValidationsGroupsEnum.default] }) name: string @ManyToOne(() => Jurisdiction, { eager: true, nullable: false }) - @Expose() - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Jurisdiction) jurisdiction: Jurisdiction } diff --git a/backend/core/src/app.module.ts b/backend/core/src/app.module.ts index 593922d7f3..be3933fb49 100644 --- a/backend/core/src/app.module.ts +++ b/backend/core/src/app.module.ts @@ -13,11 +13,8 @@ import { TypeOrmModule } from "@nestjs/typeorm" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require import { AuthModule } from "./auth/auth.module" - import { ListingsModule } from "./listings/listings.module" import { ApplicationsModule } from "./applications/applications.module" -import { EntityNotFoundExceptionFilter } from "./filters/entity-not-found-exception.filter" -import { logger } from "./middleware/logger.middleware" import { PreferencesModule } from "./preferences/preferences.module" import { UnitsModule } from "./units/units.module" import { PropertyGroupsModule } from "./property-groups/property-groups.module" @@ -26,12 +23,10 @@ import { AmiChartsModule } from "./ami-charts/ami-charts.module" import { ApplicationFlaggedSetsModule } from "./application-flagged-sets/application-flagged-sets.module" import * as bodyParser from "body-parser" import { ThrottlerModule } from "@nestjs/throttler" -import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis" -import Redis from "ioredis" import { SharedModule } from "./shared/shared.module" import { ConfigModule, ConfigService } from "@nestjs/config" import { TranslationsModule } from "./translations/translations.module" -import { Reflector } from "@nestjs/core" +import { HttpAdapterHost, Reflector } from "@nestjs/core" import { AssetsModule } from "./assets/assets.module" import { JurisdictionsModule } from "./jurisdictions/jurisdictions.module" import { ReservedCommunityTypesModule } from "./reserved-community-type/reserved-community-types.module" @@ -40,11 +35,19 @@ import { UnitRentTypesModule } from "./unit-rent-types/unit-rent-types.module" import { UnitAccessibilityPriorityTypesModule } from "./unit-accessbility-priority-types/unit-accessibility-priority-types.module" import { ApplicationMethodsModule } from "./application-methods/applications-methods.module" import { PaperApplicationsModule } from "./paper-applications/paper-applications.module" +import { SmsModule } from "./sms/sms.module" +import { ScheduleModule } from "@nestjs/schedule" +import { CronModule } from "./cron/cron.module" +import { ProgramsModule } from "./program/programs.module" +import { ActivityLogModule } from "./activity-log/activity-log.module" +import { logger } from "./shared/middlewares/logger.middleware" +import { CatchAllFilter } from "./shared/filters/catch-all-filter" export function applicationSetup(app: INestApplication) { + const { httpAdapter } = app.get(HttpAdapterHost) app.enableCors() app.use(logger) - app.useGlobalFilters(new EntityNotFoundExceptionFilter()) + app.useGlobalFilters(new CatchAllFilter(httpAdapter)) app.use(bodyParser.json({ limit: "50mb" })) app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })) app.useGlobalInterceptors( @@ -53,29 +56,11 @@ export function applicationSetup(app: INestApplication) { return app } -@Module({}) +@Module({ + imports: [ActivityLogModule], +}) export class AppModule { static register(dbOptions): DynamicModule { - /** - * DEV NOTE: - * This configuration is required due to issues with - * self signed certificates in Redis 6. - * - * { rejectUnauthorized: false } option is intentional and required - * - * Read more: - * https://help.heroku.com/HC0F8CUS/redis-connection-issues - * https://devcenter.heroku.com/articles/heroku-redis#ioredis-module - */ - const redis = - "0" === process.env.REDIS_USE_TLS - ? new Redis(process.env.REDIS_URL) - : new Redis(process.env.REDIS_TLS_URL, { - tls: { - rejectUnauthorized: false, - }, - }) - return { module: AppModule, imports: [ @@ -87,12 +72,17 @@ export class AppModule { AuthModule, JurisdictionsModule, ListingsModule, + CronModule, PaperApplicationsModule, PreferencesModule, + ProgramsModule, PropertiesModule, PropertyGroupsModule, + ProgramsModule, ReservedCommunityTypesModule, + ScheduleModule.forRoot(), SharedModule, + SmsModule, TranslationsModule, TypeOrmModule.forRoot({ ...dbOptions, @@ -104,7 +94,6 @@ export class AppModule { useFactory: (config: ConfigService) => ({ ttl: config.get("THROTTLE_TTL"), limit: config.get("THROTTLE_LIMIT"), - storage: new ThrottlerStorageRedisService(redis), }), }), UnitsModule, diff --git a/backend/core/src/application-flagged-sets/application-flagged-sets.service.ts b/backend/core/src/application-flagged-sets/application-flagged-sets.service.ts index 0388288756..400ecb3472 100644 --- a/backend/core/src/application-flagged-sets/application-flagged-sets.service.ts +++ b/backend/core/src/application-flagged-sets/application-flagged-sets.service.ts @@ -208,25 +208,18 @@ export class ApplicationFlaggedSetsService { ) const transAfsRepository = transactionalEntityManager.getRepository(ApplicationFlaggedSet) const visitedAfses = [] + const afses = await transAfsRepository + .createQueryBuilder("afs") + .leftJoin("afs.applications", "applications") + .select(["afs", "applications.id"]) + .where(`afs.listing_id = :listingId`, { listingId: newApplication.listing.id }) + .andWhere(`rule = :rule`, { rule }) + .getMany() + for (const matchedApplication of applicationsMatchingRule) { - // NOTE: Optimize it because of N^2 complexity, - // for each matched application we create a query returning a list of matching sets - // TODO: Add filtering into the query, right now all AFSes are fetched for each - // matched application which will become a performance problem soon - const afsesMatchingRule = ( - await transAfsRepository.find({ - join: { - alias: "afs", - leftJoinAndSelect: { - applications: "afs.applications", - }, - }, - where: { - listingId: newApplication.listing.id, - rule: rule, - }, - }) - ).filter((afs) => afs.applications.map((app) => app.id).includes(matchedApplication.id)) + const afsesMatchingRule = afses.filter((afs) => + afs.applications.map((app) => app.id).includes(matchedApplication.id) + ) if (afsesMatchingRule.length === 0) { const newAfs: DeepPartial = { @@ -262,6 +255,7 @@ export class ApplicationFlaggedSetsService { ) { const transApplicationsRepository = transactionalEntityManager.getRepository(Application) return await transApplicationsRepository.find({ + select: ["id"], where: (qb: SelectQueryBuilder) => { qb.where("Application.id != :id", { id: newApplication.id, @@ -308,6 +302,7 @@ export class ApplicationFlaggedSetsService { ] return await transApplicationsRepository.find({ + select: ["id"], where: (qb: SelectQueryBuilder) => { qb.where("Application.id != :id", { id: newApplication.id, diff --git a/backend/core/src/application-flagged-sets/dto/application-flagged-set-pagination-meta.ts b/backend/core/src/application-flagged-sets/dto/application-flagged-set-pagination-meta.ts index 10c66f2512..6eb793cb70 100644 --- a/backend/core/src/application-flagged-sets/dto/application-flagged-set-pagination-meta.ts +++ b/backend/core/src/application-flagged-sets/dto/application-flagged-set-pagination-meta.ts @@ -1,7 +1,9 @@ import { PaginationMeta } from "../../shared/dto/pagination.dto" import { Expose } from "class-transformer" +import { ApiProperty } from "@nestjs/swagger" export class ApplicationFlaggedSetPaginationMeta extends PaginationMeta { @Expose() + @ApiProperty() totalFlagged: number } diff --git a/backend/core/src/application-flagged-sets/entities/application-flagged-set.entity.ts b/backend/core/src/application-flagged-sets/entities/application-flagged-set.entity.ts index 3f7c36ef73..fa69f0706f 100644 --- a/backend/core/src/application-flagged-sets/entities/application-flagged-set.entity.ts +++ b/backend/core/src/application-flagged-sets/entities/application-flagged-set.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne } from "typeorm" +import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne } from "typeorm" import { AbstractEntity } from "../../shared/entities/abstract.entity" import { IsDate, IsEnum, IsOptional, IsString, ValidateNested } from "class-validator" import { Expose, Type } from "class-transformer" @@ -10,6 +10,7 @@ import { FlaggedSetStatus } from "../types/flagged-set-status-enum" import { Rule } from "../types/rule-enum" @Entity() +@Index(["listing"]) export class ApplicationFlaggedSet extends AbstractEntity { @Column({ enum: Rule, nullable: false }) @Expose() diff --git a/backend/core/src/applications/applications-submission.controller.ts b/backend/core/src/applications/applications-submission.controller.ts index a3521bf3fc..eb6a6e1917 100644 --- a/backend/core/src/applications/applications-submission.controller.ts +++ b/backend/core/src/applications/applications-submission.controller.ts @@ -2,16 +2,17 @@ import { Body, Controller, Post, UseGuards, UsePipes, ValidationPipe } from "@ne import { ApiBearerAuth, ApiExtraModels, ApiOperation, ApiTags } from "@nestjs/swagger" import { ResourceType } from "../auth/decorators/resource-type.decorator" import { OptionalAuthGuard } from "../auth/guards/optional-auth.guard" -import { ApplicationsService } from "./applications.service" -import { ApplicationCreateDto, ApplicationDto } from "./dto/application.dto" +import { ApplicationDto } from "./dto/application.dto" import { mapTo } from "../shared/mapTo" import { defaultValidationPipeOptions } from "../shared/default-validation-pipe-options" import { ResourceAction } from "../auth/decorators/resource-action.decorator" import { AuthzGuard } from "../auth/guards/authz.guard" import { ValidationsGroupsEnum } from "../shared/types/validations-groups-enum" import { ThrottlerGuard } from "@nestjs/throttler" -import { applicationPreferenceApiExtraModels } from "./application-preference-api-extra-models" +import { applicationPreferenceApiExtraModels } from "./types/application-preference-api-extra-models" import { authzActions } from "../auth/enum/authz-actions.enum" +import { ApplicationsService } from "./services/applications.service" +import { ApplicationCreateDto } from "./dto/application-create.dto" @Controller("applications") @ApiTags("applications") diff --git a/backend/core/src/applications/applications.controller.ts b/backend/core/src/applications/applications.controller.ts index 44f8dabe65..22a152de4c 100644 --- a/backend/core/src/applications/applications.controller.ts +++ b/backend/core/src/applications/applications.controller.ts @@ -5,178 +5,40 @@ import { Get, Header, Param, + ParseUUIDPipe, Post, Put, Query, UseGuards, + UseInterceptors, UsePipes, ValidationPipe, } from "@nestjs/common" -import { ApplicationsService } from "./applications.service" -import { ApiBearerAuth, ApiExtraModels, ApiOperation, ApiProperty, ApiTags } from "@nestjs/swagger" +import { ApiBearerAuth, ApiExtraModels, ApiOperation, ApiTags } from "@nestjs/swagger" import { OptionalAuthGuard } from "../auth/guards/optional-auth.guard" -import { AuthzGuard } from "../auth/guards/authz.guard" import { ResourceType } from "../auth/decorators/resource-type.decorator" import { mapTo } from "../shared/mapTo" -import { - ApplicationCreateDto, - ApplicationDto, - ApplicationUpdateDto, - PaginatedApplicationDto, -} from "./dto/application.dto" -import { Expose, Transform } from "class-transformer" -import { IsBoolean, IsOptional, IsString, IsIn } from "class-validator" -import { PaginationQueryParams } from "../shared/dto/pagination.dto" +import { ApplicationDto } from "./dto/application.dto" import { ValidationsGroupsEnum } from "../shared/types/validations-groups-enum" import { defaultValidationPipeOptions } from "../shared/default-validation-pipe-options" -import { ApplicationCsvExporter } from "../csv/application-csv-exporter" -import { applicationPreferenceApiExtraModels } from "./application-preference-api-extra-models" -import { ListingsService } from "../listings/listings.service" - -export enum OrderByParam { - firstName = "applicant.firstName", - lastName = "applicant.lastName", - submissionDate = "application.submissionDate", - createdAt = "application.createdAt", -} - -export enum OrderParam { - ASC = "ASC", - DESC = "DESC", -} - -class ApplicationsApiExtraModel { - @Expose() - @ApiProperty({ - enum: Object.keys(OrderByParam), - example: "createdAt", - default: "createdAt", - required: false, - }) - orderBy?: OrderByParam - - @Expose() - @ApiProperty({ - enum: OrderParam, - example: "DESC", - default: "DESC", - required: false, - }) - order?: OrderParam -} - -export class PaginatedApplicationListQueryParams extends PaginationQueryParams { - @Expose() - @ApiProperty({ - type: String, - example: "listingId", - required: false, - }) - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - listingId?: string - - @Expose() - @ApiProperty({ - type: String, - example: "search", - required: false, - }) - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - search?: string - - @Expose() - @ApiProperty({ - type: String, - example: "userId", - required: false, - }) - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - userId?: string - - @Expose() - @ApiProperty({ - enum: Object.keys(OrderByParam), - example: "createdAt", - default: "createdAt", - required: false, - }) - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @IsIn(Object.values(OrderByParam), { groups: [ValidationsGroupsEnum.default] }) - @Transform((value: string | undefined) => - value ? (OrderByParam[value] ? OrderByParam[value] : value) : OrderByParam.createdAt - ) - orderBy?: OrderByParam - - @Expose() - @ApiProperty({ - enum: OrderParam, - example: "DESC", - default: "DESC", - required: false, - }) - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @IsIn(Object.keys(OrderParam), { groups: [ValidationsGroupsEnum.default] }) - @Transform((value: string | undefined) => (value ? value : OrderParam.DESC)) - order?: OrderParam - - @Expose() - @ApiProperty({ - type: Boolean, - example: true, - required: false, - }) - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @Transform( - (value: string | undefined) => { - switch (value) { - case "true": - return true - case "false": - return false - default: - return undefined - } - }, - { toClassOnly: true } - ) - markedAsDuplicate?: boolean -} - -export class ApplicationsCsvListQueryParams extends PaginatedApplicationListQueryParams { - @Expose() - @ApiProperty({ - type: Boolean, - example: true, - required: false, - }) - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @Transform((value: string | undefined) => value === "true", { toClassOnly: true }) - includeHeaders?: boolean - - @Expose() - @ApiProperty({ - type: Boolean, - example: true, - required: false, - }) - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) - @Transform((value: string | undefined) => value === "true", { toClassOnly: true }) - includeDemographics?: boolean -} +import { applicationPreferenceApiExtraModels } from "./types/application-preference-api-extra-models" +import { ApplicationCsvExporterService } from "./services/application-csv-exporter.service" +import { ApplicationsService } from "./services/applications.service" +import { ActivityLogInterceptor } from "../activity-log/interceptors/activity-log.interceptor" +import { PaginatedApplicationListQueryParams } from "./dto/paginated-application-list-query-params" +import { ApplicationsCsvListQueryParams } from "./dto/applications-csv-list-query-params" +import { ApplicationsApiExtraModel } from "./types/applications-api-extra-model" +import { PaginatedApplicationDto } from "./dto/paginated-application.dto" +import { ApplicationCreateDto } from "./dto/application-create.dto" +import { ApplicationUpdateDto } from "./dto/application-update.dto" +import { IdDto } from "../shared/dto/id.dto" @Controller("applications") @ApiTags("applications") @ApiBearerAuth() @ResourceType("application") -@UseGuards(OptionalAuthGuard, AuthzGuard) +@UseGuards(OptionalAuthGuard) +@UseInterceptors(ActivityLogInterceptor) @UsePipes( new ValidationPipe({ ...defaultValidationPipeOptions, @@ -187,8 +49,7 @@ export class ApplicationsCsvListQueryParams extends PaginatedApplicationListQuer export class ApplicationsController { constructor( private readonly applicationsService: ApplicationsService, - private readonly listingsService: ListingsService, - private readonly applicationCsvExporter: ApplicationCsvExporter + private readonly applicationCsvExporter: ApplicationCsvExporterService ) {} @Get() @@ -202,13 +63,13 @@ export class ApplicationsController { @Get(`csv`) @ApiOperation({ summary: "List applications as csv", operationId: "listAsCsv" }) @Header("Content-Type", "text/csv") - async listAsCsv(@Query() queryParams: ApplicationsCsvListQueryParams): Promise { - const applications = await this.applicationsService.listWithFlagged(queryParams) - const listing = await this.listingsService.findOne(queryParams.listingId) - return this.applicationCsvExporter.export( + async listAsCsv( + @Query(new ValidationPipe(defaultValidationPipeOptions)) + queryParams: ApplicationsCsvListQueryParams + ): Promise { + const applications = await this.applicationsService.rawListWithFlagged(queryParams) + return this.applicationCsvExporter.exportFromObject( applications, - listing.CSVFormattingType, - queryParams.includeHeaders, queryParams.includeDemographics ) } @@ -220,25 +81,28 @@ export class ApplicationsController { return mapTo(ApplicationDto, application) } - @Get(`:applicationId`) + @Get(`:id`) @ApiOperation({ summary: "Get application by id", operationId: "retrieve" }) - async retrieve(@Param("applicationId") applicationId: string): Promise { + async retrieve( + @Param("id", new ParseUUIDPipe({ version: "4" })) applicationId: string + ): Promise { const app = await this.applicationsService.findOne(applicationId) return mapTo(ApplicationDto, app) } - @Put(`:applicationId`) + @Put(`:id`) @ApiOperation({ summary: "Update application by id", operationId: "update" }) async update( - @Param("applicationId") applicationId: string, + @Param("id") applicationId: string, @Body() applicationUpdateDto: ApplicationUpdateDto ): Promise { return mapTo(ApplicationDto, await this.applicationsService.update(applicationUpdateDto)) } - @Delete(`:applicationId`) + // codegen generate unusable code for this, if we don't have a body + @Delete() @ApiOperation({ summary: "Delete application by id", operationId: "delete" }) - async delete(@Param("applicationId") applicationId: string) { - await this.applicationsService.delete(applicationId) + async delete(@Body() dto: IdDto) { + await this.applicationsService.delete(dto.id) } } diff --git a/backend/core/src/applications/applications.module.ts b/backend/core/src/applications/applications.module.ts index d60f67ebcc..36db6b78fd 100644 --- a/backend/core/src/applications/applications.module.ts +++ b/backend/core/src/applications/applications.module.ts @@ -1,32 +1,36 @@ import { Module } from "@nestjs/common" import { TypeOrmModule } from "@nestjs/typeorm" import { Application } from "./entities/application.entity" -import { ApplicationsService } from "./applications.service" import { ApplicationsController } from "./applications.controller" import { AuthModule } from "../auth/auth.module" -import { CsvEncoder } from "../csv/csv-encoder.service" -import { CsvBuilder } from "../csv/csv-builder.service" import { SharedModule } from "../shared/shared.module" import { ListingsModule } from "../listings/listings.module" import { Address } from "../shared/entities/address.entity" import { Applicant } from "./entities/applicant.entity" import { ApplicationsSubmissionController } from "./applications-submission.controller" -import { ApplicationCsvExporter } from "../csv/application-csv-exporter" import { ApplicationFlaggedSetsModule } from "../application-flagged-sets/application-flagged-sets.module" -import { EmailModule } from "../shared/email/email.module" import { TranslationsModule } from "../translations/translations.module" +import { Listing } from "../listings/entities/listing.entity" +import { ScheduleModule } from "@nestjs/schedule" +import { ApplicationsService } from "./services/applications.service" +import { CsvBuilder } from "./services/csv-builder.service" +import { ApplicationCsvExporterService } from "./services/application-csv-exporter.service" +import { EmailModule } from "../email/email.module" +import { ActivityLogModule } from "../activity-log/activity-log.module" @Module({ imports: [ - TypeOrmModule.forFeature([Application, Applicant, Address]), + TypeOrmModule.forFeature([Application, Applicant, Address, Listing]), AuthModule, + ActivityLogModule, SharedModule, ListingsModule, ApplicationFlaggedSetsModule, TranslationsModule, EmailModule, + ScheduleModule.forRoot(), ], - providers: [ApplicationsService, CsvEncoder, CsvBuilder, ApplicationCsvExporter], + providers: [ApplicationsService, CsvBuilder, ApplicationCsvExporterService], exports: [ApplicationsService], controllers: [ApplicationsController, ApplicationsSubmissionController], }) diff --git a/backend/core/src/applications/applications.service.ts b/backend/core/src/applications/applications.service.ts deleted file mode 100644 index e112360ca5..0000000000 --- a/backend/core/src/applications/applications.service.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { - BadRequestException, - HttpException, - HttpStatus, - Inject, - Injectable, - Scope, -} from "@nestjs/common" -import { Application } from "./entities/application.entity" -import { ApplicationCreateDto, ApplicationUpdateDto } from "./dto/application.dto" -import { InjectRepository } from "@nestjs/typeorm" -import { QueryFailedError, Repository } from "typeorm" -import { paginate, Pagination } from "nestjs-typeorm-paginate" -import { PaginatedApplicationListQueryParams } from "./applications.controller" -import { ApplicationFlaggedSetsService } from "../application-flagged-sets/application-flagged-sets.service" -import { assignDefined } from "../shared/assign-defined" -import { AuthzService } from "../auth/services/authz.service" -import { Request as ExpressRequest } from "express" -import { ListingsService } from "../listings/listings.service" -import { EmailService } from "../shared/email/email.service" -import { REQUEST } from "@nestjs/core" -import retry from "async-retry" -import { authzActions } from "../auth/enum/authz-actions.enum" -import crypto from "crypto" - -@Injectable({ scope: Scope.REQUEST }) -export class ApplicationsService { - constructor( - @Inject(REQUEST) private req: ExpressRequest, - private readonly applicationFlaggedSetsService: ApplicationFlaggedSetsService, - private readonly authzService: AuthzService, - private readonly listingsService: ListingsService, - private readonly emailService: EmailService, - @InjectRepository(Application) private readonly repository: Repository - ) {} - - public async list(params: PaginatedApplicationListQueryParams) { - const qb = this._getQb(params) - const result = await qb.getMany() - await Promise.all( - result.map(async (application) => { - await this.authorizeUserAction(this.req.user, application, authzActions.read) - }) - ) - return result - } - - public async listWithFlagged(params: PaginatedApplicationListQueryParams) { - const qb = this._getQb(params) - const result = await qb.getMany() - - // Get flagged applications - const flaggedQuery = await this.repository - .createQueryBuilder("applications") - .leftJoin( - "application_flagged_set_applications_applications", - "application_flagged_set_applications_applications", - "application_flagged_set_applications_applications.applications_id = applications.id" - ) - .andWhere("applications.listing_id = :lid", { lid: params.listingId }) - .select( - "applications.id, count(application_flagged_set_applications_applications.applications_id) > 0 as flagged" - ) - .groupBy("applications.id") - .getRawAndEntities() - - // Reorganize flagged to object to make it faster to map - const flagged = flaggedQuery.raw.reduce((obj, application) => { - return { ...obj, [application.id]: application.flagged } - }, {}) - await Promise.all( - result.map(async (application) => { - // Because TypeOrm can't map extra flagged field we need to map it manually - application.flagged = flagged[application.id] - await this.authorizeUserAction(this.req.user, application, authzActions.read) - }) - ) - return result - } - - async listPaginated( - params: PaginatedApplicationListQueryParams - ): Promise> { - const qb = this._getQb(params) - const result = await paginate(qb, { limit: params.limit, page: params.page }) - await Promise.all( - result.items.map(async (application) => { - await this.authorizeUserAction(this.req.user, application, authzActions.read) - }) - ) - return result - } - - async submit(applicationCreateDto: ApplicationCreateDto) { - applicationCreateDto.submissionDate = new Date() - const listing = await this.listingsService.findOne(applicationCreateDto.listing.id) - if ( - listing.applicationDueDate && - applicationCreateDto.submissionDate > listing.applicationDueDate - ) { - throw new BadRequestException("Listing is not open for application submission.") - } - await this.authorizeUserAction(this.req.user, applicationCreateDto, authzActions.submit) - return await this._create(applicationCreateDto) - } - - async create(applicationCreateDto: ApplicationCreateDto) { - await this.authorizeUserAction(this.req.user, applicationCreateDto, authzActions.create) - return this._create(applicationCreateDto) - } - - async findOne(applicationId: string) { - const application = await this.repository.findOneOrFail({ - where: { - id: applicationId, - }, - }) - await this.authorizeUserAction(this.req.user, application, authzActions.read) - return application - } - - async update(applicationUpdateDto: ApplicationUpdateDto, existing?: Application) { - const application = - existing || - (await this.repository.findOneOrFail({ - where: { id: applicationUpdateDto.id }, - })) - await this.authorizeUserAction(this.req.user, application, authzActions.update) - assignDefined(application, { - ...applicationUpdateDto, - id: application.id, - }) - - return await this.repository.manager.transaction( - "SERIALIZABLE", - async (transactionalEntityManager) => { - const applicationsRepository = transactionalEntityManager.getRepository(Application) - const newApplication = await applicationsRepository.save(application) - await this.applicationFlaggedSetsService.onApplicationUpdate( - application, - transactionalEntityManager - ) - - return await applicationsRepository.findOne({ id: newApplication.id }) - } - ) - } - - async delete(applicationId: string) { - await this.findOne(applicationId) - return await this.repository.softRemove({ id: applicationId }) - } - - private _getQb(params: PaginatedApplicationListQueryParams) { - /** - * Map used to generate proper parts - * of query builder. - */ - const paramsMap = { - markedAsDuplicate: (qb, { markedAsDuplicate }) => - qb.andWhere("application.markedAsDuplicate = :markedAsDuplicate", { - markedAsDuplicate: markedAsDuplicate, - }), - userId: (qb, { userId }) => qb.andWhere("application.user_id = :uid", { uid: userId }), - listingId: (qb, { listingId }) => - qb.andWhere("application.listing_id = :lid", { lid: listingId }), - orderBy: (qb, { orderBy, order }) => qb.orderBy(orderBy, order, "NULLS LAST"), - search: (qb, { search }) => - qb.andWhere( - `to_tsvector('english', REGEXP_REPLACE(concat_ws(' ', applicant, alternateContact.emailAddress), '[_]|[-]', '/', 'g')) @@ to_tsquery(CONCAT(CAST(plainto_tsquery(REGEXP_REPLACE(:search, '[_]|[-]', '/', 'g')) as text), ':*'))`, - { - search, - } - ), - } - - // --> Build main query - const qb = this.repository.createQueryBuilder("application") - qb.leftJoinAndSelect("application.applicant", "applicant") - qb.leftJoinAndSelect("applicant.address", "applicant_address") - qb.leftJoinAndSelect("applicant.workAddress", "applicant_workAddress") - qb.leftJoinAndSelect("application.alternateAddress", "alternateAddress") - qb.leftJoinAndSelect("application.mailingAddress", "mailingAddress") - qb.leftJoinAndSelect("application.alternateContact", "alternateContact") - qb.leftJoinAndSelect("alternateContact.mailingAddress", "alternateContact_mailingAddress") - qb.leftJoinAndSelect("application.accessibility", "accessibility") - qb.leftJoinAndSelect("application.demographics", "demographics") - qb.leftJoinAndSelect("application.householdMembers", "householdMembers") - qb.leftJoinAndSelect("householdMembers.address", "householdMembers_address") - qb.leftJoinAndSelect("householdMembers.workAddress", "householdMembers_workAddress") - qb.leftJoinAndSelect("application.preferredUnit", "preferredUnit") - qb.where("application.id IS NOT NULL") - - // --> Build additional query builder parts - Object.keys(paramsMap).forEach((paramKey) => { - // e.g. markedAsDuplicate can be false and wouldn't be applied here - if (params[paramKey] !== undefined) { - paramsMap[paramKey](qb, params) - } - }) - return qb - } - - private async _createApplication(applicationCreateDto: ApplicationUpdateDto) { - return await this.repository.manager.transaction( - "SERIALIZABLE", - async (transactionalEntityManager) => { - const applicationsRepository = transactionalEntityManager.getRepository(Application) - const application = await applicationsRepository.save({ - ...applicationCreateDto, - user: this.req.user, - confirmationCode: ApplicationsService.generateConfirmationCode(), - }) - await this.applicationFlaggedSetsService.onApplicationSave( - application, - transactionalEntityManager - ) - return await applicationsRepository.findOne({ id: application.id }) - } - ) - } - - private async _create(applicationCreateDto: ApplicationUpdateDto) { - let application: Application - - try { - await retry( - async (bail) => { - try { - application = await this._createApplication(applicationCreateDto) - } catch (e) { - console.error(e.message) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if ( - !( - e instanceof QueryFailedError && - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // NOTE: 40001 could not serialize access due to read/write dependencies among transactions - (e.code === "40001" || - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // NOTE: constraint UQ_556c258a4439f1b7f53de2ed74f checks whether listing.id & confirmationCode is a unique combination - // it does make sense here to retry because it's a randomly generated 8 character string value - (e.code === "23505" && e.constraint === "UQ_556c258a4439f1b7f53de2ed74f")) - ) - ) { - bail(e) - return - } - throw e - } - }, - { retries: 6, minTimeout: 200 } - ) - } catch (e) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (e instanceof QueryFailedError && e.code === "40001") { - throw new HttpException( - { - statusCode: HttpStatus.TOO_MANY_REQUESTS, - error: "Too Many Requests", - message: "Please try again later.", - }, - 429 - ) - } - throw e - } - - // Listing is not eagerly joined on application entity so let's use the one provided with - // create dto - const listing = await this.listingsService.findOne(applicationCreateDto.listing.id) - if (application.applicant.emailAddress) { - await this.emailService.confirmation(listing, application, applicationCreateDto.appUrl) - } - return application - } - - private async authorizeUserAction( - user, - app: T, - action - ) { - let resource: T = app - - if (app instanceof Application) { - resource = { - ...app, - user_id: app.userId, - listing_id: app.listingId, - } - } else if (app instanceof ApplicationCreateDto) { - resource = { - ...app, - listing_id: app.listing.id, - } - } - return this.authzService.canOrThrow(user, "application", action, resource) - } - - public static generateConfirmationCode(): string { - return crypto.randomBytes(4).toString("hex").toUpperCase() - } -} diff --git a/backend/core/src/applications/dto/application-create.dto.ts b/backend/core/src/applications/dto/application-create.dto.ts new file mode 100644 index 0000000000..0de263e30b --- /dev/null +++ b/backend/core/src/applications/dto/application-create.dto.ts @@ -0,0 +1,86 @@ +import { OmitType } from "@nestjs/swagger" +import { Expose, Type } from "class-transformer" +import { ArrayMaxSize, IsDefined, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { IdDto } from "../../shared/dto/id.dto" +import { ApplicantCreateDto } from "./applicant.dto" +import { AddressCreateDto } from "../../shared/dto/address.dto" +import { AlternateContactCreateDto } from "./alternate-contact.dto" +import { AccessibilityCreateDto } from "./accessibility.dto" +import { DemographicsCreateDto } from "./demographics.dto" +import { HouseholdMemberCreateDto } from "./household-member.dto" +import { ApplicationDto } from "./application.dto" + +export class ApplicationCreateDto extends OmitType(ApplicationDto, [ + "id", + "createdAt", + "updatedAt", + "deletedAt", + "applicant", + "listing", + "user", + "mailingAddress", + "alternateAddress", + "alternateContact", + "accessibility", + "demographics", + "householdMembers", + "markedAsDuplicate", + "preferredUnit", + "confirmationCode", +] as const) { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDto) + listing: IdDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => ApplicantCreateDto) + applicant: ApplicantCreateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressCreateDto) + mailingAddress: AddressCreateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressCreateDto) + alternateAddress: AddressCreateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AlternateContactCreateDto) + alternateContact: AlternateContactCreateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AccessibilityCreateDto) + accessibility: AccessibilityCreateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => DemographicsCreateDto) + demographics: DemographicsCreateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @ArrayMaxSize(32, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => HouseholdMemberCreateDto) + householdMembers: HouseholdMemberCreateDto[] + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => IdDto) + preferredUnit: IdDto[] +} diff --git a/backend/core/src/applications/dto/application-update.dto.ts b/backend/core/src/applications/dto/application-update.dto.ts new file mode 100644 index 0000000000..99268080f6 --- /dev/null +++ b/backend/core/src/applications/dto/application-update.dto.ts @@ -0,0 +1,115 @@ +import { OmitType } from "@nestjs/swagger" +import { Expose, Type } from "class-transformer" +import { + ArrayMaxSize, + IsDate, + IsDefined, + IsOptional, + IsUUID, + ValidateNested, +} from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { IdDto } from "../../shared/dto/id.dto" +import { ApplicantUpdateDto } from "./applicant.dto" +import { AddressUpdateDto } from "../../shared/dto/address.dto" +import { AlternateContactUpdateDto } from "./alternate-contact.dto" +import { AccessibilityUpdateDto } from "./accessibility.dto" +import { DemographicsUpdateDto } from "./demographics.dto" +import { HouseholdMemberUpdateDto } from "./household-member.dto" +import { ApplicationDto } from "./application.dto" + +export class ApplicationUpdateDto extends OmitType(ApplicationDto, [ + "id", + "createdAt", + "updatedAt", + "deletedAt", + "applicant", + "listing", + "user", + "mailingAddress", + "alternateAddress", + "alternateContact", + "accessibility", + "demographics", + "householdMembers", + "markedAsDuplicate", + "preferredUnit", + "confirmationCode", +] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + id?: string + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + createdAt?: Date + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + updatedAt?: Date + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + deletedAt?: Date + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDto) + listing: IdDto + + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => ApplicantUpdateDto) + applicant: ApplicantUpdateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressUpdateDto) + mailingAddress: AddressUpdateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AddressUpdateDto) + alternateAddress: AddressUpdateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AlternateContactUpdateDto) + alternateContact: AlternateContactUpdateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AccessibilityUpdateDto) + accessibility: AccessibilityUpdateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => DemographicsUpdateDto) + demographics: DemographicsUpdateDto + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @ArrayMaxSize(32, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => HouseholdMemberUpdateDto) + householdMembers: HouseholdMemberUpdateDto[] + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => IdDto) + preferredUnit: IdDto[] +} diff --git a/backend/core/src/applications/dto/application.dto.ts b/backend/core/src/applications/dto/application.dto.ts index 1e1d6cb69f..052d227418 100644 --- a/backend/core/src/applications/dto/application.dto.ts +++ b/backend/core/src/applications/dto/application.dto.ts @@ -1,34 +1,14 @@ import { OmitType } from "@nestjs/swagger" -import { - ArrayMaxSize, - IsDate, - IsDefined, - IsOptional, - IsUUID, - ValidateNested, -} from "class-validator" +import { ArrayMaxSize, IsDefined, ValidateNested } from "class-validator" import { Application } from "../entities/application.entity" import { Expose, plainToClass, Transform, Type } from "class-transformer" import { IdDto } from "../../shared/dto/id.dto" -import { PaginationFactory } from "../../shared/dto/pagination.dto" -import { ApplicantCreateDto, ApplicantDto, ApplicantUpdateDto } from "./applicant.dto" -import { AddressCreateDto, AddressDto, AddressUpdateDto } from "../../shared/dto/address.dto" -import { - AlternateContactCreateDto, - AlternateContactDto, - AlternateContactUpdateDto, -} from "./alternate-contact.dto" -import { DemographicsCreateDto, DemographicsDto, DemographicsUpdateDto } from "./demographics.dto" -import { - HouseholdMemberCreateDto, - HouseholdMemberDto, - HouseholdMemberUpdateDto, -} from "./household-member.dto" -import { - AccessibilityCreateDto, - AccessibilityDto, - AccessibilityUpdateDto, -} from "./accessibility.dto" +import { ApplicantDto } from "./applicant.dto" +import { AddressDto } from "../../shared/dto/address.dto" +import { AlternateContactDto } from "./alternate-contact.dto" +import { DemographicsDto } from "./demographics.dto" +import { HouseholdMemberDto } from "./household-member.dto" +import { AccessibilityDto } from "./accessibility.dto" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { UnitTypeDto } from "../../unit-types/dto/unit-type.dto" @@ -119,175 +99,3 @@ export class ApplicationDto extends OmitType(Application, [ @Type(() => UnitTypeDto) preferredUnit: UnitTypeDto[] } - -export class PaginatedApplicationDto extends PaginationFactory(ApplicationDto) {} - -export class ApplicationCreateDto extends OmitType(ApplicationDto, [ - "id", - "createdAt", - "updatedAt", - "deletedAt", - "applicant", - "listing", - "user", - "mailingAddress", - "alternateAddress", - "alternateContact", - "accessibility", - "demographics", - "householdMembers", - "markedAsDuplicate", - "preferredUnit", - "confirmationCode", -] as const) { - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => IdDto) - listing: IdDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => ApplicantCreateDto) - applicant: ApplicantCreateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AddressCreateDto) - mailingAddress: AddressCreateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AddressCreateDto) - alternateAddress: AddressCreateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AlternateContactCreateDto) - alternateContact: AlternateContactCreateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AccessibilityCreateDto) - accessibility: AccessibilityCreateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => DemographicsCreateDto) - demographics: DemographicsCreateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @ArrayMaxSize(32, { groups: [ValidationsGroupsEnum.default] }) - @Type(() => HouseholdMemberCreateDto) - householdMembers: HouseholdMemberCreateDto[] - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => IdDto) - preferredUnit: IdDto[] -} - -export class ApplicationUpdateDto extends OmitType(ApplicationDto, [ - "id", - "createdAt", - "updatedAt", - "deletedAt", - "applicant", - "listing", - "user", - "mailingAddress", - "alternateAddress", - "alternateContact", - "accessibility", - "demographics", - "householdMembers", - "markedAsDuplicate", - "preferredUnit", - "confirmationCode", -] as const) { - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) - id?: string - - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsDate({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Date) - createdAt?: Date - - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsDate({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Date) - updatedAt?: Date - - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsDate({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Date) - deletedAt?: Date - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => IdDto) - listing: IdDto - - @Expose() - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => ApplicantUpdateDto) - applicant: ApplicantUpdateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AddressUpdateDto) - mailingAddress: AddressUpdateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AddressUpdateDto) - alternateAddress: AddressUpdateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AlternateContactUpdateDto) - alternateContact: AlternateContactUpdateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AccessibilityUpdateDto) - accessibility: AccessibilityUpdateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => DemographicsUpdateDto) - demographics: DemographicsUpdateDto - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @ArrayMaxSize(32, { groups: [ValidationsGroupsEnum.default] }) - @Type(() => HouseholdMemberUpdateDto) - householdMembers: HouseholdMemberUpdateDto[] - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => IdDto) - preferredUnit: IdDto[] -} diff --git a/backend/core/src/applications/dto/applications-csv-list-query-params.ts b/backend/core/src/applications/dto/applications-csv-list-query-params.ts new file mode 100644 index 0000000000..6e4b8c8efc --- /dev/null +++ b/backend/core/src/applications/dto/applications-csv-list-query-params.ts @@ -0,0 +1,28 @@ +import { PaginatedApplicationListQueryParams } from "./paginated-application-list-query-params" +import { Expose, Transform } from "class-transformer" +import { ApiProperty, OmitType } from "@nestjs/swagger" +import { IsBoolean, IsOptional, IsUUID } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +export class ApplicationsCsvListQueryParams extends OmitType(PaginatedApplicationListQueryParams, [ + "listingId", +]) { + @Expose() + @ApiProperty({ + type: String, + required: true, + }) + @IsUUID() + listingId: string + + @Expose() + @ApiProperty({ + type: Boolean, + example: true, + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @Transform((value: string | undefined) => value === "true", { toClassOnly: true }) + includeDemographics?: boolean +} diff --git a/backend/core/src/applications/dto/paginated-application-list-query-params.ts b/backend/core/src/applications/dto/paginated-application-list-query-params.ts new file mode 100644 index 0000000000..886b655ad5 --- /dev/null +++ b/backend/core/src/applications/dto/paginated-application-list-query-params.ts @@ -0,0 +1,95 @@ +import { PaginationQueryParams } from "../../shared/dto/pagination.dto" +import { Expose, Transform } from "class-transformer" +import { ApiProperty } from "@nestjs/swagger" +import { IsBoolean, IsIn, IsOptional, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { OrderByParam } from "../types/order-by-param" +import { OrderParam } from "../types/order-param" + +import { IsLength } from "../../shared/decorators/isLength.decorator" +export class PaginatedApplicationListQueryParams extends PaginationQueryParams { + @Expose() + @ApiProperty({ + type: String, + example: "listingId", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + listingId?: string + + @Expose() + @ApiProperty({ + type: String, + example: "search", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsLength("search", { + message: "Search must be at least 3 characters", + groups: [ValidationsGroupsEnum.default], + }) + search?: string + + @Expose() + @ApiProperty({ + type: String, + example: "userId", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + userId?: string + + @Expose() + @ApiProperty({ + enum: Object.keys(OrderByParam), + example: "createdAt", + default: "createdAt", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsIn(Object.values(OrderByParam), { groups: [ValidationsGroupsEnum.default] }) + @Transform((value: string | undefined) => + value ? (OrderByParam[value] ? OrderByParam[value] : value) : OrderByParam.createdAt + ) + orderBy?: OrderByParam + + @Expose() + @ApiProperty({ + enum: OrderParam, + example: "DESC", + default: "DESC", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsIn(Object.keys(OrderParam), { groups: [ValidationsGroupsEnum.default] }) + @Transform((value: string | undefined) => (value ? value : OrderParam.DESC)) + order?: OrderParam + + @Expose() + @ApiProperty({ + type: Boolean, + example: true, + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @Transform( + (value: string | undefined) => { + switch (value) { + case "true": + return true + case "false": + return false + default: + return undefined + } + }, + { toClassOnly: true } + ) + markedAsDuplicate?: boolean +} diff --git a/backend/core/src/applications/dto/paginated-application.dto.ts b/backend/core/src/applications/dto/paginated-application.dto.ts new file mode 100644 index 0000000000..74e7447d6d --- /dev/null +++ b/backend/core/src/applications/dto/paginated-application.dto.ts @@ -0,0 +1,4 @@ +import { PaginationFactory } from "../../shared/dto/pagination.dto" +import { ApplicationDto } from "./application.dto" + +export class PaginatedApplicationDto extends PaginationFactory(ApplicationDto) {} diff --git a/backend/core/src/applications/entities/alternate-contact.entity.ts b/backend/core/src/applications/entities/alternate-contact.entity.ts index 48b2f604d9..72ec7e10f9 100644 --- a/backend/core/src/applications/entities/alternate-contact.entity.ts +++ b/backend/core/src/applications/entities/alternate-contact.entity.ts @@ -11,6 +11,7 @@ import { } from "class-validator" import { Address } from "../../shared/entities/address.entity" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" @Entity() export class AlternateContact extends AbstractEntity { @@ -61,6 +62,7 @@ export class AlternateContact extends AbstractEntity { @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() emailAddress?: string | null @OneToOne(() => Address, { eager: true, cascade: true }) diff --git a/backend/core/src/applications/entities/applicant.entity.ts b/backend/core/src/applications/entities/applicant.entity.ts index a4f5fa4dfa..1a86d85312 100644 --- a/backend/core/src/applications/entities/applicant.entity.ts +++ b/backend/core/src/applications/entities/applicant.entity.ts @@ -14,6 +14,7 @@ import { } from "class-validator" import { Address } from "../../shared/entities/address.entity" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" @Entity() export class Applicant extends AbstractEntity { @@ -65,6 +66,7 @@ export class Applicant extends AbstractEntity { @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() emailAddress?: string | null @Column({ type: "bool", nullable: true }) diff --git a/backend/core/src/applications/entities/application-program.entity.ts b/backend/core/src/applications/entities/application-program.entity.ts new file mode 100644 index 0000000000..b26a16fec1 --- /dev/null +++ b/backend/core/src/applications/entities/application-program.entity.ts @@ -0,0 +1,21 @@ +import { Expose, Type } from "class-transformer" +import { ArrayMaxSize, IsBoolean, IsString, MaxLength, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { ApplicationProgramOption } from "../types/application-program-option" + +export class ApplicationProgram { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(128, { groups: [ValidationsGroupsEnum.default] }) + key: string + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + claimed: boolean + + @Expose() + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ApplicationProgramOption) + options: Array +} diff --git a/backend/core/src/applications/entities/application.entity.ts b/backend/core/src/applications/entities/application.entity.ts index 4dc0d0d72d..4d1ed0ade5 100644 --- a/backend/core/src/applications/entities/application.entity.ts +++ b/backend/core/src/applications/entities/application.entity.ts @@ -2,6 +2,7 @@ import { Column, DeleteDateColumn, Entity, + Index, JoinColumn, JoinTable, ManyToMany, @@ -41,9 +42,11 @@ import { ApplicationStatus } from "../types/application-status-enum" import { ApplicationSubmissionType } from "../types/application-submission-type-enum" import { IncomePeriod } from "../types/income-period-enum" import { UnitType } from "../../unit-types/entities/unit-type.entity" +import { ApplicationProgram } from "./application-program.entity" @Entity({ name: "applications" }) @Unique(["listing", "confirmationCode"]) +@Index(["listing"]) export class Application extends AbstractEntity { @DeleteDateColumn() @Expose() @@ -59,7 +62,7 @@ export class Application extends AbstractEntity { @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) appUrl?: string | null - @ManyToOne(() => User, { nullable: true }) + @ManyToOne(() => User, { nullable: true, onDelete: "SET NULL" }) user?: User | null @RelationId((application: Application) => application.user) @@ -166,6 +169,18 @@ export class Application extends AbstractEntity { @Type(() => Demographics) demographics: Demographics + @Column({ type: "bool", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.partners] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + householdExpectingChanges?: boolean | null + + @Column({ type: "bool", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.partners] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + householdStudent?: boolean | null + @Column({ type: "bool", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.partners] }) @@ -209,6 +224,14 @@ export class Application extends AbstractEntity { @Type(() => ApplicationPreference) preferences: ApplicationPreference[] + @Column({ type: "jsonb", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ApplicationProgram) + programs?: ApplicationProgram[] + @Column({ enum: ApplicationStatus }) @Expose() @IsEnum(ApplicationStatus, { groups: [ValidationsGroupsEnum.default] }) diff --git a/backend/core/src/applications/entities/demographics.entity.ts b/backend/core/src/applications/entities/demographics.entity.ts index 2b09d362f0..939a0081bc 100644 --- a/backend/core/src/applications/entities/demographics.entity.ts +++ b/backend/core/src/applications/entities/demographics.entity.ts @@ -34,10 +34,10 @@ export class Demographics extends AbstractEntity { @MaxLength(64, { groups: [ValidationsGroupsEnum.default], each: true }) howDidYouHear: string[] - @Column({ type: "text", nullable: true }) + @Column({ array: true, type: "text", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.partners] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) - race?: string | null + @ArrayMaxSize(64, { groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) + race?: string[] | null } diff --git a/backend/core/src/applications/entities/household-member.entity.ts b/backend/core/src/applications/entities/household-member.entity.ts index 68da5d8bbb..6663147d44 100644 --- a/backend/core/src/applications/entities/household-member.entity.ts +++ b/backend/core/src/applications/entities/household-member.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from "typeorm" +import { Column, Entity, Index, JoinColumn, ManyToOne, OneToOne } from "typeorm" import { AbstractEntity } from "../../shared/entities/abstract.entity" import { Expose, Type } from "class-transformer" import { @@ -14,8 +14,9 @@ import { import { Address } from "../../shared/entities/address.entity" import { Application } from "./application.entity" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" - +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" @Entity() +@Index(["application"]) export class HouseholdMember extends AbstractEntity { @Column({ nullable: true }) @Expose() @@ -77,6 +78,7 @@ export class HouseholdMember extends AbstractEntity { @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.partners] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() emailAddress?: string | null @Column({ nullable: true, type: "boolean" }) diff --git a/backend/core/src/applications/services/application-csv-exporter.service.ts b/backend/core/src/applications/services/application-csv-exporter.service.ts new file mode 100644 index 0000000000..de22f41c26 --- /dev/null +++ b/backend/core/src/applications/services/application-csv-exporter.service.ts @@ -0,0 +1,264 @@ +import { Injectable, Scope } from "@nestjs/common" +import dayjs from "dayjs" +import { CsvBuilder, KeyNumber } from "./csv-builder.service" +import { getBirthday } from "../../shared/utils/get-birthday" +import { formatBoolean } from "../../shared/utils/format-boolean" +import { capitalizeFirstLetter } from "../../shared/utils/capitalize-first-letter" +import { capAndSplit } from "../../shared/utils/cap-and-split" +import { ApplicationProgram } from "../entities/application-program.entity" +import { ApplicationPreference } from "../entities/application-preferences.entity" +import { AddressCreateDto } from "../../shared/dto/address.dto" + +@Injectable({ scope: Scope.REQUEST }) +export class ApplicationCsvExporterService { + constructor(private readonly csvBuilder: CsvBuilder) {} + + mapHouseholdMembers(app) { + const obj = { + "First Name": app.householdMembers_first_name, + "Middle Name": app.householdMembers_middle_name, + "Last Name": app.householdMembers_last_name, + Birthday: getBirthday( + app.householdMembers_birth_day, + app.householdMembers_birth_month, + app.householdMembers_birth_year + ), + "Same Address as Primary Applicant": formatBoolean(app.householdMembers_same_address), + Relationship: app.householdMembers_relationship, + "Work in Region": formatBoolean(app.householdMembers_work_in_region), + Street: app.householdMembers_address_street, + "Street 2": app.householdMembers_address_street2, + City: app.householdMembers_address_city, + State: app.householdMembers_address_state, + "Zip Code": app.householdMembers_address_zip_code, + } + return obj + } + + // could use translations + unitTypeToReadable(type) { + const typeMap = { + studio: "Studio", + oneBdrm: "One Bedroom", + twoBdrm: "Two Bedroom", + threeBdrm: "Three Bedroom", + fourBdrm: "Four+ Bedroom", + } + return typeMap[type] ?? type + } + + raceToReadable(type) { + const [rootKey, customValue = ""] = type.split(":") + const typeMap = { + americanIndianAlaskanNative: "American Indian / Alaskan Native", + asian: "Asian", + "asian-asianIndian": "Asian[Asian Indian]", + "asian-otherAsian": `Asian[Other Asian:${customValue}]`, + blackAfricanAmerican: "Black / African American", + "asian-chinese": "Asian[Chinese]", + declineToRespond: "Decline to Respond", + "asian-filipino": "Asian[Filipino]", + "nativeHawaiianOtherPacificIslander-guamanianOrChamorro": + "Native Hawaiian / Other Pacific Islander[Guamanian or Chamorro]", + "asian-japanese": "Asian[Japanese]", + "asian-korean": "Asian[Korean]", + "nativeHawaiianOtherPacificIslander-nativeHawaiian": + "Native Hawaiian / Other Pacific Islander[Native Hawaiian]", + nativeHawaiianOtherPacificIslander: "Native Hawaiian / Other Pacific Islander", + otherMultiracial: `Other / Multiracial:${customValue}`, + "nativeHawaiianOtherPacificIslander-otherPacificIslander": `Native Hawaiian / Other Pacific Islander[Other Pacific Islander:${customValue}]`, + "nativeHawaiianOtherPacificIslander-samoan": + "Native Hawaiian / Other Pacific Islander[Samoan]", + "asian-vietnamese": "Asian[Vietnamese]", + white: "White", + } + return typeMap[rootKey] ?? rootKey + } + + buildProgram(items: ApplicationProgram[], programKeys: KeyNumber) { + return this.buildPreference(items, programKeys) + } + + buildPreference( + items: ApplicationPreference[] | ApplicationProgram[], + preferenceKeys: KeyNumber + ) { + if (!items) { + return {} + } + + return items.reduce((obj, preference) => { + const root = capAndSplit(preference.key) + preference.options.forEach((option) => { + // TODO: remove temporary patch + if (option.key === "residencyNoColiseum") { + option.key = "residency" + } + const key = `${root}: ${capAndSplit(option.key)}` + preferenceKeys[key] = 1 + if (option.checked) { + obj[key] = "claimed" + } + if (option.extraData?.length) { + const extraKey = `${key} - ${option.extraData.map((obj) => obj.key).join(" and ")}` + let extraString = "" + option.extraData.forEach((extra) => { + if (extra.type === "text") { + extraString += `${capitalizeFirstLetter(extra.key)}: ${extra.value as string}, ` + } else if (extra.type === "address") { + extraString += `Street: ${(extra.value as AddressCreateDto).street}, Street 2: ${ + (extra.value as AddressCreateDto).street2 + }, City: ${(extra.value as AddressCreateDto).city}, State: ${ + (extra.value as AddressCreateDto).state + }, Zip Code: ${(extra.value as AddressCreateDto).zipCode}` + } + }) + preferenceKeys[extraKey] = 1 + obj[extraKey] = extraString + } + }) + return obj + }, {}) + } + + exportFromObject(applications: { [key: string]: any }, includeDemographics?: boolean): string { + const extraHeaders: KeyNumber = { + "Household Members": 1, + Preference: 1, + Program: 1, + } + const preferenceKeys: KeyNumber = {} + const programKeys: KeyNumber = {} + const applicationsObj = applications.reduce((obj, app) => { + let demographics = {} + + if (obj[app.application_id] === undefined) { + if (includeDemographics) { + demographics = { + Ethnicity: app.demographics_ethnicity, + Race: app.demographics_race.map((race) => this.raceToReadable(race)), + "How Did You Hear": app.demographics_how_did_you_hear.join(", "), + } + } + + obj[app.application_id] = { + "Application Id": app.application_id, + "Application Confirmation Code": app.application_confirmation_code, + "Application Type": + app.application_submission_type === "electronical" + ? "electronic" + : app.application_submission_type, + "Application Submission Date (UTC)": dayjs(app.application_submission_date).format( + "MM-DD-YYYY hh:mm:ssA" + ), + "Primary Applicant First Name": app.applicant_first_name, + "Primary Applicant Middle Name": app.applicant_middle_name, + "Primary Applicant Last Name": app.applicant_last_name, + "Primary Applicant Birthday": getBirthday( + app.applicant_birth_day, + app.applicant_birth_month, + app.applicant_birth_year + ), + "Primary Applicant Email Address": app.applicant_email_address, + "Primary Applicant Phone Number": app.applicant_phone_number, + "Primary Applicant Phone Type": app.applicant_phone_number_type, + "Primary Applicant Additional Phone Number": app.application_additional_phone_number, + "Primary Applicant Preferred Contact Type": app.application_contact_preferences.join(","), + "Primary Applicant Street": app.applicant_address_street, + "Primary Applicant Street 2": app.applicant_address_street2, + "Primary Applicant City": app.applicant_address_city, + "Primary Applicant State": app.applicant_address_state, + "Primary Applicant Zip Code": app.applicant_address_zip_code, + "Primary Applicant Mailing Street": app.mailingAddress_street, + "Primary Applicant Mailing Street 2": app.mailingAddress_street2, + "Primary Applicant Mailing City": app.mailingAddress_city, + "Primary Applicant Mailing State": app.mailingAddress_state, + "Primary Applicant Mailing Zip Code": app.mailingAddress_zip_code, + "Primary Applicant Work Street": app.applicant_workAddress_street, + "Primary Applicant Work Street 2": app.applicant_workAddress_street2, + "Primary Applicant Work City": app.applicant_workAddress_city, + "Primary Applicant Work State": app.applicant_workAddress_state, + "Primary Applicant Work Zip Code": app.applicant_workAddress_zip_code, + "Alternate Contact First Name": app.alternateContact_first_name, + "Alternate Contact Middle Name": app.alternateContact_middle_name, + "Alternate Contact Last Name": app.alternateContact_last_name, + "Alternate Contact Type": app.alternateContact_type, + "Alternate Contact Agency": app.alternateContact_agency, + "Alternate Contact Other Type": app.alternateContact_other_type, + "Alternate Contact Email Address": app.alternateContact_email_address, + "Alternate Contact Phone Number": app.alternateContact_phone_number, + "Alternate Contact Street": app.alternateContact_mailingAddress_street, + "Alternate Contact Street 2": app.alternateContact_mailingAddress_street2, + "Alternate Contact City": app.alternateContact_mailingAddress_city, + "Alternate Contact State": app.alternateContact_mailingAddress_state, + "Alternate Contact Zip Code": app.alternateContact_mailingAddress_zip_code, + Income: app.application_income, + "Income Period": app.application_income_period === "perMonth" ? "per month" : "per year", + "Accessibility Mobility": formatBoolean(app.accessibility_mobility), + "Accessibility Vision": formatBoolean(app.accessibility_vision), + "Accessibility Hearing": formatBoolean(app.accessibility_hearing), + "Expecting Household Changes": formatBoolean(app.application_household_expecting_changes), + "Household Includes Student or Member Nearing 18": formatBoolean( + app.application_household_student + ), + "Vouchers or Subsidies": formatBoolean(app.application_income_vouchers), + "Requested Unit Types": { + [app.preferredUnit_id]: this.unitTypeToReadable(app.preferredUnit_name), + }, + Preference: this.buildPreference(app.application_preferences, preferenceKeys), + Program: this.buildProgram(app.application_programs, programKeys), + "Household Size": app.application_household_size, + "Household Members": { + [app.householdMembers_id]: this.mapHouseholdMembers(app), + }, + "Marked As Duplicate": formatBoolean(app.application_marked_as_duplicate), + "Flagged As Duplicate": formatBoolean(app.flagged), + ...demographics, + } + /** + * For all conditionals below, these are for mapping the n-many relationships that applications have (since we're getting the raw query). + * While we're going through here, keep track of the extra keys created, so we don't have to loop through an extra time to create the headers + */ + } else if ( + obj[app.application_id]["Household Members"][app.householdMembers_id] === undefined + ) { + obj[app.application_id]["Household Members"][ + app.householdMembers_id + ] = this.mapHouseholdMembers(app) + extraHeaders["Household Members"] = Math.max( + extraHeaders["Household Members"], + Object.keys(obj[app.application_id]["Household Members"]).length + ) + } else if ( + obj[app.application_id]["Requested Unit Types"][app.preferredUnit_id] === undefined + ) { + obj[app.application_id]["Requested Unit Types"][ + app.preferredUnit_id + ] = this.unitTypeToReadable(app.preferredUnit_name) + } + return obj + }, {}) + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this + function extraGroupKeys(group, obj) { + const groups = { + "Household Members": { + nested: true, + keys: Object.keys(self.mapHouseholdMembers(obj)), + }, + Preference: { + nested: false, + keys: Object.keys(preferenceKeys), + }, + Program: { + nested: false, + keys: Object.keys(programKeys), + }, + } + return groups[group] + } + + return this.csvBuilder.buildFromIdIndex(applicationsObj, extraHeaders, extraGroupKeys) + } +} diff --git a/backend/core/src/applications/services/applications.service.ts b/backend/core/src/applications/services/applications.service.ts new file mode 100644 index 0000000000..fb9a170ce1 --- /dev/null +++ b/backend/core/src/applications/services/applications.service.ts @@ -0,0 +1,338 @@ +import { + BadRequestException, + HttpException, + HttpStatus, + Inject, + Injectable, + NotFoundException, + Scope, +} from "@nestjs/common" +import { InjectRepository } from "@nestjs/typeorm" +import { DeepPartial, QueryFailedError, Repository } from "typeorm" +import { paginate, Pagination, PaginationTypeEnum } from "nestjs-typeorm-paginate" +import { Request as ExpressRequest } from "express" +import { REQUEST } from "@nestjs/core" +import retry from "async-retry" +import crypto from "crypto" +import { ApplicationFlaggedSetsService } from "../../application-flagged-sets/application-flagged-sets.service" +import { AuthzService } from "../../auth/services/authz.service" +import { ListingsService } from "../../listings/listings.service" +import { Application } from "../entities/application.entity" +import { Listing } from "../../listings/entities/listing.entity" +import { authzActions } from "../../auth/enum/authz-actions.enum" +import { assignDefined } from "../../shared/utils/assign-defined" +import { EmailService } from "../../email/email.service" +import { getView } from "../views/view" +import { PaginatedApplicationListQueryParams } from "../dto/paginated-application-list-query-params" +import { ApplicationCreateDto } from "../dto/application-create.dto" +import { ApplicationUpdateDto } from "../dto/application-update.dto" +import { ApplicationsCsvListQueryParams } from "../dto/applications-csv-list-query-params" + +@Injectable({ scope: Scope.REQUEST }) +export class ApplicationsService { + constructor( + @Inject(REQUEST) private req: ExpressRequest, + private readonly applicationFlaggedSetsService: ApplicationFlaggedSetsService, + private readonly authzService: AuthzService, + private readonly listingsService: ListingsService, + private readonly emailService: EmailService, + @InjectRepository(Application) private readonly repository: Repository, + @InjectRepository(Listing) private readonly listingsRepository: Repository+ ) {} + + public async list(params: PaginatedApplicationListQueryParams) { + const qb = this._getQb(params) + const result = await qb.getMany() + await Promise.all( + result.map(async (application) => { + await this.authorizeUserAction(this.req.user, application, authzActions.read) + }) + ) + return result + } + + public async rawListWithFlagged(params: ApplicationsCsvListQueryParams) { + await this.authorizeCSVExport(this.req.user, params.listingId) + const qb = this._getQb(params) + qb.leftJoin( + "application_flagged_set_applications_applications", + "application_flagged_set_applications_applications", + "application_flagged_set_applications_applications.applications_id = application.id" + ) + qb.addSelect( + "count(application_flagged_set_applications_applications.applications_id) > 0 as flagged" + ) + qb.groupBy( + "application.id, applicant.id, applicant_address.id, applicant_workAddress.id, alternateAddress.id, mailingAddress.id, alternateContact.id, alternateContact_mailingAddress.id, accessibility.id, demographics.id, householdMembers.id, householdMembers_address.id, householdMembers_workAddress.id, preferredUnit.id" + ) + const applications = await qb.getRawMany() + + return applications + } + + async listPaginated( + params: PaginatedApplicationListQueryParams + ): Promise> { + const qb = this._getQb(params, params.listingId ? "partnerList" : undefined) + + const applicationIDQB = this._getQb(params, params.listingId ? "partnerList" : undefined, false) + applicationIDQB.select("application.id") + applicationIDQB.groupBy("application.id") + if (params.orderBy) { + applicationIDQB.addSelect(params.orderBy) + applicationIDQB.addGroupBy(params.orderBy) + } + const applicationIDResult = await paginate(applicationIDQB, { + limit: params.limit, + page: params.page, + paginationType: PaginationTypeEnum.TAKE_AND_SKIP, + }) + + if (applicationIDResult.items.length) { + qb.andWhere("application.id IN (:...applicationIDs)", { + applicationIDs: applicationIDResult.items.map((elem) => elem.id), + }) + } + + const result = await qb.getMany() + + await Promise.all( + result.map(async (application) => { + await this.authorizeUserAction(this.req.user, application, authzActions.read) + }) + ) + return { + ...applicationIDResult, + items: result, + } + } + + // Submitting an application from public + async submit(applicationCreateDto: ApplicationCreateDto) { + applicationCreateDto.submissionDate = new Date() + const listing = await this.listingsRepository + .createQueryBuilder("listings") + .where(`listings.id = :listingId`, { listingId: applicationCreateDto.listing.id }) + .select("listings.applicationDueDate") + .getOne() + if ( + listing && + listing.applicationDueDate && + applicationCreateDto.submissionDate > listing.applicationDueDate + ) { + throw new BadRequestException("Listing is not open for application submission.") + } + await this.authorizeUserAction(this.req.user, applicationCreateDto, authzActions.submit) + return await this._create( + { + ...applicationCreateDto, + user: this.req.user, + }, + true + ) + } + + // Entering a paper application from partners + async create(applicationCreateDto: ApplicationCreateDto) { + await this.authorizeUserAction(this.req.user, applicationCreateDto, authzActions.create) + return this._create(applicationCreateDto, false) + } + + async findOne(applicationId: string) { + const application = await this.repository.findOneOrFail({ + where: { + id: applicationId, + }, + relations: ["user"], + }) + await this.authorizeUserAction(this.req.user, application, authzActions.read) + return application + } + + async update(applicationUpdateDto: ApplicationUpdateDto) { + const application = await this.repository.findOne({ + where: { id: applicationUpdateDto.id }, + }) + if (!application) { + throw new NotFoundException() + } + await this.authorizeUserAction(this.req.user, application, authzActions.update) + assignDefined(application, { + ...applicationUpdateDto, + id: application.id, + }) + + return await this.repository.manager.transaction( + "SERIALIZABLE", + async (transactionalEntityManager) => { + const applicationsRepository = transactionalEntityManager.getRepository(Application) + const newApplication = await applicationsRepository.save(application) + await this.applicationFlaggedSetsService.onApplicationUpdate( + application, + transactionalEntityManager + ) + + return await applicationsRepository.findOne({ id: newApplication.id }) + } + ) + } + + async delete(applicationId: string) { + const application = await this.findOne(applicationId) + await this.authorizeUserAction(this.req.user, application, authzActions.delete) + return await this.repository.softRemove({ id: applicationId }) + } + + private _getQb(params: PaginatedApplicationListQueryParams, view = "base", withSelect = true) { + /** + * Map used to generate proper parts + * of query builder. + */ + const paramsMap = { + markedAsDuplicate: (qb, { markedAsDuplicate }) => + qb.andWhere("application.markedAsDuplicate = :markedAsDuplicate", { + markedAsDuplicate: markedAsDuplicate, + }), + userId: (qb, { userId }) => qb.andWhere("application.user_id = :uid", { uid: userId }), + listingId: (qb, { listingId }) => + qb.andWhere("application.listing_id = :lid", { lid: listingId }), + orderBy: (qb, { orderBy, order }) => qb.orderBy(orderBy, order, "NULLS LAST"), + search: (qb, { search }) => + qb.andWhere( + `to_tsvector('english', REGEXP_REPLACE(concat_ws(' ', applicant, alternateContact.emailAddress), '[_]|[-]', '/', 'g')) @@ to_tsquery(CONCAT(CAST(plainto_tsquery(REGEXP_REPLACE(:search, '[_]|[-]', '/', 'g')) as text), ':*'))`, + { + search, + } + ), + } + + // --> Build main query + const qbView = getView(this.repository.createQueryBuilder("application"), view) + const qb = qbView.getViewQb(withSelect) + qb.where("application.id IS NOT NULL") + + // --> Build additional query builder parts + Object.keys(paramsMap).forEach((paramKey) => { + // e.g. markedAsDuplicate can be false and wouldn't be applied here + if (params[paramKey] !== undefined) { + paramsMap[paramKey](qb, params) + } + }) + return qb + } + + private async _createApplication(applicationCreateDto: DeepPartial) { + return await this.repository.manager.transaction( + "SERIALIZABLE", + async (transactionalEntityManager) => { + const applicationsRepository = transactionalEntityManager.getRepository(Application) + const application = await applicationsRepository.save({ + ...applicationCreateDto, + confirmationCode: ApplicationsService.generateConfirmationCode(), + }) + await this.applicationFlaggedSetsService.onApplicationSave( + application, + transactionalEntityManager + ) + return await applicationsRepository.findOne({ id: application.id }) + } + ) + } + + private async _create( + applicationCreateDto: DeepPartial, + shouldSendConfirmation: boolean + ) { + let application: Application + + try { + await retry( + async (bail) => { + try { + application = await this._createApplication(applicationCreateDto) + } catch (e) { + console.error(e.message) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if ( + !( + e instanceof QueryFailedError && + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // NOTE: 40001 could not serialize access due to read/write dependencies among transactions + (e.code === "40001" || + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // NOTE: constraint UQ_556c258a4439f1b7f53de2ed74f checks whether listing.id & confirmationCode is a unique combination + // it does make sense here to retry because it's a randomly generated 8 character string value + (e.code === "23505" && e.constraint === "UQ_556c258a4439f1b7f53de2ed74f")) + ) + ) { + bail(e) + return + } + throw e + } + }, + { retries: 6, minTimeout: 200 } + ) + } catch (e) { + console.log("Create application error = ", e) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (e instanceof QueryFailedError && e.code === "40001") { + throw new HttpException( + { + statusCode: HttpStatus.TOO_MANY_REQUESTS, + error: "Too Many Requests", + message: "Please try again later.", + }, + 429 + ) + } + throw e + } + + // Listing is not eagerly joined on application entity so let's use the one provided with + // create dto + const listing = await this.listingsService.findOne(applicationCreateDto.listing.id) + if (application.applicant.emailAddress && shouldSendConfirmation) { + await this.emailService.confirmation(listing, application, applicationCreateDto.appUrl) + } + return application + } + + private async authorizeUserAction( + user, + app: T, + action + ) { + let resource: T = app + + if (app instanceof Application) { + resource = { + ...app, + listing_id: app.listingId, + } + } else if (app instanceof ApplicationCreateDto) { + resource = { + ...app, + listing_id: app.listing.id, + } + } + return this.authzService.canOrThrow(user, "application", action, resource) + } + + private async authorizeCSVExport(user, listingId) { + /** + * Checking authorization for each application is very expensive. By making lisitngId required, we can check if the user has update permissions for the listing, since right now if a user has that they also can run the export for that listing + */ + return await this.authzService.canOrThrow(user, "listing", authzActions.update, { + id: listingId, + }) + } + + public static generateConfirmationCode(): string { + return crypto.randomBytes(4).toString("hex").toUpperCase() + } +} diff --git a/backend/core/src/applications/services/csv-builder.service.ts b/backend/core/src/applications/services/csv-builder.service.ts new file mode 100644 index 0000000000..47b8b653ab --- /dev/null +++ b/backend/core/src/applications/services/csv-builder.service.ts @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Injectable, Scope } from "@nestjs/common" + +export interface KeyNumber { + [key: string]: number +} + +@Injectable({ scope: Scope.REQUEST }) +export class CsvBuilder { + /** + * this assumes a flat file structure since it's getting fed data from a raw query + * relational data should be handled with the use of extraHeaders and extraGroupKeys, + * see application-csv-exporter Household Members for an example of this + * All formatting should be done before passing in + */ + public buildFromIdIndex( + obj: { [key: string]: any }, + extraHeaders?: { [key: string]: number }, + extraGroupKeys?: ( + group: string, + obj?: { [key: string]: any } + ) => { nested: boolean; keys: string[] } + ): string { + const headerIndex: { [key: string]: number } = {} + // rootKeys should be the ids + const rootKeys = Object.keys(obj) + + if (rootKeys.length === 0) return "" + /** + * initialApp should have all possible keys. + * If it can't, use extraHeaders + */ + const initialApp = obj[rootKeys[0]] + let index = 0 + // set headerIndex + Object.keys(initialApp).forEach((key) => { + // if the key is in extra headers, we want to group them all together + if (extraHeaders && extraHeaders[key] && extraGroupKeys) { + const groupKeys = extraGroupKeys(key, initialApp) + for (let i = 1; i < extraHeaders[key] + 1; i++) { + const headerGroup = groupKeys.nested ? `${key} (${i})` : key + groupKeys.keys.forEach((groupKey) => { + headerIndex[`${headerGroup} ${groupKey}`] = index + index++ + }) + } + } else { + headerIndex[key] = index + index++ + } + }) + const headers = Object.keys(headerIndex) + + // initiate arrays to insert data + const rows = Array.from({ length: rootKeys.length }, () => Array(headers.length)) + + // set rows (a row is a record) + rootKeys.forEach((obj_id, row) => { + const thisObj = obj[obj_id] + Object.keys(thisObj).forEach((key) => { + const val = thisObj[key] + const groupKeys = extraGroupKeys && extraGroupKeys(key, initialApp) + if (extraHeaders && extraHeaders[key] && groupKeys) { + // val in this case is an object with ids as the keys + const ids = Object.keys(val) + if (groupKeys.nested && ids.length) { + Object.keys(val).forEach((sub_id, i) => { + const headerGroup = `${key} (${i + 1})` + groupKeys.keys.forEach((groupKey) => { + const column = headerIndex[`${headerGroup} ${groupKey}`] + const sub_val = val[sub_id][groupKey] + rows[row][column] = + sub_val !== undefined && sub_val !== null ? JSON.stringify(sub_val) : "" + }) + }) + } else if (groupKeys.nested === false) { + Object.keys(val).forEach((sub_key) => { + const column = headerIndex[`${key} ${sub_key}`] + const sub_val = val[sub_key] + rows[row][column] = + sub_val !== undefined && sub_val !== null ? JSON.stringify(sub_val) : "" + }) + } + } else { + const column = headerIndex[key] + let value + if (Array.isArray(val)) { + value = val.join(", ") + } else if (val instanceof Object) { + value = Object.keys(val) + .map((key) => val[key]) + .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) + .join(", ") + } else { + value = val + } + rows[row][column] = value !== undefined && value !== null ? JSON.stringify(value) : "" + } + }) + }) + + let csvString = headers.join(",") + csvString += "\n" + + // turn rows into csv format + rows.forEach((row) => { + if (row.length) { + csvString += row.join(",") + csvString += "\n" + } + }) + + return csvString + } +} diff --git a/backend/core/src/applications/services/csv-builder.spec.ts b/backend/core/src/applications/services/csv-builder.spec.ts new file mode 100644 index 0000000000..d2b50c071d --- /dev/null +++ b/backend/core/src/applications/services/csv-builder.spec.ts @@ -0,0 +1,205 @@ +import { Test, TestingModule } from "@nestjs/testing" +import { CsvBuilder } from "./csv-builder.service" +/* import { ApplicationCsvExporter } from "./application-csv-exporter" +import { ApplicationStatus } from "../applications/types/application-status-enum" +import { ApplicationSubmissionType } from "../applications/types/application-submission-type-enum" */ + +// Cypress brings in Chai types for the global expect, but we want to use jest +// expect here so we need to re-declare it. +// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 +declare const expect: jest.Expect + +describe("CSVBuilder", () => { + let service: CsvBuilder + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CsvBuilder], + }).compile() + service = await module.resolve(CsvBuilder) + }) + + it("should be defined", () => { + expect(service).toBeDefined() + }) + + it("create empty respsone", () => { + const response = service.buildFromIdIndex({}) + expect(response).toBe("") + }) + + it("create correctly escaped CSV for correct data", () => { + const response = service.buildFromIdIndex({ 1: { foo: "bar", bar: "foo" }, 2: { bar: "baz" } }) + expect(response).toBe('foo,bar\n"bar","foo"\n,"baz"\n') + }) + + it("create correct CSV for correct data with undefined value", () => { + const response = service.buildFromIdIndex({ + 1: { foo: "bar", bar: undefined }, + 2: { bar: "baz" }, + }) + expect(response).toBe('foo,bar\n"bar",\n,"baz"\n') + }) + + it("create correct CSV for correct data with null value", () => { + const response = service.buildFromIdIndex({ + 1: { foo: "bar", bar: null }, + 2: { bar: "baz" }, + }) + expect(response).toBe('foo,bar\n"bar",\n,"baz"\n') + }) + + it("create CSV with escaped double quotes", () => { + const response = service.buildFromIdIndex({ 1: { foo: '"', bar: "foo" } }) + expect(response).toBe('foo,bar\n"\\"","foo"\n') + }) + + it("create CSV with comma in value", () => { + const response = service.buildFromIdIndex({ 1: { foo: "with, comma", bar: "should work," } }) + expect(response).toBe('foo,bar\n"with, comma","should work,"\n') + }) + + it("create a CSV with an array of strings", () => { + const response = service.buildFromIdIndex({ 1: { foo: ["foo", "bar"] } }) + expect(response).toBe('foo\n"foo, bar"\n') + }) + + it("create a CSV with a nested object of key: string pairs that converts it to an array", () => { + const response = service.buildFromIdIndex({ + 1: { foo: "bar", bar: { 1: "bar-sub-1", 2: "bar-sub-2" } }, + }) + expect(response).toBe('foo,bar\n"bar","bar-sub-1, bar-sub-2"\n') + }) + + it("create CSV with extraHeaders and nested groupKeys", () => { + const response = service.buildFromIdIndex( + { + 1: { foo: "bar", bar: "foo", baz: { 1: { sub: "sub-foo" }, 2: { sub: "sub-bar" } } }, + }, + { baz: 2 }, + (group) => { + const groups = { + baz: { + nested: true, + keys: ["sub"], + }, + } + + return groups[group] + } + ) + expect(response).toBe('foo,bar,baz (1) sub,baz (2) sub\n"bar","foo","sub-foo","sub-bar"\n') + }) + + it("create CSV with extraHeaders and non nested groupKeys", () => { + const response = service.buildFromIdIndex( + { + 1: { foo: "bar", bar: "foo", baz: { sub: "baz-sub", bus: "baz-bus" } }, + }, + { baz: 1 }, + (group) => { + const groups = { + baz: { + nested: false, + keys: ["sub", "bus"], + }, + } + + return groups[group] + } + ) + expect(response).toBe('foo,bar,baz sub,baz bus\n"bar","foo","baz-sub","baz-bus"\n') + }) +}) + +// TODO: add tests specific to ApplicationCsvExporter +/* describe("ApplicationCsvExporter", () => { + let service: ApplicationCsvExporter + const now = new Date() + + const BASE_ADDRESS = { + city: "city", + state: "state", + street: "street", + zipCode: "zipcode", + } + + const BASE_APPLICATIONS = [ + { + id: "app_1", + listingId: "listing_1", + applicant: { + id: "applicant_1", + firstName: "first name", + middleName: "middle name", + lastName: "last name", + address: { + id: "address_1", + createdAt: now, + updatedAt: now, + ...BASE_ADDRESS, + }, + workAddress: { + id: "work_address_1", + createdAt: now, + updatedAt: now, + ...BASE_ADDRESS, + }, + createdAt: now, + updatedAt: now, + }, + contactPreferences: [], + updatedAt: now, + createdAt: now, + mailingAddress: { + id: "mailing_address_1", + createdAt: now, + updatedAt: now, + ...BASE_ADDRESS, + }, + alternateAddress: { + id: "alternate_address_1", + createdAt: now, + updatedAt: now, + ...BASE_ADDRESS, + }, + alternateContact: { + id: "alternate_contact_1", + createdAt: now, + updatedAt: now, + mailingAddress: { + id: "mailing_address_2", + createdAt: now, + updatedAt: now, + ...BASE_ADDRESS, + }, + }, + accessibility: { + id: "accessibility_1", + createdAt: now, + updatedAt: now, + }, + demographics: { + howDidYouHear: ["ears"], + id: "demographics_1", + createdAt: now, + updatedAt: now, + }, + householdMembers: [], + preferredUnit: [], + preferences: [], + status: ApplicationStatus.submitted, + submissionType: ApplicationSubmissionType.electronical, + markedAsDuplicate: false, + flagged: false, + confirmationCode: "code_1", + }, + ] + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CsvBuilder, ApplicationCsvExporter], + }).compile() + service = module.get(ApplicationCsvExporter) + }) +}) */ diff --git a/backend/core/src/applications/types/application-preference-api-extra-models.ts b/backend/core/src/applications/types/application-preference-api-extra-models.ts new file mode 100644 index 0000000000..e216c4b615 --- /dev/null +++ b/backend/core/src/applications/types/application-preference-api-extra-models.ts @@ -0,0 +1,5 @@ +import { BooleanInput } from "./form-metadata/boolean-input" +import { TextInput } from "./form-metadata/text-input" +import { AddressInput } from "./form-metadata/address-input" + +export const applicationPreferenceApiExtraModels = [BooleanInput, TextInput, AddressInput] diff --git a/backend/core/src/applications/types/application-program-option.ts b/backend/core/src/applications/types/application-program-option.ts new file mode 100644 index 0000000000..305168813e --- /dev/null +++ b/backend/core/src/applications/types/application-program-option.ts @@ -0,0 +1,3 @@ +import { ApplicationPreferenceOption } from "./application-preference-option" + +export class ApplicationProgramOption extends ApplicationPreferenceOption {} diff --git a/backend/core/src/applications/types/applications-api-extra-model.ts b/backend/core/src/applications/types/applications-api-extra-model.ts new file mode 100644 index 0000000000..10a1abf871 --- /dev/null +++ b/backend/core/src/applications/types/applications-api-extra-model.ts @@ -0,0 +1,24 @@ +import { Expose } from "class-transformer" +import { ApiProperty } from "@nestjs/swagger" +import { OrderByParam } from "./order-by-param" +import { OrderParam } from "./order-param" + +export class ApplicationsApiExtraModel { + @Expose() + @ApiProperty({ + enum: Object.keys(OrderByParam), + example: "createdAt", + default: "createdAt", + required: false, + }) + orderBy?: OrderByParam + + @Expose() + @ApiProperty({ + enum: OrderParam, + example: "DESC", + default: "DESC", + required: false, + }) + order?: OrderParam +} diff --git a/backend/core/src/applications/types/form-metadata/form-metadata.ts b/backend/core/src/applications/types/form-metadata/form-metadata.ts index 54a52a81e8..daa1c93c2c 100644 --- a/backend/core/src/applications/types/form-metadata/form-metadata.ts +++ b/backend/core/src/applications/types/form-metadata/form-metadata.ts @@ -11,6 +11,11 @@ import { ValidationsGroupsEnum } from "../../../shared/types/validations-groups- import { FormMetadataOptions } from "./form-metadata-options" import { ApiProperty } from "@nestjs/swagger" +export enum FormMetaDataType { + radio = "radio", + checkbox = "checkbox", +} + export class FormMetadata { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @@ -42,4 +47,9 @@ export class FormMetadata { @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() hideFromListing?: boolean + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: FormMetaDataType, enumName: "FormMetaDataType" }) + type?: FormMetaDataType } diff --git a/backend/core/src/applications/types/order-by-param.ts b/backend/core/src/applications/types/order-by-param.ts new file mode 100644 index 0000000000..610b165139 --- /dev/null +++ b/backend/core/src/applications/types/order-by-param.ts @@ -0,0 +1,6 @@ +export enum OrderByParam { + firstName = "applicant.firstName", + lastName = "applicant.lastName", + submissionDate = "application.submissionDate", + createdAt = "application.createdAt", +} diff --git a/backend/core/src/applications/types/order-param.ts b/backend/core/src/applications/types/order-param.ts new file mode 100644 index 0000000000..36e26d1d0b --- /dev/null +++ b/backend/core/src/applications/types/order-param.ts @@ -0,0 +1,4 @@ +export enum OrderParam { + ASC = "ASC", + DESC = "DESC", +} diff --git a/backend/core/src/applications/views/config.ts b/backend/core/src/applications/views/config.ts new file mode 100644 index 0000000000..f02c58955d --- /dev/null +++ b/backend/core/src/applications/views/config.ts @@ -0,0 +1,37 @@ +import { Views } from "./types" + +const views: Views = { + base: { + leftJoinAndSelect: [ + ["application.applicant", "applicant"], + ["applicant.address", "applicant_address"], + ["applicant.workAddress", "applicant_workAddress"], + ["application.alternateAddress", "alternateAddress"], + ["application.mailingAddress", "mailingAddress"], + ["application.alternateContact", "alternateContact"], + ["alternateContact.mailingAddress", "alternateContact_mailingAddress"], + ["application.accessibility", "accessibility"], + ["application.demographics", "demographics"], + ["application.householdMembers", "householdMembers"], + ["householdMembers.address", "householdMembers_address"], + ["householdMembers.workAddress", "householdMembers_workAddress"], + ["application.preferredUnit", "preferredUnit"], + ], + }, +} + +views.partnerList = { + leftJoinAndSelect: [ + ["application.applicant", "applicant"], + ["application.householdMembers", "householdMembers"], + ["application.accessibility", "accessibility"], + ["applicant.address", "applicant_address"], + ["application.mailingAddress", "mailingAddress"], + ["applicant.workAddress", "applicant_workAddress"], + ["application.alternateContact", "alternateContact"], + ["application.alternateAddress", "alternateAddress"], + ["alternateContact.mailingAddress", "alternateContact_mailingAddress"], + ], +} + +export { views } diff --git a/backend/core/src/applications/views/types.ts b/backend/core/src/applications/views/types.ts new file mode 100644 index 0000000000..f5161809ce --- /dev/null +++ b/backend/core/src/applications/views/types.ts @@ -0,0 +1,10 @@ +import { View } from "../../views/base.view" + +export enum ApplicationViewEnum { + base = "base", + partnerList = "partnerList", +} + +export type Views = { + [key in ApplicationViewEnum]?: View +} diff --git a/backend/core/src/applications/views/view.ts b/backend/core/src/applications/views/view.ts new file mode 100644 index 0000000000..3a161fe6f6 --- /dev/null +++ b/backend/core/src/applications/views/view.ts @@ -0,0 +1,39 @@ +import { SelectQueryBuilder } from "typeorm" +import { Application } from "../entities/application.entity" +import { views } from "./config" +import { View, BaseView } from "../../views/base.view" + +export function getView(qb: SelectQueryBuilder, view?: string) { + switch (views[view]) { + case views.partnerList: + return new PartnerList(qb) + default: + return new BaseApplicationView(qb) + } +} + +export class BaseApplicationView extends BaseView { + qb: SelectQueryBuilder + view: View + constructor(qb: SelectQueryBuilder) { + super(qb) + this.view = views.base + } + + getViewQb(withSelect = true): SelectQueryBuilder { + if (withSelect) { + this.view.leftJoinAndSelect.forEach((tuple) => this.qb.leftJoinAndSelect(...tuple)) + } else { + this.view.leftJoinAndSelect.forEach((tuple) => this.qb.leftJoin(...tuple)) + } + + return this.qb + } +} + +export class PartnerList extends BaseApplicationView { + constructor(qb: SelectQueryBuilder) { + super(qb) + this.view = views.partnerList + } +} diff --git a/backend/core/src/assets/assets.controller.spec.ts b/backend/core/src/assets/assets.controller.spec.ts index 419d7e1daa..36c58c2a7c 100644 --- a/backend/core/src/assets/assets.controller.spec.ts +++ b/backend/core/src/assets/assets.controller.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from "@nestjs/testing" import { AssetsController } from "./assets.controller" import { AuthModule } from "../auth/auth.module" -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import { TypeOrmModule } from "@nestjs/typeorm" import { AssetsService } from "./services/assets.service" diff --git a/backend/core/src/auth/auth.module.ts b/backend/core/src/auth/auth.module.ts index 432a0987d6..6df6beaa96 100644 --- a/backend/core/src/auth/auth.module.ts +++ b/backend/core/src/auth/auth.module.ts @@ -1,9 +1,10 @@ -import { Module } from "@nestjs/common" +import { forwardRef, Module } from "@nestjs/common" import { JwtModule } from "@nestjs/jwt" -import { LocalStrategy } from "./passport-strategies/local.strategy" +import { LocalMfaStrategy } from "./passport-strategies/local-mfa.strategy" import { JwtStrategy } from "./passport-strategies/jwt.strategy" import { PassportModule } from "@nestjs/passport" import { TypeOrmModule } from "@nestjs/typeorm" +import { TwilioModule } from "nestjs-twilio" import { RevokedToken } from "./entities/revoked-token.entity" import { SharedModule } from "../shared/shared.module" import { ConfigModule, ConfigService } from "@nestjs/config" @@ -13,11 +14,19 @@ import { AuthController } from "./controllers/auth.controller" import { User } from "./entities/user.entity" import { UserService } from "./services/user.service" import { UserController } from "./controllers/user.controller" -import { EmailModule } from "../shared/email/email.module" import { PasswordService } from "./services/password.service" import { JurisdictionsModule } from "../jurisdictions/jurisdictions.module" import { Application } from "../applications/entities/application.entity" import { UserProfileController } from "./controllers/user-profile.controller" +import { ActivityLogModule } from "../activity-log/activity-log.module" +import { EmailModule } from "../email/email.module" +import { SmsMfaService } from "./services/sms-mfa.service" +import { UserPreferencesController } from "./controllers/user-preferences.controller" +import { UserPreferencesService } from "./services/user-preferences.services" +import { UserPreferences } from "./entities/user-preferences.entity" +import { UserRepository } from "./repositories/user-repository" +import { UserCsvExporterService } from "./services/user-csv-exporter.service" +import { CsvBuilder } from "../applications/services/csv-builder.service" @Module({ imports: [ @@ -32,13 +41,33 @@ import { UserProfileController } from "./controllers/user-profile.controller" }, }), }), - TypeOrmModule.forFeature([RevokedToken, User, Application]), + TwilioModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + accountSid: configService.get("TWILIO_ACCOUNT_SID"), + authToken: configService.get("TWILIO_AUTH_TOKEN"), + }), + inject: [ConfigService], + }), + TypeOrmModule.forFeature([RevokedToken, User, UserRepository, Application, UserPreferences]), SharedModule, JurisdictionsModule, EmailModule, + forwardRef(() => ActivityLogModule), + ], + providers: [ + LocalMfaStrategy, + JwtStrategy, + AuthService, + AuthzService, + UserService, + PasswordService, + SmsMfaService, + UserPreferencesService, + CsvBuilder, + UserCsvExporterService, ], - providers: [LocalStrategy, JwtStrategy, AuthService, AuthzService, UserService, PasswordService], - exports: [AuthzService, AuthService, UserService], - controllers: [AuthController, UserController, UserProfileController], + exports: [AuthzService, AuthService, UserService, UserPreferencesService], + controllers: [AuthController, UserController, UserProfileController, UserPreferencesController], }) export class AuthModule {} diff --git a/backend/core/src/auth/authz_policy.csv b/backend/core/src/auth/authz_policy.csv index e4b03d1c56..9a44ba1b50 100644 --- a/backend/core/src/auth/authz_policy.csv +++ b/backend/core/src/auth/authz_policy.csv @@ -1,18 +1,23 @@ p, admin, application, true, .* -p, user, application, !r.obj || (r.sub == r.obj.user_id), (read|submit) +p, user, application, true, submit +p, user, application, !r.obj || (r.sub == r.obj.userId), read p, anonymous, application, true, submit p, admin, user, true, .* p, admin, userProfile, true, .* -p, user, user, !r.obj || (r.sub == r.obj.id), read -p, user, userProfile, !r.obj || (r.sub == r.obj.id), (read|update) +p, user, user, !!r.obj && (r.sub == r.obj.id), read +p, user, userProfile, !!r.obj && (r.sub == r.obj.id), (read|update) p, anonymous, user, true, create p, admin, asset, true, .* p, partner, asset, true, .* +p, admin, program, true, .* +p, partner, program, true, .* +p, anonymous, program, true, read + p, admin, preference, true, .* -p, partner, preference, true, read +p, partner, preference, true, .* p, admin, applicationMethod, true, .* p, partner, applicationMethod, true, read @@ -61,6 +66,9 @@ p, anonymous, applicationMethod, true, read p, admin, paperApplication, true, .* p, anonymous, paperApplication, true, read +p, admin, userPreference, true, .* +p, user, userPreference, !!r.obj && (r.sub == r.obj.id), (read|update) + g, admin, partner g, partner, user g, user, anonymous diff --git a/backend/core/src/auth/controllers/auth.controller.spec.ts b/backend/core/src/auth/controllers/auth.controller.spec.ts index 5ef859ba03..b5e0fe7160 100644 --- a/backend/core/src/auth/controllers/auth.controller.spec.ts +++ b/backend/core/src/auth/controllers/auth.controller.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from "@nestjs/testing" import { AuthController } from "./auth.controller" import { AuthService } from "../services/auth.service" import { UserService } from "../services/user.service" -import { EmailService } from "../../shared/email/email.service" +import { EmailService } from "../../email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. diff --git a/backend/core/src/auth/controllers/auth.controller.ts b/backend/core/src/auth/controllers/auth.controller.ts index 6f054d8588..0c795ed570 100644 --- a/backend/core/src/auth/controllers/auth.controller.ts +++ b/backend/core/src/auth/controllers/auth.controller.ts @@ -1,20 +1,39 @@ -import { Controller, Request, Post, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common" -import { LocalAuthGuard } from "../guards/local-auth.guard" +import { + Controller, + Request, + Post, + UseGuards, + UsePipes, + ValidationPipe, + Body, +} from "@nestjs/common" +import { LocalMfaAuthGuard } from "../guards/local-mfa-auth.guard" import { AuthService } from "../services/auth.service" import { DefaultAuthGuard } from "../guards/default.guard" -import { ApiBody, ApiOperation, ApiTags } from "@nestjs/swagger" +import { ApiBody, ApiExtraModels, ApiOperation, ApiTags } from "@nestjs/swagger" import { LoginDto } from "../dto/login.dto" import { mapTo } from "../../shared/mapTo" import { defaultValidationPipeOptions } from "../../shared/default-validation-pipe-options" import { LoginResponseDto } from "../dto/login-response.dto" +import { RequestMfaCodeDto } from "../dto/request-mfa-code.dto" +import { RequestMfaCodeResponseDto } from "../dto/request-mfa-code-response.dto" +import { UserService } from "../services/user.service" +import { GetMfaInfoDto } from "../dto/get-mfa-info.dto" +import { GetMfaInfoResponseDto } from "../dto/get-mfa-info-response.dto" +import { UserErrorExtraModel } from "../user-errors" +import { TokenDto } from "../dto/token.dto" @Controller("auth") @ApiTags("auth") @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +@ApiExtraModels(UserErrorExtraModel) export class AuthController { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + private readonly userService: UserService + ) {} - @UseGuards(LocalAuthGuard) + @UseGuards(LocalMfaAuthGuard) @Post("login") @ApiBody({ type: LoginDto }) @ApiOperation({ summary: "Login", operationId: "login" }) @@ -25,9 +44,26 @@ export class AuthController { @UseGuards(DefaultAuthGuard) @Post("token") + @ApiBody({ type: TokenDto }) @ApiOperation({ summary: "Token", operationId: "token" }) token(@Request() req): LoginResponseDto { const accessToken = this.authService.generateAccessToken(req.user) return mapTo(LoginResponseDto, { accessToken }) } + + @Post("request-mfa-code") + @ApiOperation({ summary: "Request mfa code", operationId: "requestMfaCode" }) + async requestMfaCode( + @Body() requestMfaCodeDto: RequestMfaCodeDto + ): Promise { + const requestMfaCodeResponse = await this.userService.requestMfaCode(requestMfaCodeDto) + return mapTo(RequestMfaCodeResponseDto, requestMfaCodeResponse) + } + + @Post("mfa-info") + @ApiOperation({ summary: "Get mfa info", operationId: "getMfaInfo" }) + async getMfaInfo(@Body() getMfaInfoDto: GetMfaInfoDto): Promise { + const getMfaInfoResponseDto = await this.userService.getMfaInfo(getMfaInfoDto) + return mapTo(GetMfaInfoResponseDto, getMfaInfoResponseDto) + } } diff --git a/backend/core/src/auth/controllers/user-preferences.controller.ts b/backend/core/src/auth/controllers/user-preferences.controller.ts new file mode 100644 index 0000000000..315aab7888 --- /dev/null +++ b/backend/core/src/auth/controllers/user-preferences.controller.ts @@ -0,0 +1,44 @@ +import { + Body, + Controller, + Put, + UseGuards, + UsePipes, + ValidationPipe, + Request, + Param, +} from "@nestjs/common" +import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger" +import { ResourceType } from "../decorators/resource-type.decorator" +import { mapTo } from "../../shared/mapTo" +import { UserPreferencesService } from "../services/user-preferences.services" +import { UserPreferencesDto } from "../dto/user-preferences.dto" +import { defaultValidationPipeOptions } from "../../shared/default-validation-pipe-options" +import { AuthContext } from "../types/auth-context" +import { User } from "../entities/user.entity" +import { Request as ExpressRequest } from "express" +import { OptionalAuthGuard } from "../guards/optional-auth.guard" +import { UserPreferencesAuthzGuard } from "../guards/user-preferences-authz.guard" + +@Controller("/userPreferences") +@ApiTags("userPreferences") +@ApiBearerAuth() +@ResourceType("userPreference") +@UseGuards(OptionalAuthGuard, UserPreferencesAuthzGuard) +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +export class UserPreferencesController { + constructor(private readonly userPreferencesService: UserPreferencesService) {} + + @Put(`:id`) + @ApiOperation({ summary: "Update user preferences", operationId: "update" }) + async update( + @Request() req: ExpressRequest, + @Param("id") userId: string, + @Body() userPrefence: UserPreferencesDto + ): Promise { + return mapTo( + UserPreferencesDto, + await this.userPreferencesService.update(userPrefence, new AuthContext(req.user as User)) + ) + } +} diff --git a/backend/core/src/auth/controllers/user.controller.spec.ts b/backend/core/src/auth/controllers/user.controller.spec.ts index 4c8430c718..79f5cdd180 100644 --- a/backend/core/src/auth/controllers/user.controller.spec.ts +++ b/backend/core/src/auth/controllers/user.controller.spec.ts @@ -4,7 +4,9 @@ import { PassportModule } from "@nestjs/passport" import { AuthService } from "../services/auth.service" import { UserService } from "../services/user.service" import { AuthzService } from "../services/authz.service" -import { EmailService } from "../../shared/email/email.service" +import { ActivityLogService } from "../../activity-log/services/activity-log.service" +import { EmailService } from "../../email/email.service" +import { UserCsvExporterService } from "../services/user-csv-exporter.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. @@ -21,7 +23,9 @@ describe("User Controller", () => { { provide: AuthService, useValue: {} }, { provide: AuthzService, useValue: {} }, { provide: UserService, useValue: {} }, + { provide: UserCsvExporterService, useValue: {} }, { provide: EmailService, useValue: {} }, + { provide: ActivityLogService, useValue: {} }, ], controllers: [UserController], }).compile() diff --git a/backend/core/src/auth/controllers/user.controller.ts b/backend/core/src/auth/controllers/user.controller.ts index 17f2fab92d..b3f981fde5 100644 --- a/backend/core/src/auth/controllers/user.controller.ts +++ b/backend/core/src/auth/controllers/user.controller.ts @@ -1,12 +1,17 @@ import { Body, Controller, + Delete, Get, + Header, + Param, + ParseUUIDPipe, Post, Put, Query, Request, UseGuards, + UseInterceptors, UsePipes, ValidationPipe, } from "@nestjs/common" @@ -39,6 +44,12 @@ import { authzActions } from "../enum/authz-actions.enum" import { UserCreateQueryParams } from "../dto/user-create-query-params" import { UserFilterParams } from "../dto/user-filter-params" import { DefaultAuthGuard } from "../guards/default.guard" +import { UserProfileAuthzGuard } from "../guards/user-profile-authz.guard" +import { ActivityLogInterceptor } from "../../activity-log/interceptors/activity-log.interceptor" +import { IdDto } from "../../shared/dto/id.dto" +import { UserCsvExporterService } from "../services/user-csv-exporter.service" +import { Compare } from "../../shared/dto/filter.dto" +import { UnitsCsvQueryParams } from "../../../src/units/dto/units-csv-query-params" @Controller("user") @ApiBearerAuth() @@ -46,10 +57,13 @@ import { DefaultAuthGuard } from "../guards/default.guard" @ResourceType("user") @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) export class UserController { - constructor(private readonly userService: UserService) {} + constructor( + private readonly userService: UserService, + private readonly userCsvExporter: UserCsvExporterService + ) {} @Get() - @UseGuards(DefaultAuthGuard, AuthzGuard) + @UseGuards(DefaultAuthGuard, UserProfileAuthzGuard) profile(@Request() req): UserDto { return mapTo(UserDto, req.user) } @@ -64,7 +78,7 @@ export class UserController { ): Promise { return mapTo( UserBasicDto, - await this.userService.createUser( + await this.userService.createPublicUser( dto, new AuthContext(req.user as User), queryParams.noWelcomeEmail !== true @@ -72,11 +86,40 @@ export class UserController { ) } + @Post("resend-partner-confirmation") + @UseGuards(OptionalAuthGuard, AuthzGuard) + @ApiOperation({ + summary: "Resend partner confirmation", + operationId: "resendPartnerConfirmation", + }) + async requestConfirmationResend(@Body() dto: EmailDto): Promise { + await this.userService.resendPartnerConfirmation(dto) + return mapTo(StatusDto, { status: "ok" }) + } + + @Post("is-confirmation-token-valid") + @UseGuards(OptionalAuthGuard, AuthzGuard) + @ApiOperation({ + summary: "Verifies token is valid", + operationId: "isUserConfirmationTokenValid", + }) + async isUserConfirmationTokenValid(@Body() dto: ConfirmDto): Promise { + return await this.userService.isUserConfirmationTokenValid(dto) + } + @Post("resend-confirmation") @UseGuards(OptionalAuthGuard, AuthzGuard) @ApiOperation({ summary: "Resend confirmation", operationId: "resendConfirmation" }) async confirmation(@Body() dto: EmailDto): Promise { - await this.userService.resendConfirmation(dto) + await this.userService.resendPublicConfirmation(dto) + return mapTo(StatusDto, { status: "ok" }) + } + + @Post("resend-partner-confirmation") + @UseGuards(OptionalAuthGuard, AuthzGuard) + @ApiOperation({ summary: "Resend confirmation", operationId: "resendPartnerConfirmation" }) + async resendPartnerConfirmation(@Body() dto: EmailDto): Promise { + await this.userService.resendPartnerConfirmation(dto) return mapTo(StatusDto, { status: "ok" }) } @@ -104,6 +147,7 @@ export class UserController { @Put(":id") @UseGuards(DefaultAuthGuard, AuthzGuard) @ApiOperation({ summary: "Update user", operationId: "update" }) + @UseInterceptors(ActivityLogInterceptor) async update(@Request() req: ExpressRequest, @Body() dto: UserUpdateDto): Promise { return mapTo(UserDto, await this.userService.update(dto, new AuthContext(req.user as User))) } @@ -122,14 +166,58 @@ export class UserController { ) } + @Get("/csv") + @UseGuards(OptionalAuthGuard, AuthzGuard) + @ApiOperation({ summary: "List users in CSV", operationId: "listAsCsv" }) + @Header("Content-Type", "text/csv") + async listAsCsv( + @Request() req: ExpressRequest, + @Query(new ValidationPipe(defaultValidationPipeOptions)) + queryParams: UnitsCsvQueryParams + ): Promise { + const users = await this.userService.list( + { + page: 1, + limit: 300, + filter: [ + { + isPortalUser: true, + $comparison: Compare["="], + }, + ], + }, + new AuthContext(req.user as User) + ) + return this.userCsvExporter.exportFromObject(users, queryParams.timeZone) + } + @Post("/invite") @UseGuards(OptionalAuthGuard, AuthzGuard) @ApiOperation({ summary: "Invite user", operationId: "invite" }) @ResourceAction(authzActions.invite) + @UseInterceptors(ActivityLogInterceptor) async invite(@Request() req: ExpressRequest, @Body() dto: UserInviteDto): Promise { return mapTo( UserBasicDto, - await this.userService.invite(dto, new AuthContext(req.user as User)) + await this.userService.invitePartnersPortalUser(dto, new AuthContext(req.user as User)) ) } + + @Get(`:id`) + @ApiOperation({ summary: "Get user by id", operationId: "retrieve" }) + @UseGuards(DefaultAuthGuard, AuthzGuard) + async retrieve( + @Param("id", new ParseUUIDPipe({ version: "4" })) userId: string + ): Promise { + return mapTo(UserDto, await this.userService.findOneOrFail({ id: userId })) + } + + // codegen generate unusable code for this, if we don't have a body + @Delete() + @UseGuards(OptionalAuthGuard, AuthzGuard) + @ApiOperation({ summary: "Delete user by id", operationId: "delete" }) + @UseInterceptors(ActivityLogInterceptor) + async delete(@Body() dto: IdDto): Promise { + return await this.userService.delete(dto.id) + } } diff --git a/backend/core/src/auth/dto/email.dto.ts b/backend/core/src/auth/dto/email.dto.ts index 860a6b48e1..9457c55b58 100644 --- a/backend/core/src/auth/dto/email.dto.ts +++ b/backend/core/src/auth/dto/email.dto.ts @@ -1,10 +1,12 @@ import { Expose } from "class-transformer" import { IsEmail, IsOptional, IsString, MaxLength } from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" export class EmailDto { @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() email: string @Expose() diff --git a/backend/core/src/auth/dto/forgot-password.dto.ts b/backend/core/src/auth/dto/forgot-password.dto.ts index b87df04ded..b61932dfee 100644 --- a/backend/core/src/auth/dto/forgot-password.dto.ts +++ b/backend/core/src/auth/dto/forgot-password.dto.ts @@ -1,11 +1,13 @@ import { IsEmail, IsOptional, IsString, MaxLength } from "class-validator" import { Expose } from "class-transformer" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" export class ForgotPasswordDto { @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() email: string @Expose() diff --git a/backend/core/src/auth/dto/get-mfa-info-response.dto.ts b/backend/core/src/auth/dto/get-mfa-info-response.dto.ts new file mode 100644 index 0000000000..feeb789b3e --- /dev/null +++ b/backend/core/src/auth/dto/get-mfa-info-response.dto.ts @@ -0,0 +1,15 @@ +import { Expose } from "class-transformer" + +export class GetMfaInfoResponseDto { + @Expose() + phoneNumber?: string + + @Expose() + email?: string + + @Expose() + isMfaEnabled: boolean + + @Expose() + mfaUsedInThePast: boolean +} diff --git a/backend/core/src/auth/dto/get-mfa-info.dto.ts b/backend/core/src/auth/dto/get-mfa-info.dto.ts new file mode 100644 index 0000000000..dcee16253c --- /dev/null +++ b/backend/core/src/auth/dto/get-mfa-info.dto.ts @@ -0,0 +1,15 @@ +import { Expose } from "class-transformer" +import { IsEmail, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" + +export class GetMfaInfoDto { + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + email: string + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + password: string +} diff --git a/backend/core/src/auth/dto/login.dto.ts b/backend/core/src/auth/dto/login.dto.ts index bb04605b37..9c462567b7 100644 --- a/backend/core/src/auth/dto/login.dto.ts +++ b/backend/core/src/auth/dto/login.dto.ts @@ -1,13 +1,27 @@ -import { IsEmail, IsString } from "class-validator" +import { IsEmail, IsOptional, IsString, MaxLength, IsEnum } from "class-validator" import { Expose } from "class-transformer" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" +import { MfaType } from "../types/mfa-type" export class LoginDto { @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() email: string @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) password: string + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + mfaCode?: string + + @Expose() + @IsEnum(MfaType, { groups: [ValidationsGroupsEnum.default] }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + mfaType?: MfaType } diff --git a/backend/core/src/auth/dto/request-mfa-code-response.dto.ts b/backend/core/src/auth/dto/request-mfa-code-response.dto.ts new file mode 100644 index 0000000000..e7de50987c --- /dev/null +++ b/backend/core/src/auth/dto/request-mfa-code-response.dto.ts @@ -0,0 +1,12 @@ +import { Expose } from "class-transformer" + +export class RequestMfaCodeResponseDto { + @Expose() + phoneNumber?: string + + @Expose() + email?: string + + @Expose() + phoneNumberVerified?: boolean +} diff --git a/backend/core/src/auth/dto/request-mfa-code.dto.ts b/backend/core/src/auth/dto/request-mfa-code.dto.ts new file mode 100644 index 0000000000..64d0f35192 --- /dev/null +++ b/backend/core/src/auth/dto/request-mfa-code.dto.ts @@ -0,0 +1,25 @@ +import { Expose } from "class-transformer" +import { IsEmail, IsEnum, IsOptional, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" +import { MfaType } from "../types/mfa-type" + +export class RequestMfaCodeDto { + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + email: string + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + password: string + + @Expose() + @IsEnum(MfaType, { groups: [ValidationsGroupsEnum.default] }) + mfaType: MfaType + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + phoneNumber?: string +} diff --git a/backend/core/src/auth/dto/token.dto.ts b/backend/core/src/auth/dto/token.dto.ts new file mode 100644 index 0000000000..b35955aee6 --- /dev/null +++ b/backend/core/src/auth/dto/token.dto.ts @@ -0,0 +1 @@ +export class TokenDto {} diff --git a/backend/core/src/auth/dto/user-basic.dto.ts b/backend/core/src/auth/dto/user-basic.dto.ts index 349a6e9042..5c11eeebd2 100644 --- a/backend/core/src/auth/dto/user-basic.dto.ts +++ b/backend/core/src/auth/dto/user-basic.dto.ts @@ -6,6 +6,7 @@ import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enu import { UserRolesDto } from "./user-roles.dto" import { JurisdictionDto } from "../../jurisdictions/dto/jurisdiction.dto" import { IdDto } from "../../shared/dto/id.dto" +import { UserPreferencesDto } from "./user-preferences.dto" export class UserBasicDto extends OmitType(User, [ "leasingAgentInListings", @@ -14,6 +15,9 @@ export class UserBasicDto extends OmitType(User, [ "resetToken", "roles", "jurisdictions", + "mfaCode", + "mfaCodeUpdatedAt", + "preferences", ] as const) { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @@ -33,4 +37,10 @@ export class UserBasicDto extends OmitType(User, [ @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => IdDto) leasingAgentInListings?: IdDto[] | null + + @Expose() + @IsOptional() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UserPreferencesDto) + preferences?: UserPreferencesDto | null } diff --git a/backend/core/src/auth/dto/user-create.dto.ts b/backend/core/src/auth/dto/user-create.dto.ts index 22df3a2dbc..bfad0c8e9a 100644 --- a/backend/core/src/auth/dto/user-create.dto.ts +++ b/backend/core/src/auth/dto/user-create.dto.ts @@ -4,6 +4,7 @@ import { IsEmail, IsOptional, IsString, Matches, MaxLength, ValidateNested } fro import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { passwordRegex } from "../../shared/password-regex" import { Match } from "../../shared/decorators/match.decorator" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" import { UserDto } from "./user.dto" import { IdDto } from "../../shared/dto/id.dto" @@ -14,6 +15,13 @@ export class UserCreateDto extends OmitType(UserDto, [ "leasingAgentInListings", "roles", "jurisdictions", + "email", + "mfaEnabled", + "passwordUpdatedAt", + "passwordValidForDays", + "lastLoginAt", + "failedLoginAttemptsCount", + "agreedToTermsOfService", ] as const) { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @@ -32,6 +40,7 @@ export class UserCreateDto extends OmitType(UserDto, [ @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @Match("email", { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() emailConfirmation: string @Expose() @@ -45,4 +54,9 @@ export class UserCreateDto extends OmitType(UserDto, [ @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => IdDto) jurisdictions?: IdDto[] + + @Expose() + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + email: string } diff --git a/backend/core/src/auth/dto/user-filter-params.ts b/backend/core/src/auth/dto/user-filter-params.ts index 0ebb7948ff..f3ba81bb49 100644 --- a/backend/core/src/auth/dto/user-filter-params.ts +++ b/backend/core/src/auth/dto/user-filter-params.ts @@ -14,5 +14,15 @@ export class UserFilterParams extends BaseFilter { }) @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) - [UserFilterKeys.isPartner]?: boolean + [UserFilterKeys.isPartner]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: true, + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [UserFilterKeys.isPortalUser]?: boolean } diff --git a/backend/core/src/auth/dto/user-filter-type-to-field-map.ts b/backend/core/src/auth/dto/user-filter-type-to-field-map.ts index f8aaac8a11..62f182305c 100644 --- a/backend/core/src/auth/dto/user-filter-type-to-field-map.ts +++ b/backend/core/src/auth/dto/user-filter-type-to-field-map.ts @@ -1,5 +1,6 @@ import { UserFilterKeys } from "../types/user-filter-keys" export const userFilterTypeToFieldMap: Record = { - isPartner: "user_roles.isPartner", + isPartner: "userRoles.isPartner", + isPortalUser: "userRoles", } diff --git a/backend/core/src/auth/dto/user-invite.dto.ts b/backend/core/src/auth/dto/user-invite.dto.ts index 0601e290e0..360ff09ced 100644 --- a/backend/core/src/auth/dto/user-invite.dto.ts +++ b/backend/core/src/auth/dto/user-invite.dto.ts @@ -13,6 +13,12 @@ export class UserInviteDto extends OmitType(UserDto, [ "roles", "jurisdictions", "leasingAgentInListings", + "mfaEnabled", + "passwordUpdatedAt", + "passwordValidForDays", + "lastLoginAt", + "failedLoginAttemptsCount", + "agreedToTermsOfService", ] as const) { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @@ -27,7 +33,7 @@ export class UserInviteDto extends OmitType(UserDto, [ jurisdictions: IdDto[] @Expose() - @IsOptional() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => IdDto) diff --git a/backend/core/src/auth/dto/user-list-query-params.ts b/backend/core/src/auth/dto/user-list-query-params.ts index d10bd42096..99ac6e3503 100644 --- a/backend/core/src/auth/dto/user-list-query-params.ts +++ b/backend/core/src/auth/dto/user-list-query-params.ts @@ -1,7 +1,14 @@ import { PaginationAllowsAllQueryParams } from "../../shared/dto/pagination.dto" import { Expose, Type } from "class-transformer" import { ApiProperty, getSchemaPath } from "@nestjs/swagger" -import { ArrayMaxSize, IsArray, IsOptional, ValidateNested } from "class-validator" +import { + ArrayMaxSize, + IsArray, + IsOptional, + IsString, + MaxLength, + ValidateNested, +} from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { UserFilterParams } from "./user-filter-params" @@ -22,4 +29,15 @@ export class UserListQueryParams extends PaginationAllowsAllQueryParams { @Type(() => UserFilterParams) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) filter?: UserFilterParams[] + + @Expose() + @ApiProperty({ + type: String, + example: "search", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(128, { groups: [ValidationsGroupsEnum.default] }) + search?: string } diff --git a/backend/core/src/auth/dto/user-preferences.dto.ts b/backend/core/src/auth/dto/user-preferences.dto.ts new file mode 100644 index 0000000000..91b3da8a62 --- /dev/null +++ b/backend/core/src/auth/dto/user-preferences.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from "@nestjs/swagger" +import { UserPreferences } from "../entities/user-preferences.entity" + +export class UserPreferencesDto extends OmitType(UserPreferences, ["user"] as const) {} diff --git a/backend/core/src/auth/dto/user-profile.dto.ts b/backend/core/src/auth/dto/user-profile.dto.ts index 6a537ea082..13a044a5ae 100644 --- a/backend/core/src/auth/dto/user-profile.dto.ts +++ b/backend/core/src/auth/dto/user-profile.dto.ts @@ -3,16 +3,20 @@ import { User } from "../entities/user.entity" import { Expose, Type } from "class-transformer" import { IsDefined, + IsEmail, IsNotEmpty, IsOptional, IsString, Matches, + MaxLength, ValidateIf, ValidateNested, } from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { passwordRegex } from "../../shared/password-regex" import { IdDto } from "../../shared/dto/id.dto" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" +import { UserPreferencesDto } from "./user-preferences.dto" export class UserProfileUpdateDto extends PickType(User, [ "id", @@ -23,6 +27,8 @@ export class UserProfileUpdateDto extends PickType(User, [ "createdAt", "updatedAt", "language", + "phoneNumber", + "agreedToTermsOfService", ] as const) { @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @@ -43,4 +49,21 @@ export class UserProfileUpdateDto extends PickType(User, [ @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => IdDto) jurisdictions: IdDto[] + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + newEmail?: string + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + appUrl?: string | null + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UserPreferencesDto) + preferences?: UserPreferencesDto } diff --git a/backend/core/src/auth/dto/user-update.dto.ts b/backend/core/src/auth/dto/user-update.dto.ts index d30e32c69b..68160176dc 100644 --- a/backend/core/src/auth/dto/user-update.dto.ts +++ b/backend/core/src/auth/dto/user-update.dto.ts @@ -9,6 +9,7 @@ import { IsString, IsUUID, Matches, + MaxLength, ValidateIf, ValidateNested, } from "class-validator" @@ -16,6 +17,8 @@ import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enu import { passwordRegex } from "../../shared/password-regex" import { IdDto } from "../../shared/dto/id.dto" import { UserDto } from "./user.dto" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" +import { UserRolesUpdateDto } from "./user-roles-update.dto" export class UserUpdateDto extends OmitType(UserDto, [ "id", @@ -25,6 +28,11 @@ export class UserUpdateDto extends OmitType(UserDto, [ "leasingAgentInListings", "roles", "jurisdictions", + "mfaEnabled", + "passwordUpdatedAt", + "passwordValidForDays", + "lastLoginAt", + "failedLoginAttemptsCount", ] as const) { @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @@ -34,6 +42,7 @@ export class UserUpdateDto extends OmitType(UserDto, [ @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() email?: string @Expose() @@ -62,9 +71,34 @@ export class UserUpdateDto extends OmitType(UserDto, [ @IsNotEmpty({ groups: [ValidationsGroupsEnum.default] }) currentPassword?: string + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UserRolesUpdateDto) + roles?: UserRolesUpdateDto | null + @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => IdDto) jurisdictions: IdDto[] + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => IdDto) + leasingAgentInListings?: IdDto[] | null + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() + newEmail?: string + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(256, { groups: [ValidationsGroupsEnum.default] }) + appUrl?: string | null } diff --git a/backend/core/src/auth/dto/user.dto.ts b/backend/core/src/auth/dto/user.dto.ts index 8cde05a9c4..430411e0b9 100644 --- a/backend/core/src/auth/dto/user.dto.ts +++ b/backend/core/src/auth/dto/user.dto.ts @@ -6,6 +6,7 @@ import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enu import { IdNameDto } from "../../shared/dto/idName.dto" import { UserRolesDto } from "./user-roles.dto" import { JurisdictionDto } from "../../jurisdictions/dto/jurisdiction.dto" +import { UserPreferencesDto } from "./user-preferences.dto" export class UserDto extends OmitType(User, [ "leasingAgentInListings", @@ -14,6 +15,9 @@ export class UserDto extends OmitType(User, [ "confirmationToken", "roles", "jurisdictions", + "mfaCode", + "mfaCodeUpdatedAt", + "preferences", ] as const) { @Expose() @IsOptional() @@ -33,4 +37,9 @@ export class UserDto extends OmitType(User, [ @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @Type(() => JurisdictionDto) jurisdictions: JurisdictionDto[] + + @Expose() + @IsOptional() + @Type(() => UserPreferencesDto) + preferences?: UserPreferencesDto | null } diff --git a/backend/core/src/auth/entities/user-preferences.entity.ts b/backend/core/src/auth/entities/user-preferences.entity.ts new file mode 100644 index 0000000000..914a32fffb --- /dev/null +++ b/backend/core/src/auth/entities/user-preferences.entity.ts @@ -0,0 +1,28 @@ +import { User } from "./user.entity" +import { Column, Entity, JoinColumn, OneToOne } from "typeorm" +import { Expose } from "class-transformer" +import { IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +@Entity({ name: "user_preferences" }) +export class UserPreferences { + @OneToOne(() => User, (user) => user.preferences, { + primary: true, + onDelete: "CASCADE", + }) + @JoinColumn() + user: User + + @Column("boolean", { default: false }) + @Expose() + sendEmailNotifications?: boolean + + @Column("boolean", { default: false }) + @Expose() + sendSmsNotifications?: boolean + + @Column("text", { array: true, default: [] }) + @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) + @Expose() + favoriteIds?: string[] +} diff --git a/backend/core/src/auth/entities/user-roles.entity.ts b/backend/core/src/auth/entities/user-roles.entity.ts index 93e99454ce..f07c9b318c 100644 --- a/backend/core/src/auth/entities/user-roles.entity.ts +++ b/backend/core/src/auth/entities/user-roles.entity.ts @@ -6,6 +6,8 @@ import { User } from "./user.entity" export class UserRoles { @OneToOne(() => User, (user) => user.roles, { primary: true, + onDelete: "CASCADE", + onUpdate: "CASCADE", }) @JoinColumn() user: User diff --git a/backend/core/src/auth/entities/user.entity.ts b/backend/core/src/auth/entities/user.entity.ts index 24d7afd74d..b980cbc1e7 100644 --- a/backend/core/src/auth/entities/user.entity.ts +++ b/backend/core/src/auth/entities/user.entity.ts @@ -12,12 +12,24 @@ import { } from "typeorm" import { Listing } from "../../listings/entities/listing.entity" import { Expose, Type } from "class-transformer" -import { IsDate, IsEmail, IsEnum, IsOptional, IsString, IsUUID, MaxLength } from "class-validator" +import { + IsBoolean, + IsDate, + IsEmail, + IsEnum, + IsOptional, + IsPhoneNumber, + IsString, + IsUUID, + MaxLength, +} from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { ApiProperty } from "@nestjs/swagger" import { Language } from "../../shared/types/language-enum" import { UserRoles } from "./user-roles.entity" import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" +import { UserPreferences } from "./user-preferences.entity" @Entity({ name: "user_accounts" }) @Unique(["email"]) @@ -31,6 +43,15 @@ export class User { @Column("varchar", { select: false }) passwordHash: string + @Column({ default: () => "NOW()" }) + @Expose() + @Type(() => Date) + passwordUpdatedAt: Date + + @Column({ default: 180 }) + @Expose() + passwordValidForDays: number + @Column("varchar", { nullable: true }) resetToken: string @@ -47,6 +68,7 @@ export class User { @Column("varchar") @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() email: string @Column("varchar") @@ -75,6 +97,12 @@ export class User { @Type(() => Date) dob?: Date | null + @Column("varchar", { nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsPhoneNumber("US", { groups: [ValidationsGroupsEnum.default] }) + phoneNumber?: string + @CreateDateColumn() @Expose() @IsDate({ groups: [ValidationsGroupsEnum.default] }) @@ -94,6 +122,8 @@ export class User { eager: true, cascade: true, nullable: true, + onDelete: "CASCADE", + onUpdate: "CASCADE", }) @Expose() roles?: UserRoles @@ -108,4 +138,58 @@ export class User { @ManyToMany(() => Jurisdiction, { cascade: true, eager: true }) @JoinTable() jurisdictions: Jurisdiction[] + + @Column({ type: "bool", default: false }) + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + mfaEnabled?: boolean + + @Column("varchar", { nullable: true }) + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @MaxLength(16, { groups: [ValidationsGroupsEnum.default] }) + mfaCode?: string + + @Column({ type: "timestamptz", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + mfaCodeUpdatedAt?: Date | null + + @Column({ default: () => "NOW()" }) + @Expose() + @Type(() => Date) + lastLoginAt?: Date + + @Column({ default: 0 }) + @Expose() + @Type(() => Date) + failedLoginAttemptsCount?: number + + @Column({ type: "bool", nullable: true, default: false }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + phoneNumberVerified?: boolean + + @Column({ type: "bool", nullable: false, default: false }) + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + agreedToTermsOfService: boolean + + @Column({ type: "timestamptz", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + hitConfirmationURL?: Date | null + + @OneToOne(() => UserPreferences, (preferences) => preferences.user, { + eager: true, + cascade: true, + nullable: true, + }) + @Expose() + preferences?: UserPreferences } diff --git a/backend/core/src/auth/filters/user-query-filter.ts b/backend/core/src/auth/filters/user-query-filter.ts new file mode 100644 index 0000000000..09c2ccca4f --- /dev/null +++ b/backend/core/src/auth/filters/user-query-filter.ts @@ -0,0 +1,79 @@ +import { BaseQueryFilter } from "../../shared/query-filter/base-query-filter" +import { Brackets, WhereExpression } from "typeorm" +import { UserFilterKeys } from "../types/user-filter-keys" +import { userFilterTypeToFieldMap } from "../dto/user-filter-type-to-field-map" + +export class UserQueryFilter extends BaseQueryFilter { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addFilters( + filters: FilterParams, + filterTypeToFieldMap: FilterFieldMap, + qb: WhereExpression + ) { + for (const [index, filter] of filters.entries()) { + for (const filterKey in filter) { + if (BaseQueryFilter._shouldSkipKey(filter, filterKey)) { + continue + } + BaseQueryFilter._isSupportedFilterTypeOrThrow(filterKey, filterTypeToFieldMap) + const filterValue = BaseQueryFilter._getFilterValue(filter, filterKey) + switch (filterKey) { + case UserFilterKeys.isPortalUser: + this.addIsPortalUserQuery(qb, filterValue) + continue + } + BaseQueryFilter._compare(qb, filter, filterKey, filterTypeToFieldMap, index) + } + } + } + + private addIsPortalUserQuery(qb: WhereExpression, filterValue: string) { + const userRolesColumnName = userFilterTypeToFieldMap[UserFilterKeys.isPortalUser] + if (filterValue == "true") { + qb.andWhere( + new Brackets((subQb) => { + subQb.where(`${userRolesColumnName}.isPartner = true`) + subQb.orWhere(`${userRolesColumnName}.isAdmin = true`) + }) + ) + } else if (filterValue == "false") { + qb.andWhere( + new Brackets((subQb) => { + subQb.where(`${userRolesColumnName}.isPartner IS NULL`) + subQb.orWhere(`${userRolesColumnName}.isPartner = false`) + }) + ) + qb.andWhere( + new Brackets((subQb) => { + subQb.where(`${userRolesColumnName}.isAdmin IS NULL`) + subQb.orWhere(`${userRolesColumnName}.isAdmin = false`) + }) + ) + } + } +} + +export function addIsPortalUserQuery(qb: WhereExpression, filterValue: string) { + const userRolesColumnName = userFilterTypeToFieldMap[UserFilterKeys.isPortalUser] + if (filterValue == "true") { + qb.andWhere( + new Brackets((subQb) => { + subQb.where(`${userRolesColumnName}.isPartner = true`) + subQb.orWhere(`${userRolesColumnName}.isAdmin = true`) + }) + ) + } else if (filterValue == "false") { + qb.andWhere( + new Brackets((subQb) => { + subQb.where(`${userRolesColumnName}.isPartner IS NULL`) + subQb.orWhere(`${userRolesColumnName}.isPartner = false`) + }) + ) + qb.andWhere( + new Brackets((subQb) => { + subQb.where(`${userRolesColumnName}.isAdmin IS NULL`) + subQb.orWhere(`${userRolesColumnName}.isAdmin = false`) + }) + ) + } +} diff --git a/backend/core/src/auth/guards/authz.guard.ts b/backend/core/src/auth/guards/authz.guard.ts index 1751b4916c..4c4320f411 100644 --- a/backend/core/src/auth/guards/authz.guard.ts +++ b/backend/core/src/auth/guards/authz.guard.ts @@ -1,14 +1,7 @@ -import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common" +import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common" import { Reflector } from "@nestjs/core" import { AuthzService } from "../services/authz.service" - -const httpMethodsToAction = { - PUT: "update", - PATCH: "update", - DELETE: "delete", - POST: "create", - GET: "read", -} +import { httpMethodsToAction } from "../../shared/http-methods-to-actions" @Injectable() export class AuthzGuard implements CanActivate { @@ -27,9 +20,13 @@ export class AuthzGuard implements CanActivate { let resource if (req.params.id) { - // NOTE: implicit assumption that if request.params contains an ID it also means that body contains one too and it should be the same - // This prevents a security hole where user specifies params.id different than dto.id to pass authorization but actually edit a different resource - resource = { id: req.body.id } + // NOTE: implicit assumption that if request.params contains an ID it also means that for requests other + // than GET and DELETE body also contains one too and it should be the same + // This prevents a security hole where user specifies params.id different than dto.id to pass authorization + // but actually edits a different resource + resource = ["GET", "DELETE"].includes(req.method) + ? { id: req.params.id } + : { id: req.body.id } } return this.authzService.can(authUser, type, action, resource) diff --git a/backend/core/src/auth/guards/local-auth.guard.ts b/backend/core/src/auth/guards/local-auth.guard.ts deleted file mode 100644 index b96aadaa50..0000000000 --- a/backend/core/src/auth/guards/local-auth.guard.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Injectable } from "@nestjs/common" -import { AuthGuard } from "@nestjs/passport" - -@Injectable() -export class LocalAuthGuard extends AuthGuard("local") {} diff --git a/backend/core/src/auth/guards/local-mfa-auth.guard.ts b/backend/core/src/auth/guards/local-mfa-auth.guard.ts new file mode 100644 index 0000000000..378df8c5ea --- /dev/null +++ b/backend/core/src/auth/guards/local-mfa-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from "@nestjs/common" +import { AuthGuard } from "@nestjs/passport" + +@Injectable() +export class LocalMfaAuthGuard extends AuthGuard("localMfa") {} diff --git a/backend/core/src/auth/guards/user-preferences-authz.guard.ts b/backend/core/src/auth/guards/user-preferences-authz.guard.ts new file mode 100644 index 0000000000..1ea1d62ae7 --- /dev/null +++ b/backend/core/src/auth/guards/user-preferences-authz.guard.ts @@ -0,0 +1,21 @@ +import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common" +import { AuthzService } from "../services/authz.service" +import { Reflector } from "@nestjs/core" +import { httpMethodsToAction } from "../../shared/http-methods-to-actions" + +@Injectable() +export class UserPreferencesAuthzGuard implements CanActivate { + constructor(private authzService: AuthzService, private reflector: Reflector) {} + + async canActivate(context: ExecutionContext) { + const req = context.switchToHttp().getRequest() + const authUser = req.user + const action = + this.reflector.get("authz_action", context.getHandler()) || + httpMethodsToAction[req.method] + + return await this.authzService.can(authUser, "userPreference", action, { + id: req.params.id, + }) + } +} diff --git a/backend/core/src/auth/guards/user-profile-authz.guard.ts b/backend/core/src/auth/guards/user-profile-authz.guard.ts new file mode 100644 index 0000000000..708931d57a --- /dev/null +++ b/backend/core/src/auth/guards/user-profile-authz.guard.ts @@ -0,0 +1,19 @@ +import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common" +import { AuthzService } from "../services/authz.service" +import { Reflector } from "@nestjs/core" +import { authzActions } from "../enum/authz-actions.enum" + +@Injectable() +export class UserProfileAuthzGuard implements CanActivate { + constructor(private authzService: AuthzService, private reflector: Reflector) {} + + async canActivate(context: ExecutionContext) { + const req = context.switchToHttp().getRequest() + const authUser = req.user + const type = this.reflector.getAllAndOverride("authz_type", [ + context.getClass(), + context.getHandler(), + ]) + return this.authzService.can(authUser, type, authzActions.read, { id: authUser.id }) + } +} diff --git a/backend/core/src/auth/passport-strategies/jwt.strategy.ts b/backend/core/src/auth/passport-strategies/jwt.strategy.ts index 271247cfc9..88a0bc7cb2 100644 --- a/backend/core/src/auth/passport-strategies/jwt.strategy.ts +++ b/backend/core/src/auth/passport-strategies/jwt.strategy.ts @@ -1,12 +1,14 @@ import { ExtractJwt, Strategy } from "passport-jwt" import { PassportStrategy } from "@nestjs/passport" -import { Injectable, UnauthorizedException } from "@nestjs/common" +import { HttpException, Injectable, UnauthorizedException } from "@nestjs/common" import { Request } from "express" import { ConfigService } from "@nestjs/config" import { AuthService } from "../services/auth.service" import { InjectRepository } from "@nestjs/typeorm" import { User } from "../entities/user.entity" import { Repository } from "typeorm" +import { UserService } from "../services/user.service" +import { USER_ERRORS } from "../user-errors" function extractTokenFromAuthHeader(req: Request) { const authHeader = req.get("Authorization") @@ -35,9 +37,18 @@ export class JwtStrategy extends PassportStrategy(Strategy) { throw new UnauthorizedException() } const userId = payload.sub - return this.userRepository.findOne({ + const user = await this.userRepository.findOne({ where: { id: userId }, relations: ["leasingAgentInListings"], }) + + if (user && UserService.isPasswordOutdated(user)) { + throw new HttpException( + USER_ERRORS.PASSWORD_OUTDATED.message, + USER_ERRORS.PASSWORD_OUTDATED.status + ) + } + + return user } } diff --git a/backend/core/src/auth/passport-strategies/local-mfa.strategy.ts b/backend/core/src/auth/passport-strategies/local-mfa.strategy.ts new file mode 100644 index 0000000000..ecb8c5ce92 --- /dev/null +++ b/backend/core/src/auth/passport-strategies/local-mfa.strategy.ts @@ -0,0 +1,121 @@ +import { Strategy } from "passport-custom" +import { PassportStrategy } from "@nestjs/passport" +import { + HttpException, + HttpStatus, + Injectable, + UnauthorizedException, + ValidationPipe, +} from "@nestjs/common" +import { User } from "../entities/user.entity" +import { InjectRepository } from "@nestjs/typeorm" +import { Repository } from "typeorm" +import { PasswordService } from "../services/password.service" +import { defaultValidationPipeOptions } from "../../shared/default-validation-pipe-options" +import { LoginDto } from "../dto/login.dto" +import { ConfigService } from "@nestjs/config" +import { UserService } from "../services/user.service" +import { USER_ERRORS } from "../user-errors" +import { MfaType } from "../types/mfa-type" + +@Injectable() +export class LocalMfaStrategy extends PassportStrategy(Strategy, "localMfa") { + constructor( + @InjectRepository(User) private readonly userRepository: Repository, + private readonly passwordService: PasswordService, + private readonly configService: ConfigService + ) { + super() + } + + async validate(req: Request): Promise { + const validationPipe = new ValidationPipe(defaultValidationPipeOptions) + const loginDto: LoginDto = await validationPipe.transform(req.body, { + type: "body", + metatype: LoginDto, + }) + + const user = await this.userRepository.findOne({ + where: { email: loginDto.email.toLowerCase() }, + relations: ["leasingAgentInListings"], + }) + + if (user) { + if (user.lastLoginAt) { + const retryAfter = new Date( + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + user.lastLoginAt.getTime() + this.configService.get("AUTH_LOCK_LOGIN_COOLDOWN_MS") + ) + if ( + user.failedLoginAttemptsCount >= + this.configService.get("AUTH_LOCK_LOGIN_AFTER_FAILED_ATTEMPTS") && + retryAfter > new Date() + ) { + throw new HttpException( + { + statusCode: HttpStatus.TOO_MANY_REQUESTS, + error: "Too Many Requests", + message: "Failed login attempts exceeded.", + retryAfter, + }, + 429 + ) + } + } + + if (!user.confirmedAt) { + throw new HttpException( + USER_ERRORS.ACCOUNT_NOT_CONFIRMED.message, + USER_ERRORS.ACCOUNT_NOT_CONFIRMED.status + ) + } + + if (UserService.isPasswordOutdated(user)) { + throw new HttpException( + USER_ERRORS.PASSWORD_OUTDATED.message, + USER_ERRORS.PASSWORD_OUTDATED.status + ) + } + + const validPassword = await this.passwordService.isPasswordValid(user, loginDto.password) + + let mfaAuthSuccessful = true + if (validPassword && user.mfaEnabled) { + if (!loginDto.mfaCode || !user.mfaCode || !user.mfaCodeUpdatedAt) { + throw new UnauthorizedException({ name: "mfaCodeIsMissing" }) + } + if ( + new Date( + user.mfaCodeUpdatedAt.getTime() + this.configService.get("MFA_CODE_VALID_MS") + ) < new Date() || + user.mfaCode !== loginDto.mfaCode + ) { + mfaAuthSuccessful = false + } else { + user.mfaCode = null + user.mfaCodeUpdatedAt = new Date() + } + } + + if (validPassword && mfaAuthSuccessful) { + user.failedLoginAttemptsCount = 0 + if (!user.phoneNumberVerified && loginDto.mfaType === MfaType.sms) { + user.phoneNumberVerified = true + } + } else { + user.failedLoginAttemptsCount += 1 + } + + user.lastLoginAt = new Date() + await this.userRepository.save(user) + + if (validPassword && mfaAuthSuccessful) { + return user + } else if (validPassword && user.mfaEnabled && !mfaAuthSuccessful) { + throw new UnauthorizedException({ message: "mfaUnauthorized" }) + } + } + + throw new UnauthorizedException() + } +} diff --git a/backend/core/src/auth/passport-strategies/local.strategy.ts b/backend/core/src/auth/passport-strategies/local.strategy.ts deleted file mode 100644 index 268d2bdc93..0000000000 --- a/backend/core/src/auth/passport-strategies/local.strategy.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Strategy } from "passport-local" -import { PassportStrategy } from "@nestjs/passport" -import { Injectable, UnauthorizedException } from "@nestjs/common" -import { User } from "../entities/user.entity" -import { InjectRepository } from "@nestjs/typeorm" -import { Repository } from "typeorm" -import { PasswordService } from "../services/password.service" - -@Injectable() -export class LocalStrategy extends PassportStrategy(Strategy) { - constructor( - @InjectRepository(User) private readonly userRepository: Repository, - private readonly passwordService: PasswordService - ) { - super({ - usernameField: "email", - }) - } - - async validate(email: string, password: string): Promise { - const user = await this.userRepository.findOne({ - where: { email }, - relations: ["leasingAgentInListings"], - }) - - if (user) { - const validPassword = await this.passwordService.verifyUserPassword(user, password) - if (validPassword && user.confirmedAt) { - return user - } - } - throw new UnauthorizedException() - } -} diff --git a/backend/core/src/auth/repositories/user-repository.ts b/backend/core/src/auth/repositories/user-repository.ts new file mode 100644 index 0000000000..9a6a1f1396 --- /dev/null +++ b/backend/core/src/auth/repositories/user-repository.ts @@ -0,0 +1,35 @@ +import { EntityRepository, Repository, SelectQueryBuilder } from "typeorm" +import { User } from "../entities/user.entity" + +@EntityRepository(User) +export class UserRepository extends Repository { + public getQb(): SelectQueryBuilder { + return this.createQueryBuilder("user") + .leftJoin("user.leasingAgentInListings", "leasingAgentInListings") + .leftJoin("user.jurisdictions", "jurisdictions") + .leftJoin("user.roles", "userRoles") + .select([ + "user", + "jurisdictions.id", + "userRoles", + "leasingAgentInListings.id", + "leasingAgentInListings.name", + ]) + } + + public async findByEmail(email: string) { + return this.getQb().where("user.email = :email", { email: email.toLowerCase() }).getOne() + } + + public async findById(id: string) { + return this.getQb().where("user.id = :id", { id }).getOne() + } + + public async findByConfirmationToken(token: string) { + return this.getQb().where("user.confirmationToken = :token", { token }).getOne() + } + + public async findByResetToken(token: string) { + return this.getQb().where("user.resetToken = :token", { token }).getOne() + } +} diff --git a/backend/core/src/auth/services/authz.service.ts b/backend/core/src/auth/services/authz.service.ts index f51f9848b6..97b90875e8 100644 --- a/backend/core/src/auth/services/authz.service.ts +++ b/backend/core/src/auth/services/authz.service.ts @@ -55,13 +55,14 @@ export class AuthzService { void e.addPermissionForUser( user.id, "listing", - `!r.obj || r.obj.listing_id == '${listing.id}'`, + `!r.obj || r.obj.id == '${listing.id}'`, `(${authzActions.read}|${authzActions.update})` ) }) ) } - return e.enforce(user ? user.id : "anonymous", type, action, obj) + + return await e.enforce(user ? user.id : "anonymous", type, action, obj) } /** diff --git a/backend/core/src/auth/services/password.service.ts b/backend/core/src/auth/services/password.service.ts index d84ccd4068..977bed7cf7 100644 --- a/backend/core/src/auth/services/password.service.ts +++ b/backend/core/src/auth/services/password.service.ts @@ -12,14 +12,13 @@ export class PasswordService { // passwordHash is a hidden field - we need to build a query to get it directly public async getUserWithPassword(user: User) { return await this.userRepository - .createQueryBuilder() + .createQueryBuilder("user") .addSelect("user.passwordHash") - .from(User, "user") .where("user.id = :id", { id: user.id }) .getOne() } - public async verifyUserPassword(user: User, password: string) { + public async isPasswordValid(user: User, password: string) { const userWithPassword = await this.getUserWithPassword(user) const [salt, savedPasswordHash] = userWithPassword.passwordHash.split("#") const verifyPasswordHash = await this.hashPassword(password, Buffer.from(salt, "hex")) diff --git a/backend/core/src/auth/services/sms-mfa.service.ts b/backend/core/src/auth/services/sms-mfa.service.ts new file mode 100644 index 0000000000..7fae784ea2 --- /dev/null +++ b/backend/core/src/auth/services/sms-mfa.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@nestjs/common" +import { User } from "../entities/user.entity" +import { InjectTwilio, TwilioClient } from "nestjs-twilio" +import { ConfigService } from "@nestjs/config" + +@Injectable() +export class SmsMfaService { + public constructor( + @InjectTwilio() private readonly client: TwilioClient, + private readonly configService: ConfigService + ) {} + public async sendMfaCode(user: User, phoneNumber: string, mfaCode: string) { + return await this.client.messages.create({ + body: `Your Partners Portal account access token: ${mfaCode}`, + from: this.configService.get("TWILIO_PHONE_NUMBER"), + to: phoneNumber, + }) + } +} diff --git a/backend/core/src/auth/services/user-csv-exporter.service.ts b/backend/core/src/auth/services/user-csv-exporter.service.ts new file mode 100644 index 0000000000..bd9441a250 --- /dev/null +++ b/backend/core/src/auth/services/user-csv-exporter.service.ts @@ -0,0 +1,36 @@ +import { Injectable, Scope } from "@nestjs/common" +import { Pagination } from "nestjs-typeorm-paginate" +import { formatLocalDate } from "../../shared/utils/format-local-date" +import { CsvBuilder } from "../../applications/services/csv-builder.service" +import { User } from "../entities/user.entity" + +@Injectable({ scope: Scope.REQUEST }) +export class UserCsvExporterService { + constructor(private readonly csvBuilder: CsvBuilder) {} + + exportFromObject(users: Pagination, timeZone: string): string { + const userObj = users.items.reduce((obj, user) => { + const status = [] + if (user.roles?.isAdmin) { + status.push("Administrator") + } + if (user.roles?.isPartner) { + status.push("Partner") + } + obj[user.id] = { + "First Name": user.firstName, + "Last Name": user.lastName, + Email: user.email, + Role: status.join(", "), + "Date Created": formatLocalDate(user.createdAt, "MM-DD-YYYY hh:mmA z", timeZone), + Status: user.confirmedAt ? "Confirmed" : "Unconfirmed", + "Listing Names": + user.leasingAgentInListings?.map((listing) => listing.name).join(", ") || "", + "Listing Ids": user.leasingAgentInListings?.map((listing) => listing.id).join(", ") || "", + "Last Logged In": formatLocalDate(user.lastLoginAt, "MM-DD-YYYY hh:mmA z", timeZone), + } + return obj + }, {}) + return this.csvBuilder.buildFromIdIndex(userObj) + } +} diff --git a/backend/core/src/auth/services/user-preferences.services.ts b/backend/core/src/auth/services/user-preferences.services.ts new file mode 100644 index 0000000000..480eecd778 --- /dev/null +++ b/backend/core/src/auth/services/user-preferences.services.ts @@ -0,0 +1,20 @@ +import { UserPreferences } from "../entities/user-preferences.entity" +import { UserPreferencesDto } from "../dto/user-preferences.dto" +import { InjectRepository } from "@nestjs/typeorm" +import { Repository } from "typeorm" +import { AuthContext } from "../types/auth-context" + +export class UserPreferencesService { + constructor( + @InjectRepository(UserPreferences) + private readonly repository: Repository + ) {} + + async update(dto: UserPreferencesDto, authContext: AuthContext) { + await this.repository.save({ + user: authContext.user, + ...dto, + }) + return dto + } +} diff --git a/backend/core/src/auth/services/user.service.spec.ts b/backend/core/src/auth/services/user.service.spec.ts index 362550fca7..f2973d0d38 100644 --- a/backend/core/src/auth/services/user.service.spec.ts +++ b/backend/core/src/auth/services/user.service.spec.ts @@ -4,7 +4,6 @@ import { UserService } from "./user.service" import { getRepositoryToken } from "@nestjs/typeorm" import { User } from "../entities/user.entity" import { USER_ERRORS } from "../user-errors" -import { EmailService } from "../../shared/email/email.service" import { AuthService } from "./auth.service" import { AuthzService } from "./authz.service" import { PasswordService } from "./password.service" @@ -12,38 +11,46 @@ import { JurisdictionResolverService } from "../../jurisdictions/services/jurisd import { ConfigService } from "@nestjs/config" import { UserCreateDto } from "../dto/user-create.dto" import { Application } from "../../applications/entities/application.entity" +import { EmailService } from "../../email/email.service" +import { SmsMfaService } from "./sms-mfa.service" +import { UserInviteDto } from "../dto/user-invite.dto" +import { UserRepository } from "../repositories/user-repository" +import { JurisdictionsService } from "../../jurisdictions/services/jurisdictions.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. // see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 declare const expect: jest.Expect -const mockedUser = { id: "123", email: "abc@xyz.com" } -const mockUserRepo = { findOne: jest.fn().mockResolvedValue(mockedUser), save: jest.fn() } -const mockApplicationRepo = { - createQueryBuilder: jest.fn().mockResolvedValue(mockedUser), - save: jest.fn(), -} - describe("UserService", () => { let service: UserService + const mockUserRepo = { findOne: jest.fn(), save: jest.fn() } + const mockApplicationRepo = { + createQueryBuilder: jest.fn(), + save: jest.fn(), + } beforeEach(async () => { process.env.APP_SECRET = "SECRET" const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, + UserRepository, { provide: getRepositoryToken(User), useValue: mockUserRepo, }, + { + provide: getRepositoryToken(UserRepository), + useValue: mockUserRepo, + }, { provide: getRepositoryToken(Application), useValue: mockApplicationRepo, }, { provide: EmailService, - useValue: { forgotPassword: jest.fn() }, + useValue: { forgotPassword: jest.fn(), invite: jest.fn() }, }, { provide: AuthService, @@ -55,7 +62,14 @@ describe("UserService", () => { getJurisdiction: jest.fn(), }, }, + { + provide: JurisdictionsService, + useValue: { + findOne: jest.fn(), + }, + }, AuthzService, + { provide: SmsMfaService, useValue: { sendMfaCode: jest.fn() } }, PasswordService, { provide: ConfigService, @@ -67,12 +81,18 @@ describe("UserService", () => { service = await module.resolve(UserService) }) + afterEach(() => { + jest.resetAllMocks() + }) + it("should be defined", () => { expect(service).toBeDefined() }) describe("createUser", () => { it("should return EMAIL_IN_USE error if email is already in use", async () => { + const mockedUser = { id: "123", email: "abc@xyz.com" } + mockUserRepo.findOne.mockResolvedValueOnce(mockedUser) const user: UserCreateDto = { email: "abc@xyz.com", emailConfirmation: "abc@xyz.com", @@ -82,7 +102,7 @@ describe("UserService", () => { lastName: "Last", dob: new Date(), } - await expect(service.createUser(user, null, null)).rejects.toThrow( + await expect(service.createPublicUser(user, null, null)).rejects.toThrow( new HttpException(USER_ERRORS.EMAIL_IN_USE.message, USER_ERRORS.EMAIL_IN_USE.status) ) }) @@ -98,25 +118,153 @@ describe("UserService", () => { dob: new Date(), } mockUserRepo.findOne = jest.fn().mockResolvedValue(null) - mockUserRepo.save = jest.fn().mockRejectedValue(new Error("failed to save")) - await expect(service.createUser(user, null, null)).rejects.toThrow( + mockUserRepo.save = jest.fn().mockRejectedValue(new Error(USER_ERRORS.ERROR_SAVING.message)) + await expect(service.createPublicUser(user, null, null)).rejects.toThrow( new HttpException(USER_ERRORS.ERROR_SAVING.message, USER_ERRORS.ERROR_SAVING.status) ) // Reset mockUserRepo.save mockUserRepo.save = jest.fn() }) + + it("should return EMAIL_IN_USE if attempting to resave existing public user", async () => { + const user: UserCreateDto = { + email: "new@email.com", + emailConfirmation: "new@email.com", + password: "qwerty", + passwordConfirmation: "qwerty", + firstName: "First", + lastName: "Last", + dob: new Date(), + } + mockUserRepo.findOne = jest.fn().mockResolvedValue({ ...user, confirmedAt: new Date() }) + await expect(service._createUser(user, null)).rejects.toThrow( + new HttpException(USER_ERRORS.EMAIL_IN_USE.message, USER_ERRORS.EMAIL_IN_USE.status) + ) + + // Reset mockUserRepo.save + mockUserRepo.save = jest.fn() + }) + + it("should return EMAIL_IN_USE if attempting to create public user from existing partner user", async () => { + const existingUser: User = { + id: "mock id", + email: "new@email.com", + firstName: "First", + lastName: "Last", + dob: new Date(), + confirmedAt: new Date(), + passwordHash: "", + passwordUpdatedAt: new Date(), + passwordValidForDays: 0, + resetToken: "", + createdAt: new Date(), + updatedAt: new Date(), + jurisdictions: [], + agreedToTermsOfService: true, + } + existingUser.roles = { user: existingUser } + + const user: UserCreateDto = { + email: "new@email.com", + emailConfirmation: "new@email.com", + password: "qwerty", + passwordConfirmation: "qwerty", + firstName: "First", + lastName: "Last", + dob: new Date(), + } + mockUserRepo.findOne = jest.fn().mockResolvedValue(existingUser) + await expect(service._createUser(user, null)).rejects.toThrow( + new HttpException(USER_ERRORS.EMAIL_IN_USE.message, USER_ERRORS.EMAIL_IN_USE.status) + ) + + // Reset mockUserRepo.save + mockUserRepo.save = jest.fn() + }) + + it("should save successfully if attempting to create partner user from public user", async () => { + const existingUser: User = { + id: "mock id", + email: "new@email.com", + firstName: "First", + lastName: "Last", + dob: new Date(), + confirmedAt: new Date(), + passwordHash: "", + passwordUpdatedAt: new Date(), + passwordValidForDays: 0, + resetToken: "", + createdAt: new Date(), + updatedAt: new Date(), + jurisdictions: [], + agreedToTermsOfService: true, + } + + const user: UserInviteDto = { + email: "new@email.com", + firstName: "First", + lastName: "Last", + dob: new Date(), + roles: { isPartner: true }, + jurisdictions: [], + } + + mockUserRepo.findOne = jest.fn().mockResolvedValue(existingUser) + mockUserRepo.save = jest.fn().mockResolvedValue(user) + const savedUser = await service.invitePartnersPortalUser(user, null) + expect(savedUser).toBe(user) + + // Reset mockUserRepo.save + mockUserRepo.save = jest.fn() + }) + + it("should return EMAIL_IN_USE if attempting to recreate existing partner user", async () => { + const existingUser: User = { + id: "mock id", + email: "new@email.com", + firstName: "First", + lastName: "Last", + dob: new Date(), + confirmedAt: new Date(), + passwordHash: "", + passwordUpdatedAt: new Date(), + passwordValidForDays: 0, + resetToken: "", + createdAt: new Date(), + updatedAt: new Date(), + jurisdictions: [], + agreedToTermsOfService: true, + } + existingUser.roles = { user: existingUser } + + const user: UserInviteDto = { + email: "new@email.com", + firstName: "First", + lastName: "Last", + dob: new Date(), + roles: { isPartner: true }, + jurisdictions: [], + } + + mockUserRepo.findOne = jest.fn().mockResolvedValue(existingUser) + await expect(service._createUser(user, null)).rejects.toThrow( + new HttpException(USER_ERRORS.EMAIL_IN_USE.message, USER_ERRORS.EMAIL_IN_USE.status) + ) + + // Reset mockUserRepo.save + mockUserRepo.save = jest.fn() + }) }) describe("forgotPassword", () => { - it("should return 400 if email is not found", async () => { + it("should return undefined if email is not found", async () => { mockUserRepo.findOne = jest.fn().mockResolvedValue(null) - await expect(service.forgotPassword({ email: "abc@xyz.com" })).rejects.toThrow( - new HttpException(USER_ERRORS.NOT_FOUND.message, USER_ERRORS.NOT_FOUND.status) - ) + await expect(service.forgotPassword({ email: "abc@xyz.com" })).resolves.toBeUndefined() }) it("should set resetToken", async () => { + const mockedUser = { id: "123", email: "abc@xyz.com" } mockUserRepo.findOne = jest.fn().mockResolvedValue({ ...mockedUser, resetToken: null }) const user = await service.forgotPassword({ email: "abc@xyz.com" }) expect(user["resetToken"]).toBeDefined() @@ -133,8 +281,10 @@ describe("UserService", () => { }) it("should set resetToken", async () => { - mockUserRepo.findOne = jest.fn().mockResolvedValue({ ...mockedUser }) + const mockedUser = { id: "123", email: "abc@xyz.com" } + mockUserRepo.findOne = jest.fn().mockResolvedValue(mockedUser) // Sets resetToken + console.log({ service }) await service.forgotPassword({ email: "abc@xyz.com" }) const accessToken = await service.updatePassword(updateDto) expect(accessToken).toBeDefined() diff --git a/backend/core/src/auth/services/user.service.ts b/backend/core/src/auth/services/user.service.ts index 8d15b6904d..13432e3468 100644 --- a/backend/core/src/auth/services/user.service.ts +++ b/backend/core/src/auth/services/user.service.ts @@ -1,23 +1,22 @@ import { BadRequestException, HttpException, + HttpStatus, Injectable, NotFoundException, Scope, UnauthorizedException, } from "@nestjs/common" import { InjectRepository } from "@nestjs/typeorm" -import { FindConditions, Repository } from "typeorm" -import { paginate, Pagination } from "nestjs-typeorm-paginate" +import { Brackets, DeepPartial, FindConditions, Repository } from "typeorm" +import { paginate, Pagination, PaginationTypeEnum } from "nestjs-typeorm-paginate" import { decode, encode } from "jwt-simple" -import moment from "moment" +import dayjs from "dayjs" import crypto from "crypto" import { User } from "../entities/user.entity" -import { assignDefined } from "../../shared/assign-defined" import { ConfirmDto } from "../dto/confirm.dto" import { USER_ERRORS } from "../user-errors" import { UpdatePasswordDto } from "../dto/update-password.dto" -import { EmailService } from "../../shared/email/email.service" import { AuthService } from "./auth.service" import { AuthzService } from "./authz.service" import { ForgotPasswordDto } from "../dto/forgot-password.dto" @@ -31,34 +30,73 @@ import { UserUpdateDto } from "../dto/user-update.dto" import { UserListQueryParams } from "../dto/user-list-query-params" import { UserInviteDto } from "../dto/user-invite.dto" import { ConfigService } from "@nestjs/config" -import { JurisdictionDto } from "../../jurisdictions/dto/jurisdiction.dto" import { authzActions } from "../enum/authz-actions.enum" -import { addFilters } from "../../shared/filter" -import { UserFilterParams } from "../dto/user-filter-params" import { userFilterTypeToFieldMap } from "../dto/user-filter-type-to-field-map" import { Application } from "../../applications/entities/application.entity" import { Listing } from "../../listings/entities/listing.entity" import { UserRoles } from "../entities/user-roles.entity" +import { UserPreferences } from "../entities/user-preferences.entity" +import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" +import { assignDefined } from "../../shared/utils/assign-defined" +import { EmailService } from "../../email/email.service" +import { RequestMfaCodeDto } from "../dto/request-mfa-code.dto" +import { RequestMfaCodeResponseDto } from "../dto/request-mfa-code-response.dto" +import { MfaType } from "../types/mfa-type" +import { SmsMfaService } from "./sms-mfa.service" +import { GetMfaInfoDto } from "../dto/get-mfa-info.dto" +import { GetMfaInfoResponseDto } from "../dto/get-mfa-info-response.dto" +import { addFilters } from "../../shared/query-filter" +import { UserFilterParams } from "../dto/user-filter-params" +import { UserRepository } from "../repositories/user-repository" +import advancedFormat from "dayjs/plugin/advancedFormat" +import { JurisdictionsService } from "../../jurisdictions/services/jurisdictions.service" + +dayjs.extend(advancedFormat) @Injectable({ scope: Scope.REQUEST }) export class UserService { constructor( - @InjectRepository(User) private readonly userRepository: Repository, + @InjectRepository(UserRepository) private readonly userRepository: UserRepository, @InjectRepository(Application) private readonly applicationsRepository: Repository, private readonly emailService: EmailService, private readonly configService: ConfigService, private readonly authService: AuthService, private readonly authzService: AuthzService, private readonly passwordService: PasswordService, - private readonly jurisdictionResolverService: JurisdictionResolverService + private readonly jurisdictionResolverService: JurisdictionResolverService, + private readonly jurisdictionService: JurisdictionsService, + private readonly smsMfaService: SmsMfaService ) {} public async findByEmail(email: string) { - return this.userRepository.findOne({ where: { email }, relations: ["leasingAgentInListings"] }) + return await this.userRepository.findOne({ + where: { email: email.toLowerCase() }, + relations: ["leasingAgentInListings"], + }) } public async find(options: FindConditions) { - return this.userRepository.findOne({ where: options, relations: ["leasingAgentInListings"] }) + return await this.userRepository.findOne({ + where: options, + relations: ["leasingAgentInListings"], + }) + } + + public static isPasswordOutdated(user: User) { + return ( + new Date(user.passwordUpdatedAt.getTime() + user.passwordValidForDays * 24 * 60 * 60 * 1000) < + new Date() && + user.roles && + (user.roles.isAdmin || user.roles.isPartner) + ) + } + + public async findOneOrFail(options: FindConditions) { + const user = await this.find(options) + if (!user) { + throw new NotFoundException() + } + return user } public async list( @@ -68,11 +106,21 @@ export class UserService { const options = { limit: params.limit === "all" ? undefined : params.limit, page: params.page || 10, + PaginationType: PaginationTypeEnum.TAKE_AND_SKIP, } // https://www.npmjs.com/package/nestjs-typeorm-paginate - const qb = this._getQb() + const distinctIDQB = this.userRepository.getQb() + distinctIDQB.select("user.id") + distinctIDQB.groupBy("user.id") + distinctIDQB.orderBy("user.id") + const qb = this.userRepository.getQb() if (params.filter) { + addFilters, typeof userFilterTypeToFieldMap>( + params.filter, + userFilterTypeToFieldMap, + distinctIDQB + ) addFilters, typeof userFilterTypeToFieldMap>( params.filter, userFilterTypeToFieldMap, @@ -80,18 +128,47 @@ export class UserService { ) } - const result = await paginate(qb, options) + if (params.search) { + distinctIDQB.andWhere( + new Brackets((subQb) => { + subQb.where("user.firstName ILIKE :search", { search: `%${params.search}%` }) + subQb.orWhere("user.lastName ILIKE :search", { search: `%${params.search}%` }) + subQb.orWhere("user.email ILIKE :search", { search: `%${params.search}%` }) + subQb.orWhere("leasingAgentInListings.name ILIKE :search", { + search: `%${params.search}%`, + }) + subQb.orWhere( + "CONCAT(user.firstName, ' ', user.lastName, ' ', user.email, ' ', leasingAgentInListings.name) ILIKE :search", + { search: `%${params.search}%` } + ) + }) + ) + } + + const distinctIDResult = await paginate(distinctIDQB, options) + + qb.andWhere("user.id IN (:...distinctIDs)", { + distinctIDs: distinctIDResult.items.map((elem) => elem.id), + }) + const result = distinctIDResult.items.length ? await qb.getMany() : [] /** * admin are the only ones that can access all users * so this will throw on the first user that isn't their own (non admin users can access themselves) */ await Promise.all( - result.items.map(async (user) => { + result.map(async (user) => { await this.authzService.canOrThrow(authContext.user, "user", authzActions.read, user) }) ) - return result + return { + ...distinctIDResult, + items: result, + } + } + + public async listAllUsers(): Promise { + return await this.userRepository.find() } async update(dto: UserUpdateDto, authContext: AuthContext) { @@ -101,21 +178,29 @@ export class UserService { if (!user) { throw new NotFoundException() } - let passwordHash + let passwordUpdatedAt if (dto.password) { if (!dto.currentPassword) { // Validation is handled at DTO definition level throw new BadRequestException() } - if (!(await this.passwordService.verifyUserPassword(user, dto.currentPassword))) { + if (!(await this.passwordService.isPasswordValid(user, dto.currentPassword))) { throw new UnauthorizedException("invalidPassword") } passwordHash = await this.passwordService.passwordToHash(dto.password) + passwordUpdatedAt = new Date() delete dto.password } + /** + * only admin users can update roles + */ + if (!authContext.user?.roles?.isAdmin) { + delete dto.roles + } + /** * jurisdictions should be filtered based off of what the authContext user has */ @@ -130,16 +215,31 @@ export class UserService { delete dto.jurisdictions } + if (dto.newEmail && dto.appUrl) { + user.confirmationToken = UserService.createConfirmationToken(user.id, dto.newEmail) + const confirmationUrl = UserService.getPublicConfirmationUrl(dto.appUrl, user) + await this.emailService.changeEmail(user, dto.appUrl, confirmationUrl, dto.newEmail) + } + + delete dto.newEmail + delete dto.appUrl + assignDefined(user, { ...dto, passwordHash, + passwordUpdatedAt, }) return await this.userRepository.save(user) } public async confirm(dto: ConfirmDto) { - const token = decode(dto.token, process.env.APP_SECRET) + let token: Record = {} + try { + token = decode(dto.token, process.env.APP_SECRET) + } catch (e) { + throw new HttpException(USER_ERRORS.TOKEN_EXPIRED.message, USER_ERRORS.TOKEN_EXPIRED.status) + } const user = await this.find({ id: token.id }) if (!user) { @@ -147,14 +247,6 @@ export class UserService { throw new HttpException(USER_ERRORS.NOT_FOUND.message, USER_ERRORS.NOT_FOUND.status) } - if (user.confirmedAt) { - console.error(`User ${token.id} already confirmed.`) - throw new HttpException( - USER_ERRORS.ACCOUNT_CONFIRMED.message, - USER_ERRORS.ACCOUNT_CONFIRMED.status - ) - } - if (user.confirmationToken !== dto.token) { console.error(`Confirmation token mismatch for user ${token.id}.`) throw new HttpException(USER_ERRORS.TOKEN_MISSING.message, USER_ERRORS.TOKEN_MISSING.status) @@ -165,17 +257,62 @@ export class UserService { if (dto.password) { user.passwordHash = await this.passwordService.passwordToHash(dto.password) + user.passwordUpdatedAt = new Date() } try { - await this.userRepository.save(user) + await this.userRepository.save({ + ...user, + ...(token.email && { email: token.email }), + }) return this.authService.generateAccessToken(user) } catch (err) { throw new HttpException(USER_ERRORS.ERROR_SAVING.message, USER_ERRORS.ERROR_SAVING.status) } } - public async resendConfirmation(dto: EmailDto) { + private async setHitConfirmationURl(user: User, token: string) { + if (!user) { + throw new HttpException(USER_ERRORS.NOT_FOUND.message, USER_ERRORS.NOT_FOUND.status) + } + + if (user.confirmationToken !== token) { + throw new HttpException(USER_ERRORS.TOKEN_MISSING.message, USER_ERRORS.TOKEN_MISSING.status) + } + user.hitConfirmationURL = new Date() + await this.userRepository.save({ + ...user, + }) + } + + public async isUserConfirmationTokenValid(dto: ConfirmDto) { + try { + const token = decode(dto.token, process.env.APP_SECRET) + const user = await this.find({ id: token.id }) + await this.setHitConfirmationURl(user, dto.token) + return true + } catch (e) { + console.error("isUserConfirmationTokenValid error = ", e) + try { + const user = await this.find({ confirmationToken: dto.token }) + await this.setHitConfirmationURl(user, dto.token) + } catch (e) { + console.error("isUserConfirmationTokenValid error = ", e) + } + return false + } + } + + private static createConfirmationToken(userId: string, email: string) { + const payload = { + id: userId, + email, + exp: Number.parseInt(dayjs().add(24, "hours").format("X")), + } + return encode(payload, process.env.APP_SECRET) + } + + public async resendPublicConfirmation(dto: EmailDto) { const user = await this.findByEmail(dto.email) if (!user) { throw new HttpException(USER_ERRORS.NOT_FOUND.message, USER_ERRORS.NOT_FOUND.status) @@ -186,8 +323,7 @@ export class UserService { USER_ERRORS.ACCOUNT_CONFIRMED.status ) } else { - const payload = { id: user.id, exp: Number.parseInt(moment().add(24, "hours").format("X")) } - user.confirmationToken = encode(payload, process.env.APP_SECRET) + user.confirmationToken = UserService.createConfirmationToken(user.id, user.email) try { await this.userRepository.save(user) const confirmationUrl = UserService.getPublicConfirmationUrl(dto.appUrl, user) @@ -199,6 +335,28 @@ export class UserService { } } + public async resendPartnerConfirmation(dto: EmailDto) { + const user = await this.findByEmail(dto.email) + if (!user) { + throw new HttpException(USER_ERRORS.NOT_FOUND.message, USER_ERRORS.NOT_FOUND.status) + } + if (user.confirmedAt) { + // if the user is already confirmed, we do nothing + // this is so on the front end people can't cheat to find out who has an email in the system + return {} + } else { + user.confirmationToken = UserService.createConfirmationToken(user.id, user.email) + try { + await this.userRepository.save(user) + const confirmationUrl = UserService.getPartnersConfirmationUrl(dto.appUrl, user) + await this.emailService.invite(user, dto.appUrl, confirmationUrl) + return user + } catch (err) { + throw new HttpException(USER_ERRORS.ERROR_SAVING.message, USER_ERRORS.ERROR_SAVING.status) + } + } + } + private static getPublicConfirmationUrl(appUrl: string, user: User) { return `${appUrl}?token=${user.confirmationToken}` } @@ -222,42 +380,60 @@ export class UserService { await this.applicationsRepository.save(applications) } - public async _createUser(dto: Partial, authContext: AuthContext) { + public async _createUser(dto: DeepPartial, authContext: AuthContext) { if (dto.confirmedAt) { await this.authzService.canOrThrow(authContext.user, "user", authzActions.confirm, { ...dto, }) } const existingUser = await this.findByEmail(dto.email) - if (existingUser) { - throw new HttpException(USER_ERRORS.EMAIL_IN_USE.message, USER_ERRORS.EMAIL_IN_USE.status) - } - try { - let newUser = await this.userRepository.save(dto) - - const payload = { - id: newUser.id, - expiresAt: Number.parseInt(moment().add(24, "hours").format("X")), + if (existingUser) { + if (!existingUser.roles && dto.roles) { + // existing user && public user && user will get roles -> trying to grant partner access to a public user + return await this.userRepository.save({ + ...existingUser, + roles: dto.roles, + leasingAgentInListings: dto.leasingAgentInListings, + confirmationToken: + existingUser.confirmationToken || + UserService.createConfirmationToken(existingUser.id, existingUser.email), + confirmedAt: null, + }) + } else { + // existing user && ((partner user -> trying to recreate user) || (public user -> trying to recreate a public user)) + throw new HttpException(USER_ERRORS.EMAIL_IN_USE.message, USER_ERRORS.EMAIL_IN_USE.status) } - newUser.confirmationToken = encode(payload, process.env.APP_SECRET) - newUser = await this.userRepository.save(newUser) - - return newUser - } catch (err) { - console.error(err) - throw new HttpException(USER_ERRORS.ERROR_SAVING.message, USER_ERRORS.ERROR_SAVING.status) } + const newUser = await this.userRepository.save(dto) + newUser.confirmationToken = UserService.createConfirmationToken(newUser.id, newUser.email) + return await this.userRepository.save(newUser) } - public async createUser(dto: UserCreateDto, authContext: AuthContext, sendWelcomeEmail = false) { + containsInvalidCharacters(value: string): boolean { + return value.includes(".") || value.includes("http") + } + + public async createPublicUser( + dto: UserCreateDto, + authContext: AuthContext, + sendWelcomeEmail = false + ) { + if ( + this.containsInvalidCharacters(dto.firstName) || + this.containsInvalidCharacters(dto.lastName) + ) { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN) + } const newUser = await this._createUser( { ...dto, passwordHash: await this.passwordService.passwordToHash(dto.password), jurisdictions: dto.jurisdictions - ? (dto.jurisdictions as JurisdictionDto[]) + ? (dto.jurisdictions as Jurisdiction[]) : [await this.jurisdictionResolverService.getJurisdiction()], + mfaEnabled: false, + preferences: dto.preferences as UserPreferences, }, authContext ) @@ -271,16 +447,14 @@ export class UserService { public async forgotPassword(dto: ForgotPasswordDto) { const user = await this.findByEmail(dto.email) - if (!user) { - throw new HttpException(USER_ERRORS.NOT_FOUND.message, USER_ERRORS.NOT_FOUND.status) + if (user) { + // Token expires in 1 hour + const payload = { id: user.id, exp: Number.parseInt(dayjs().add(1, "hour").format("X")) } + user.resetToken = encode(payload, process.env.APP_SECRET) + await this.userRepository.save(user) + await this.emailService.forgotPassword(user, dto.appUrl) + return user } - - // Token expires in 1 hour - const payload = { id: user.id, exp: Number.parseInt(moment().add(1, "hour").format("X")) } - user.resetToken = encode(payload, process.env.APP_SECRET) - await this.userRepository.save(user) - await this.emailService.forgotPassword(user, dto.appUrl) - return user } public async updatePassword(dto: UpdatePasswordDto) { @@ -295,20 +469,13 @@ export class UserService { } user.passwordHash = await this.passwordService.passwordToHash(dto.password) + user.passwordUpdatedAt = new Date() user.resetToken = null await this.userRepository.save(user) return this.authService.generateAccessToken(user) } - private _getQb() { - const qb = this.userRepository.createQueryBuilder("user") - qb.leftJoinAndSelect("user.leasingAgentInListings", "listings") - qb.leftJoinAndSelect("user.roles", "user_roles") - - return qb - } - - async invite(dto: UserInviteDto, authContext: AuthContext) { + async invitePartnersPortalUser(dto: UserInviteDto, authContext: AuthContext) { const password = crypto.randomBytes(8).toString("hex") const user = await this._createUser( { @@ -317,8 +484,10 @@ export class UserService { leasingAgentInListings: dto.leasingAgentInListings as Listing[], roles: dto.roles as UserRoles, jurisdictions: dto.jurisdictions - ? (dto.jurisdictions as JurisdictionDto[]) - : [await this.jurisdictionResolverService.getJurisdiction()], + ? (dto.jurisdictions as Jurisdiction[]) + : [await this.jurisdictionService.findOne({ where: { name: "Detroit" } })], + preferences: (dto.preferences as unknown) as UserPreferences, + mfaEnabled: false, }, authContext ) @@ -330,4 +499,101 @@ export class UserService { ) return user } + + async delete(userId: string) { + const user = await this.userRepository.findOne({ id: userId }) + if (!user) { + throw new NotFoundException() + } + await this.userRepository.remove(user) + } + + generateMfaCode() { + let out = "" + const characters = "0123456789" + for (let i = 0; i < this.configService.get("MFA_CODE_LENGTH"); i++) { + out += characters.charAt(Math.floor(Math.random() * characters.length)) + } + return out + } + + private static hasUsedMfaInThePast(user: User): boolean { + return !!user.mfaCodeUpdatedAt + } + + async getMfaInfo(getMfaInfoDto: GetMfaInfoDto): Promise { + const user = await this.userRepository.findOne({ + where: { email: getMfaInfoDto.email.toLowerCase() }, + }) + if (!user) { + throw new UnauthorizedException() + } + + const validPassword = await this.passwordService.isPasswordValid(user, getMfaInfoDto.password) + if (!validPassword) { + throw new UnauthorizedException() + } + + return { + email: user.email, + phoneNumber: user.phoneNumber ?? undefined, + isMfaEnabled: user.mfaEnabled, + mfaUsedInThePast: UserService.hasUsedMfaInThePast(user), + } + } + + async requestMfaCode(requestMfaCodeDto: RequestMfaCodeDto): Promise { + let user = await this.userRepository.findOne({ + where: { email: requestMfaCodeDto.email.toLowerCase() }, + }) + + if (!user || !user.mfaEnabled) { + throw new UnauthorizedException() + } + + const validPassword = await this.passwordService.isPasswordValid( + user, + requestMfaCodeDto.password + ) + if (!validPassword) { + throw new UnauthorizedException() + } + + if (requestMfaCodeDto.mfaType === MfaType.sms) { + if (requestMfaCodeDto.phoneNumber) { + if (user.phoneNumberVerified) { + throw new UnauthorizedException( + "phone number can only be specified the first time using mfa" + ) + } + user.phoneNumber = requestMfaCodeDto.phoneNumber + } else if (!requestMfaCodeDto.phoneNumber && !user.phoneNumber) { + throw new HttpException( + { name: "phoneNumberMissing", message: "no valid phone number was found" }, + 400 + ) + } + } + const mfaCode = this.generateMfaCode() + user.mfaCode = mfaCode + user.mfaCodeUpdatedAt = new Date() + + user = await this.userRepository.manager.transaction(async (entityManager) => { + const transactionalRepository = entityManager.getRepository(User) + await transactionalRepository.save(user) + if (requestMfaCodeDto.mfaType === MfaType.email) { + await this.emailService.sendMfaCode(user, user.email, mfaCode) + } else if (requestMfaCodeDto.mfaType === MfaType.sms) { + await this.smsMfaService.sendMfaCode(user, user.phoneNumber, mfaCode) + } + return user + }) + + return requestMfaCodeDto.mfaType === MfaType.email + ? { email: user.email, phoneNumberVerified: user.phoneNumberVerified } + : { + phoneNumber: user.phoneNumber, + phoneNumberVerified: user.phoneNumberVerified, + } + } } diff --git a/backend/core/src/auth/types/mfa-type.ts b/backend/core/src/auth/types/mfa-type.ts new file mode 100644 index 0000000000..475671f710 --- /dev/null +++ b/backend/core/src/auth/types/mfa-type.ts @@ -0,0 +1,4 @@ +export enum MfaType { + sms = "sms", + email = "email", +} diff --git a/backend/core/src/auth/types/user-filter-keys.ts b/backend/core/src/auth/types/user-filter-keys.ts index 080c5988d1..2c1f954844 100644 --- a/backend/core/src/auth/types/user-filter-keys.ts +++ b/backend/core/src/auth/types/user-filter-keys.ts @@ -1,3 +1,4 @@ export enum UserFilterKeys { isPartner = "isPartner", + isPortalUser = "isPortalUser", } diff --git a/backend/core/src/auth/user-errors.ts b/backend/core/src/auth/user-errors.ts index 0ead1c8cee..8277a072dd 100644 --- a/backend/core/src/auth/user-errors.ts +++ b/backend/core/src/auth/user-errors.ts @@ -1,10 +1,40 @@ import { HttpStatus } from "@nestjs/common" +import { ApiProperty } from "@nestjs/swagger" +import { Expose } from "class-transformer" + +export enum UserErrorMessages { + accountConfirmed = "accountConfirmed", + accountNotConfirmed = "accountNotConfirmed", + errorSaving = "errorSaving", + emailNotFound = "emailNotFound", + tokenExpired = "tokenExpired", + tokenMissing = "tokenMissing", + emailInUse = "emailInUse", + passwordOutdated = "passwordOutdated", +} export const USER_ERRORS = { - ACCOUNT_CONFIRMED: { message: "accountConfirmed", status: HttpStatus.NOT_ACCEPTABLE }, - ERROR_SAVING: { message: "errorSaving", status: HttpStatus.BAD_REQUEST }, - NOT_FOUND: { message: "emailNotFound", status: HttpStatus.NOT_FOUND }, - TOKEN_EXPIRED: { message: "tokenExpired", status: HttpStatus.BAD_REQUEST }, - TOKEN_MISSING: { message: "tokenMissing", status: HttpStatus.BAD_REQUEST }, - EMAIL_IN_USE: { message: "emailInUse", status: HttpStatus.BAD_REQUEST }, + ACCOUNT_CONFIRMED: { + message: UserErrorMessages.accountConfirmed, + status: HttpStatus.NOT_ACCEPTABLE, + }, + ACCOUNT_NOT_CONFIRMED: { + message: UserErrorMessages.accountNotConfirmed, + status: HttpStatus.UNAUTHORIZED, + }, + ERROR_SAVING: { message: UserErrorMessages.errorSaving, status: HttpStatus.BAD_REQUEST }, + NOT_FOUND: { message: UserErrorMessages.emailNotFound, status: HttpStatus.NOT_FOUND }, + TOKEN_EXPIRED: { message: UserErrorMessages.tokenExpired, status: HttpStatus.BAD_REQUEST }, + TOKEN_MISSING: { message: UserErrorMessages.tokenMissing, status: HttpStatus.BAD_REQUEST }, + EMAIL_IN_USE: { message: UserErrorMessages.emailInUse, status: HttpStatus.BAD_REQUEST }, + PASSWORD_OUTDATED: { + message: UserErrorMessages.passwordOutdated, + status: HttpStatus.UNAUTHORIZED, + }, +} + +export class UserErrorExtraModel { + @ApiProperty({ enum: UserErrorMessages }) + @Expose() + userErrorMessages: UserErrorMessages } diff --git a/backend/core/src/cache/types/redis-types.ts b/backend/core/src/cache/types/redis-types.ts deleted file mode 100644 index 1867aeb2a9..0000000000 --- a/backend/core/src/cache/types/redis-types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Store } from "cache-manager" -import Redis from "redis" - -interface RedisStore extends Store { - name: "redis" - getClient: () => Redis.RedisClient - isCacheableValue: (value: unknown) => boolean -} - -export interface RedisCache extends Cache { - store: RedisStore -} diff --git a/backend/core/src/cron/cron.module.ts b/backend/core/src/cron/cron.module.ts new file mode 100644 index 0000000000..449e2594f5 --- /dev/null +++ b/backend/core/src/cron/cron.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common" +import { AuthModule } from "../auth/auth.module" +import { ListingsModule } from "../listings/listings.module" +import { EmailModule } from "../email/email.module" +import { CronService } from "./cron.service" + +@Module({ + imports: [ListingsModule, EmailModule, AuthModule], + providers: [CronService], +}) +export class CronModule {} diff --git a/backend/core/src/cron/cron.service.ts b/backend/core/src/cron/cron.service.ts new file mode 100644 index 0000000000..07e3ed8a72 --- /dev/null +++ b/backend/core/src/cron/cron.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from "@nestjs/common" +import { Cron, CronExpression } from "@nestjs/schedule" +import { UserListQueryParams } from "../auth/dto/user-list-query-params" +import { AuthContext } from "../auth/types/auth-context" +import { Compare } from "../shared/dto/filter.dto" +import { EmailService } from "../email/email.service" +import { ListingsService } from "../listings/listings.service" +import { UserService } from "../auth/services/user.service" + +@Injectable() +export class CronService { + constructor( + private readonly emailService: EmailService, + private readonly listingsService: ListingsService, + private readonly userService: UserService + ) {} + + @Cron(CronExpression.EVERY_1ST_DAY_OF_MONTH_AT_NOON) + async handleCron() { + if (!process.env.SEND_NOTIFICATIONS_FOR_UPDATE_LISTINGS_REMINDER) { + return + } + + const userQueryParams: UserListQueryParams = { + filter: [ + { + $comparison: Compare["EQUALS"], + isPartner: true, + }, + ], + } + + const users = await this.userService.list(userQueryParams, new AuthContext()) + const allListings = await this.listingsService.list({}) + + // For each listing, check whether the listed leasing agents are in the list of partner users. + // If the leasing agent is a partner and has their email notifications turned on, send the reminder email. + for (const listing of allListings.items) { + const recipients = [] + for (const leasingAgent of listing.leasingAgents) { + if (users.items.includes(leasingAgent) && leasingAgent.preferences.sendEmailNotifications) { + recipients.push(leasingAgent.email) + } + } + // await this.emailService.updateListingReminder(listing, recipients) + } + } +} diff --git a/backend/core/src/csv/application-csv-exporter.ts b/backend/core/src/csv/application-csv-exporter.ts deleted file mode 100644 index 756ffa20a8..0000000000 --- a/backend/core/src/csv/application-csv-exporter.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from "@nestjs/common" -import { CsvBuilder } from "./csv-builder.service" -import { Application } from "../applications/entities/application.entity" -import { applicationFormattingMetadataAggregateFactory } from "./formatting/application-formatting-metadata-factory" -import { - formatDemographicsEthnicity, - formatDemographicsGender, - formatDemographicsHowDidYouHear, - formatDemographicsRace, - formatDemographicsSexualOrientation, -} from "./formatting/formatters" -import { CSVFormattingType } from "./types/csv-formatting-type-enum" - -@Injectable() -export class ApplicationCsvExporter { - constructor(private readonly csvBuilder: CsvBuilder) {} - export( - applications: Application[], - csvFormattingType: CSVFormattingType, - includeHeaders?: boolean, - includeDemographics?: boolean - ): string { - return this.csvBuilder.build( - applications, - applicationFormattingMetadataAggregateFactory, - // Every application points to the same listing - csvFormattingType, - includeHeaders, - includeDemographics - ? [ - formatDemographicsEthnicity, - formatDemographicsRace, - formatDemographicsGender, - formatDemographicsSexualOrientation, - formatDemographicsHowDidYouHear, - ] - : [] - ) - } -} diff --git a/backend/core/src/csv/csv-builder.service.ts b/backend/core/src/csv/csv-builder.service.ts deleted file mode 100644 index ab6601ed04..0000000000 --- a/backend/core/src/csv/csv-builder.service.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { CsvEncoder } from "./csv-encoder.service" -import { Injectable } from "@nestjs/common" -import { CSVFormattingType } from "./types/csv-formatting-type-enum" -import { FormattingMetadata } from "./types/formatting-metadata" -import { FormattingMetadataArray } from "./types/formatting-metadata-array" -import { FormattingMetadataAggregate } from "./types/formatting-metadata-aggregate" -import { FormattingMetadataAggregateFactory } from "./types/formatting-metadata-aggregate-factory" - -@Injectable() -export class CsvBuilder { - constructor(private readonly csvEncoder: CsvEncoder) {} - - private incrementMetadataArrayItemLabel(item: FormattingMetadata, index: number) { - const newItem = { ...item } - newItem.label += ` (${index})` - return newItem - } - - private retrieveValueByDiscriminator(obj, discriminator: string) { - let value: any = obj - for (const key of discriminator.split(".")) { - if (!key) { - continue - } - value = value[key] - } - return value - } - - private flattenJson(obj: any, metadataAggregate: FormattingMetadataAggregate): Array { - let outputRows: string[] = [] - for (const metadataObj of metadataAggregate) { - let value - try { - value = this.retrieveValueByDiscriminator(obj, metadataObj.discriminator) - } catch (e) { - value = undefined - } - if (metadataObj.type === "array") { - const metadataArray: FormattingMetadataArray = metadataObj as FormattingMetadataArray - for (let i = 0; i < metadataArray.size; i++) { - let row - try { - row = value[i] - } catch (e) { - row = {} - } - const metadataArrayItems = metadataArray.items.map((item) => - this.incrementMetadataArrayItemLabel(item, i + 1) - ) - outputRows = outputRows.concat(this.flattenJson(row, metadataArrayItems)) - } - } else { - const metadata: FormattingMetadata = metadataObj as FormattingMetadata - try { - outputRows.push(metadata.formatter(value)) - } catch (e) { - outputRows.push("") - } - } - } - return outputRows - } - - private getHeaders(metadataArray: any[]) { - let headers: string[] = [] - for (const metadata of metadataArray) { - if (metadata.type === "array") { - for (let i = 0; i < metadata.size; i++) { - const items = metadata.items.map((item) => - this.incrementMetadataArrayItemLabel(item, i + 1) - ) - headers = headers.concat(this.getHeaders(items)) - } - } else { - headers.push(metadata.label) - } - } - return headers - } - - private normalizeMetadataArrays( - arr: any[], - formattingMetadataAggregate: FormattingMetadataAggregate - ) { - formattingMetadataAggregate - .filter((metadata) => metadata.type === "array") - .forEach((metadata) => { - const md = metadata as FormattingMetadataArray - if (md.size !== null) { - return - } - md.size = Math.max( - ...arr.map((item) => { - const value = this.retrieveValueByDiscriminator(item, md.discriminator) - if (!value || !Array.isArray(value)) { - return 0 - } - return value.length - }) - ) - }) - return formattingMetadataAggregate - } - - public build( - arr: any[], - formattingMetadataAggregateFactory: FormattingMetadataAggregateFactory, - csvFormattingType: CSVFormattingType, - includeHeaders?: boolean, - extraFormatters?: Array - ): string { - let formattingMetadataAggregate = formattingMetadataAggregateFactory(csvFormattingType) - if (!formattingMetadataAggregate) { - return "" - } - if (extraFormatters) { - formattingMetadataAggregate = formattingMetadataAggregate.concat(extraFormatters) - } - const normalizedMetadataAggregate = this.normalizeMetadataArrays( - arr, - formattingMetadataAggregate - ) - const rows: Array> = [] - rows.push(this.getHeaders(normalizedMetadataAggregate)) - arr.forEach((item) => rows.push(this.flattenJson(item, normalizedMetadataAggregate))) - return this.csvEncoder.encode(rows, includeHeaders) - } -} diff --git a/backend/core/src/csv/csv-builder.spec.ts b/backend/core/src/csv/csv-builder.spec.ts deleted file mode 100644 index 2f39b88d42..0000000000 --- a/backend/core/src/csv/csv-builder.spec.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Test, TestingModule } from "@nestjs/testing" -import { CsvBuilder } from "./csv-builder.service" -import { CsvEncoder } from "./csv-encoder.service" -import { CSVFormattingType } from "./types/csv-formatting-type-enum" -import { FormattingMetadataAggregateFactory } from "./types/formatting-metadata-aggregate-factory" -import { defaultFormatter } from "./formatting/formatters" - -// Cypress brings in Chai types for the global expect, but we want to use jest -// expect here so we need to re-declare it. -// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 -declare const expect: jest.Expect - -const testMetadataAggregateFactory: FormattingMetadataAggregateFactory = () => { - return [ - { - discriminator: "foo", - formatter: defaultFormatter, - label: "Foo", - type: undefined, - }, - { - discriminator: "bar", - formatter: defaultFormatter, - label: "Bar", - type: undefined, - }, - ] -} -const testMetadataAggregateWithArrayFactory: FormattingMetadataAggregateFactory = () => { - return [ - { - discriminator: "arr", - type: "array", - size: null, - items: [ - { - formatter: defaultFormatter, - discriminator: "foo", - label: "Foo", - }, - ], - }, - ] -} - -describe("CSVBuilder", () => { - let service: CsvBuilder - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [CsvEncoder, CsvBuilder], - }).compile() - service = module.get(CsvBuilder) - }) - - it("should be defined", () => { - expect(service).toBeDefined() - }) - - it("create empty response", () => { - const response = service.build([], () => [], CSVFormattingType.basic, false) - expect(response).toBe("") - }) - it("create only headers for empty data array", () => { - const response = service.build([], testMetadataAggregateFactory, CSVFormattingType.basic, true) - expect(response).toBe('"Foo","Bar"\n') - }) - it("should not include headers when such option is specified", () => { - const response = service.build([], testMetadataAggregateFactory, CSVFormattingType.basic, false) - expect(response).toBe("") - }) - it("create correctly escaped CSV for correct data", () => { - const response = service.build( - [{ foo: "foo", bar: "bar" }], - testMetadataAggregateFactory, - CSVFormattingType.basic, - true - ) - expect(response).toBe('"Foo","Bar"\n"foo","bar"\n') - }) - it("create CSV with escaped double quotes", () => { - const response = service.build( - [{ foo: '"', bar: "bar" }], - testMetadataAggregateFactory, - CSVFormattingType.basic, - true - ) - expect(response).toBe('"Foo","Bar"\n"""","bar"\n') - }) - it("create create a correct CSV for undefined fields", () => { - const response = service.build( - [{}], - testMetadataAggregateFactory, - CSVFormattingType.basic, - true - ) - expect(response).toBe('"Foo","Bar"\n"",""\n') - }) - it("create create a correct CSV for array type of metadata", () => { - const response = service.build( - [{ arr: [{ foo: "foo" }, { foo: "foo 2" }] }], - testMetadataAggregateWithArrayFactory, - CSVFormattingType.basic, - true - ) - expect(response).toBe('"Foo (1)","Foo (2)"\n"foo","foo 2"\n') - }) - it("create an empty CSV for array type of metadata for undefined input array", () => { - const response = service.build( - [], - testMetadataAggregateWithArrayFactory, - CSVFormattingType.basic, - true - ) - expect(response).toBe("") - }) - it("create an empty CSV for array type of metadata for null input array", () => { - const response = service.build( - [{ arr: null }], - testMetadataAggregateWithArrayFactory, - CSVFormattingType.basic, - true - ) - expect(response).toBe("") - }) - it("create an empty CSV for array type of metadata for empty input array", () => { - const response = service.build( - [{ arr: [] }], - testMetadataAggregateWithArrayFactory, - CSVFormattingType.basic, - true - ) - expect(response).toBe("") - }) - it("create an a correct CSV for array type of metadata for single input array", () => { - const response = service.build( - [{ arr: [{}] }], - testMetadataAggregateWithArrayFactory, - CSVFormattingType.basic, - true - ) - expect(response).toBe('"Foo (1)"\n""\n') - }) - it("create an a correct CSV for array type of metadata for single malformed input array", () => { - const response = service.build( - [{ arr: [{ foo: null }] }], - testMetadataAggregateWithArrayFactory, - CSVFormattingType.basic, - true - ) - expect(response).toBe('"Foo (1)"\n""\n') - }) -}) diff --git a/backend/core/src/csv/csv-encoder.service.ts b/backend/core/src/csv/csv-encoder.service.ts deleted file mode 100644 index f4373c9135..0000000000 --- a/backend/core/src/csv/csv-encoder.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable } from "@nestjs/common" - -@Injectable() -export class CsvEncoder { - public encode(input: Array>, includeHeaders?: boolean): string { - let output = "" - - if (input && !input.length) { - return "" - } - this._validateInput(input) - - const headers = input.shift() - if (includeHeaders && headers && Array.isArray(headers) && headers.length) { - output += this._encode(headers) - output += "\n" - } - - for (const row of input) { - if (row && Array.isArray(row) && row.length) { - output += this._encode(row) - output += "\n" - } - } - - return output - } - - private _validateInput(input: Array>) { - let cmp = input[0].length - input.forEach((item) => { - if (item.length != cmp) { - throw new Error( - "Unable to encode CSV - input rows are of different length. Check formatting metadata array." - ) - } - cmp = item.length - }) - } - - private _encode(input: Array): string { - const encodedDoubleQuotesInput = input.map( - (value) => '"' + this._encodeDoubleQuotes(value) + '"' - ) - return encodedDoubleQuotesInput.join(",") - } - - private _encodeDoubleQuotes(input: string) { - const regex = /"/gi - try { - return input.replace(regex, '""') - } catch (e) { - return "null" - } - } -} diff --git a/backend/core/src/csv/formatting/application-formatting-metadata-factory.ts b/backend/core/src/csv/formatting/application-formatting-metadata-factory.ts deleted file mode 100644 index 36299df11f..0000000000 --- a/backend/core/src/csv/formatting/application-formatting-metadata-factory.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { haywardFormattingMetadata } from "./metadata/hayward-formatting-metadata" -import { basicFormattingMetadata } from "./metadata/basic-formatting-metadata" -import { CSVFormattingType } from "../types/csv-formatting-type-enum" -import { FormattingMetadataAggregateFactory } from "../types/formatting-metadata-aggregate-factory" -import { ohaFormattingMetadata } from "./metadata/oha-formatting-metadata" -import { bhaFormattingMetadata } from "./metadata/bha-formatting-metadata" - -export const applicationFormattingMetadataAggregateFactory: FormattingMetadataAggregateFactory = ( - type: CSVFormattingType -) => { - switch (type) { - case CSVFormattingType.basic: - return basicFormattingMetadata - case CSVFormattingType.withDisplaceeNameAndAddress: - return haywardFormattingMetadata - case CSVFormattingType.ohaFormat: - return ohaFormattingMetadata - case CSVFormattingType.bhaFormat: - return bhaFormattingMetadata - } -} diff --git a/backend/core/src/csv/formatting/formatters.ts b/backend/core/src/csv/formatting/formatters.ts deleted file mode 100644 index 3e776f2a05..0000000000 --- a/backend/core/src/csv/formatting/formatters.ts +++ /dev/null @@ -1,642 +0,0 @@ -import { Application } from "../../applications/entities/application.entity" -import { HouseholdMember } from "../../applications/entities/household-member.entity" -import { ApplicationPreference } from "../../applications/entities/application-preferences.entity" -import { TextInput } from "../../applications/types/form-metadata/text-input" -import { AddressInput } from "../../applications/types/form-metadata/address-input" - -export const defaultFormatter = (obj?) => (obj ? obj.toString() : "") -export const booleanFormatter = (obj?: boolean) => (obj ? "Yes" : "No") -export const streetFormatter = (obj?: { street?: string; street2?: string }) => { - if (!obj) { - return defaultFormatter(obj) - } - if (!obj.street && !obj.street2) { - return "" - } - if (!obj.street) { - return obj.street2 - } - if (!obj.street2) { - return obj.street - } - return `${obj.street}, ${obj.street2}` -} -export const dobFormatter = (obj?: { - birthMonth?: string - birthDay?: string - birthYear?: string -}) => { - // TODO Use locale variable Date string - return obj ? `${obj.birthMonth}/${obj.birthDay}/${obj.birthYear}` : defaultFormatter(obj) -} -export const joinArrayFormatter = (obj?: string[]) => (obj ? obj.join(",") : "") -export const keysToJoinedStringFormatter = (obj: unknown) => { - if (!obj) { - return defaultFormatter(obj) - } - const keys = Object.keys(obj).filter((key) => obj[key]) - return keys.join(",") -} - -export const formatApplicationNumber = { - label: "Application Number", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.id) - }, -} -export const formatApplicatonSubmissionDate = { - label: "Application Submission Date", - discriminator: "", - formatter: (application: Application) => - new Date(application.createdAt).toLocaleString("en-US", { - timeZone: "America/Los_Angeles", - }), -} -export const formatPrimaryApplicantFirstName = { - label: "Primary Applicant First Name", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.firstName) - }, -} -export const formatPrimaryApplicantMiddleName = { - label: "Primary Applicant Middle Name", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.middleName) - }, -} -export const formatPrimaryApplicantLastName = { - label: "Primary Applicant Last Name", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.lastName) - }, -} -export const formatPrimaryApplicantDOB = { - label: "Primary Applicant Date of Birth", - discriminator: "", - formatter: (application: Application) => { - return dobFormatter(application.applicant) - }, -} -export const formatPrimaryApplicantEmail = { - label: "Primary Applicant Email", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.emailAddress) - }, -} -export const formatPrimaryApplicantPhone = { - label: "Primary Applicant Phone", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.phoneNumber) - }, -} -export const formatPrimaryApplicantPhoneType = { - label: "Primary Applicant Phone Type", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.phoneNumberType) - }, -} -export const formatPrimaryApplicantAdditionalPhone = { - label: "Primary Applicant Additional Phone", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.additionalPhoneNumber) - }, -} -export const formatPrimaryApplicantAdditionalPhoneType = { - label: "Primary Applicant Additional Phone Type", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.additionalPhoneNumberType) - }, -} -export const formatPrimaryApplicantPreferredContactType = { - label: "Primary Applicant Preferred Contact Type", - discriminator: "", - formatter: (application: Application) => { - return joinArrayFormatter(application.contactPreferences) - }, -} -export const formatPrimaryApplicantResidenceAddress = { - label: "Primary Applicant Residence Street Address", - discriminator: "", - formatter: (application: Application) => { - return streetFormatter(application.applicant.address) - }, -} -export const formatPrimaryApplicantResidenceCity = { - label: "Primary Applicant Residence City", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.address.city) - }, -} -export const formatPrimaryApplicantResidenceState = { - label: "Primary Applicant Residence State", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.address.state) - }, -} -export const formatPrimaryApplicantResidenceZip = { - label: "Primary Applicant Residence Zip", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.address.zipCode) - }, -} - -export const formatPrimaryApplicantMailingStreetAddress = { - label: "Primary Applicant Mailing Street Address", - discriminator: "", - formatter: (application: Application) => { - return streetFormatter(application.mailingAddress) - }, -} -export const formatPrimaryApplicantMailingCity = { - label: "Primary Applicant Mailing City", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.mailingAddress.city) - }, -} -export const formatPrimaryApplicantMailingState = { - label: "Primary Applicant Mailing State", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.mailingAddress.state) - }, -} -export const formatPrimaryApplicantMailingZip = { - label: "Primary Applicant Mailing Zip", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.mailingAddress.zipCode) - }, -} -export const formatPrimaryApplicantWorkStreetAddress = { - label: "Primary Applicant Work Street Address", - discriminator: "", - formatter: (application: Application) => { - return streetFormatter(application.applicant.workAddress) - }, -} -export const formatPrimaryApplicantWorkCity = { - label: "Primary Applicant Work City", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.workAddress.city) - }, -} -export const formatPrimaryApplicantWorkState = { - label: "Primary Applicant Work State", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.workAddress.state) - }, -} -export const formatPrimaryApplicantWorkZip = { - label: "Primary Applicant Work Zip", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.applicant.workAddress.zipCode) - }, -} -export const formatAlternateContactFirstName = { - label: "Alternate Contact First Name", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.alternateContact.firstName) - }, -} -export const formatAlternateContactLastName = { - label: "Alternate Contact Last Name", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.alternateContact.lastName) - }, -} -export const formatAlternateContactType = { - label: "Alternate Contact Type", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.alternateContact.type) - }, -} -export const formatAlternateContactAgency = { - label: "Alternate Contact Agency", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.alternateContact.agency) - }, -} -export const formatAlternateContactOther = { - label: "Alternate Contact Other", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.alternateContact.otherType) - }, -} -export const formatAlternateContactEmail = { - label: "Alternate Contact Email", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.alternateContact.emailAddress) - }, -} -export const formatAlternateContactPhone = { - label: "Alternate Contact Phone", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.alternateContact.phoneNumber) - }, -} -export const formatAlternateContactStreetAddress = { - label: "Alternate Contact Street Address", - discriminator: "", - formatter: (application: Application) => { - return streetFormatter(application.alternateContact.mailingAddress) - }, -} -export const formatAlternateContactCity = { - label: "Alternate Contact City", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.alternateContact.mailingAddress.city) - }, -} -export const formatAlternateContactState = { - label: "Alternate Contact State", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.alternateContact.mailingAddress.state) - }, -} -export const formatAlternateContactZip = { - label: "Alternate Contact Zip", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.alternateContact.mailingAddress.zipCode) - }, -} -export const formatMonthlyIncome = { - label: "Monthly Income", - discriminator: "", - formatter: (application: Application) => { - switch (application.incomePeriod) { - case "perYear": - return "" - case "perMonth": - return application.income - default: - return "" - } - }, -} -export const formatAnnualIncome = { - label: "Annual Income", - discriminator: "", - formatter: (application: Application) => { - switch (application.incomePeriod) { - case "perYear": - return application.income - case "perMonth": - return "" - default: - return "" - } - }, -} -export const formatAccessibility = { - label: "Requested accessibility", - discriminator: "", - formatter: (application: Application) => { - return keysToJoinedStringFormatter({ - hearing: application.accessibility.hearing, - mobility: application.accessibility.mobility, - vision: application.accessibility.vision, - }) - }, -} -export const formatVouchersOrSubsidies = { - label: "Receives Vouchers or Subsidies", - discriminator: "", - formatter: (application: Application) => { - return booleanFormatter(application.incomeVouchers) - }, -} -export const formatRequestUnitType = { - label: "Requested unit type", - discriminator: "", - formatter: (application: Application) => { - return joinArrayFormatter(application.preferredUnit.map((unit) => unit.name)) - }, -} -export const formatHouseholdSize = { - label: "Household Size", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.householdSize) - }, -} -export const formatHoueholdMembers = { - type: "array", - size: null, - discriminator: "householdMembers", - items: [ - { - label: "Household First Name", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return defaultFormatter(householdMember.firstName) - }, - }, - { - label: "Household Middle Name", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return defaultFormatter(householdMember.middleName) - }, - }, - { - label: "Household Last Name", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return defaultFormatter(householdMember.lastName) - }, - }, - { - label: "Household Relationship", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return defaultFormatter(householdMember.relationship) - }, - }, - { - label: "Household Date of Birth", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return dobFormatter(householdMember) - }, - }, - { - label: "Household Residence Street Address", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return streetFormatter(householdMember.address) - }, - }, - { - label: "Household Residence City", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return defaultFormatter(householdMember.address.city) - }, - }, - { - label: "Household Residence State", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return defaultFormatter(householdMember.address.state) - }, - }, - { - label: "Household Residence Zip", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return defaultFormatter(householdMember.address.zipCode) - }, - }, - { - label: "Household Work Street Address", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return streetFormatter(householdMember.workAddress) - }, - }, - { - label: "Household Work City", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return defaultFormatter(householdMember.workAddress.city) - }, - }, - { - label: "Household Work State", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return defaultFormatter(householdMember.workAddress.state) - }, - }, - { - label: "Household Work Zip", - discriminator: "", - formatter: (householdMember: HouseholdMember) => { - return defaultFormatter(householdMember.workAddress.zipCode) - }, - }, - ], -} -const preferenceClaimedFormatter = ( - preferenceKey: string, - option: string, - application: Application -) => { - const liveOrWorkPreferences = application.preferences.filter( - (pref) => pref.key === preferenceKey && pref.claimed - ) - if (liveOrWorkPreferences.length !== 1) { - return "" - } - const liveOrWorkPreference: ApplicationPreference = liveOrWorkPreferences[0] - return liveOrWorkPreference.options.filter((pref) => pref.checked && pref.key === option).length - ? "claimed" - : "" -} - -export const formatLivePreference = { - label: "Live preference", - discriminator: "", - formatter: preferenceClaimedFormatter.bind(this, "liveWork", "live"), -} -export const formatWorkPreference = { - label: "Work preference", - discriminator: "", - formatter: preferenceClaimedFormatter.bind(this, "liveWork", "work"), -} - -const displacedTenantFormatter = (optionKey: string, application: Application) => { - const displacedTenantPreferences = application.preferences.filter( - (pref) => pref.key === "displacedTenant" && pref.claimed - ) - if (displacedTenantPreferences.length !== 1) { - return "" - } - const displacedTenantSubPreference = displacedTenantPreferences[0].options.filter( - (pref) => pref.checked && pref.key === optionKey - ) - if (!displacedTenantSubPreference.length) { - return "" - } - const nameExtraDataFilter = displacedTenantSubPreference[0].extraData.filter( - (val) => val.key === "name" - ) - const addressExtraDataFilter = displacedTenantSubPreference[0].extraData.filter( - (val) => val.key === "address" - ) - if (!nameExtraDataFilter.length || !addressExtraDataFilter.length) { - return "" - } - const nameExtraData = nameExtraDataFilter[0] as TextInput - const addressExtraData = addressExtraDataFilter[0] as AddressInput - - return `Name: ${nameExtraData.value} Street: ${addressExtraData.value.street || ""}, Street2: ${ - addressExtraData.value.street2 || "" - }, Zip Code: ${addressExtraData.value.zipCode || ""}, City: ${ - addressExtraData.value.city || "" - }, County: ${addressExtraData.value.county || ""}, State: ${addressExtraData.value.state || ""}` -} - -export const formatDisplacedTenantPreferenceGeneralClaimed = { - label: "Displaced tenant preference (general)", - discriminator: "", - formatter: preferenceClaimedFormatter.bind(this, "displacedTenant", "general"), -} - -export const formatDisplacedTenantPreferenceGeneralData = { - label: "Displaced tenant preference (general) name and address", - discriminator: "", - formatter: displacedTenantFormatter.bind(this, "general"), -} - -export const formatDisplacedTenantPreferenceMissionCorridorClaimed = { - label: "Displaced tenant preference (mission corridor)", - discriminator: "", - formatter: preferenceClaimedFormatter.bind(this, "displacedTenant", "missionCorridor"), -} - -export const formatDisplacedTenantPreferenceMissionCorridorData = { - label: "Displaced tenant preference (mission corridor) name and address", - discriminator: "", - formatter: displacedTenantFormatter.bind(this, "missionCorridor"), -} - -export const formatApplicationType = { - label: "Application Type", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.submissionType) - }, -} - -export const formatDemographicsEthnicity = { - label: "Demographics Ethnicity", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.demographics.ethnicity) - }, -} - -export const formatDemographicsRace = { - label: "Demographics Race", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.demographics.race) - }, -} - -export const formatDemographicsGender = { - label: "Demographics Gender", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.demographics.gender) - }, -} - -export const formatDemographicsSexualOrientation = { - label: "Demographics Sexual Orientation", - discriminator: "", - formatter: (application: Application) => { - return defaultFormatter(application.demographics.sexualOrientation) - }, -} - -export const formatDemographicsHowDidYouHear = { - label: "Demographics How Did You Hear About Us", - discriminator: "", - formatter: (application: Application) => { - return joinArrayFormatter(application.demographics.howDidYouHear) - }, -} - -export const formatOHAPreference = { - label: "Oakland Housing Authority Project Based Vouchers", - discriminator: "", - formatter: (application: Application) => { - const pbvPreferences = application.preferences.filter((pref) => pref.key === "PBV") - if (pbvPreferences.length !== 1) { - return "" - } - return ( - pbvPreferences[0].options - .filter((option) => option.checked) - .map((option) => option.key) - .join(",") || "" - ) - }, -} - -export const formatHOPWAPreference = { - label: "Housing Opportunities for Persons with AIDS", - discriminator: "", - formatter: (application: Application) => { - const hopwaPreferences = application.preferences.filter((pref) => pref.key === "HOPWA") - if (hopwaPreferences.length !== 1) { - return "" - } - return ( - hopwaPreferences[0].options - .filter((option) => option.checked) - .map((option) => option.key) - .join(",") || "" - ) - }, -} - -export const formatBHAPreference = { - label: "Berkeley Housing Authority", - discriminator: "", - formatter: (application: Application) => { - const bhaPreferences = application.preferences.filter((pref) => pref.key === "BHA") - if (bhaPreferences.length !== 1) { - return "" - } - return bhaPreferences[0].options - .filter((option) => option.checked) - .map((option) => option.key)[0] === "bha" - ? "claimed" - : "do not consider" - }, -} - -export const formatMarkedAsDuplicate = { - label: "Marked as duplicate", - discriminator: "", - formatter: (application: Application) => { - return booleanFormatter(application.markedAsDuplicate) - }, -} - -export const formatFlagged = { - label: "Flagged", - discriminator: "", - formatter: (application: Application) => { - return booleanFormatter(application.flagged) - }, -} diff --git a/backend/core/src/csv/formatting/metadata/basic-formatting-metadata.ts b/backend/core/src/csv/formatting/metadata/basic-formatting-metadata.ts deleted file mode 100644 index 761aef8e07..0000000000 --- a/backend/core/src/csv/formatting/metadata/basic-formatting-metadata.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - formatAccessibility, - formatAlternateContactAgency, - formatAlternateContactCity, - formatAlternateContactEmail, - formatAlternateContactFirstName, - formatAlternateContactLastName, - formatAlternateContactOther, - formatAlternateContactPhone, - formatAlternateContactState, - formatAlternateContactStreetAddress, - formatAlternateContactType, - formatAlternateContactZip, - formatAnnualIncome, - formatApplicationNumber, - formatApplicationType, - formatApplicatonSubmissionDate, - formatHoueholdMembers, - formatHouseholdSize, - formatLivePreference, - formatMonthlyIncome, - formatPrimaryApplicantAdditionalPhone, - formatPrimaryApplicantAdditionalPhoneType, - formatPrimaryApplicantDOB, - formatPrimaryApplicantEmail, - formatPrimaryApplicantFirstName, - formatPrimaryApplicantLastName, - formatPrimaryApplicantMailingCity, - formatPrimaryApplicantMailingState, - formatPrimaryApplicantMailingStreetAddress, - formatPrimaryApplicantMailingZip, - formatPrimaryApplicantMiddleName, - formatPrimaryApplicantPhone, - formatPrimaryApplicantPhoneType, - formatPrimaryApplicantPreferredContactType, - formatPrimaryApplicantResidenceAddress, - formatPrimaryApplicantResidenceCity, - formatPrimaryApplicantResidenceState, - formatPrimaryApplicantResidenceZip, - formatPrimaryApplicantWorkCity, - formatPrimaryApplicantWorkState, - formatPrimaryApplicantWorkStreetAddress, - formatPrimaryApplicantWorkZip, - formatRequestUnitType, - formatVouchersOrSubsidies, - formatWorkPreference, - formatMarkedAsDuplicate, - formatFlagged, -} from "../formatters" - -export const basicFormattingMetadata = [ - formatApplicationNumber, - formatApplicationType, - formatApplicatonSubmissionDate, - formatPrimaryApplicantFirstName, - formatPrimaryApplicantMiddleName, - formatPrimaryApplicantLastName, - formatPrimaryApplicantDOB, - formatPrimaryApplicantEmail, - formatPrimaryApplicantPhone, - formatPrimaryApplicantPhoneType, - formatPrimaryApplicantAdditionalPhone, - formatPrimaryApplicantAdditionalPhoneType, - formatPrimaryApplicantPreferredContactType, - formatPrimaryApplicantResidenceAddress, - formatPrimaryApplicantResidenceCity, - formatPrimaryApplicantResidenceState, - formatPrimaryApplicantResidenceZip, - formatPrimaryApplicantMailingStreetAddress, - formatPrimaryApplicantMailingCity, - formatPrimaryApplicantMailingState, - formatPrimaryApplicantMailingZip, - formatPrimaryApplicantWorkStreetAddress, - formatPrimaryApplicantWorkCity, - formatPrimaryApplicantWorkState, - formatPrimaryApplicantWorkZip, - formatAlternateContactFirstName, - formatAlternateContactLastName, - formatAlternateContactType, - formatAlternateContactAgency, - formatAlternateContactOther, - formatAlternateContactEmail, - formatAlternateContactPhone, - formatAlternateContactStreetAddress, - formatAlternateContactCity, - formatAlternateContactState, - formatAlternateContactZip, - formatMonthlyIncome, - formatAnnualIncome, - formatAccessibility, - formatVouchersOrSubsidies, - formatRequestUnitType, - formatLivePreference, - formatWorkPreference, - formatHouseholdSize, - formatHoueholdMembers, - formatMarkedAsDuplicate, - formatFlagged, -] diff --git a/backend/core/src/csv/formatting/metadata/bha-formatting-metadata.ts b/backend/core/src/csv/formatting/metadata/bha-formatting-metadata.ts deleted file mode 100644 index 2f41c99444..0000000000 --- a/backend/core/src/csv/formatting/metadata/bha-formatting-metadata.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { - formatAccessibility, - formatAlternateContactAgency, - formatAlternateContactCity, - formatAlternateContactEmail, - formatAlternateContactFirstName, - formatAlternateContactLastName, - formatAlternateContactOther, - formatAlternateContactPhone, - formatAlternateContactState, - formatAlternateContactStreetAddress, - formatAlternateContactType, - formatAlternateContactZip, - formatAnnualIncome, - formatApplicationNumber, - formatApplicationType, - formatApplicatonSubmissionDate, - formatBHAPreference, - formatHoueholdMembers, - formatHouseholdSize, - formatLivePreference, - formatMonthlyIncome, - formatPrimaryApplicantAdditionalPhone, - formatPrimaryApplicantAdditionalPhoneType, - formatPrimaryApplicantDOB, - formatPrimaryApplicantEmail, - formatPrimaryApplicantFirstName, - formatPrimaryApplicantLastName, - formatPrimaryApplicantMailingCity, - formatPrimaryApplicantMailingState, - formatPrimaryApplicantMailingStreetAddress, - formatPrimaryApplicantMailingZip, - formatPrimaryApplicantMiddleName, - formatPrimaryApplicantPhone, - formatPrimaryApplicantPhoneType, - formatPrimaryApplicantPreferredContactType, - formatPrimaryApplicantResidenceAddress, - formatPrimaryApplicantResidenceCity, - formatPrimaryApplicantResidenceState, - formatPrimaryApplicantResidenceZip, - formatPrimaryApplicantWorkCity, - formatPrimaryApplicantWorkState, - formatPrimaryApplicantWorkStreetAddress, - formatPrimaryApplicantWorkZip, - formatRequestUnitType, - formatVouchersOrSubsidies, - formatWorkPreference, - formatMarkedAsDuplicate, - formatFlagged, -} from "../formatters" - -export const bhaFormattingMetadata = [ - formatApplicationNumber, - formatApplicationType, - formatApplicatonSubmissionDate, - formatPrimaryApplicantFirstName, - formatPrimaryApplicantMiddleName, - formatPrimaryApplicantLastName, - formatPrimaryApplicantDOB, - formatPrimaryApplicantEmail, - formatPrimaryApplicantPhone, - formatPrimaryApplicantPhoneType, - formatPrimaryApplicantAdditionalPhone, - formatPrimaryApplicantAdditionalPhoneType, - formatPrimaryApplicantPreferredContactType, - formatPrimaryApplicantResidenceAddress, - formatPrimaryApplicantResidenceCity, - formatPrimaryApplicantResidenceState, - formatPrimaryApplicantResidenceZip, - formatPrimaryApplicantMailingStreetAddress, - formatPrimaryApplicantMailingCity, - formatPrimaryApplicantMailingState, - formatPrimaryApplicantMailingZip, - formatPrimaryApplicantWorkStreetAddress, - formatPrimaryApplicantWorkCity, - formatPrimaryApplicantWorkState, - formatPrimaryApplicantWorkZip, - formatAlternateContactFirstName, - formatAlternateContactLastName, - formatAlternateContactType, - formatAlternateContactAgency, - formatAlternateContactOther, - formatAlternateContactEmail, - formatAlternateContactPhone, - formatAlternateContactStreetAddress, - formatAlternateContactCity, - formatAlternateContactState, - formatAlternateContactZip, - formatMonthlyIncome, - formatAnnualIncome, - formatAccessibility, - formatVouchersOrSubsidies, - formatRequestUnitType, - formatLivePreference, - formatWorkPreference, - formatBHAPreference, - formatHouseholdSize, - formatHoueholdMembers, - formatMarkedAsDuplicate, - formatFlagged, -] diff --git a/backend/core/src/csv/formatting/metadata/hayward-formatting-metadata.ts b/backend/core/src/csv/formatting/metadata/hayward-formatting-metadata.ts deleted file mode 100644 index f6614ecc66..0000000000 --- a/backend/core/src/csv/formatting/metadata/hayward-formatting-metadata.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - formatAccessibility, - formatAlternateContactAgency, - formatAlternateContactCity, - formatAlternateContactEmail, - formatAlternateContactFirstName, - formatAlternateContactLastName, - formatAlternateContactOther, - formatAlternateContactPhone, - formatAlternateContactState, - formatAlternateContactStreetAddress, - formatAlternateContactType, - formatAlternateContactZip, - formatAnnualIncome, - formatApplicationNumber, - formatApplicationType, - formatApplicatonSubmissionDate, - formatDisplacedTenantPreferenceGeneralClaimed, - formatDisplacedTenantPreferenceGeneralData, - formatDisplacedTenantPreferenceMissionCorridorClaimed, - formatDisplacedTenantPreferenceMissionCorridorData, - formatHoueholdMembers, - formatHouseholdSize, - formatLivePreference, - formatMonthlyIncome, - formatPrimaryApplicantAdditionalPhone, - formatPrimaryApplicantAdditionalPhoneType, - formatPrimaryApplicantDOB, - formatPrimaryApplicantEmail, - formatPrimaryApplicantFirstName, - formatPrimaryApplicantLastName, - formatPrimaryApplicantMailingCity, - formatPrimaryApplicantMailingState, - formatPrimaryApplicantMailingStreetAddress, - formatPrimaryApplicantMailingZip, - formatPrimaryApplicantMiddleName, - formatPrimaryApplicantPhone, - formatPrimaryApplicantPhoneType, - formatPrimaryApplicantPreferredContactType, - formatPrimaryApplicantResidenceAddress, - formatPrimaryApplicantResidenceCity, - formatPrimaryApplicantResidenceState, - formatPrimaryApplicantResidenceZip, - formatPrimaryApplicantWorkCity, - formatPrimaryApplicantWorkState, - formatPrimaryApplicantWorkStreetAddress, - formatPrimaryApplicantWorkZip, - formatRequestUnitType, - formatVouchersOrSubsidies, - formatWorkPreference, -} from "../formatters" - -export const haywardFormattingMetadata = [ - formatApplicationNumber, - formatApplicationType, - formatApplicatonSubmissionDate, - formatPrimaryApplicantFirstName, - formatPrimaryApplicantMiddleName, - formatPrimaryApplicantLastName, - formatPrimaryApplicantDOB, - formatPrimaryApplicantEmail, - formatPrimaryApplicantPhone, - formatPrimaryApplicantPhoneType, - formatPrimaryApplicantAdditionalPhone, - formatPrimaryApplicantAdditionalPhoneType, - formatPrimaryApplicantPreferredContactType, - formatPrimaryApplicantResidenceAddress, - formatPrimaryApplicantResidenceCity, - formatPrimaryApplicantResidenceState, - formatPrimaryApplicantResidenceZip, - formatPrimaryApplicantMailingStreetAddress, - formatPrimaryApplicantMailingCity, - formatPrimaryApplicantMailingState, - formatPrimaryApplicantMailingZip, - formatPrimaryApplicantWorkStreetAddress, - formatPrimaryApplicantWorkCity, - formatPrimaryApplicantWorkState, - formatPrimaryApplicantWorkZip, - formatAlternateContactFirstName, - formatAlternateContactLastName, - formatAlternateContactType, - formatAlternateContactAgency, - formatAlternateContactOther, - formatAlternateContactEmail, - formatAlternateContactPhone, - formatAlternateContactStreetAddress, - formatAlternateContactCity, - formatAlternateContactState, - formatAlternateContactZip, - formatMonthlyIncome, - formatAnnualIncome, - formatAccessibility, - formatVouchersOrSubsidies, - formatRequestUnitType, - formatLivePreference, - formatWorkPreference, - formatDisplacedTenantPreferenceGeneralClaimed, - formatDisplacedTenantPreferenceGeneralData, - formatDisplacedTenantPreferenceMissionCorridorClaimed, - formatDisplacedTenantPreferenceMissionCorridorData, - formatHouseholdSize, - formatHoueholdMembers, -] diff --git a/backend/core/src/csv/formatting/metadata/oha-formatting-metadata.ts b/backend/core/src/csv/formatting/metadata/oha-formatting-metadata.ts deleted file mode 100644 index acafb9bb41..0000000000 --- a/backend/core/src/csv/formatting/metadata/oha-formatting-metadata.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - formatAccessibility, - formatAlternateContactAgency, - formatAlternateContactCity, - formatAlternateContactEmail, - formatAlternateContactFirstName, - formatAlternateContactLastName, - formatAlternateContactOther, - formatAlternateContactPhone, - formatAlternateContactState, - formatAlternateContactStreetAddress, - formatAlternateContactType, - formatAlternateContactZip, - formatAnnualIncome, - formatApplicationNumber, - formatApplicationType, - formatApplicatonSubmissionDate, - formatHOPWAPreference, - formatHoueholdMembers, - formatHouseholdSize, - formatLivePreference, - formatMonthlyIncome, - formatOHAPreference, - formatPrimaryApplicantAdditionalPhone, - formatPrimaryApplicantAdditionalPhoneType, - formatPrimaryApplicantDOB, - formatPrimaryApplicantEmail, - formatPrimaryApplicantFirstName, - formatPrimaryApplicantLastName, - formatPrimaryApplicantMailingCity, - formatPrimaryApplicantMailingState, - formatPrimaryApplicantMailingStreetAddress, - formatPrimaryApplicantMailingZip, - formatPrimaryApplicantMiddleName, - formatPrimaryApplicantPhone, - formatPrimaryApplicantPhoneType, - formatPrimaryApplicantPreferredContactType, - formatPrimaryApplicantResidenceAddress, - formatPrimaryApplicantResidenceCity, - formatPrimaryApplicantResidenceState, - formatPrimaryApplicantResidenceZip, - formatPrimaryApplicantWorkCity, - formatPrimaryApplicantWorkState, - formatPrimaryApplicantWorkStreetAddress, - formatPrimaryApplicantWorkZip, - formatRequestUnitType, - formatVouchersOrSubsidies, - formatWorkPreference, -} from "../formatters" - -export const ohaFormattingMetadata = [ - formatApplicationNumber, - formatApplicationType, - formatApplicatonSubmissionDate, - formatPrimaryApplicantFirstName, - formatPrimaryApplicantMiddleName, - formatPrimaryApplicantLastName, - formatPrimaryApplicantDOB, - formatPrimaryApplicantEmail, - formatPrimaryApplicantPhone, - formatPrimaryApplicantPhoneType, - formatPrimaryApplicantAdditionalPhone, - formatPrimaryApplicantAdditionalPhoneType, - formatPrimaryApplicantPreferredContactType, - formatPrimaryApplicantResidenceAddress, - formatPrimaryApplicantResidenceCity, - formatPrimaryApplicantResidenceState, - formatPrimaryApplicantResidenceZip, - formatPrimaryApplicantMailingStreetAddress, - formatPrimaryApplicantMailingCity, - formatPrimaryApplicantMailingState, - formatPrimaryApplicantMailingZip, - formatPrimaryApplicantWorkStreetAddress, - formatPrimaryApplicantWorkCity, - formatPrimaryApplicantWorkState, - formatPrimaryApplicantWorkZip, - formatAlternateContactFirstName, - formatAlternateContactLastName, - formatAlternateContactType, - formatAlternateContactAgency, - formatAlternateContactOther, - formatAlternateContactEmail, - formatAlternateContactPhone, - formatAlternateContactStreetAddress, - formatAlternateContactCity, - formatAlternateContactState, - formatAlternateContactZip, - formatMonthlyIncome, - formatAnnualIncome, - formatAccessibility, - formatVouchersOrSubsidies, - formatRequestUnitType, - formatLivePreference, - formatWorkPreference, - formatOHAPreference, - formatHOPWAPreference, - formatHouseholdSize, - formatHoueholdMembers, -] diff --git a/backend/core/src/csv/types/csv-formatting-type-enum.ts b/backend/core/src/csv/types/csv-formatting-type-enum.ts deleted file mode 100644 index 2e51e50adb..0000000000 --- a/backend/core/src/csv/types/csv-formatting-type-enum.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum CSVFormattingType { - basic = "basic", - withDisplaceeNameAndAddress = "withDisplaceeNameAndAddress", - ohaFormat = "ohaFormat", - bhaFormat = "bhaFormat", -} diff --git a/backend/core/src/csv/types/formatting-metadata-aggregate-factory.ts b/backend/core/src/csv/types/formatting-metadata-aggregate-factory.ts deleted file mode 100644 index 7c47e1e5ff..0000000000 --- a/backend/core/src/csv/types/formatting-metadata-aggregate-factory.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CSVFormattingType } from "./csv-formatting-type-enum" -import { FormattingMetadataAggregate } from "./formatting-metadata-aggregate" - -export type FormattingMetadataAggregateFactory = ( - type: CSVFormattingType -) => FormattingMetadataAggregate diff --git a/backend/core/src/csv/types/formatting-metadata-aggregate.ts b/backend/core/src/csv/types/formatting-metadata-aggregate.ts deleted file mode 100644 index 59dfbad1c1..0000000000 --- a/backend/core/src/csv/types/formatting-metadata-aggregate.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { FormattingMetadata } from "./formatting-metadata" -import { FormattingMetadataArray } from "./formatting-metadata-array" - -export type FormattingMetadataAggregate = Array diff --git a/backend/core/src/csv/types/formatting-metadata-array.ts b/backend/core/src/csv/types/formatting-metadata-array.ts deleted file mode 100644 index 55c6ffb6ec..0000000000 --- a/backend/core/src/csv/types/formatting-metadata-array.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { FormattingMetadata } from "./formatting-metadata" - -export type FormattingMetadataArray = { - discriminator: string - type: string - items: FormattingMetadata[] - size: number | null -} diff --git a/backend/core/src/csv/types/formatting-metadata.ts b/backend/core/src/csv/types/formatting-metadata.ts deleted file mode 100644 index 0377b5895e..0000000000 --- a/backend/core/src/csv/types/formatting-metadata.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type FormattingMetadata = { - label: string - discriminator: string - formatter: (obj) => string - type?: "array" | "object" -} diff --git a/backend/core/src/shared/email/email.module.ts b/backend/core/src/email/email.module.ts similarity index 77% rename from backend/core/src/shared/email/email.module.ts rename to backend/core/src/email/email.module.ts index 81a8a2ec46..74b51b102e 100644 --- a/backend/core/src/shared/email/email.module.ts +++ b/backend/core/src/email/email.module.ts @@ -1,10 +1,10 @@ import { forwardRef, Module } from "@nestjs/common" import { SendGridModule } from "@anchan828/nest-sendgrid" -import { TranslationsModule } from "../../translations/translations.module" import { EmailService } from "./email.service" import { ConfigModule, ConfigService } from "@nestjs/config" -import { SharedModule } from "../shared.module" -import { JurisdictionsModule } from "../../jurisdictions/jurisdictions.module" +import { SharedModule } from "../shared/shared.module" +import { TranslationsModule } from "../translations/translations.module" +import { JurisdictionsModule } from "../jurisdictions/jurisdictions.module" @Module({ imports: [ diff --git a/backend/core/src/shared/email/email.service.spec.ts b/backend/core/src/email/email.service.spec.ts similarity index 79% rename from backend/core/src/shared/email/email.service.spec.ts rename to backend/core/src/email/email.service.spec.ts index 674d2cd06c..f7d13a80a0 100644 --- a/backend/core/src/shared/email/email.service.spec.ts +++ b/backend/core/src/email/email.service.spec.ts @@ -1,22 +1,24 @@ import { Test, TestingModule } from "@nestjs/testing" import { SendGridModule, SendGridService } from "@anchan828/nest-sendgrid" -import { User } from "../../auth/entities/user.entity" +import { User } from "../auth/entities/user.entity" import { EmailService } from "./email.service" import { ConfigModule } from "@nestjs/config" -import { ArcherListing } from "../../../types" +import { ArcherListing, Language } from "../../types" import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm" -import { TranslationsService } from "../../translations/translations.service" -import { Translation } from "../../translations/entities/translation.entity" -import { Language } from "../types/language-enum" +import { TranslationsService } from "../translations/services/translations.service" +import { Translation } from "../translations/entities/translation.entity" import { Repository } from "typeorm" import { REQUEST } from "@nestjs/core" -import dbOptions = require("../../../ormconfig.test") -import { JurisdictionResolverService } from "../../jurisdictions/services/jurisdiction-resolver.service" -import { JurisdictionsService } from "../../jurisdictions/services/jurisdictions.service" -import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" +import dbOptions from "../../ormconfig.test" +import { JurisdictionResolverService } from "../jurisdictions/services/jurisdiction-resolver.service" +import { JurisdictionsService } from "../jurisdictions/services/jurisdictions.service" +import { Jurisdiction } from "../jurisdictions/entities/jurisdiction.entity" +import { GeneratedListingTranslation } from "../translations/entities/generated-listing-translation.entity" +import { GoogleTranslateService } from "../translations/services/google-translate.service" declare const expect: jest.Expect +jest.setTimeout(30000) const user = new User() user.firstName = "Test" user.lastName = "User" @@ -27,6 +29,7 @@ const listing = Object.assign({}, ArcherListing) const application = { applicant: { emailAddress: "test@xample.com", firstName: "Test", lastName: "User" }, id: "abcdefg", + confirmationCode: "abc123", } let sendMock @@ -39,7 +42,7 @@ describe("EmailService", () => { module = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot(dbOptions), - TypeOrmModule.forFeature([Translation, Jurisdiction]), + TypeOrmModule.forFeature([Translation, Jurisdiction, GeneratedListingTranslation]), ConfigModule, SendGridModule.forRoot({ apikey: "SG.fake", @@ -49,12 +52,13 @@ describe("EmailService", () => { EmailService, TranslationsService, JurisdictionsService, + GoogleTranslateService, JurisdictionResolverService, { provide: REQUEST, useValue: { get: () => { - return "Alameda" + return "Detroit" }, }, }, @@ -62,13 +66,27 @@ describe("EmailService", () => { }).compile() const jurisdictionService = await module.resolve(JurisdictionsService) - const jurisdiction = await jurisdictionService.findOne({ where: { name: "Alameda" } }) + const jurisdiction = await jurisdictionService.findOne({ where: { name: "Detroit" } }) const translationsRepository = module.get>( getRepositoryToken(Translation) ) await translationsRepository.createQueryBuilder().delete().execute() const translationsService = await module.resolve(TranslationsService) + + await translationsService.create({ + jurisdiction: { + id: null, + }, + language: Language.en, + translations: { + footer: { + footer: "Generic footer", + thankYou: "Thank you!", + }, + }, + }) + await translationsService.create({ jurisdiction: { id: jurisdiction.id, @@ -144,13 +162,16 @@ describe("EmailService", () => { }) describe("welcome", () => { - it("should generate html body", async () => { + it("should generate html body, jurisdictional footer", async () => { await service.welcome(user, "http://localhost:3000", "http://localhost:3000/?token=") expect(sendMock).toHaveBeenCalled() expect(sendMock.mock.calls[0][0].to).toEqual(user.email) - expect(sendMock.mock.calls[0][0].subject).toEqual("Welcome to Bloom") + expect(sendMock.mock.calls[0][0].subject).toEqual("Welcome to Detroit Home Connect") // Check if translation is working correctly - expect(sendMock.mock.calls[0][0].html.substring(0, 26)).toEqual("

Hello Test \n User

") + expect(sendMock.mock.calls[0][0].html).toContain( + "Alameda County - Housing and Community Development (HCD) Department" + ) + expect(sendMock.mock.calls[0][0].html).toContain("

Hello Test \n User,

") }) }) @@ -171,7 +192,7 @@ describe("EmailService", () => { /http:\/\/localhost:3000\/listing\/Uvbk5qurpB2WI9V6WnNdH/ ) // contains application id - expect(sendMock.mock.calls[0][0].html).toMatch(/abcdefg/) + expect(sendMock.mock.calls[0][0].html).toMatch(/abc123/) }) }) diff --git a/backend/core/src/email/email.service.ts b/backend/core/src/email/email.service.ts new file mode 100644 index 0000000000..8722287cc3 --- /dev/null +++ b/backend/core/src/email/email.service.ts @@ -0,0 +1,273 @@ +import { Injectable, Logger, Scope } from "@nestjs/common" +import { SendGridService } from "@anchan828/nest-sendgrid" +import { ResponseError } from "@sendgrid/helpers/classes" +import merge from "lodash/merge" +import Handlebars from "handlebars" +import path from "path" +import Polyglot from "node-polyglot" +import fs from "fs" +import { ConfigService } from "@nestjs/config" +import { TranslationsService } from "../translations/services/translations.service" +import { JurisdictionResolverService } from "../jurisdictions/services/jurisdiction-resolver.service" +import { User } from "../auth/entities/user.entity" +import { Listing } from "../listings/entities/listing.entity" +import { Application } from "../applications/entities/application.entity" +import { ListingReviewOrder } from "../listings/types/listing-review-order-enum" +import { Jurisdiction } from "../jurisdictions/entities/jurisdiction.entity" +import { Language } from "../shared/types/language-enum" +import { JurisdictionsService } from "../jurisdictions/services/jurisdictions.service" + +@Injectable({ scope: Scope.REQUEST }) +export class EmailService { + polyglot: Polyglot + + constructor( + private readonly sendGrid: SendGridService, + private readonly configService: ConfigService, + private readonly translationService: TranslationsService, + private readonly jurisdictionResolverService: JurisdictionResolverService, + private readonly jurisdictionService: JurisdictionsService + ) { + this.polyglot = new Polyglot({ + phrases: {}, + }) + const polyglot = this.polyglot + Handlebars.registerHelper("t", function ( + phrase: string, + options?: number | Polyglot.InterpolationOptions + ) { + return polyglot.t(phrase, options) + }) + const parts = this.partials() + Handlebars.registerPartial(parts) + } + + public async welcome(user: User, appUrl: string, confirmationUrl: string) { + const jurisdiction = await this.getUserJurisdiction(user) + await this.loadTranslationsForUser(user) + if (this.configService.get("NODE_ENV") === "production") { + Logger.log( + `Preparing to send a welcome email to ${user.email} from ${jurisdiction.emailFromAddress}...` + ) + } + await this.send( + user.email, + jurisdiction.emailFromAddress, + "Welcome to Detroit Home Connect", + this.template("register-email")({ + user: user, + confirmationUrl: confirmationUrl, + appOptions: { appUrl: appUrl }, + }) + ) + } + + private async getUserJurisdiction(user?: User) { + let jurisdiction = await this.jurisdictionResolverService.getJurisdiction() + if (!jurisdiction && user?.jurisdictions) { + jurisdiction = await this.jurisdictionService.findOne({ + where: { id: user.jurisdictions[0].id }, + }) + } + return jurisdiction + } + + private async getListingJurisdiction(listing?: Listing) { + let jurisdiction = await this.jurisdictionResolverService.getJurisdiction() + if (!jurisdiction && listing?.jurisdiction) { + jurisdiction = listing.jurisdiction + } + + return jurisdiction + } + + private async loadTranslationsForUser(user: User) { + const language = user.language || Language.en + const jurisdiction = await this.getUserJurisdiction(user) + void (await this.loadTranslations(jurisdiction, language)) + } + + public async changeEmail(user: User, appUrl: string, confirmationUrl: string, newEmail: string) { + const jurisdiction = await this.getUserJurisdiction(user) + await this.loadTranslationsForUser(user) + await this.send( + newEmail, + jurisdiction.emailFromAddress, + "Email change request", + this.template("change-email")({ + user: user, + confirmationUrl: confirmationUrl, + appOptions: { appUrl: appUrl }, + }) + ) + } + + public async sendMfaCode(user: User, email: string, mfaCode: string) { + const jurisdiction = await this.getUserJurisdiction(user) + await this.loadTranslationsForUser(user) + await this.send( + email, + jurisdiction.emailFromAddress, + "Partners Portal account access token", + this.template("mfa-code")({ + user: user, + mfaCodeOptions: { mfaCode }, + }) + ) + } + + public async confirmation(listing: Listing, application: Application, appUrl: string) { + const jurisdiction = await this.getListingJurisdiction(listing) + void (await this.loadTranslations(jurisdiction, application.language || Language.en)) + let whatToExpectText + const listingUrl = `${appUrl}/listing/${listing.id}` + const compiledTemplate = this.template("confirmation") + + if (this.configService.get("NODE_ENV") == "production") { + Logger.log( + `Preparing to send a confirmation email to ${application.applicant.emailAddress} from ${jurisdiction.emailFromAddress}...` + ) + } + + if (listing.applicationDueDate) { + if (listing.reviewOrderType === ListingReviewOrder.lottery) { + whatToExpectText = this.polyglot.t("confirmation.whatToExpect.lottery", { + lotteryDate: listing.applicationDueDate, + }) + } else { + whatToExpectText = this.polyglot.t("confirmation.whatToExpect.noLottery", { + lotteryDate: listing.applicationDueDate, + }) + } + } else { + whatToExpectText = this.polyglot.t("confirmation.whatToExpect.FCFS") + } + const user = { + firstName: application.applicant.firstName, + middleName: application.applicant.middleName, + lastName: application.applicant.lastName, + } + await this.send( + application.applicant.emailAddress, + jurisdiction.emailFromAddress, + this.polyglot.t("confirmation.subject"), + compiledTemplate({ + listing: listing, + listingUrl: listingUrl, + application: application, + whatToExpectText: whatToExpectText, + user: user, + }) + ) + } + + public async forgotPassword(user: User, appUrl: string) { + const jurisdiction = await this.getUserJurisdiction(user) + void (await this.loadTranslations(jurisdiction, user.language)) + const compiledTemplate = this.template("forgot-password") + const resetUrl = `${appUrl}/reset-password?token=${user.resetToken}` + + if (this.configService.get("NODE_ENV") == "production") { + Logger.log( + `Preparing to send a forget password email to ${user.email} from ${jurisdiction.emailFromAddress}...` + ) + } + + await this.send( + user.email, + jurisdiction.emailFromAddress, + this.polyglot.t("forgotPassword.subject"), + compiledTemplate({ + resetUrl: resetUrl, + resetOptions: { appUrl: appUrl }, + user: user, + }) + ) + } + + private async loadTranslations(jurisdiction: Jurisdiction | null, language: Language) { + const jurisdictionalTranslations = await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( + language, + jurisdiction ? jurisdiction.id : null + ) + const genericTranslations = await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( + language, + null + ) + + // Deep merge + const translations = merge( + genericTranslations.translations, + jurisdictionalTranslations.translations + ) + + this.polyglot.replace(translations) + } + + private template(view: string) { + return Handlebars.compile( + fs.readFileSync( + path.join(path.resolve(__dirname, "..", "shared", "views"), `/${view}.hbs`), + "utf8" + ) + ) + } + + private partial(view: string) { + return fs.readFileSync( + path.join(path.resolve(__dirname, "..", "shared", "views"), `/${view}`), + "utf8" + ) + } + + private partials() { + const partials = {} + const dirName = path.resolve(__dirname, "..", "shared", "views/partials") + + fs.readdirSync(dirName).forEach((filename) => { + partials[filename.slice(0, -4)] = this.partial("partials/" + filename) + }) + + return partials + } + + private async send(to: string, from: string, subject: string, body: string, retry = 3) { + await this.sendGrid.send( + { + to: to, + from, + subject: subject, + html: body, + }, + false, + (error) => { + if (error instanceof ResponseError) { + const { response } = error + const { body: errBody } = response + console.error(`Error sending email to: ${to}! Error body: ${errBody}`) + if (retry > 0) { + void this.send(to, from, subject, body, retry - 1) + } + } + } + ) + } + + async invite(user: User, appUrl: string, confirmationUrl: string) { + void (await this.loadTranslations( + user.jurisdictions?.length === 1 ? user.jurisdictions[0] : null, + user.language || Language.en + )) + const jurisdiction = await this.getUserJurisdiction(user) + await this.send( + user.email, + jurisdiction.emailFromAddress, + this.polyglot.t("invite.hello"), + this.template("invite")({ + user: user, + confirmationUrl: confirmationUrl, + appOptions: { appUrl }, + }) + ) + } +} diff --git a/backend/core/src/filters/entity-not-found-exception.filter.ts b/backend/core/src/filters/entity-not-found-exception.filter.ts deleted file mode 100644 index 48ae9e72bb..0000000000 --- a/backend/core/src/filters/entity-not-found-exception.filter.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ExceptionFilter, Catch, ArgumentsHost } from "@nestjs/common" -import { EntityNotFoundError } from "typeorm/error/EntityNotFoundError" - -@Catch(EntityNotFoundError) -export class EntityNotFoundExceptionFilter implements ExceptionFilter { - catch(exception: EntityNotFoundError, host: ArgumentsHost) { - const response = host.switchToHttp().getResponse() - response.status(404).json({ message: exception.message }) - } -} diff --git a/backend/core/src/jurisdictions/dto/jurisdiction-create.dto.ts b/backend/core/src/jurisdictions/dto/jurisdiction-create.dto.ts new file mode 100644 index 0000000000..70ef81bd89 --- /dev/null +++ b/backend/core/src/jurisdictions/dto/jurisdiction-create.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from "@nestjs/swagger" +import { JurisdictionDto } from "./jurisdiction.dto" + +export class JurisdictionCreateDto extends OmitType(JurisdictionDto, [ + "id", + "createdAt", + "updatedAt", +] as const) {} diff --git a/backend/core/src/jurisdictions/dto/jurisdiction-update.dto.ts b/backend/core/src/jurisdictions/dto/jurisdiction-update.dto.ts new file mode 100644 index 0000000000..33cabae2c3 --- /dev/null +++ b/backend/core/src/jurisdictions/dto/jurisdiction-update.dto.ts @@ -0,0 +1,28 @@ +import { OmitType } from "@nestjs/swagger" +import { Expose, Type } from "class-transformer" +import { IsDate, IsOptional, IsUUID } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { JurisdictionDto } from "./jurisdiction.dto" + +export class JurisdictionUpdateDto extends OmitType(JurisdictionDto, [ + "id", + "createdAt", + "updatedAt", +] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + id?: string + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + createdAt?: Date + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + updatedAt?: Date +} diff --git a/backend/core/src/jurisdictions/dto/jurisdiction.dto.ts b/backend/core/src/jurisdictions/dto/jurisdiction.dto.ts index 30042600dd..302966b2b2 100644 --- a/backend/core/src/jurisdictions/dto/jurisdiction.dto.ts +++ b/backend/core/src/jurisdictions/dto/jurisdiction.dto.ts @@ -1,36 +1,29 @@ import { OmitType } from "@nestjs/swagger" import { Expose, Type } from "class-transformer" -import { IsDate, IsOptional, IsUUID } from "class-validator" +import { ArrayMaxSize, IsArray, IsString, ValidateNested } from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { Jurisdiction } from "../entities/jurisdiction.entity" +import { IdDto } from "../../shared/dto/id.dto" +import { IdNameDto } from "../../shared/dto/idName.dto" -export class JurisdictionDto extends OmitType(Jurisdiction, [] as const) {} - -export class JurisdictionCreateDto extends OmitType(JurisdictionDto, [ - "id", - "createdAt", - "updatedAt", -] as const) {} - -export class JurisdictionUpdateDto extends OmitType(JurisdictionDto, [ - "id", - "createdAt", - "updatedAt", -] as const) { +export class JurisdictionDto extends OmitType(Jurisdiction, ["preferences", "programs"] as const) { @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) - id?: string + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(1024, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDto) + programs: IdDto[] @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsDate({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Date) - createdAt?: Date + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(1024, { groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDto) + preferences: IdDto[] +} +export class JurisdictionSlimDto extends IdNameDto { @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsDate({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Date) - updatedAt?: Date + @IsString({ groups: [ValidationsGroupsEnum.default] }) + publicUrl: string } diff --git a/backend/core/src/jurisdictions/entities/jurisdiction.entity.ts b/backend/core/src/jurisdictions/entities/jurisdiction.entity.ts index baf93336c7..efbd79e9ea 100644 --- a/backend/core/src/jurisdictions/entities/jurisdiction.entity.ts +++ b/backend/core/src/jurisdictions/entities/jurisdiction.entity.ts @@ -1,8 +1,19 @@ -import { Column, Entity } from "typeorm" +import { Column, Entity, JoinTable, ManyToMany } from "typeorm" import { AbstractEntity } from "../../shared/entities/abstract.entity" -import { Expose } from "class-transformer" -import { IsString, MaxLength, IsOptional } from "class-validator" +import { Program } from "../../program/entities/program.entity" +import { + IsString, + MaxLength, + IsOptional, + IsEnum, + ArrayMaxSize, + IsArray, + ValidateNested, +} from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { Language } from "../../shared/types/language-enum" +import { Expose, Type } from "class-transformer" +import { Preference } from "../../preferences/entities/preference.entity" @Entity({ name: "jurisdictions" }) export class Jurisdiction extends AbstractEntity { @@ -17,4 +28,41 @@ export class Jurisdiction extends AbstractEntity { @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) notificationsSignUpURL?: string | null + + @ManyToMany(() => Program, (program) => program.jurisdictions, { cascade: true }) + @JoinTable() + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Program) + programs: Program[] + + @Column({ type: "enum", enum: Language, array: true, default: [Language.en] }) + @Expose() + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(256, { groups: [ValidationsGroupsEnum.default] }) + @IsEnum(Language, { groups: [ValidationsGroupsEnum.default], each: true }) + languages: Language[] + + @ManyToMany(() => Preference, (preference) => preference.jurisdictions, { cascade: true }) + @JoinTable() + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Preference) + preferences: Preference[] + + @Column({ nullable: true, type: "text" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + partnerTerms?: string | null + + @Column({ type: "text", default: "" }) + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + publicUrl: string + + @Column({ nullable: true, type: "text" }) + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + emailFromAddress: string } diff --git a/backend/core/src/jurisdictions/jurisdictions.controller.ts b/backend/core/src/jurisdictions/jurisdictions.controller.ts index 8963b21c85..99cff62c5d 100644 --- a/backend/core/src/jurisdictions/jurisdictions.controller.ts +++ b/backend/core/src/jurisdictions/jurisdictions.controller.ts @@ -17,11 +17,9 @@ import { ResourceType } from "../auth/decorators/resource-type.decorator" import { mapTo } from "../shared/mapTo" import { defaultValidationPipeOptions } from "../shared/default-validation-pipe-options" import { JurisdictionsService } from "./services/jurisdictions.service" -import { - JurisdictionCreateDto, - JurisdictionDto, - JurisdictionUpdateDto, -} from "./dto/jurisdiction.dto" +import { JurisdictionDto } from "./dto/jurisdiction.dto" +import { JurisdictionCreateDto } from "./dto/jurisdiction-create.dto" +import { JurisdictionUpdateDto } from "./dto/jurisdiction-update.dto" @Controller("jurisdictions") @ApiTags("jurisdictions") diff --git a/backend/core/src/jurisdictions/jurisdictions.module.ts b/backend/core/src/jurisdictions/jurisdictions.module.ts index e512661ea1..6a615aa085 100644 --- a/backend/core/src/jurisdictions/jurisdictions.module.ts +++ b/backend/core/src/jurisdictions/jurisdictions.module.ts @@ -10,6 +10,6 @@ import { JurisdictionResolverService } from "./services/jurisdiction-resolver.se imports: [TypeOrmModule.forFeature([Jurisdiction]), forwardRef(() => AuthModule)], controllers: [JurisdictionsController], providers: [JurisdictionsService, JurisdictionResolverService], - exports: [JurisdictionResolverService], + exports: [JurisdictionsService, JurisdictionResolverService], }) export class JurisdictionsModule {} diff --git a/backend/core/src/jurisdictions/services/jurisdiction-resolver.service.ts b/backend/core/src/jurisdictions/services/jurisdiction-resolver.service.ts index 14babbfe47..ab9654ee00 100644 --- a/backend/core/src/jurisdictions/services/jurisdiction-resolver.service.ts +++ b/backend/core/src/jurisdictions/services/jurisdiction-resolver.service.ts @@ -15,6 +15,8 @@ export class JurisdictionResolverService { async getJurisdiction(): Promise { const jurisdictionName = this.req.get("jurisdictionName") + if (jurisdictionName === "undefined") return undefined + const jurisdiction = await this.jurisdictionRepository.findOne({ where: { name: jurisdictionName }, }) diff --git a/backend/core/src/jurisdictions/services/jurisdictions.service.ts b/backend/core/src/jurisdictions/services/jurisdictions.service.ts index 55ec25ad31..140874bd41 100644 --- a/backend/core/src/jurisdictions/services/jurisdictions.service.ts +++ b/backend/core/src/jurisdictions/services/jurisdictions.service.ts @@ -1,9 +1,58 @@ -import { AbstractServiceFactory } from "../../shared/services/abstract-service" +import { NotFoundException } from "@nestjs/common" +import { InjectRepository } from "@nestjs/typeorm" +import { FindOneOptions, Repository } from "typeorm" import { Jurisdiction } from "../entities/jurisdiction.entity" -import { JurisdictionCreateDto, JurisdictionUpdateDto } from "../dto/jurisdiction.dto" +import { JurisdictionCreateDto } from "../dto/jurisdiction-create.dto" +import { JurisdictionUpdateDto } from "../dto/jurisdiction-update.dto" +import { assignDefined } from "../../shared/utils/assign-defined" -export class JurisdictionsService extends AbstractServiceFactory< - Jurisdiction, - JurisdictionCreateDto, - JurisdictionUpdateDto ->(Jurisdiction) {} +export class JurisdictionsService { + constructor( + @InjectRepository(Jurisdiction) + private readonly repository: Repository + ) {} + joinOptions = { + alias: "jurisdiction", + leftJoinAndSelect: { + programs: "jurisdiction.programs", + preferences: "jurisdiction.preferences", + }, + } + + list(): Promise { + return this.repository.find({ + join: this.joinOptions, + }) + } + + async create(dto: JurisdictionCreateDto): Promise { + return await this.repository.save(dto) + } + + async findOne(findOneOptions: FindOneOptions): Promise { + const obj = await this.repository.findOne({ ...findOneOptions, join: this.joinOptions }) + if (!obj) { + throw new NotFoundException() + } + return obj + } + + async delete(objId: string) { + await this.repository.delete(objId) + } + + async update(dto: JurisdictionUpdateDto) { + const obj = await this.repository.findOne({ + where: { + id: dto.id, + }, + join: this.joinOptions, + }) + if (!obj) { + throw new NotFoundException() + } + assignDefined(obj, dto) + await this.repository.save(obj) + return obj + } +} diff --git a/backend/core/src/libs/cacheLib/index.ts b/backend/core/src/libs/cacheLib/index.ts deleted file mode 100644 index 945d3503b6..0000000000 --- a/backend/core/src/libs/cacheLib/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Cache } from "cache-manager" - -export async function clearCacheKeys(cacheManager: Cache, keys: string[]): Promise { - for (const key of keys) { - await cacheManager.del(key) - } -} diff --git a/backend/core/src/listings/dto/filter-type-to-field-map.ts b/backend/core/src/listings/dto/filter-type-to-field-map.ts index 892571ac35..37878641c6 100644 --- a/backend/core/src/listings/dto/filter-type-to-field-map.ts +++ b/backend/core/src/listings/dto/filter-type-to-field-map.ts @@ -12,33 +12,72 @@ type keysWithMappedField = Exclude< > export const filterTypeToFieldMap: Record = { + id: "listings.id", status: "listings.status", name: "listings.name", - neighborhood: "property.neighborhood", - bedrooms: "unitTypeRef.num_bedrooms", + isVerified: "listings.isVerified", + bedrooms: "summaryUnitType.num_bedrooms", zipcode: "buildingAddress.zipCode", leasingAgents: "leasingAgents.id", - seniorHousing: "reservedCommunityType.name", + program: "listingsProgramsProgram.title", // This is the inverse of the explanation for maxRent below. - minRent: "unitsSummary.monthly_rent_max", + minRent: "amilevels.flat_rent_value", /** * The maxRent filter uses the monthly_rent_min field to avoid missing units - * in the unitsSummary's rent range. For example, if there's a unitsSummary with + * in the unitGroups's rent range. For example, if there's a unitGroups with * monthly_rent_min of $300 and monthly_rent_max of $800, we could have a * real unit with rent $500, which would look like: * * $300 ---------------- $500 ------ $600 ----------- $800 * ^ ^ ^ ^ * ^ ^ ^ ^ - * | | | unitsSummary.monthly_rent_max + * | | | unitGroups.monthly_rent_max * | | maxRent filter value * | actual unit's rent - * unitsSummary.monthly_rent_min + * unitGroups.monthly_rent_min * * If a user sets the maxRent filter to $600 we should show this potential unit. * To make sure we show this potential unit in results, we want to search for * listings with a monthly_rent_min that's <= $600. If we used the * monthly_rent_max field, we'd miss it. */ - maxRent: "unitsSummary.monthly_rent_min", + maxRent: "amilevels.flat_rent_value", + elevator: "listing_features.elevator", + wheelchairRamp: "listing_features.wheelchairRamp", + serviceAnimalsAllowed: "listing_features.serviceAnimalsAllowed", + accessibleParking: "listing_features.accessibleParking", + parkingOnSite: "listing_features.parkingOnSite", + inUnitWasherDryer: "listing_features.inUnitWasherDryer", + laundryInBuilding: "listing_features.laundryInBuilding", + barrierFreeEntrance: "listing_features.barrierFreeEntrance", + rollInShower: "listing_features.rollInShower", + grabBars: "listing_features.grabBars", + heatingInUnit: "listing_features.heatingInUnit", + acInUnit: "listing_features.acInUnit", + jurisdiction: "jurisdiction.id", + favorited: "", + marketingType: "listings.marketingType", + region: "property.region", + hearing: "", + mobility: "", + visual: "", + vacantUnits: "", + openWaitlist: "", + closedWaitlist: "", + Families: "", + ResidentswithDisabilities: "", + Seniors55: "", + Seniors62: "", + SupportiveHousingfortheHomeless: "", + Veterans: "", + bedRoomSize: "", + communityPrograms: "", + accessibility: "", + barrierFreeUnitEntrance: "listing_features.barrierFreeUnitEntrance", + loweredLightSwitch: "listing_features.loweredLightSwitch", + barrierFreeBathroom: "listing_features.barrierFreeBathroom", + wideDoorways: "listing_features.wideDoorways", + loweredCabinets: "listing_features.loweredCabinets", + section8Acceptance: "listings.section8Acceptance", + homeType: "listings.homeType", } diff --git a/backend/core/src/listings/dto/listing-create.dto.ts b/backend/core/src/listings/dto/listing-create.dto.ts index 0cf9a5f8a9..ae5ed9322c 100644 --- a/backend/core/src/listings/dto/listing-create.dto.ts +++ b/backend/core/src/listings/dto/listing-create.dto.ts @@ -10,24 +10,28 @@ import { } from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { IdDto } from "../../shared/dto/id.dto" -import { PreferenceCreateDto } from "../../preferences/dto/preference.dto" import { AddressCreateDto } from "../../shared/dto/address.dto" import { ListingEventCreateDto } from "./listing-event.dto" import { AssetCreateDto } from "../../assets/dto/asset.dto" -import { UnitsSummaryCreateDto } from "../../units-summary/dto/units-summary.dto" +import { UnitGroupCreateDto } from "../../units-summary/dto/unit-group.dto" import { ListingDto } from "./listing.dto" import { ApplicationMethodCreateDto } from "../../application-methods/dto/application-method.dto" import { UnitCreateDto } from "../../units/dto/unit-create.dto" +import { ListingPreferenceUpdateDto } from "../../preferences/dto/listing-preference-update.dto" +import { ListingProgramUpdateDto } from "../../program/dto/listing-program-update.dto" +import { ListingImageUpdateDto } from "./listing-image-update.dto" export class ListingCreateDto extends OmitType(ListingDto, [ "id", + "applicationPickUpAddress", + "applicationDropOffAddress", + "applicationMailingAddress", "createdAt", "updatedAt", "applicationMethods", "buildingSelectionCriteriaFile", - "preferences", "events", - "image", + "images", "leasingAgentAddress", "leasingAgents", "urlSlug", @@ -41,18 +45,23 @@ export class ListingCreateDto extends OmitType(ListingDto, [ "householdSizeMax", "householdSizeMin", "neighborhood", + "region", "petPolicy", "smokingPolicy", "unitsAvailable", "unitAmenities", "servicesOffered", "yearBuilt", - "unitsSummarized", + "unitSummaries", "jurisdiction", "reservedCommunityType", "result", - "unitsSummary", + "unitGroups", "referralApplication", + "listingPreferences", + "listingPrograms", + "publishedAt", + "closedAt", ] as const) { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @@ -60,30 +69,21 @@ export class ListingCreateDto extends OmitType(ListingDto, [ @Type(() => ApplicationMethodCreateDto) applicationMethods: ApplicationMethodCreateDto[] - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => PreferenceCreateDto) - preferences: PreferenceCreateDto[] - @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressCreateDto) - applicationAddress?: AddressCreateDto | null - - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AddressCreateDto) applicationPickUpAddress?: AddressCreateDto | null @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressCreateDto) applicationDropOffAddress: AddressCreateDto | null @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressCreateDto) applicationMailingAddress: AddressCreateDto | null @@ -101,12 +101,13 @@ export class ListingCreateDto extends OmitType(ListingDto, [ @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AssetCreateDto) - image?: AssetCreateDto | null + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingImageUpdateDto) + images?: ListingImageUpdateDto[] | null @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressCreateDto) leasingAgentAddress?: AddressCreateDto | null @@ -120,7 +121,7 @@ export class ListingCreateDto extends OmitType(ListingDto, [ @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @ArrayMaxSize(256, { groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(512, { groups: [ValidationsGroupsEnum.default] }) @Type(() => UnitCreateDto) units: UnitCreateDto[] @@ -137,6 +138,7 @@ export class ListingCreateDto extends OmitType(ListingDto, [ @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressCreateDto) buildingAddress?: AddressCreateDto | null @@ -165,6 +167,11 @@ export class ListingCreateDto extends OmitType(ListingDto, [ @IsString({ groups: [ValidationsGroupsEnum.default] }) neighborhood?: string | null + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + region?: string | null + @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) @@ -217,6 +224,18 @@ export class ListingCreateDto extends OmitType(ListingDto, [ @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => UnitsSummaryCreateDto) - unitsSummary?: UnitsSummaryCreateDto[] + @Type(() => UnitGroupCreateDto) + unitGroups?: UnitGroupCreateDto[] + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingPreferenceUpdateDto) + listingPreferences: ListingPreferenceUpdateDto[] + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingProgramUpdateDto) + listingPrograms?: ListingProgramUpdateDto[] } diff --git a/backend/core/src/listings/dto/listing-features.dto.ts b/backend/core/src/listings/dto/listing-features.dto.ts new file mode 100644 index 0000000000..b3b4562363 --- /dev/null +++ b/backend/core/src/listings/dto/listing-features.dto.ts @@ -0,0 +1,9 @@ +import { OmitType } from "@nestjs/swagger" +import { ListingFeatures } from "../entities/listing-features.entity" + +export class ListingFeaturesDto extends OmitType(ListingFeatures, [ + "id", + "createdAt", + "updatedAt", + "listing", +] as const) {} diff --git a/backend/core/src/listings/dto/listing-filter-params.ts b/backend/core/src/listings/dto/listing-filter-params.ts index a09c80329e..cdef7f33b7 100644 --- a/backend/core/src/listings/dto/listing-filter-params.ts +++ b/backend/core/src/listings/dto/listing-filter-params.ts @@ -4,11 +4,22 @@ import { Expose } from "class-transformer" import { ApiProperty } from "@nestjs/swagger" import { IsBooleanString, IsEnum, IsNumberString, IsOptional, IsString } from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" -import { AvailabilityFilterEnum, ListingFilterKeys } from "../../.." +import { ListingFilterKeys } from "../types/listing-filter-keys-enum" import { ListingStatus } from "../types/listing-status-enum" +import { ListingMarketingTypeEnum } from "../types/listing-marketing-type-enum" // add other listing filter params here export class ListingFilterParams extends BaseFilter { + @Expose() + @ApiProperty({ + type: String, + example: "FAB1A3C6-965E-4054-9A48-A282E92E9426", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.id]?: string; + @Expose() @ApiProperty({ type: String, @@ -32,51 +43,52 @@ export class ListingFilterParams extends BaseFilter { @Expose() @ApiProperty({ type: String, - example: "Fox Creek", + example: "2, 3", required: false, }) @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) - [ListingFilterKeys.neighborhood]?: string; + [ListingFilterKeys.bedRoomSize]?: string; @Expose() @ApiProperty({ - type: Number, - example: "3", + type: String, + example: "48211, 48212", required: false, }) @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - [ListingFilterKeys.bedrooms]?: number; + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.zipcode]?: string; @Expose() @ApiProperty({ type: String, - example: "48211", + example: "FAB1A3C6-965E-4054-9A48-A282E92E9426", required: false, }) @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - [ListingFilterKeys.zipcode]?: string; + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.leasingAgents]?: string; @Expose() @ApiProperty({ type: String, - example: "FAB1A3C6-965E-4054-9A48-A282E92E9426", + example: "hasAvailability", required: false, }) @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) - [ListingFilterKeys.leasingAgents]?: string; + [ListingFilterKeys.availability]?: string; @Expose() @ApiProperty({ - enum: Object.keys(AvailabilityFilterEnum), - example: "hasAvailability", + type: String, + example: "senior62", required: false, }) @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsEnum(AvailabilityFilterEnum, { groups: [ValidationsGroupsEnum.default] }) - [ListingFilterKeys.availability]?: AvailabilityFilterEnum; + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.program]?: string; @Expose() @ApiProperty({ @@ -86,7 +98,7 @@ export class ListingFilterParams extends BaseFilter { }) @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) - [ListingFilterKeys.seniorHousing]?: boolean; + [ListingFilterKeys.isVerified]?: boolean; @Expose() @ApiProperty({ @@ -116,5 +128,205 @@ export class ListingFilterParams extends BaseFilter { }) @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - [ListingFilterKeys.minAmiPercentage]?: number + [ListingFilterKeys.minAmiPercentage]?: number; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.elevator]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.wheelchairRamp]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.serviceAnimalsAllowed]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.accessibleParking]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.parkingOnSite]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.inUnitWasherDryer]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.laundryInBuilding]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.barrierFreeEntrance]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.rollInShower]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.grabBars]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.heatingInUnit]?: boolean; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.acInUnit]?: boolean; + + @Expose() + @ApiProperty({ + type: String, + example: "bab6cb4f-7a5a-4ee5-b327-0c2508807780", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.jurisdiction]?: string; + + @Expose() + @ApiProperty({ + enum: Object.keys(ListingMarketingTypeEnum), + example: "marketing", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(ListingMarketingTypeEnum, { groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.marketingType]?: ListingMarketingTypeEnum; + + @Expose() + @ApiProperty({ + type: String, + example: "bab6cb4f-7a5a-4ee5-b327-0c2508807780", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.favorited]?: string; + + @Expose() + @ApiProperty({ + type: String, + example: "senior55", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.communityPrograms]?: string; + + @Expose() + @ApiProperty({ + type: String, + example: "visual", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.accessibility]?: string; + + @Expose() + @ApiProperty({ + type: String, + example: "downtown,eastside", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.region]?: string; + + @Expose() + @ApiProperty({ + type: Boolean, + example: "true", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBooleanString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.section8Acceptance]?: boolean; + + @Expose() + @ApiProperty({ + type: String, + example: "apartment,townhome", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ListingFilterKeys.homeType]?: string } diff --git a/backend/core/src/listings/dto/listing-image-update.dto.ts b/backend/core/src/listings/dto/listing-image-update.dto.ts new file mode 100644 index 0000000000..cce469daa0 --- /dev/null +++ b/backend/core/src/listings/dto/listing-image-update.dto.ts @@ -0,0 +1,15 @@ +import { OmitType } from "@nestjs/swagger" +import { Expose, Type } from "class-transformer" +import { IsDefined, IsOptional, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { ListingImageDto } from "./listing-image.dto" +import { AssetUpdateDto } from "../../assets/dto/asset.dto" + +export class ListingImageUpdateDto extends OmitType(ListingImageDto, ["image"] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AssetUpdateDto) + image: AssetUpdateDto +} diff --git a/backend/core/src/listings/dto/listing-image.dto.ts b/backend/core/src/listings/dto/listing-image.dto.ts new file mode 100644 index 0000000000..05b40027de --- /dev/null +++ b/backend/core/src/listings/dto/listing-image.dto.ts @@ -0,0 +1,15 @@ +import { OmitType } from "@nestjs/swagger" +import { Expose, Type } from "class-transformer" +import { IsDefined, IsOptional, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { ListingImage } from "../entities/listing-image.entity" +import { AssetUpdateDto } from "../../assets/dto/asset.dto" + +export class ListingImageDto extends OmitType(ListingImage, ["listing", "image"] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => AssetUpdateDto) + image: AssetUpdateDto +} diff --git a/backend/core/src/listings/dto/listing-neighborhood-amenities.dto.ts b/backend/core/src/listings/dto/listing-neighborhood-amenities.dto.ts new file mode 100644 index 0000000000..a77a485c04 --- /dev/null +++ b/backend/core/src/listings/dto/listing-neighborhood-amenities.dto.ts @@ -0,0 +1,42 @@ +import { OmitType } from "@nestjs/swagger" +import { Expose } from "class-transformer" +import { IsOptional, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { ListingNeighborhoodAmenities } from "../entities/listing-neighborhood-amenities.entity" + +export class ListingNeighborhoodAmenitiesDto extends OmitType(ListingNeighborhoodAmenities, [ + "listing", + "id", + "createdAt", + "updatedAt", +] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + groceryStores?: string | null + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + publicTransportation?: string | null + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + schools?: string | null + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + parksAndCommunityCenters?: string | null + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + pharmacies?: string | null + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + healthCareResources?: string | null +} diff --git a/backend/core/src/listings/dto/listing-published-create.dto.ts b/backend/core/src/listings/dto/listing-published-create.dto.ts index c300a9c555..526b484478 100644 --- a/backend/core/src/listings/dto/listing-published-create.dto.ts +++ b/backend/core/src/listings/dto/listing-published-create.dto.ts @@ -18,6 +18,8 @@ import { import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { OmitType } from "@nestjs/swagger" import { UnitCreateDto } from "../../units/dto/unit-create.dto" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" +import { ListingImageUpdateDto } from "./listing-image-update.dto" export class ListingPublishedCreateDto extends OmitType(ListingCreateDto, [ "assets", @@ -26,7 +28,7 @@ export class ListingPublishedCreateDto extends OmitType(ListingCreateDto, [ "depositMin", "developer", "digitalApplication", - "image", + "images", "isWaitlistOpen", "leasingAgentEmail", "leasingAgentName", @@ -70,9 +72,9 @@ export class ListingPublishedCreateDto extends OmitType(ListingCreateDto, [ @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AssetCreateDto) - image: AssetCreateDto + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingImageUpdateDto) + images: ListingImageUpdateDto[] @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @@ -80,6 +82,7 @@ export class ListingPublishedCreateDto extends OmitType(ListingCreateDto, [ @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() leasingAgentEmail: string @Expose() diff --git a/backend/core/src/listings/dto/listing-published-update.dto.ts b/backend/core/src/listings/dto/listing-published-update.dto.ts index 74a5cd722a..e3ef6bf350 100644 --- a/backend/core/src/listings/dto/listing-published-update.dto.ts +++ b/backend/core/src/listings/dto/listing-published-update.dto.ts @@ -18,6 +18,8 @@ import { ListingReviewOrder } from "../types/listing-review-order-enum" import { OmitType } from "@nestjs/swagger" import { AssetUpdateDto } from "../../assets/dto/asset.dto" import { UnitUpdateDto } from "../../units/dto/unit-update.dto" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" +import { ListingImageUpdateDto } from "./listing-image-update.dto" export class ListingPublishedUpdateDto extends OmitType(ListingUpdateDto, [ "assets", @@ -26,7 +28,7 @@ export class ListingPublishedUpdateDto extends OmitType(ListingUpdateDto, [ "depositMin", "developer", "digitalApplication", - "image", + "images", "isWaitlistOpen", "leasingAgentEmail", "leasingAgentName", @@ -70,9 +72,9 @@ export class ListingPublishedUpdateDto extends OmitType(ListingUpdateDto, [ @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AssetUpdateDto) - image: AssetUpdateDto + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingImageUpdateDto) + images: ListingImageUpdateDto[] @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) @@ -80,6 +82,7 @@ export class ListingPublishedUpdateDto extends OmitType(ListingUpdateDto, [ @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() leasingAgentEmail: string @Expose() diff --git a/backend/core/src/listings/dto/listing-update.dto.ts b/backend/core/src/listings/dto/listing-update.dto.ts index 681b219cf2..bda8618522 100644 --- a/backend/core/src/listings/dto/listing-update.dto.ts +++ b/backend/core/src/listings/dto/listing-update.dto.ts @@ -12,23 +12,27 @@ import { } from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { IdDto } from "../../shared/dto/id.dto" -import { PreferenceUpdateDto } from "../../preferences/dto/preference.dto" import { AddressUpdateDto } from "../../shared/dto/address.dto" import { ListingEventUpdateDto } from "./listing-event.dto" import { AssetUpdateDto } from "../../assets/dto/asset.dto" -import { UnitsSummaryUpdateDto } from "../../units-summary/dto/units-summary.dto" +import { UnitGroupUpdateDto } from "../../units-summary/dto/unit-group.dto" import { ListingDto } from "./listing.dto" import { ApplicationMethodUpdateDto } from "../../application-methods/dto/application-method.dto" import { UnitUpdateDto } from "../../units/dto/unit-update.dto" +import { ListingPreferenceUpdateDto } from "../../preferences/dto/listing-preference-update.dto" +import { ListingProgramUpdateDto } from "../../program/dto/listing-program-update.dto" +import { ListingImageUpdateDto } from "./listing-image-update.dto" export class ListingUpdateDto extends OmitType(ListingDto, [ "id", "createdAt", "updatedAt", + "applicationMailingAddress", + "applicationDropOffAddress", + "applicationPickUpAddress", "applicationMethods", "buildingSelectionCriteriaFile", - "preferences", - "image", + "images", "events", "leasingAgentAddress", "urlSlug", @@ -43,18 +47,23 @@ export class ListingUpdateDto extends OmitType(ListingDto, [ "householdSizeMax", "householdSizeMin", "neighborhood", + "region", "petPolicy", "smokingPolicy", "unitsAvailable", "unitAmenities", "servicesOffered", "yearBuilt", - "unitsSummarized", + "unitSummaries", "jurisdiction", "reservedCommunityType", "result", - "unitsSummary", + "unitGroups", "referralApplication", + "listingPreferences", + "listingPrograms", + "publishedAt", + "closedAt", ] as const) { @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @@ -79,29 +88,21 @@ export class ListingUpdateDto extends OmitType(ListingDto, [ @Type(() => ApplicationMethodUpdateDto) applicationMethods: ApplicationMethodUpdateDto[] - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => PreferenceUpdateDto) - preferences: PreferenceUpdateDto[] - - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AddressUpdateDto) - applicationAddress?: AddressUpdateDto | null - @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressUpdateDto) applicationPickUpAddress?: AddressUpdateDto | null @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressUpdateDto) applicationDropOffAddress: AddressUpdateDto | null @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressUpdateDto) applicationMailingAddress: AddressUpdateDto | null @@ -119,12 +120,13 @@ export class ListingUpdateDto extends OmitType(ListingDto, [ @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AssetUpdateDto) - image?: AssetUpdateDto + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingImageUpdateDto) + images?: ListingImageUpdateDto[] | null @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressUpdateDto) leasingAgentAddress?: AddressUpdateDto | null @@ -155,6 +157,7 @@ export class ListingUpdateDto extends OmitType(ListingDto, [ @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @Type(() => AddressUpdateDto) buildingAddress?: AddressUpdateDto | null @@ -183,6 +186,11 @@ export class ListingUpdateDto extends OmitType(ListingDto, [ @IsString({ groups: [ValidationsGroupsEnum.default] }) neighborhood?: string | null + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + region?: string | null + @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) @@ -235,6 +243,18 @@ export class ListingUpdateDto extends OmitType(ListingDto, [ @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => UnitsSummaryUpdateDto) - unitsSummary?: UnitsSummaryUpdateDto[] + @Type(() => UnitGroupUpdateDto) + unitGroups?: UnitGroupUpdateDto[] + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingPreferenceUpdateDto) + listingPreferences: ListingPreferenceUpdateDto[] + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingProgramUpdateDto) + listingPrograms?: ListingProgramUpdateDto[] } diff --git a/backend/core/src/listings/dto/listing-utilities.dto.ts b/backend/core/src/listings/dto/listing-utilities.dto.ts new file mode 100644 index 0000000000..144dc987a2 --- /dev/null +++ b/backend/core/src/listings/dto/listing-utilities.dto.ts @@ -0,0 +1,9 @@ +import { OmitType } from "@nestjs/swagger" +import { ListingUtilities } from "../entities/listing-utilities.entity" + +export class ListingUtilitiesDto extends OmitType(ListingUtilities, [ + "id", + "createdAt", + "updatedAt", + "listing", +] as const) {} diff --git a/backend/core/src/listings/dto/listing.dto.ts b/backend/core/src/listings/dto/listing.dto.ts index 5cb5212ed3..602f7d12b6 100644 --- a/backend/core/src/listings/dto/listing.dto.ts +++ b/backend/core/src/listings/dto/listing.dto.ts @@ -1,9 +1,8 @@ import { Listing } from "../entities/listing.entity" import { Expose, plainToClass, Transform, Type } from "class-transformer" -import { IsDefined, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator" -import moment from "moment" -import { PreferenceDto } from "../../preferences/dto/preference.dto" -import { OmitType } from "@nestjs/swagger" +import { IsDefined, IsEnum, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator" +import { ApiProperty, OmitType } from "@nestjs/swagger" +import { Column } from "typeorm" import { AddressDto } from "../../shared/dto/address.dto" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { ListingStatus } from "../types/listing-status-enum" @@ -12,13 +11,19 @@ import { ReservedCommunityTypeDto } from "../../reserved-community-type/dto/rese import { AssetDto } from "../../assets/dto/asset.dto" import { ListingEventDto } from "./listing-event.dto" import { listingUrlSlug } from "../../shared/url-helper" -import { IdNameDto } from "../../shared/dto/idName.dto" +import { JurisdictionSlimDto } from "../../jurisdictions/dto/jurisdiction.dto" import { UserBasicDto } from "../../auth/dto/user-basic.dto" import { ApplicationMethodDto } from "../../application-methods/dto/application-method.dto" -import { UnitsSummaryDto } from "../../units-summary/dto/units-summary.dto" +import { UnitGroupDto } from "../../units-summary/dto/unit-group.dto" +import { ListingFeaturesDto } from "./listing-features.dto" +import { ListingUtilitiesDto } from "./listing-utilities.dto" +import { Region } from "../../property/types/region-enum" +import { ListingPreferenceDto } from "../../preferences/dto/listing-preference.dto" +import { ListingProgramDto } from "../../program/dto/listing-program.dto" +import { ListingImageDto } from "./listing-image.dto" +import { ListingNeighborhoodAmenitiesDto } from "./listing-neighborhood-amenities.dto" export class ListingDto extends OmitType(Listing, [ - "applicationAddress", "applicationPickUpAddress", "applicationDropOffAddress", "applicationMailingAddress", @@ -26,15 +31,19 @@ export class ListingDto extends OmitType(Listing, [ "applicationMethods", "buildingSelectionCriteriaFile", "events", - "image", + "images", "jurisdiction", "leasingAgents", "leasingAgentAddress", - "preferences", + "listingPreferences", + "listingPrograms", + "neighborhoodAmenities", "property", "reservedCommunityType", "result", - "unitsSummary", + "unitGroups", + "features", + "utilities", ] as const) { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @@ -42,18 +51,6 @@ export class ListingDto extends OmitType(Listing, [ @Type(() => ApplicationMethodDto) applicationMethods: ApplicationMethodDto[] - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => PreferenceDto) - preferences: PreferenceDto[] - - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AddressDto) - applicationAddress?: AddressDto | null - @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) @@ -86,9 +83,9 @@ export class ListingDto extends OmitType(Listing, [ @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => AssetDto) - image?: AssetDto | null + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingImageDto) + images?: ListingImageDto[] | null @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @@ -103,11 +100,31 @@ export class ListingDto extends OmitType(Listing, [ @Type(() => UserBasicDto) leasingAgents?: UserBasicDto[] | null + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default], each: true }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingProgramDto) + listingPrograms?: ListingProgramDto[] + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingPreferenceDto) + listingPreferences: ListingPreferenceDto[] + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => ListingNeighborhoodAmenitiesDto) + neighborhoodAmenities?: ListingNeighborhoodAmenitiesDto | null + @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => IdNameDto) - jurisdiction: IdNameDto + @Type(() => JurisdictionSlimDto) + jurisdiction: JurisdictionSlimDto @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @@ -123,16 +140,8 @@ export class ListingDto extends OmitType(Listing, [ result?: AssetDto | null @Expose() - @Transform( - (_value, listing) => { - if (moment(listing.applicationDueDate).isBefore()) { - listing.status = ListingStatus.closed - } - - return listing.status - }, - { toClassOnly: true } - ) + @IsEnum(ListingStatus, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: ListingStatus, enumName: "ListingStatus" }) status: ListingStatus @Expose() @@ -300,6 +309,22 @@ export class ListingDto extends OmitType(Listing, [ ) yearBuilt?: number | null + @Column({ type: "enum", enum: Region, nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(Region, { groups: [ValidationsGroupsEnum.default] }) + @Transform( + (value, obj: Listing) => { + return obj.property?.region + }, + { toClassOnly: true } + ) + @ApiProperty({ + enum: Region, + enumName: "Region", + }) + region?: Region | null + @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @Transform( @@ -313,8 +338,8 @@ export class ListingDto extends OmitType(Listing, [ @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => UnitsSummaryDto) - unitsSummary?: UnitsSummaryDto[] + @Type(() => UnitGroupDto) + unitGroups?: UnitGroupDto[] // Keep countyCode so we don't have to update frontend apps yet @Expose() @@ -327,4 +352,14 @@ export class ListingDto extends OmitType(Listing, [ { toClassOnly: true } ) countyCode?: string + + @Expose() + @Type(() => ListingFeaturesDto) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + features?: ListingFeaturesDto + + @Expose() + @Type(() => ListingUtilitiesDto) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + utilities?: ListingUtilitiesDto } diff --git a/backend/core/src/listings/dto/listings-metadata.dto.ts b/backend/core/src/listings/dto/listings-metadata.dto.ts new file mode 100644 index 0000000000..ebd9317469 --- /dev/null +++ b/backend/core/src/listings/dto/listings-metadata.dto.ts @@ -0,0 +1,21 @@ +import { Expose, Type } from "class-transformer" +import { IsDefined, IsOptional, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { UnitTypeDto } from "../../unit-types/dto/unit-type.dto" +import { ProgramDto } from "../../program/dto/program.dto" + +export class ListingMetadataDto { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default], each: true }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ProgramDto) + programs?: ProgramDto[] + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default], each: true }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitTypeDto) + unitTypes?: UnitTypeDto[] +} diff --git a/backend/core/src/listings/dto/listings-query-params.ts b/backend/core/src/listings/dto/listings-query-params.ts index dca2e1b2f2..32650b441b 100644 --- a/backend/core/src/listings/dto/listings-query-params.ts +++ b/backend/core/src/listings/dto/listings-query-params.ts @@ -9,9 +9,11 @@ import { IsOptional, IsString, ValidateNested, + MinLength, } from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { OrderByFieldsEnum } from "../types/listing-orderby-enum" +import { OrderDirEnum } from "../../shared/types/orderdir-enum" export class ListingsQueryParams extends PaginationAllowsAllQueryParams { @Expose() @@ -53,12 +55,29 @@ export class ListingsQueryParams extends PaginationAllowsAllQueryParams { @IsEnum(OrderByFieldsEnum, { groups: [ValidationsGroupsEnum.default] }) orderBy?: OrderByFieldsEnum + @Expose() + @ApiProperty({ + name: "orderDir", + required: false, + enum: OrderDirEnum, + enumName: "OrderDirEnum", + example: "ASC", + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(OrderDirEnum, { groups: [ValidationsGroupsEnum.default] }) + orderDir?: OrderDirEnum + @Expose() @ApiProperty({ type: String, + example: "search", required: false, }) @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) - jsonpath?: string + @MinLength(3, { + message: "Search must be at least 3 characters", + groups: [ValidationsGroupsEnum.default], + }) + search?: string } diff --git a/backend/core/src/listings/dto/listings-zip-query-params.ts b/backend/core/src/listings/dto/listings-zip-query-params.ts new file mode 100644 index 0000000000..4d25b17c8b --- /dev/null +++ b/backend/core/src/listings/dto/listings-zip-query-params.ts @@ -0,0 +1,15 @@ +import { Expose } from "class-transformer" +import { ApiProperty } from "@nestjs/swagger" +import { IsOptional, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +export class ListingsZipQueryParams { + @Expose() + @ApiProperty({ + type: String, + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + timeZone?: string +} diff --git a/backend/core/src/listings/entities/listing-features.entity.ts b/backend/core/src/listings/entities/listing-features.entity.ts new file mode 100644 index 0000000000..1174e15d34 --- /dev/null +++ b/backend/core/src/listings/entities/listing-features.entity.ts @@ -0,0 +1,135 @@ +import { Expose, Type } from "class-transformer" +import { IsBoolean, IsOptional, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { AbstractEntity } from "../../shared/entities/abstract.entity" +import { Column, Entity, OneToOne } from "typeorm" +import { Listing } from "./listing.entity" + +@Entity({ name: "listing_features" }) +export class ListingFeatures extends AbstractEntity { + @OneToOne(() => Listing, (listing) => listing.features) + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Listing) + listing: Listing + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + elevator?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + wheelchairRamp?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + serviceAnimalsAllowed?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + accessibleParking?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + parkingOnSite?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + inUnitWasherDryer?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + laundryInBuilding?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + barrierFreeEntrance?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + rollInShower?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + grabBars?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + heatingInUnit?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + acInUnit?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + hearing?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + visual?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + mobility?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + barrierFreeUnitEntrance?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + loweredLightSwitch?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + barrierFreeBathroom?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + wideDoorways?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + loweredCabinets?: boolean | null +} diff --git a/backend/core/src/listings/entities/listing-image.entity.ts b/backend/core/src/listings/entities/listing-image.entity.ts new file mode 100644 index 0000000000..1d137f3082 --- /dev/null +++ b/backend/core/src/listings/entities/listing-image.entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity, Index, ManyToOne } from "typeorm" +import { Expose, Type } from "class-transformer" +import { IsNumber, IsOptional } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { Listing } from "./listing.entity" +import { Asset } from "../../assets/entities/asset.entity" + +@Entity({ name: "listing_images" }) +export class ListingImage { + @ManyToOne(() => Listing, (listing) => listing.images, { + primary: true, + orphanedRowAction: "delete", + }) + @Index() + @Type(() => Listing) + listing: Listing + + @ManyToOne(() => Asset, { + primary: true, + eager: true, + cascade: true, + }) + @Expose() + @Type(() => Asset) + image: Asset + + @Column({ type: "integer", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + ordinal?: number | null +} diff --git a/backend/core/src/listings/entities/listing-neighborhood-amenities.entity.ts b/backend/core/src/listings/entities/listing-neighborhood-amenities.entity.ts new file mode 100644 index 0000000000..7b3a83b5c2 --- /dev/null +++ b/backend/core/src/listings/entities/listing-neighborhood-amenities.entity.ts @@ -0,0 +1,51 @@ +import { AbstractEntity } from "../../shared/entities/abstract.entity" +import { Column, Entity, OneToOne } from "typeorm" +import Listing from "./listing.entity" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { IsOptional, IsString, ValidateNested } from "class-validator" +import { Expose, Type } from "class-transformer" + +@Entity({ name: "listing_neighborhood_amenities" }) +export class ListingNeighborhoodAmenities extends AbstractEntity { + @OneToOne(() => Listing, (listing) => listing.features) + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Listing) + listing: Listing + + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + groceryStores?: string | null + + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + publicTransportation?: string | null + + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + schools?: string | null + + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + parksAndCommunityCenters?: string | null + + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + pharmacies?: string | null + + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + healthCareResources?: string | null +} diff --git a/backend/core/src/listings/entities/listing-utilities.entity.ts b/backend/core/src/listings/entities/listing-utilities.entity.ts new file mode 100644 index 0000000000..62196903d0 --- /dev/null +++ b/backend/core/src/listings/entities/listing-utilities.entity.ts @@ -0,0 +1,63 @@ +import { Expose, Type } from "class-transformer" +import { IsBoolean, IsOptional, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { AbstractEntity } from "../../shared/entities/abstract.entity" +import { Column, Entity, OneToOne } from "typeorm" +import { Listing } from "./listing.entity" + +@Entity({ name: "listing_utilities" }) +export class ListingUtilities extends AbstractEntity { + @OneToOne(() => Listing, (listing) => listing.utilities) + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Listing) + listing: Listing + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + water?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + gas?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + trash?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + sewer?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + electricity?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + cable?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + phone?: boolean | null + + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + internet?: boolean | null +} diff --git a/backend/core/src/listings/entities/listing.entity.ts b/backend/core/src/listings/entities/listing.entity.ts index a278e22d0a..7dbb967f6c 100644 --- a/backend/core/src/listings/entities/listing.entity.ts +++ b/backend/core/src/listings/entities/listing.entity.ts @@ -3,16 +3,18 @@ import { Column, CreateDateColumn, Entity, + JoinColumn, + Index, JoinTable, ManyToMany, ManyToOne, OneToMany, + OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from "typeorm" import { Application } from "../../applications/entities/application.entity" import { User } from "../../auth/entities/user.entity" -import { Preference } from "../../preferences/entities/preference.entity" import { Expose, Type } from "class-transformer" import { IsBoolean, @@ -22,6 +24,7 @@ import { IsNumber, IsOptional, IsString, + IsUrl, IsUUID, MaxLength, ValidateNested, @@ -30,7 +33,6 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger" import { Property } from "../../property/entities/property.entity" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { ListingStatus } from "../types/listing-status-enum" -import { CSVFormattingType } from "../../csv/types/csv-formatting-type-enum" import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" import { ReservedCommunityType } from "../../reserved-community-type/entities/reserved-community-type.entity" import { Asset } from "../../assets/entities/asset.entity" @@ -39,13 +41,24 @@ import { ListingApplicationAddressType } from "../types/listing-application-addr import { ListingEvent } from "./listing-event.entity" import { Address } from "../../shared/entities/address.entity" import { ApplicationMethod } from "../../application-methods/entities/application-method.entity" -import { UnitsSummarized } from "../../units/types/units-summarized" -import { UnitsSummary } from "../../units-summary/entities/units-summary.entity" +import { UnitSummaries } from "../../units/types/unit-summaries" +import { UnitGroup } from "../../units-summary/entities/unit-group.entity" import { ListingReviewOrder } from "../types/listing-review-order-enum" import { ApplicationMethodDto } from "../../application-methods/dto/application-method.dto" import { ApplicationMethodType } from "../../application-methods/types/application-method-type-enum" +import { ListingFeatures } from "./listing-features.entity" +import { ListingProgram } from "../../program/entities/listing-program.entity" +import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" +import { ListingPreference } from "../../preferences/entities/listing-preference.entity" +import { ListingImage } from "./listing-image.entity" +import { ListingMarketingTypeEnum } from "../types/listing-marketing-type-enum" +import { ListingSeasonEnum } from "../types/listing-season-enum" +import { ListingUtilities } from "./listing-utilities.entity" +import { ListingNeighborhoodAmenities } from "./listing-neighborhood-amenities.entity" +import { HomeTypeEnum } from "../types/home-type-enum" @Entity({ name: "listings" }) +@Index(["jurisdiction"]) class Listing extends BaseEntity { @PrimaryGeneratedColumn("uuid") @Expose() @@ -65,17 +78,27 @@ class Listing extends BaseEntity { @Type(() => Date) updatedAt: Date + // The HRD ID is a Detroit-specific ID. + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + hrdId?: string | null + @Column({ type: "text", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) additionalApplicationSubmissionNotes?: string | null - @OneToMany(() => Preference, (preference) => preference.listing, { cascade: true }) + @OneToMany(() => ListingPreference, (listingPreference) => listingPreference.listing, { + cascade: true, + eager: true, + }) @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => Preference) - preferences: Preference[] + @Type(() => ListingPreference) + listingPreferences: ListingPreference[] @OneToMany(() => ApplicationMethod, (am) => am.listing, { cascade: true, eager: true }) @Expose() @@ -86,34 +109,42 @@ class Listing extends BaseEntity { @Expose() @ApiPropertyOptional() get referralApplication(): ApplicationMethodDto | undefined { - return this.applicationMethods.find((method) => method.type === ApplicationMethodType.Referral) + return this.applicationMethods + ? this.applicationMethods.find((method) => method.type === ApplicationMethodType.Referral) + : undefined } // booleans to make dealing with different application methods easier to parse - @Column({ type: "boolean", default: false }) + @Column({ type: "boolean", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) digitalApplication?: boolean - @Column({ type: "boolean", default: true }) + @Column({ type: "boolean", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) commonDigitalApplication?: boolean - @Column({ type: "boolean", default: false }) + @Column({ type: "boolean", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) paperApplication?: boolean - @Column({ type: "boolean", default: false }) + @Column({ type: "boolean", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) referralOpportunity?: boolean + @Column({ type: "boolean", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + section8Acceptance?: boolean | null + // end application method booleans @Column("jsonb") @@ -149,13 +180,6 @@ class Listing extends BaseEntity { @Type(() => Date) applicationDueDate?: Date | null - @Column({ type: "timestamptz", nullable: true }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsDate({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Date) - applicationDueTime?: Date | null - @Column({ type: "timestamptz", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @@ -175,13 +199,6 @@ class Listing extends BaseEntity { @IsString({ groups: [ValidationsGroupsEnum.default] }) applicationOrganization?: string | null - @ManyToOne(() => Address, { eager: true, nullable: true, cascade: true }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Address) - applicationAddress?: Address | null - @ManyToOne(() => Address, { eager: true, nullable: true, cascade: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @@ -235,6 +252,16 @@ class Listing extends BaseEntity { @Type(() => Address) applicationMailingAddress?: Address | null + @Column({ type: "enum", enum: ListingApplicationAddressType, nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(ListingApplicationAddressType, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ + enum: ListingApplicationAddressType, + enumName: "ListingApplicationAddressType", + }) + applicationMailingAddressType?: ListingApplicationAddressType | null + @Column({ type: "text", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @@ -278,6 +305,12 @@ class Listing extends BaseEntity { @IsString({ groups: [ValidationsGroupsEnum.default] }) depositMax?: string | null + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + depositHelperText?: string | null + @Column({ type: "boolean", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @@ -301,6 +334,7 @@ class Listing extends BaseEntity { @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) + @EnforceLowerCase() leasingAgentEmail?: string | null @Column({ type: "text", nullable: true }) @@ -397,6 +431,12 @@ class Listing extends BaseEntity { @IsString({ groups: [ValidationsGroupsEnum.default] }) whatToExpect?: string | null + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + whatToExpectAdditionalText?: string | null + @Column({ type: "enum", enum: ListingStatus, @@ -425,12 +465,6 @@ class Listing extends BaseEntity { @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) displayWaitlistSize: boolean - @Column({ enum: CSVFormattingType, default: CSVFormattingType.basic }) - @Expose() - @IsEnum(CSVFormattingType, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ enum: CSVFormattingType, enumName: "CSVFormattingType" }) - CSVFormattingType: CSVFormattingType - @Expose() @ApiProperty() get showWaitlist(): boolean { @@ -460,12 +494,15 @@ class Listing extends BaseEntity { @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) reservedCommunityMinAge?: number | null - @ManyToOne(() => Asset, { eager: true, nullable: true, cascade: true }) + @OneToMany(() => ListingImage, (listingImage) => listingImage.listing, { + cascade: true, + eager: true, + }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => Asset) - image?: Asset | null + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingImage) + images?: ListingImage[] | null @ManyToOne(() => Asset, { eager: true, nullable: true, cascade: true }) @Expose() @@ -493,9 +530,42 @@ class Listing extends BaseEntity { @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) waitlistOpenSpots?: number | null + @Column({ type: "text", nullable: true }) @Expose() - @ApiProperty({ type: UnitsSummarized }) - unitsSummarized: UnitsSummarized | undefined + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + ownerCompany?: string | null + + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + managementCompany?: string | null + + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUrl({ require_protocol: true }, { groups: [ValidationsGroupsEnum.default] }) + managementWebsite?: string | null + + // In the absence of AMI percentage information at the unit level, amiPercentageMin and + // amiPercentageMax will store the AMI percentage range for the listing as a whole. + @Column({ type: "integer", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + amiPercentageMin?: number | null + + @Column({ type: "integer", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + amiPercentageMax?: number | null + + @Expose() + @ApiProperty({ type: UnitSummaries }) + unitSummaries: UnitSummaries | undefined @Column({ type: "boolean", nullable: true }) @Expose() @@ -503,7 +573,13 @@ class Listing extends BaseEntity { @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) customMapPin?: boolean | null - @OneToMany(() => UnitsSummary, (summary) => summary.listing, { + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + phoneNumber?: string | null + + @OneToMany(() => UnitGroup, (summary) => summary.listing, { nullable: true, eager: true, cascade: true, @@ -511,8 +587,132 @@ class Listing extends BaseEntity { @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default], each: true }) @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => UnitsSummary) - unitsSummary: UnitsSummary[] + @Type(() => UnitGroup) + unitGroups: UnitGroup[] + + @OneToMany(() => ListingProgram, (listingProgram) => listingProgram.listing, { + cascade: true, + eager: true, + }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default], each: true }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingProgram) + listingPrograms?: ListingProgram[] + + @Column({ type: "timestamptz", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + publishedAt?: Date | null + + @Column({ type: "timestamptz", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + closedAt?: Date | null + + @OneToOne(() => ListingFeatures, { + nullable: true, + eager: true, + cascade: true, + }) + @JoinColumn() + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default], each: true }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingFeatures) + features?: ListingFeatures + + @OneToOne(() => ListingUtilities, { + nullable: true, + eager: true, + cascade: true, + }) + @JoinColumn() + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default], each: true }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingUtilities) + utilities?: ListingUtilities + + @OneToOne(() => ListingNeighborhoodAmenities, { + nullable: true, + eager: true, + cascade: true, + }) + @JoinColumn() + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default], each: true }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingNeighborhoodAmenities) + neighborhoodAmenities?: ListingNeighborhoodAmenities + + @Column({ type: "boolean", default: false, nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + isVerified?: boolean + + @Column({ type: "timestamptz", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + verifiedAt?: Date | null + + @Column({ type: "integer", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + temporaryListingId?: number | null + + @Column({ + type: "enum", + enum: ListingMarketingTypeEnum, + default: ListingMarketingTypeEnum.Marketing, + }) + @Expose() + @IsEnum(ListingMarketingTypeEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ + enum: ListingMarketingTypeEnum, + enumName: "ListingMarketingTypeEnum", + }) + marketingType: ListingMarketingTypeEnum + + @Column({ type: "timestamptz", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDate({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Date) + marketingDate?: Date | null + + @Column({ + type: "enum", + enum: ListingSeasonEnum, + nullable: true, + }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(ListingSeasonEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ + enum: ListingSeasonEnum, + enumName: "ListingSeasonEnum", + }) + marketingSeason?: ListingSeasonEnum | null + + @Column({ + type: "enum", + enum: HomeTypeEnum, + nullable: true, + }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(HomeTypeEnum, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: HomeTypeEnum, enumName: "HomeTypeEnum" }) + homeType?: HomeTypeEnum | null } export { Listing as default, Listing } diff --git a/backend/core/src/listings/helpers.ts b/backend/core/src/listings/helpers.ts new file mode 100644 index 0000000000..4b30e4f4bf --- /dev/null +++ b/backend/core/src/listings/helpers.ts @@ -0,0 +1,87 @@ +import { MinMax } from "../../types" +import { UnitGroupAmiLevelDto } from "../../src/units-summary/dto/unit-group-ami-level.dto" +import { PaperApplication } from "../../src/paper-applications/entities/paper-application.entity" +import { isEmpty } from "../shared/utils/is-empty" + +export const cloudinaryPdfFromId = (publicId: string): string => { + if (isEmpty(publicId)) return "" + const cloudName = process.env.cloudinaryCloudName || process.env.CLOUDINARY_CLOUD_NAME + return `https://res.cloudinary.com/${cloudName}/image/upload/${publicId}.pdf` +} + +export const getPaperAppUrls = (paperApps: PaperApplication[]) => { + if (isEmpty(paperApps)) return "" + const urlArr = paperApps.map((paperApplication) => + cloudinaryPdfFromId(paperApplication.file?.fileId) + ) + const formattedResults = urlArr.join(", ") + return formattedResults +} + +export const getRentTypes = (amiLevels: UnitGroupAmiLevelDto[]): string => { + if (isEmpty(amiLevels)) return "" + const uniqueTypes = [] + amiLevels?.forEach((elem) => { + if (!uniqueTypes.includes(elem.monthlyRentDeterminationType)) + uniqueTypes.push(elem.monthlyRentDeterminationType) + }) + const formattedResults = uniqueTypes.map((elem) => convertToTitleCase(elem)).join(", ") + return formattedResults +} + +export const formatYesNo = (value: boolean | null): string => { + if (value === null || typeof value == "undefined") return "" + else if (value) return "Yes" + else return "No" +} + +export const formatStatus = { + active: "Public", + pending: "Draft", +} + +export const formatBedroom = { + oneBdrm: "1 BR", + twoBdrm: "2 BR", + threeBdrm: "3 BR", + fourBdrm: "4 BR", + fiveBdrm: "5 BR", + studio: "Studio", +} + +export const formatCurrency = (value: string): string => { + return value ? `$${value}` : "" +} + +export const convertToTitleCase = (value: string): string => { + if (isEmpty(value)) return "" + const spacedValue = value.replace(/([A-Z])/g, (match) => ` ${match}`) + const result = spacedValue.charAt(0).toUpperCase() + spacedValue.slice(1) + return result +} + +export const formatRange = ( + min: string | number, + max: string | number, + prefix: string, + postfix: string +): string => { + if (isEmpty(min) && isEmpty(max)) return "" + if (min == max || isEmpty(max)) return `${prefix}${min}${postfix}` + if (isEmpty(min)) return `${prefix}${max}${postfix}` + return `${prefix}${min}${postfix} - ${prefix}${max}${postfix}` +} + +export function formatRentRange(rent: MinMax, percent: MinMax): string { + let toReturn = "" + if (rent) { + toReturn += formatRange(rent.min, rent.max, "", "") + } + if (rent && percent) { + toReturn += ", " + } + if (percent) { + toReturn += formatRange(percent.min, percent.max, "", "%") + } + return toReturn +} diff --git a/backend/core/src/listings/listings-csv-exporter.service.ts b/backend/core/src/listings/listings-csv-exporter.service.ts new file mode 100644 index 0000000000..2ca7920194 --- /dev/null +++ b/backend/core/src/listings/listings-csv-exporter.service.ts @@ -0,0 +1,187 @@ +import { Injectable, Scope } from "@nestjs/common" +import { CsvBuilder } from "../applications/services/csv-builder.service" +import { + cloudinaryPdfFromId, + formatCurrency, + formatRange, + formatRentRange, + formatStatus, + formatYesNo, + getRentTypes, + convertToTitleCase, + formatBedroom, + getPaperAppUrls, +} from "./helpers" +import { formatLocalDate } from "../shared/utils/format-local-date" +@Injectable({ scope: Scope.REQUEST }) +export class ListingsCsvExporterService { + constructor(private readonly csvBuilder: CsvBuilder) {} + + exportListingsFromObject(listings: any[], users: any[], timeZone: string): string { + // restructure user information to listingId->user rather than user->listingId + const partnerAccessHelper = {} + users.forEach((user) => { + const userName = `${user.firstName} ${user.lastName}` + user.leasingAgentInListings.forEach((listing) => { + partnerAccessHelper[listing.id] + ? partnerAccessHelper[listing.id].push(userName) + : (partnerAccessHelper[listing.id] = [userName]) + }) + }) + const listingObj = listings.map((listing) => { + return { + ID: listing.id, + "Created At Date": formatLocalDate(listing.createdAt, "MM-DD-YYYY hh:mm:ssA z", timeZone), + "Listing Status": formatStatus[listing.status], + "Publish Date": formatLocalDate(listing.publishedAt, "MM-DD-YYYY hh:mm:ssA z", timeZone), + Verified: formatYesNo(listing.isVerified), + "Verified Date": formatLocalDate(listing.verifiedAt, "MM-DD-YYYY hh:mm:ssA z", timeZone), + "Last Updated": formatLocalDate(listing.updatedAt, "MM-DD-YYYY hh:mm:ssA z", timeZone), + "Listing Name": listing.name, + "Developer/Property Owner": listing.property.developer, + "Street Address": listing.property.buildingAddress?.street, + City: listing.property.buildingAddress?.city, + State: listing.property.buildingAddress?.state, + Zip: listing.property.buildingAddress?.zipCode, + "Year Built": listing.property.yearBuilt, + Neighborhood: listing.property.neighborhood, + Region: listing.property.region, + Latitude: listing.property.buildingAddress?.latitude, + Longitude: listing.property.buildingAddress?.longitude, + "Home Type": convertToTitleCase(listing.homeType), + "Accept Section 8": formatYesNo(listing.section8Acceptance), + "Number Of Unit Groups": listing.unitGroups?.length, + "Community Types": listing.listingPrograms + ?.map((listingProgram) => listingProgram.program.title) + .join(", "), + "Application Fee": formatCurrency(listing.applicationFee), + "Deposit Min": formatCurrency(listing.depositMin), + "Deposit Max": formatCurrency(listing.depositMax), + "Deposit Helper": listing.depositHelperText, + "Costs Not Included": listing.costsNotIncluded, + "Utilities Included": Object.entries(listing.utilities ?? {}) + .filter((entry) => entry[1] === true) + .map((entry) => convertToTitleCase(entry[0])) + .join(", "), + "Property Amenities": listing.property.amenities, + "Additional Accessibility Details": listing.property.accessibility, + "Unit Amenities": listing.property.unitAmenities, + "Smoking Policy": listing.property.smokingPolicy, + "Pets Policy": listing.property.petPolicy, + "Services Offered": listing.property.servicesOffered, + "Accessibility Features": Object.entries(listing.features ?? {}) + ?.filter((entry) => entry[1] === true) + .map((entry) => convertToTitleCase(entry[0])) + .join(", "), + "Grocery Stores": listing.neighborhoodAmenities?.groceryStores, + "Public Transportation": listing.neighborhoodAmenities?.publicTransportation, + Schools: listing.neighborhoodAmenities?.schools, + "Parks and Community Centers": listing.neighborhoodAmenities?.parksAndCommunityCenters, + Pharmacies: listing.neighborhoodAmenities?.pharmacies, + "Health Care Resources": listing.neighborhoodAmenities?.healthCareResources, + "Credit History": listing.creditHistory, + "Rental History": listing.rentalHistory, + "Criminal Background": listing.criminalBackground, + "Building Selection Criteria": cloudinaryPdfFromId( + listing.buildingSelectionCriteriaFile?.fileId + ), + "Required Documents": listing.requiredDocuments, + "Important Program Rules": listing.programRules, + "Special Notes": listing.specialNotes, + "Review Order": convertToTitleCase(listing.reviewOrderType), + "Lottery Date": formatLocalDate(listing.events[0]?.startTime, "MM-DD-YYYY", timeZone), + "Lottery Start": formatLocalDate(listing.events[0]?.startTime, "hh:mmA z", timeZone), + "Lottery End": formatLocalDate(listing.events[0]?.endTime, "hh:mmA z", timeZone), + "Lottery Notes": listing.events[0]?.note, + Waitlist: formatYesNo(listing.isWaitlistOpen), + "Max Waitlist Size": listing.waitlistMaxSize, + "How many people on the current list": listing.waitlistCurrentSize, + "How many open spots on the waitlist": listing.waitlistOpenSpots, + "Marketing Status": convertToTitleCase(listing.marketingType), + "Marketing Season": convertToTitleCase(listing.marketingSeason), + "Marketing Date": formatLocalDate(listing.marketingDate, "YYYY"), + "Leasing Company": listing.leasingAgentName, + "Leasing Email": listing.leasingAgentEmail, + "Leasing Phone": listing.leasingAgentPhone, + "Leasing Agent Title": listing.leasingAgentTitle, + "Leasing Agent Company Hours": listing.leasingAgentOfficeHours, + "Leasing Agency Website": listing.managementWebsite, + "Leasing Agency Street Address": listing.leasingAgentAddress?.street, + "Leasing Agency Street 2": listing.leasingAgentAddress?.street2, + "Leasing Agency City": listing.leasingAgentAddress?.city, + "Leasing Agency Zip": listing.leasingAgentAddress?.zipCode, + "Leasing Agency Mailing Address": listing.applicationMailingAddress?.street, + "Leasing Agency Mailing Address Street 2": listing.applicationMailingAddress?.street2, + "Leasing Agency Mailing Address City": listing.applicationMailingAddress?.city, + "Leasing Agency Mailing Address Zip": listing.applicationMailingAddress?.zipCode, + "Leasing Agency Pickup Address": listing.applicationPickUpAddress?.street, + "Leasing Agency Pickup Address Street 2": listing.applicationPickUpAddress?.street2, + "Leasing Agency Pickup Address City": listing.applicationPickUpAddress?.city, + "Leasing Agency Pickup Address Zip": listing.applicationPickUpAddress?.zipCode, + "Leasing Pick Up Office Hours": listing.applicationPickUpAddressOfficeHours, + Postmark: formatLocalDate( + listing.postmarkedApplicationsReceivedByDate, + "MM-DD-YYYY hh:mm:ssA z", + timeZone + ), + "Digital Application": formatYesNo(listing.digitalApplication), + "Digital Application URL": listing.applicationMethods[1]?.externalReference, + "Paper Application": formatYesNo(listing.paperApplication), + "Paper Application URL": getPaperAppUrls(listing.applicationMethods[0]?.paperApplications), + "Partners Who Have Access": partnerAccessHelper[listing.id]?.join(", "), + } + }) + return this.csvBuilder.buildFromIdIndex(listingObj) + } + + exportUnitsFromObject(listings: any[]): string { + const reformattedListings = [] + listings.forEach((listing) => { + listing.unitGroups.forEach((unitGroup, idx) => { + reformattedListings.push({ + id: listing.id, + name: listing.name, + unitGroup, + unitGroupSummary: listing.unitSummaries.unitGroupSummary[idx], + }) + }) + }) + + const unitsFormatted = reformattedListings.map((listing) => { + return { + "Listing ID": listing.id, + "Listing Name": listing.name, + "Unit Group ID": listing.unitGroup.id, + "Unit Types": listing.unitGroupSummary?.unitTypes + .map((unitType) => formatBedroom[unitType]) + .join(", "), + "AMI Chart": [ + ...new Set(listing.unitGroup?.amiLevels.map((level) => level.amiChart?.name)), + ].join(", "), + "AMI Level": formatRange( + listing.unitGroupSummary?.amiPercentageRange?.min, + listing.unitGroupSummary?.amiPercentageRange?.max, + "", + "%" + ), + "Rent Type": getRentTypes(listing.unitGroup?.amiLevels), + "Monthly Rent": formatRentRange( + listing.unitGroupSummary.rentRange, + listing.unitGroupSummary.rentAsPercentIncomeRange + ), + "Affordable Unit Group Quantity": listing.unitGroup?.totalCount, + "Unit Group Vacancies": listing.unitGroup?.totalAvailable, + "Waitlist Status": formatYesNo(listing.unitGroup?.openWaitlist), + "Minimum Occupancy": listing.unitGroup?.minOccupancy, + "Maximum Occupancy": listing.unitGroup?.maxOccupancy, + "Minimum Sq ft": listing.unitGroup?.sqFeetMin, + "Maximum Sq ft": listing.unitGroup?.sqFeetMax, + "Minimum Floor": listing.unitGroup?.floorMin, + "Maximum Floor": listing.unitGroup?.floorMax, + "Minimum Bathrooms": listing.unitGroup?.bathroomMin, + "Maximum Bathrooms": listing.unitGroup?.bathroomMax, + } + }) + return this.csvBuilder.buildFromIdIndex(unitsFormatted) + } +} diff --git a/backend/core/src/listings/listings.controller.spec.ts b/backend/core/src/listings/listings.controller.spec.ts new file mode 100644 index 0000000000..de4d68f343 --- /dev/null +++ b/backend/core/src/listings/listings.controller.spec.ts @@ -0,0 +1,42 @@ +import { Test, TestingModule } from "@nestjs/testing" +import { ListingsController } from "./listings.controller" +import { PassportModule } from "@nestjs/passport" +import { ListingsService } from "./listings.service" +import { AuthzService } from "../auth/services/authz.service" +import { CacheModule } from "@nestjs/common" +import { ActivityLogService } from "../activity-log/services/activity-log.service" +import { ListingsCsvExporterService } from "./listings-csv-exporter.service" + +// Cypress brings in Chai types for the global expect, but we want to use jest +// expect here so we need to re-declare it. +// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 +declare const expect: jest.Expect + +describe("Listings Controller", () => { + let controller: ListingsController + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + PassportModule, + CacheModule.register({ + ttl: 60, + max: 2, + }), + ], + providers: [ + { provide: AuthzService, useValue: {} }, + { provide: ListingsService, useValue: {} }, + { provide: ActivityLogService, useValue: {} }, + { provide: ListingsCsvExporterService, useValue: {} }, + ], + controllers: [ListingsController], + }).compile() + + controller = module.get(ListingsController) + }) + + it("should be defined", () => { + expect(controller).toBeDefined() + }) +}) diff --git a/backend/core/src/listings/listings.controller.ts b/backend/core/src/listings/listings.controller.ts index 10827f341b..dac4e18bb5 100644 --- a/backend/core/src/listings/listings.controller.ts +++ b/backend/core/src/listings/listings.controller.ts @@ -1,11 +1,8 @@ import { Body, - CacheInterceptor, - CACHE_MANAGER, Controller, Delete, Get, - Inject, Param, Post, Put, @@ -16,10 +13,11 @@ import { ValidationPipe, ClassSerializerInterceptor, Headers, + Header, + ParseUUIDPipe, } from "@nestjs/common" import { ListingsService } from "./listings.service" import { ApiBearerAuth, ApiExtraModels, ApiOperation, ApiTags } from "@nestjs/swagger" -import { Cache } from "cache-manager" import { ListingDto } from "./dto/listing.dto" import { ResourceType } from "../auth/decorators/resource-type.decorator" import { OptionalAuthGuard } from "../auth/guards/optional-auth.guard" @@ -27,35 +25,46 @@ import { AuthzGuard } from "../auth/guards/authz.guard" import { mapTo } from "../shared/mapTo" import { defaultValidationPipeOptions } from "../shared/default-validation-pipe-options" import { Language } from "../shared/types/language-enum" -import { ListingLangCacheInterceptor } from "../cache/listing-lang-cache.interceptor" import { PaginatedListingDto } from "./dto/paginated-listing.dto" import { ListingCreateDto } from "./dto/listing-create.dto" import { ListingUpdateDto } from "./dto/listing-update.dto" import { ListingFilterParams } from "./dto/listing-filter-params" import { ListingsQueryParams } from "./dto/listings-query-params" import { ListingsRetrieveQueryParams } from "./dto/listings-retrieve-query-params" +import { ListingMetadataDto } from "./dto/listings-metadata.dto" import { ListingCreateValidationPipe } from "./validation-pipes/listing-create-validation-pipe" import { ListingUpdateValidationPipe } from "./validation-pipes/listing-update-validation-pipe" +import { ActivityLogInterceptor } from "../activity-log/interceptors/activity-log.interceptor" +import { ActivityLogMetadata } from "../activity-log/decorators/activity-log-metadata.decorator" +import { ListingsCsvExporterService } from "../listings/listings-csv-exporter.service" +import { ListingsZipQueryParams } from "./dto/listings-zip-query-params" @Controller("listings") @ApiTags("listings") @ApiBearerAuth() @ResourceType("listing") -@ApiExtraModels(ListingFilterParams) @UseGuards(OptionalAuthGuard, AuthzGuard) +@ActivityLogMetadata([{ targetPropertyName: "status", propertyPath: "status" }]) +@UseInterceptors(ActivityLogInterceptor) export class ListingsController { - cacheKeys: string[] constructor( - @Inject(CACHE_MANAGER) private cacheManager: Cache, - private readonly listingsService: ListingsService + private readonly listingsService: ListingsService, + private readonly listingsCsvExporter: ListingsCsvExporterService ) {} + @Get("meta") + @ApiOperation({ summary: "Returns Listing Metadata", operationId: "metadata" }) + @UseInterceptors(ClassSerializerInterceptor) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + public async getListingMetaData(): Promise { + return mapTo(ListingMetadataDto, await this.listingsService.getMetadata()) + } + // TODO: Limit requests to defined fields @Get() @ApiExtraModels(ListingFilterParams) @ApiOperation({ summary: "List listings", operationId: "list" }) - // ClassSerializerInterceptor has to come after CacheInterceptor - @UseInterceptors(CacheInterceptor, ClassSerializerInterceptor) + @UseInterceptors(ClassSerializerInterceptor) @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) public async getAll(@Query() queryParams: ListingsQueryParams): Promise { return mapTo(PaginatedListingDto, await this.listingsService.list(queryParams)) @@ -66,17 +75,34 @@ export class ListingsController { @UsePipes(new ListingCreateValidationPipe(defaultValidationPipeOptions)) async create(@Body() listingDto: ListingCreateDto): Promise { const listing = await this.listingsService.create(listingDto) - await this.cacheManager.reset() return mapTo(ListingDto, listing) } - @Get(`:listingId`) + @Get(`csv`) + @UseGuards(OptionalAuthGuard, AuthzGuard) + @ApiOperation({ summary: "Retrieve listings and units in csv", operationId: "listAsCsv" }) + @Header("Content-Type", "text/csv") + async listAsCsv( + @Query(new ValidationPipe(defaultValidationPipeOptions)) + queryParams: ListingsZipQueryParams + ): Promise<{ listingCsv: string; unitCsv: string }> { + const data = await this.listingsService.rawListWithFlagged() + const listingCsv = this.listingsCsvExporter.exportListingsFromObject( + data?.listingData, + data?.userAccessData, + queryParams.timeZone + ) + const unitCsv = this.listingsCsvExporter.exportUnitsFromObject(data?.unitData) + return { listingCsv, unitCsv } + } + + @Get(`:id`) @ApiOperation({ summary: "Get listing by id", operationId: "retrieve" }) - @UseInterceptors(ListingLangCacheInterceptor, ClassSerializerInterceptor) + @UseInterceptors(ClassSerializerInterceptor) @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) async retrieve( @Headers("language") language: Language, - @Param("listingId") listingId: string, + @Param("id", new ParseUUIDPipe({ version: "4" })) listingId: string, @Query() queryParams: ListingsRetrieveQueryParams ): Promise { if (listingId === undefined || listingId === "undefined") { @@ -88,23 +114,21 @@ export class ListingsController { ) } - @Put(`:listingId`) + @Put(`:id`) @ApiOperation({ summary: "Update listing by id", operationId: "update" }) @UsePipes(new ListingUpdateValidationPipe(defaultValidationPipeOptions)) async update( - @Param("listingId") listingId: string, + @Param("id") listingId: string, @Body() listingUpdateDto: ListingUpdateDto ): Promise { const listing = await this.listingsService.update(listingUpdateDto) - await this.cacheManager.reset() return mapTo(ListingDto, listing) } - @Delete(`:listingId`) + @Delete(`:id`) @ApiOperation({ summary: "Delete listing by id", operationId: "delete" }) @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) - async delete(@Param("listingId") listingId: string) { + async delete(@Param("id", new ParseUUIDPipe({ version: "4" })) listingId: string) { await this.listingsService.delete(listingId) - await this.cacheManager.reset() } } diff --git a/backend/core/src/listings/listings.module.ts b/backend/core/src/listings/listings.module.ts index 8fdb077a95..f12ebe9db8 100644 --- a/backend/core/src/listings/listings.module.ts +++ b/backend/core/src/listings/listings.module.ts @@ -1,8 +1,5 @@ -import { CacheModule, CACHE_MANAGER, Inject, Module, OnModuleDestroy } from "@nestjs/common" +import { Module } from "@nestjs/common" import { TypeOrmModule } from "@nestjs/typeorm" -import * as redisStore from "cache-manager-redis-store" -import { Store } from "cache-manager" -import Redis from "redis" import { ListingsService } from "./listings.service" import { ListingsController } from "./listings.controller" import { Listing } from "./entities/listing.entity" @@ -13,61 +10,38 @@ import { User } from "../auth/entities/user.entity" import { Property } from "../property/entities/property.entity" import { TranslationsModule } from "../translations/translations.module" import { AmiChart } from "../ami-charts/entities/ami-chart.entity" - -interface RedisCache extends Cache { - store: RedisStore -} - -interface RedisStore extends Store { - name: "redis" - getClient: () => Redis.RedisClient - isCacheableValue: (value: unknown) => boolean -} - -const cacheConfig = { - ttl: 24 * 60 * 60, - store: redisStore, - url: process.env.REDIS_URL, - tls: undefined, -} - -if (process.env.REDIS_USE_TLS !== "0") { - cacheConfig.url = process.env.REDIS_TLS_URL - cacheConfig.tls = { - rejectUnauthorized: false, - } -} +import { SmsModule } from "../sms/sms.module" +import { ListingFeatures } from "./entities/listing-features.entity" +import { ActivityLogModule } from "../activity-log/activity-log.module" +import { UnitGroup } from "../units-summary/entities/unit-group.entity" +import { UnitType } from "../unit-types/entities/unit-type.entity" +import { Program } from "../program/entities/program.entity" +import { ListingUtilities } from "./entities/listing-utilities.entity" +import { ListingsCsvExporterService } from "./listings-csv-exporter.service" +import { CsvBuilder } from "../../src/applications/services/csv-builder.service" @Module({ imports: [ - CacheModule.register(cacheConfig), - TypeOrmModule.forFeature([Listing, Preference, Unit, User, Property, AmiChart]), + TypeOrmModule.forFeature([ + Listing, + Preference, + Unit, + User, + Property, + AmiChart, + ListingFeatures, + ListingUtilities, + UnitGroup, + UnitType, + Program, + ]), AuthModule, TranslationsModule, + SmsModule, + ActivityLogModule, ], - providers: [ListingsService], + providers: [ListingsService, CsvBuilder, ListingsCsvExporterService], exports: [ListingsService], controllers: [ListingsController], }) -// We have to manually disconnect from redis on app close -export class ListingsModule implements OnModuleDestroy { - redisClient: Redis.RedisClient - constructor(@Inject(CACHE_MANAGER) private cacheManager: RedisCache) { - this.redisClient = this.cacheManager.store.getClient() - - this.redisClient.on("error", (error) => { - console.log("redis error = ", error) - }) - } - onModuleDestroy() { - console.log("Disconnect from Redis") - this.redisClient.quit() - } - - onModuleInit() { - console.log("Reset Redis Cache") - // this.redisClient.reset() doesn't seem to clear all keys - // below actually calls flushdb, so we may want to change this, if other modules use the cache - void this.cacheManager.store.reset() - } -} +export class ListingsModule {} diff --git a/backend/core/src/listings/listings.service.spec.ts b/backend/core/src/listings/listings.service.spec.ts deleted file mode 100644 index 27c209e640..0000000000 --- a/backend/core/src/listings/listings.service.spec.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { Test, TestingModule } from "@nestjs/testing" -import { ListingsService } from "./listings.service" -import { getRepositoryToken } from "@nestjs/typeorm" -import { HttpException, HttpStatus } from "@nestjs/common" -import { Listing } from "./entities/listing.entity" -import { Compare } from "../shared/dto/filter.dto" -import { TranslationsService } from "../translations/translations.service" -import { AmiChart } from "../ami-charts/entities/ami-chart.entity" -import { OrderByFieldsEnum } from "./types/listing-orderby-enum" -import { AvailabilityFilterEnum } from "./types/listing-filter-keys-enum" -import { ListingFilterParams } from "./dto/listing-filter-params" -import { ListingsQueryParams } from "./dto/listings-query-params" - -// Cypress brings in Chai types for the global expect, but we want to use jest -// expect here so we need to re-declare it. -// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 -declare const expect: jest.Expect - -let service: ListingsService -const mockListings = [ - { - id: "asdf1", - property: { id: "test-property1", units: [] }, - preferences: [], - status: "closed", - unitsSummarized: { byUnitTypeAndRent: [] }, - }, - { - id: "asdf2", - property: { id: "test-property2", units: [] }, - preferences: [], - status: "closed", - unitsSummarized: { byUnitTypeAndRent: [] }, - }, - { - id: "asdf3", - property: { id: "test-property3", units: [] }, - preferences: [], - status: "closed", - unitsSummarized: { byUnitTypeAndRent: [] }, - }, - { - id: "asdf4", - property: { id: "test-property4", units: [] }, - preferences: [], - status: "closed", - unitsSummarized: { byUnitTypeAndRent: [] }, - }, - { - id: "asdf5", - property: { id: "test-property5", units: [] }, - preferences: [], - status: "closed", - unitsSummarized: { byUnitTypeAndRent: [] }, - }, - { - id: "asdf6", - property: { id: "test-property6", units: [] }, - preferences: [], - status: "closed", - unitsSummarized: { byUnitTypeAndRent: [] }, - }, - { - id: "asdf7", - property: { id: "test-property7", units: [] }, - preferences: [], - status: "closed", - unitsSummarized: { byUnitTypeAndRent: [] }, - }, -] -const mockFilteredListings = mockListings.slice(0, 2) -const mockInnerQueryBuilder = { - select: jest.fn().mockReturnThis(), - leftJoin: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - addOrderBy: jest.fn().mockReturnThis(), - groupBy: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - offset: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - getParameters: jest.fn().mockReturnValue({ param1: "param1value" }), - getQuery: jest.fn().mockReturnValue("innerQuery"), - getCount: jest.fn().mockReturnValue(7), -} -const mockQueryBuilder = { - select: jest.fn().mockReturnThis(), - leftJoin: jest.fn().mockReturnThis(), - leftJoinAndSelect: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - setParameters: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - addOrderBy: jest.fn().mockReturnThis(), - getMany: jest.fn().mockReturnValue(mockListings), -} -const mockListingsRepo = { - createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder), - count: jest.fn().mockReturnValue(100), -} - -describe("ListingsService", () => { - beforeEach(async () => { - process.env.APP_SECRET = "SECRET" - process.env.EMAIL_API_KEY = "SG.KEY" - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ListingsService, - { - provide: getRepositoryToken(Listing), - useValue: mockListingsRepo, - }, - { - provide: getRepositoryToken(AmiChart), - useValue: jest.fn(), - }, - { - provide: TranslationsService, - useValue: { translateListing: jest.fn() }, - }, - ], - }).compile() - - service = module.get(ListingsService) - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - it("should be defined", () => { - expect(service).toBeDefined() - }) - - describe("getListingsList", () => { - it("should not add a WHERE clause if no filters are applied", async () => { - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - - const listings = await service.list({}) - - expect(listings.items).toEqual(mockListings) - expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledTimes(0) - }) - - it("should add a WHERE clause if the neighborhood filter is applied", async () => { - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - const expectedNeighborhood = "Fox Creek" - - const queryParams: ListingsQueryParams = { - filter: [ - { - $comparison: Compare["="], - neighborhood: expectedNeighborhood, - }, - ], - } - - const listings = await service.list(queryParams) - - expect(listings.items).toEqual(mockListings) - expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledWith( - "(LOWER(CAST(property.neighborhood as text)) = LOWER(:neighborhood_0))", - { - neighborhood_0: expectedNeighborhood, - } - ) - }) - - it("should support filters with comma-separated arrays", async () => { - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - const zipCodeString = "10011, , 10014," // intentional extra and trailing commas for test - // lowercased, trimmed spaces, filtered empty - const expectedZipCodeArray = ["10011", "10014"] - - const queryParams: ListingsQueryParams = { - filter: [ - { - $comparison: Compare["IN"], - zipcode: zipCodeString, - }, - ], - } - - const listings = await service.list(queryParams) - - expect(listings.items).toEqual(mockListings) - expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledWith( - "(LOWER(CAST(buildingAddress.zipCode as text)) IN (:...zipcode_0))", - { - zipcode_0: expectedZipCodeArray, - } - ) - }) - - it("should include listings with missing data if $include_nulls is true", async () => { - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - const queryParams: ListingsQueryParams = { - filter: [ - { - $comparison: Compare["="], - name: "minRent", - $include_nulls: true, - }, - ], - } - - const listings = await service.list(queryParams) - - expect(listings.items).toEqual(mockListings) - expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledWith( - "(LOWER(CAST(listings.name as text)) = LOWER(:name_0) OR listings.name IS NULL)", - { - name_0: "minRent", - } - ) - }) - - it("should include listings with missing data if $include_nulls is true for custom filters", async () => { - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - const queryParams: ListingsQueryParams = { - filter: [ - { - $comparison: Compare["NA"], - availability: AvailabilityFilterEnum.waitlist, - $include_nulls: true, - }, - ], - } - - const listings = await service.list(queryParams) - - expect(listings.items).toEqual(mockListings) - expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledWith( - "(listings.is_waitlist_open = :availability OR listings.is_waitlist_open is NULL)", - { - availability: true, - } - ) - }) - - it("should throw an exception if an unsupported filter is used", async () => { - mockListingsRepo.createQueryBuilder.mockReturnValueOnce(mockInnerQueryBuilder) - - const queryParams: ListingsQueryParams = { - filter: [ - { - $comparison: Compare["="], - otherField: "otherField", - // The querystring can contain unknown fields that aren't on the - // ListingFilterParams type, so we force it to the type for testing. - } as ListingFilterParams, - ], - } - - await expect(service.list(queryParams)).rejects.toThrow( - new HttpException("Filter Not Implemented", HttpStatus.NOT_IMPLEMENTED) - ) - }) - - //TODO(avaleske): A lot of these tests should be moved to a spec file specific to the filters code. - it("should throw an exception if an unsupported comparison is used", async () => { - mockListingsRepo.createQueryBuilder.mockReturnValueOnce(mockInnerQueryBuilder) - - const queryParams: ListingsQueryParams = { - filter: [ - { - // The value of the filter[$comparison] query param is not validated, - // and the type system trusts that whatever is provided is correct, - // so we force it to an invalid type for testing. - $comparison: "); DROP TABLE Students;" as Compare, - name: "test name", - } as ListingFilterParams, - ], - } - - await expect(service.list(queryParams)).rejects.toThrow( - new HttpException("Comparison Not Implemented", HttpStatus.NOT_IMPLEMENTED) - ) - }) - - it("should not call limit() and offset() if pagination params are not specified", async () => { - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - - // Empty params (no pagination) -> no limit/offset - const params = {} - const listings = await service.list(params) - - expect(listings.items).toEqual(mockListings) - expect(mockInnerQueryBuilder.limit).toHaveBeenCalledTimes(0) - expect(mockInnerQueryBuilder.offset).toHaveBeenCalledTimes(0) - }) - - it("should not call limit() and offset() if incomplete pagination params are specified", async () => { - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - - // Invalid pagination params (page specified, but not limit) -> no limit/offset - const params = { page: 3 } - const listings = await service.list(params) - - expect(listings.items).toEqual(mockListings) - expect(mockInnerQueryBuilder.limit).toHaveBeenCalledTimes(0) - expect(mockInnerQueryBuilder.offset).toHaveBeenCalledTimes(0) - expect(listings.meta).toEqual({ - currentPage: 1, - itemCount: mockListings.length, - itemsPerPage: mockListings.length, - totalItems: mockListings.length, - totalPages: 1, - }) - }) - - it("should not call limit() and offset() if invalid pagination params are specified", async () => { - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - - // Invalid pagination params (page specified, but not limit) -> no limit/offset - const params = { page: ("hello" as unknown) as number } // force the type for testing - const listings = await service.list(params) - - expect(listings.items).toEqual(mockListings) - expect(mockInnerQueryBuilder.limit).toHaveBeenCalledTimes(0) - expect(mockInnerQueryBuilder.offset).toHaveBeenCalledTimes(0) - expect(listings.meta).toEqual({ - currentPage: 1, - itemCount: mockListings.length, - itemsPerPage: mockListings.length, - totalItems: mockListings.length, - totalPages: 1, - }) - }) - - it("should call limit() and offset() if pagination params are specified", async () => { - mockQueryBuilder.getMany.mockReturnValueOnce(mockFilteredListings) - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - - // Valid pagination params -> offset and limit called appropriately - const params = { page: 3, limit: 2 } - const listings = await service.list(params) - - expect(listings.items).toEqual(mockFilteredListings) - expect(mockInnerQueryBuilder.limit).toHaveBeenCalledWith(2) - expect(mockInnerQueryBuilder.offset).toHaveBeenCalledWith(4) - expect(mockInnerQueryBuilder.getCount).toHaveBeenCalledTimes(1) - expect(listings.meta).toEqual({ - currentPage: 3, - itemCount: 2, - itemsPerPage: 2, - totalItems: mockListings.length, - totalPages: 4, - }) - }) - }) - - describe("ListingsService.list sorting", () => { - it("defaults to ordering by application dates when no orderBy param is set", async () => { - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - - await service.list({}) - - const expectedOrderByArgument = { - "listings.applicationDueDate": "ASC", - "listings.applicationOpenDate": "DESC", - } - - // The inner query must be ordered so that the ordering applies across all pages (if pagination is requested) - expect(mockInnerQueryBuilder.orderBy).toHaveBeenCalledTimes(1) - expect(mockInnerQueryBuilder.orderBy).toHaveBeenCalledWith(expectedOrderByArgument) - - // The full query must be ordered so that the ordering is applied within a page (if pagination is requested) - expect(mockQueryBuilder.orderBy).toHaveBeenCalledTimes(1) - expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith(expectedOrderByArgument) - - // The full query is additionally ordered by the number of bedrooms (or max_occupancy) at the unit level. - expect(mockQueryBuilder.addOrderBy).toHaveBeenCalledTimes(1) - expect(mockQueryBuilder.addOrderBy).toHaveBeenCalledWith( - "units.max_occupancy", - "ASC", - "NULLS LAST" - ) - }) - - it("orders by the orderBy param (when set)", async () => { - mockListingsRepo.createQueryBuilder - .mockReturnValueOnce(mockInnerQueryBuilder) - .mockReturnValueOnce(mockQueryBuilder) - - await service.list({ orderBy: OrderByFieldsEnum.mostRecentlyUpdated }) - - const expectedOrderByArgument = { "listings.updated_at": "DESC" } - - expect(mockInnerQueryBuilder.orderBy).toHaveBeenCalledTimes(1) - expect(mockInnerQueryBuilder.orderBy).toHaveBeenCalledWith(expectedOrderByArgument) - - expect(mockQueryBuilder.orderBy).toHaveBeenCalledTimes(1) - expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith(expectedOrderByArgument) - - // Verify that the full query is still also ordered by the number of bedrooms - // (or max_occupancy) at the unit level. - expect(mockQueryBuilder.addOrderBy).toHaveBeenCalledTimes(1) - expect(mockQueryBuilder.addOrderBy).toHaveBeenCalledWith( - "units.max_occupancy", - "ASC", - "NULLS LAST" - ) - }) - }) -}) diff --git a/backend/core/src/listings/listings.service.ts b/backend/core/src/listings/listings.service.ts index 7e93aae5ab..0f2032462f 100644 --- a/backend/core/src/listings/listings.service.ts +++ b/backend/core/src/listings/listings.service.ts @@ -1,30 +1,49 @@ -import { Injectable, NotFoundException } from "@nestjs/common" -import jp from "jsonpath" -import { Listing } from "./entities/listing.entity" +import { + HttpException, + HttpStatus, + Inject, + Injectable, + NotFoundException, + UnauthorizedException, +} from "@nestjs/common" import { InjectRepository } from "@nestjs/typeorm" import { Pagination } from "nestjs-typeorm-paginate" -import { In, OrderByCondition, Repository } from "typeorm" +import { In, Repository, SelectQueryBuilder } from "typeorm" import { plainToClass } from "class-transformer" +import { Listing } from "./entities/listing.entity" import { PropertyCreateDto, PropertyUpdateDto } from "../property/dto/property.dto" -import { addFilters } from "../shared/filter" +import { addFilters } from "../shared/query-filter" import { getView } from "./views/view" import { summarizeUnits } from "../shared/units-transformations" import { Language } from "../../types" -import { TranslationsService } from "../translations/translations.service" import { AmiChart } from "../ami-charts/entities/ami-chart.entity" -import { HttpException, HttpStatus } from "@nestjs/common" import { OrderByFieldsEnum } from "./types/listing-orderby-enum" import { ListingCreateDto } from "./dto/listing-create.dto" import { ListingUpdateDto } from "./dto/listing-update.dto" import { ListingFilterParams } from "./dto/listing-filter-params" import { ListingsQueryParams } from "./dto/listings-query-params" import { filterTypeToFieldMap } from "./dto/filter-type-to-field-map" +import { ListingStatus } from "./types/listing-status-enum" +import { TranslationsService } from "../translations/services/translations.service" +import { UnitGroup } from "../units-summary/entities/unit-group.entity" +import { ListingMetadataDto } from "./dto/listings-metadata.dto" +import { UnitType } from "../unit-types/entities/unit-type.entity" +import { Program } from "../program/entities/program.entity" +import { ListingSeasonEnum } from "./types/listing-season-enum" +import { User } from "../auth/entities/user.entity" +import { REQUEST } from "@nestjs/core" +import { Request as ExpressRequest } from "express" @Injectable() export class ListingsService { constructor( @InjectRepository(Listing) private readonly listingRepository: Repository, @InjectRepository(AmiChart) private readonly amiChartsRepository: Repository, + @InjectRepository(UnitGroup) private readonly unitGroupRepository: Repository, + @InjectRepository(UnitType) private readonly unitTypeRepository: Repository, + @InjectRepository(Program) private readonly programRepository: Repository, + @InjectRepository(User) private readonly userRepository: Repository, + @Inject(REQUEST) private req: ExpressRequest, private readonly translationService: TranslationsService ) {} @@ -33,39 +52,27 @@ export class ListingsService { } public async list(params: ListingsQueryParams): Promise> { - const getOrderByCondition = (params: ListingsQueryParams): OrderByCondition => { - switch (params.orderBy) { - case OrderByFieldsEnum.mostRecentlyUpdated: - return { "listings.updated_at": "DESC" } - case OrderByFieldsEnum.applicationDates: - case undefined: - // Default to ordering by applicationDates (i.e. applicationDueDate - // and applicationOpenDate) if no orderBy param is specified. - return { - "listings.applicationDueDate": "ASC", - "listings.applicationOpenDate": "DESC", - } - default: - throw new HttpException( - `OrderBy parameter not recognized or not yet implemented.`, - HttpStatus.NOT_IMPLEMENTED - ) - } - } - // Inner query to get the sorted listing ids of the listings to display // TODO(avaleske): Only join the tables we need for the filters that are applied - const innerFilteredQuery = this.listingRepository + let innerFilteredQuery = this.listingRepository .createQueryBuilder("listings") .select("listings.id", "listings_id") .leftJoin("listings.property", "property") - .leftJoin("listings.leasingAgents", "leasingAgents") .leftJoin("property.buildingAddress", "buildingAddress") - .leftJoin("property.units", "units") - .leftJoin("units.unitType", "unitTypeRef") + .leftJoin("listings.reservedCommunityType", "reservedCommunityType") + .leftJoin("listings.features", "listing_features") + .leftJoin("listings.listingPrograms", "listing_programs") + .leftJoin("listing_programs.program", "programs") + .leftJoin("listings.unitGroups", "unitgroups") + .leftJoin("unitgroups.amiLevels", "amilevels") + .leftJoin("unitgroups.unitType", "unitTypes") .groupBy("listings.id") - .orderBy(getOrderByCondition(params)) + innerFilteredQuery = ListingsService.addOrderByToQb(innerFilteredQuery, params) + innerFilteredQuery = ListingsService.addSearchByListingNameCondition( + innerFilteredQuery, + params.search + ) if (params.filter) { addFilters, typeof filterTypeToFieldMap>( params.filter, @@ -87,21 +94,19 @@ export class ListingsService { } const view = getView(this.listingRepository.createQueryBuilder("listings"), params.view) - let listings = await view + let mainQuery = view .getViewQb() .andWhere("listings.id IN (" + innerFilteredQuery.getQuery() + ")") // Set the inner WHERE params on the outer query, as noted in the TypeORM docs. // (WHERE params are the values passed to andWhere() that TypeORM escapes // and substitues for the `:paramName` placeholders in the WHERE clause.) .setParameters(innerFilteredQuery.getParameters()) - .orderBy(getOrderByCondition(params)) - // Order by units.maxOccupancy is applied last so that it affects the order - // of units _within_ a listing, rather than the overall listing order) - .addOrderBy("units.max_occupancy", "ASC", "NULLS LAST") - .getMany() - // get summarized units from view - listings = view.mapUnitSummary(listings) + mainQuery = ListingsService.addOrderByToQb(mainQuery, params) + + let listings = await mainQuery.getMany() + + listings = await this.addUnitSummariesToListings(listings) // Set pagination info const itemsPerPage = paginate ? (params.limit as number) : listings.length const totalItems = paginate ? await innerFilteredQuery.getCount() : listings.length @@ -113,11 +118,6 @@ export class ListingsService { totalPages: Math.ceil(totalItems / itemsPerPage), // will be 1 if no pagination } - // TODO(https://github.com/CityOfDetroit/bloom/issues/135): Decide whether to remove jsonpath - if (params.jsonpath) { - listings = jp.query(listings, params.jsonpath) - } - // There is a bug in nestjs-typeorm-paginate's handling of complex, nested // queries (https://github.com/nestjsx/nestjs-typeorm-paginate/issues/6) so // we build the pagination metadata manually. Additional details are in @@ -138,13 +138,49 @@ export class ListingsService { return paginatedListings } - async create(listingDto: ListingCreateDto) { + private async addUnitSummariesToListings(listings: Listing[]) { + const res = await this.unitGroupRepository.find({ + cache: true, + where: { + listing: { + id: In(listings.map((listing) => listing.id)), + }, + }, + }) + + const unitGroupMap = res.reduce( + ( + obj: Record>, + current: UnitGroup + ): Record> => { + if (obj[current.listingId] !== undefined) { + obj[current.listingId].push(current) + } else { + obj[current.listingId] = [current] + } + + return obj + }, + {} + ) + + // using map with {...listing, unitSummaries} throws a type error + listings.forEach((listing) => { + listing.unitSummaries = summarizeUnits(unitGroupMap[listing.id], []) + }) + + return listings + } + + async create(listingDto: ListingCreateDto): Promise { const listing = this.listingRepository.create({ ...listingDto, + verifiedAt: listingDto.isVerified === true ? new Date() : null, + publishedAt: listingDto.status === ListingStatus.active ? new Date() : null, + closedAt: listingDto.status === ListingStatus.closed ? new Date() : null, property: plainToClass(PropertyCreateDto, listingDto), }) - const saveResult = await listing.save() - return saveResult + return await listing.save() } async update(listingDto: ListingUpdateDto) { @@ -155,13 +191,36 @@ export class ListingsService { if (!listing) { throw new NotFoundException() } + let availableUnits = 0 listingDto.units.forEach((unit) => { if (!unit.id) { delete unit.id } + if (unit.status === "available") { + availableUnits++ + } + }) + listingDto.unitGroups.forEach((summary) => { + if (!summary.id) { + delete summary.id + } }) + listingDto.unitsAvailable = availableUnits + let newVerifyDate = listingDto.isVerified === false ? null : listing.verifiedAt + if (listingDto.isVerified === true && !listing.verifiedAt) { + newVerifyDate = new Date() + } Object.assign(listing, { - ...plainToClass(Listing, listingDto, { excludeExtraneousValues: true }), + ...plainToClass(Listing, listingDto, { excludeExtraneousValues: false }), + publishedAt: + listing.status !== ListingStatus.active && listingDto.status === ListingStatus.active + ? new Date() + : listing.publishedAt, + closedAt: + listing.status !== ListingStatus.closed && listingDto.status === ListingStatus.closed + ? new Date() + : listing.closedAt, + verifiedAt: newVerifyDate, property: plainToClass( PropertyUpdateDto, { @@ -190,7 +249,7 @@ export class ListingsService { const result = await qb .where("listings.id = :id", { id: listingId }) .orderBy({ - "preferences.ordinal": "ASC", + "listingPreferences.ordinal": "ASC", }) .getOne() if (!result) { @@ -201,17 +260,216 @@ export class ListingsService { await this.translationService.translateListing(result, lang) } - await this.addUnitsSummarized(result) + await this.addUnitSummaries(result) return result } - private async addUnitsSummarized(listing: Listing) { - if (Array.isArray(listing.property.units) && listing.property.units.length > 0) { + async rawListWithFlagged() { + const userAccess = await this.userRepository + .createQueryBuilder("user") + .select("user.id") + .leftJoin("user.roles", "userRole") + .where("user.id = :id", { id: (this.req.user as User)?.id }) + .andWhere("userRole.is_admin = :is_admin", { is_admin: true }) + .getOne() + + if (!userAccess) { + throw new UnauthorizedException() + } + + // generated out list of permissioned listings + const permissionedListings = await this.listingRepository + .createQueryBuilder("listing") + .select("listing.id") + .getMany() + + // pulled out on the ids + const listingIds = permissionedListings.map((listing) => listing.id) + + // Building and excecuting query for listings csv + const listingsQb = getView( + this.listingRepository.createQueryBuilder("listing"), + "listingsExport" + ).getViewQb() + const listingData = await listingsQb + .where("listing.id IN (:...listingIds)", { listingIds }) + .getMany() + + // User data to determine listing access for csv + const userAccessData = await this.userRepository + .createQueryBuilder("user") + .select([ + "user.id", + "user.firstName", + "user.lastName", + "userRoles.isAdmin", + "userRoles.isPartner", + "leasingAgentInListings.id", + ]) + .leftJoin("user.leasingAgentInListings", "leasingAgentInListings") + .leftJoin("user.jurisdictions", "jurisdictions") + .leftJoin("user.roles", "userRoles") + .where("userRoles.is_partner = :is_partner", { is_partner: true }) + .getMany() + + // Building and excecuting query for units csv + const unitsQb = getView( + this.listingRepository.createQueryBuilder("listing"), + "unitsExport" + ).getViewQb() + + const unitData = await unitsQb.where("listing.id IN (:...listingIds)", { listingIds }).getMany() + + unitData.forEach((listing) => { + listing.unitSummaries = summarizeUnits(listing.unitGroups, []) + }) + + return { + unitData, + listingData, + userAccessData, + } + } + + private async addUnitSummaries(listing: Listing) { + if (Array.isArray(listing.unitGroups) && listing.unitGroups.length > 0) { + const amiChartIds = listing.unitGroups.reduce((acc: string[], curr: UnitGroup) => { + curr.amiLevels.forEach((level) => { + if (acc.includes(level.amiChartId) === false) { + acc.push(level.amiChartId) + } + }) + return acc + }, []) const amiCharts = await this.amiChartsRepository.find({ - where: { id: In(listing.property.units.map((unit) => unit.amiChartId)) }, + where: { id: In(amiChartIds) }, }) - listing.unitsSummarized = summarizeUnits(listing.property.units, amiCharts) + listing.unitSummaries = summarizeUnits(listing.unitGroups, amiCharts) } return listing } + + /** + * + * @param user + * @param listing + * @param action + * + * authz gaurd should already be used at this point, + * so we know the user has general permissions to do this action. + * We also have to check what the previous status was. + * A partner can save a listing as any status as long as the previous status was active. Otherwise they can only save as pending + */ + private userCanUpdateOrThrow( + user, + listing: ListingUpdateDto, + previousListingStatus: ListingStatus + ): boolean { + const { isAdmin } = user.roles + let canUpdate = false + + if (isAdmin) { + canUpdate = true + } else if (previousListingStatus !== ListingStatus.pending) { + canUpdate = true + } else if (listing.status === ListingStatus.pending) { + canUpdate = true + } + + if (!canUpdate) { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN) + } + + return canUpdate + } + + public async getMetadata(): Promise { + const unitTypes = await this.unitTypeRepository + .createQueryBuilder("unitTypes") + .select("unitTypes.id") + .addSelect("unitTypes.name") + .addSelect("unitTypes.numBedrooms") + .orderBy("unitTypes.numBedrooms") + .getMany() + + const programs = await this.programRepository + .createQueryBuilder("programs") + .select("programs.id") + .addSelect("programs.title") + .orderBy("programs.title") + .getMany() + + return { programs, unitTypes } + } + + private static addSearchByListingNameCondition( + qb: SelectQueryBuilder, + searchName?: string + ) { + if (searchName) { + qb.andWhere(`${qb.alias}.name ILIKE :search`, { search: `%${searchName}%` }) + } + return qb + } + + private static addOrderByToQb(qb: SelectQueryBuilder, params: ListingsQueryParams) { + switch (params.orderBy) { + case OrderByFieldsEnum.mostRecentlyUpdated: + qb.orderBy({ "listings.updated_at": params.orderDir ?? "DESC" }) + break + case OrderByFieldsEnum.mostRecentlyClosed: + qb.orderBy({ + "listings.closedAt": { order: params.orderDir ?? "DESC", nulls: "NULLS LAST" }, + "listings.publishedAt": { order: "DESC", nulls: "NULLS LAST" }, + }) + break + case OrderByFieldsEnum.applicationDates: + qb.orderBy({ + "listings.applicationDueDate": params.orderDir ?? "ASC", + }) + break + case OrderByFieldsEnum.comingSoon: + qb.orderBy("listings.marketingType", "DESC", "NULLS LAST") + qb.addOrderBy(`to_char(listings.marketingDate, 'YYYY')`, "ASC") + qb.addOrderBy( + `CASE listings.marketingSeason WHEN '${ListingSeasonEnum.Spring}' THEN 1 WHEN '${ListingSeasonEnum.Summer}' THEN 2 WHEN '${ListingSeasonEnum.Fall}' THEN 3 WHEN '${ListingSeasonEnum.Winter}' THEN 4 END`, + "ASC" + ) + qb.addOrderBy("listings.updatedAt", "DESC") + break + case OrderByFieldsEnum.status: + qb.orderBy({ + "listings.status": params.orderDir ?? "ASC", + "listings.name": "ASC", + }) + break + case OrderByFieldsEnum.verified: + qb.orderBy({ + "listings.isVerified": params.orderDir ?? "ASC", + "listings.name": "ASC", + }) + break + case OrderByFieldsEnum.updatedAt: + qb.orderBy({ + "listings.updatedAt": params.orderDir ?? "ASC", + "listings.name": "ASC", + }) + break + case OrderByFieldsEnum.name: + case undefined: + // Default to ordering by applicationDates (i.e. applicationDueDate + // and applicationOpenDate) if no orderBy param is specified. + qb.orderBy({ + "listings.name": params.orderDir ?? "ASC", + }) + break + default: + throw new HttpException( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `OrderBy parameter (${params.orderBy}) not recognized or not yet implemented.`, + HttpStatus.NOT_IMPLEMENTED + ) + } + return qb + } } diff --git a/backend/core/src/listings/tests/listings.service.spec.ts b/backend/core/src/listings/tests/listings.service.spec.ts new file mode 100644 index 0000000000..562a815ee4 --- /dev/null +++ b/backend/core/src/listings/tests/listings.service.spec.ts @@ -0,0 +1,488 @@ +import { Test, TestingModule } from "@nestjs/testing" +import { getRepositoryToken } from "@nestjs/typeorm" +import { HttpException, HttpStatus } from "@nestjs/common" +import { ListingStatus } from "../types/listing-status-enum" +import { ListingsService } from "../listings.service" +import { Listing } from "../entities/listing.entity" +import { TranslationsService } from "../../translations/services/translations.service" +import { AmiChart } from "../../ami-charts/entities/ami-chart.entity" +import { ListingsQueryParams } from "../dto/listings-query-params" +import { Compare } from "../../shared/dto/filter.dto" +import { ListingFilterParams } from "../dto/listing-filter-params" +import { OrderByFieldsEnum } from "../types/listing-orderby-enum" +import { ContextIdFactory } from "@nestjs/core" +import { UnitGroup } from "../../units-summary/entities/unit-group.entity" +import { UnitType } from "../../unit-types/entities/unit-type.entity" +import { Program } from "../../program/entities/program.entity" +import { User } from "../../../src/auth/entities/user.entity" + +// Cypress brings in Chai types for the global expect, but we want to use jest +// expect here so we need to re-declare it. +// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 +declare const expect: jest.Expect + +let service: ListingsService +const mockListings = [ + { + id: "asdf1", + property: { id: "test-property1", units: [] }, + preferences: [], + status: "closed", + unitSummaries: { byUnitTypeAndRent: [] }, + }, + { + id: "asdf2", + property: { id: "test-property2", units: [] }, + preferences: [], + status: "closed", + unitSummaries: { byUnitTypeAndRent: [] }, + }, + { + id: "asdf3", + property: { id: "test-property3", units: [] }, + preferences: [], + status: "closed", + unitSummaries: { byUnitTypeAndRent: [] }, + }, + { + id: "asdf4", + property: { id: "test-property4", units: [] }, + preferences: [], + status: "closed", + unitSummaries: { byUnitTypeAndRent: [] }, + }, + { + id: "asdf5", + property: { id: "test-property5", units: [] }, + preferences: [], + status: "closed", + unitSummaries: { byUnitTypeAndRent: [] }, + }, + { + id: "asdf6", + property: { id: "test-property6", units: [] }, + preferences: [], + status: "closed", + unitSummaries: { byUnitTypeAndRent: [] }, + }, + { + id: "asdf7", + property: { id: "test-property7", units: [] }, + preferences: [], + status: "closed", + unitSummaries: { byUnitTypeAndRent: [] }, + }, +] +const mockFilteredListings = mockListings.slice(0, 2) +const mockInnerQueryBuilder = { + select: jest.fn().mockReturnThis(), + leftJoin: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), + addGroupBy: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getParameters: jest.fn().mockReturnValue({ param1: "param1value" }), + getQuery: jest.fn().mockReturnValue("innerQuery"), + getCount: jest.fn().mockReturnValue(7), +} +const mockQueryBuilder = { + select: jest.fn().mockReturnThis(), + leftJoin: jest.fn().mockReturnThis(), + leftJoinAndSelect: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + setParameters: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + getMany: jest.fn().mockReturnValue(mockListings), + getOne: jest.fn(), +} +const mockListingsRepo = { + createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder), + count: jest.fn().mockReturnValue(100), + create: jest.fn(), + save: jest.fn(), +} + +describe("ListingsService", () => { + beforeEach(async () => { + process.env.APP_SECRET = "SECRET" + process.env.EMAIL_API_KEY = "SG.KEY" + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ListingsService, + { + provide: getRepositoryToken(Listing), + useValue: mockListingsRepo, + }, + { + provide: getRepositoryToken(AmiChart), + useValue: jest.fn(), + }, + { + provide: getRepositoryToken(UnitGroup), + useValue: { + find: jest.fn(() => { + return [] + }), + }, + }, + { + provide: getRepositoryToken(UnitType), + useValue: jest.fn(), + }, + { + provide: getRepositoryToken(Program), + useValue: jest.fn(), + }, + { + provide: TranslationsService, + useValue: { translateListing: jest.fn() }, + }, + { provide: getRepositoryToken(User), useValue: jest.fn() }, + ], + }).compile() + + const contextId = ContextIdFactory.create() + jest.spyOn(ContextIdFactory, "getByRequest").mockImplementation(() => contextId) + + service = await module.resolve(ListingsService, contextId) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it("should be defined", () => { + expect(service).toBeDefined() + }) + + describe("getListingsList", () => { + it("should not add a WHERE clause if no filters are applied", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + + const listings = await service.list({}) + + expect(listings.items).toEqual(mockListings) + expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledTimes(0) + }) + + it("should add a WHERE clause if the status filter is applied", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + const expectedStatus = ListingStatus.active + + const queryParams: ListingsQueryParams = { + filter: [ + { + $comparison: Compare["="], + status: expectedStatus, + }, + ], + } + + const listings = await service.list(queryParams) + + expect(listings.items).toEqual(mockListings) + expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledWith( + "(LOWER(CAST(listings.status as text)) = LOWER(:status_0))", + { + status_0: expectedStatus, + } + ) + }) + + it("should support filters with comma-separated arrays", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + const expectedRegionString = "Greater Downtown,Eastside," // intentional extra and trailing commas for test + // lowercased, trimmed spaces, filtered empty + const expectedRegionArray = ["Greater Downtown", "Eastside"] + + const queryParams: ListingsQueryParams = { + filter: [ + { + $comparison: Compare["IN"], + region: expectedRegionString, + }, + ], + } + + const listings = await service.list(queryParams) + + expect(listings.items).toEqual(mockListings) + expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledWith( + "property.region IN (:...region) ", + { + region: expectedRegionArray, + } + ) + }) + + it("should support filtering on neighborhoods", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + const neighborhoodString = "Greater Downtown,Eastside," // intentional extra and trailing commas for test + // lowercased, trimmed spaces, filtered empty + const expectedNeighborhoodArray = ["Greater Downtown", "Eastside"] + + const queryParams: ListingsQueryParams = { + filter: [ + { + $comparison: Compare["IN"], + region: neighborhoodString, + }, + ], + } + + const listings = await service.list(queryParams) + + expect(listings.items).toEqual(mockListings) + expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledWith( + "property.region IN (:...region) ", + { + region: expectedNeighborhoodArray, + } + ) + }) + + it("should support filtering on features", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + + const queryParams: ListingsQueryParams = { + filter: [ + { + $comparison: Compare["IN"], + elevator: true, + }, + ], + } + + const listings = await service.list(queryParams) + + expect(listings.items).toEqual(mockListings) + expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledWith( + "(LOWER(CAST(listing_features.elevator as text)) IN (:...elevator_0))", + { + elevator_0: ["true"], + } + ) + }) + + it("should include listings with missing data if $include_nulls is true", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + const queryParams: ListingsQueryParams = { + filter: [ + { + $comparison: Compare["="], + name: "minRent", + $include_nulls: true, + }, + ], + } + + const listings = await service.list(queryParams) + + expect(listings.items).toEqual(mockListings) + expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledWith( + "(LOWER(CAST(listings.name as text)) = LOWER(:name_0) OR listings.name IS NULL)", + { + name_0: "minRent", + } + ) + }) + + it("should filter by availability", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + const queryParams: ListingsQueryParams = { + filter: [ + { + $comparison: Compare["="], + availability: "openWaitlist", + }, + ], + } + + const listings = await service.list(queryParams) + + expect(listings.items).toEqual(mockListings) + expect(mockInnerQueryBuilder.andWhere).toHaveBeenCalledWith( + "(coalesce(unitgroups.open_waitlist, false) = :openWaitlist)", + { + openWaitlist: true, + } + ) + }) + + it("should throw an exception if an unsupported filter is used", async () => { + mockListingsRepo.createQueryBuilder.mockReturnValueOnce(mockInnerQueryBuilder) + + const queryParams: ListingsQueryParams = { + filter: [ + { + $comparison: Compare["="], + otherField: "otherField", + // The querystring can contain unknown fields that aren't on the + // ListingFilterParams type, so we force it to the type for testing. + } as ListingFilterParams, + ], + } + + await expect(service.list(queryParams)).rejects.toThrow( + new HttpException("Filter Not Implemented", HttpStatus.NOT_IMPLEMENTED) + ) + }) + + //TODO(avaleske): A lot of these tests should be moved to a spec file specific to the filters code. + it("should throw an exception if an unsupported comparison is used", async () => { + mockListingsRepo.createQueryBuilder.mockReturnValueOnce(mockInnerQueryBuilder) + + const queryParams: ListingsQueryParams = { + filter: [ + { + // The value of the filter[$comparison] query param is not validated, + // and the type system trusts that whatever is provided is correct, + // so we force it to an invalid type for testing. + $comparison: "); DROP TABLE Students;" as Compare, + name: "test name", + } as ListingFilterParams, + ], + } + + await expect(service.list(queryParams)).rejects.toThrow( + new HttpException("Comparison Not Implemented", HttpStatus.NOT_IMPLEMENTED) + ) + }) + + it("should not call limit() and offset() if pagination params are not specified", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + + // Empty params (no pagination) -> no limit/offset + const params = {} + const listings = await service.list(params) + + expect(listings.items).toEqual(mockListings) + expect(mockInnerQueryBuilder.limit).toHaveBeenCalledTimes(0) + expect(mockInnerQueryBuilder.offset).toHaveBeenCalledTimes(0) + }) + + it("should not call limit() and offset() if incomplete pagination params are specified", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + + // Invalid pagination params (page specified, but not limit) -> no limit/offset + const params = { page: 3 } + const listings = await service.list(params) + + expect(listings.items).toEqual(mockListings) + expect(mockInnerQueryBuilder.limit).toHaveBeenCalledTimes(0) + expect(mockInnerQueryBuilder.offset).toHaveBeenCalledTimes(0) + expect(listings.meta).toEqual({ + currentPage: 1, + itemCount: mockListings.length, + itemsPerPage: mockListings.length, + totalItems: mockListings.length, + totalPages: 1, + }) + }) + + it("should not call limit() and offset() if invalid pagination params are specified", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + + // Invalid pagination params (page specified, but not limit) -> no limit/offset + const params = { page: ("hello" as unknown) as number } // force the type for testing + const listings = await service.list(params) + + expect(listings.items).toEqual(mockListings) + expect(mockInnerQueryBuilder.limit).toHaveBeenCalledTimes(0) + expect(mockInnerQueryBuilder.offset).toHaveBeenCalledTimes(0) + expect(listings.meta).toEqual({ + currentPage: 1, + itemCount: mockListings.length, + itemsPerPage: mockListings.length, + totalItems: mockListings.length, + totalPages: 1, + }) + }) + + it("should call limit() and offset() if pagination params are specified", async () => { + mockQueryBuilder.getMany.mockReturnValueOnce(mockFilteredListings) + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + + // Valid pagination params -> offset and limit called appropriately + const params = { page: 3, limit: 2 } + const listings = await service.list(params) + + expect(listings.items).toEqual(mockFilteredListings) + expect(mockInnerQueryBuilder.limit).toHaveBeenCalledWith(2) + expect(mockInnerQueryBuilder.offset).toHaveBeenCalledWith(4) + expect(mockInnerQueryBuilder.getCount).toHaveBeenCalledTimes(1) + expect(listings.meta).toEqual({ + currentPage: 3, + itemCount: 2, + itemsPerPage: 2, + totalItems: mockListings.length, + totalPages: 4, + }) + }) + }) + + describe("ListingsService.list sorting", () => { + it("defaults to ordering by name when no orderBy param is set", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + + await service.list({}) + + const expectedOrderByArgument = { + "listings.name": "ASC", + } + + // The inner query must be ordered so that the ordering applies across all pages (if pagination is requested) + expect(mockInnerQueryBuilder.orderBy).toHaveBeenCalledTimes(1) + expect(mockInnerQueryBuilder.orderBy).toHaveBeenCalledWith(expectedOrderByArgument) + + // The full query must be ordered so that the ordering is applied within a page (if pagination is requested) + expect(mockQueryBuilder.orderBy).toHaveBeenCalledTimes(1) + expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith(expectedOrderByArgument) + }) + + it("orders by the orderBy param (when set)", async () => { + mockListingsRepo.createQueryBuilder + .mockReturnValueOnce(mockInnerQueryBuilder) + .mockReturnValueOnce(mockQueryBuilder) + + await service.list({ orderBy: OrderByFieldsEnum.mostRecentlyUpdated }) + + const expectedOrderByArgument = { "listings.updated_at": "DESC" } + + expect(mockInnerQueryBuilder.orderBy).toHaveBeenCalledTimes(1) + expect(mockInnerQueryBuilder.orderBy).toHaveBeenCalledWith(expectedOrderByArgument) + + expect(mockQueryBuilder.orderBy).toHaveBeenCalledTimes(1) + expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith(expectedOrderByArgument) + }) + }) +}) diff --git a/backend/core/src/listings/types/home-type-enum.ts b/backend/core/src/listings/types/home-type-enum.ts new file mode 100644 index 0000000000..2173a041b6 --- /dev/null +++ b/backend/core/src/listings/types/home-type-enum.ts @@ -0,0 +1,6 @@ +export enum HomeTypeEnum { + apartment = "apartment", + duplex = "duplex", + house = "house", + townhome = "townhome", +} diff --git a/backend/core/src/listings/types/listing-application-address-type.ts b/backend/core/src/listings/types/listing-application-address-type.ts index 803c9d882c..72a140f4fc 100644 --- a/backend/core/src/listings/types/listing-application-address-type.ts +++ b/backend/core/src/listings/types/listing-application-address-type.ts @@ -1,4 +1,3 @@ export enum ListingApplicationAddressType { leasingAgent = "leasingAgent", - mailingAddress = "mailingAddress", } diff --git a/backend/core/src/listings/types/listing-filter-keys-enum.ts b/backend/core/src/listings/types/listing-filter-keys-enum.ts index aa2baf6655..7e64355d1d 100644 --- a/backend/core/src/listings/types/listing-filter-keys-enum.ts +++ b/backend/core/src/listings/types/listing-filter-keys-enum.ts @@ -1,16 +1,55 @@ // The names of supported filters on /listings export enum ListingFilterKeys { + id = "id", status = "status", name = "name", - neighborhood = "neighborhood", + isVerified = "isVerified", bedrooms = "bedrooms", zipcode = "zipcode", availability = "availability", - seniorHousing = "seniorHousing", + program = "program", minRent = "minRent", maxRent = "maxRent", minAmiPercentage = "minAmiPercentage", + elevator = "elevator", + wheelchairRamp = "wheelchairRamp", + serviceAnimalsAllowed = "serviceAnimalsAllowed", + accessibleParking = "accessibleParking", + parkingOnSite = "parkingOnSite", + inUnitWasherDryer = "inUnitWasherDryer", + laundryInBuilding = "laundryInBuilding", + barrierFreeEntrance = "barrierFreeEntrance", + rollInShower = "rollInShower", + grabBars = "grabBars", + heatingInUnit = "heatingInUnit", + acInUnit = "acInUnit", + favorited = "favorited", + marketingType = "marketingType", + hearing = "hearing", + mobility = "mobility", + visual = "visual", + vacantUnits = "vacantUnits", + openWaitlist = "openWaitlist", + closedWaitlist = "closedWaitlist", + Families = "Families", + ResidentswithDisabilities = "ResidentswithDisabilities", + Seniors55 = "Seniors55", + Seniors62 = "Seniors62", + SupportiveHousingfortheHomeless = "SupportiveHousingfortheHomeless", + Veterans = "Veterans", + region = "region", + bedRoomSize = "bedRoomSize", + communityPrograms = "communityPrograms", + accessibility = "accessibility", leasingAgents = "leasingAgents", + jurisdiction = "jurisdiction", + barrierFreeUnitEntrance = "barrierFreeUnitEntrance", + loweredLightSwitch = "loweredLightSwitch", + barrierFreeBathroom = "barrierFreeBathroom", + wideDoorways = "wideDoorways", + loweredCabinets = "loweredCabinets", + section8Acceptance = "section8Acceptance", + homeType = "homeType", } export enum AvailabilityFilterEnum { diff --git a/backend/core/src/listings/types/listing-marketing-type-enum.ts b/backend/core/src/listings/types/listing-marketing-type-enum.ts new file mode 100644 index 0000000000..5b8864ebda --- /dev/null +++ b/backend/core/src/listings/types/listing-marketing-type-enum.ts @@ -0,0 +1,4 @@ +export enum ListingMarketingTypeEnum { + Marketing = "marketing", + ComingSoon = "comingSoon", +} diff --git a/backend/core/src/listings/types/listing-orderby-enum.ts b/backend/core/src/listings/types/listing-orderby-enum.ts index 217bce8275..c28db59309 100644 --- a/backend/core/src/listings/types/listing-orderby-enum.ts +++ b/backend/core/src/listings/types/listing-orderby-enum.ts @@ -1,4 +1,10 @@ export enum OrderByFieldsEnum { mostRecentlyUpdated = "mostRecentlyUpdated", applicationDates = "applicationDates", + mostRecentlyClosed = "mostRecentlyClosed", + comingSoon = "comingSoon", + name = "name", + status = "status", + verified = "verified", + updatedAt = "updatedAt", } diff --git a/backend/core/src/listings/types/listing-season-enum.ts b/backend/core/src/listings/types/listing-season-enum.ts new file mode 100644 index 0000000000..5224c6b114 --- /dev/null +++ b/backend/core/src/listings/types/listing-season-enum.ts @@ -0,0 +1,6 @@ +export enum ListingSeasonEnum { + Spring = "spring", + Summer = "summer", + Fall = "fall", + Winter = "winter", +} diff --git a/backend/core/src/listings/validation-pipes/listing-create-validation-pipe.ts b/backend/core/src/listings/validation-pipes/listing-create-validation-pipe.ts index 2b593adc0c..0da9d38499 100644 --- a/backend/core/src/listings/validation-pipes/listing-create-validation-pipe.ts +++ b/backend/core/src/listings/validation-pipes/listing-create-validation-pipe.ts @@ -1,13 +1,12 @@ import { ArgumentMetadata, ValidationPipe } from "@nestjs/common" import { ListingStatus } from "../types/listing-status-enum" import { ListingCreateDto } from "../dto/listing-create.dto" -import { ListingPublishedCreateDto } from "../dto/listing-published-create.dto" export class ListingCreateValidationPipe extends ValidationPipe { statusToListingValidationModelMap: Record = { [ListingStatus.closed]: ListingCreateDto, [ListingStatus.pending]: ListingCreateDto, - [ListingStatus.active]: ListingPublishedCreateDto, + [ListingStatus.active]: ListingCreateDto, } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/backend/core/src/listings/validation-pipes/listing-update-validation-pipe.ts b/backend/core/src/listings/validation-pipes/listing-update-validation-pipe.ts index 9fc3d42880..58cddf2727 100644 --- a/backend/core/src/listings/validation-pipes/listing-update-validation-pipe.ts +++ b/backend/core/src/listings/validation-pipes/listing-update-validation-pipe.ts @@ -1,13 +1,12 @@ import { ArgumentMetadata, ValidationPipe } from "@nestjs/common" import { ListingStatus } from "../types/listing-status-enum" import { ListingUpdateDto } from "../dto/listing-update.dto" -import { ListingPublishedUpdateDto } from "../dto/listing-published-update.dto" export class ListingUpdateValidationPipe extends ValidationPipe { statusToListingValidationModelMap: Record = { [ListingStatus.closed]: ListingUpdateDto, [ListingStatus.pending]: ListingUpdateDto, - [ListingStatus.active]: ListingPublishedUpdateDto, + [ListingStatus.active]: ListingUpdateDto, } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/backend/core/src/listings/validators/order-query-param-validator.ts b/backend/core/src/listings/validators/order-query-param-validator.ts new file mode 100644 index 0000000000..e1f77f665c --- /dev/null +++ b/backend/core/src/listings/validators/order-query-param-validator.ts @@ -0,0 +1,26 @@ +import { + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, +} from "class-validator" +import { ListingsQueryParams } from "../dto/listings-query-params" + +@ValidatorConstraint({ name: "orderDir", async: false }) +export class OrderQueryParamValidator implements ValidatorConstraintInterface { + validate(order: Array | undefined, args: ValidationArguments) { + if (args.property === "orderDir") { + return Array.isArray(order) + ? (args.object as ListingsQueryParams).orderBy?.length === order.length + : false + } else if (args.property === "orderBy") { + return Array.isArray(order) + ? (args.object as ListingsQueryParams).orderDir?.length === order.length + : false + } + return false + } + + defaultMessage(args: ValidationArguments) { + return "order array length must be equal to orderBy array length" + } +} diff --git a/backend/core/src/listings/views/config.ts b/backend/core/src/listings/views/config.ts index fc7d5b31d0..4a35b76ae8 100644 --- a/backend/core/src/listings/views/config.ts +++ b/backend/core/src/listings/views/config.ts @@ -1,14 +1,5 @@ import { Views } from "./types" - -function getBaseAddressSelect(schemas: string[]): string[] { - const fields = ["city", "state", "street", "street2", "zipCode", "latitude", "longitude"] - - let select: string[] = [] - schemas.forEach((schema) => { - select = select.concat(fields.map((field) => `${schema}.${field}`)) - }) - return select -} +import { getBaseAddressSelect } from "../../views/base.view" const views: Views = { base: { @@ -16,47 +7,71 @@ const views: Views = { "listings.id", "listings.name", "listings.applicationDueDate", - "listings.applicationDueTime", "listings.applicationOpenDate", + "listings.marketingType", + "listings.marketingDate", + "listings.marketingSeason", "listings.reviewOrderType", "listings.status", - "listings.waitlistMaxSize", - "listings.waitlistCurrentSize", "listings.assets", - "image.id", - "image.fileId", - "image.label", + "listings.isVerified", + "listings.section8Acceptance", + "listings.homeType", "jurisdiction.id", "jurisdiction.name", "reservedCommunityType.id", "reservedCommunityType.name", "property.id", - "property.unitsAvailable", ...getBaseAddressSelect(["buildingAddress"]), - "units.id", - "units.floor", - "units.minOccupancy", - "units.maxOccupancy", - "units.monthlyIncomeMin", - "units.monthlyRent", - "units.monthlyRentAsPercentOfIncome", - "units.sqFeet", - "units.status", - "amiChartOverride.id", - "amiChartOverride.items", - "unitType.id", - "unitType.name", + "listingImages.ordinal", + "listingImagesImage.id", + "listingImagesImage.fileId", + "listingImagesImage.label", + "features.id", + "features.elevator", + "features.wheelchairRamp", + "features.serviceAnimalsAllowed", + "features.accessibleParking", + "features.parkingOnSite", + "features.inUnitWasherDryer", + "features.barrierFreeEntrance", + "features.rollInShower", + "features.grabBars", + "features.heatingInUnit", + "features.acInUnit", + "features.laundryInBuilding", + "features.barrierFreeUnitEntrance", + "features.loweredLightSwitch", + "features.barrierFreeBathroom", + "features.wideDoorways", + "features.loweredCabinets", + "utilities.id", + "utilities.water", + "utilities.gas", + "utilities.trash", + "utilities.sewer", + "utilities.electricity", + "utilities.cable", + "utilities.phone", + "utilities.internet", + "listingPrograms.ordinal", + "listingsProgramsProgram.id", + "listingsProgramsProgram.title", + "summaryUnitType.numBedrooms", ], leftJoins: [ { join: "listings.jurisdiction", alias: "jurisdiction" }, - { join: "listings.image", alias: "image" }, { join: "listings.property", alias: "property" }, { join: "property.buildingAddress", alias: "buildingAddress" }, - { join: "property.units", alias: "units" }, - { join: "units.unitType", alias: "unitType" }, - { join: "units.amiChartOverride", alias: "amiChartOverride" }, { join: "listings.reservedCommunityType", alias: "reservedCommunityType" }, - { join: "listings.preferences", alias: "preferences" }, + { join: "listings.images", alias: "listingImages" }, + { join: "listingImages.image", alias: "listingImagesImage" }, + { join: "listings.features", alias: "features" }, + { join: "listings.utilities", alias: "utilities" }, + { join: "listings.listingPrograms", alias: "listingPrograms" }, + { join: "listingPrograms.program", alias: "listingsProgramsProgram" }, + { join: "listings.unitGroups", alias: "unitGroups" }, + { join: "unitGroups.unitType", alias: "summaryUnitType" }, ], }, } @@ -65,14 +80,16 @@ views.partnerList = { select: [ "listings.id", "listings.name", - "listings.applicationDueDate", - "listings.applicationDueTime", + "property.id", + ...getBaseAddressSelect(["buildingAddress"]), "listings.status", - "listings.waitlistMaxSize", - "listings.waitlistCurrentSize", - "property.unitsAvailable", + "listings.isVerified", + "listings.updatedAt", + ], + leftJoins: [ + { join: "listings.property", alias: "property" }, + { join: "property.buildingAddress", alias: "buildingAddress" }, ], - leftJoins: [{ join: "listings.property", alias: "property" }], } views.detail = { @@ -85,6 +102,7 @@ views.detail = { "listings.applicationPickUpAddressType", "listings.applicationDropOffAddressOfficeHours", "listings.applicationDropOffAddressType", + "listings.applicationMailingAddressType", "listings.buildingSelectionCriteria", "listings.costsNotIncluded", "listings.creditHistory", @@ -112,6 +130,8 @@ views.detail = { "listings.isWaitlistOpen", "listings.waitlistOpenSpots", "listings.customMapPin", + "listings.features", + "listings.utilities", "buildingSelectionCriteriaFile.id", "buildingSelectionCriteriaFile.fileId", "buildingSelectionCriteriaFile.label", @@ -140,21 +160,20 @@ views.detail = { "result.fileId", "result.label", ...getBaseAddressSelect([ - "applicationAddress", "leasingAgentAddress", "applicationPickUpAddress", "applicationMailingAddress", + "applicationDropOffAddress", ]), "leasingAgents.firstName", "leasingAgents.lastName", "leasingAgents.email", - "preferences.title", - "preferences.subtitle", - "preferences.description", - "preferences.ordinal", - "preferences.links", - "preferences.formMetadata", - "preferences.page", + "listingPreferencesPreference.title", + "listingPreferencesPreference.subtitle", + "listingPreferencesPreference.description", + "listingPreferencesPreference.ordinal", + "listingPreferencesPreference.links", + "listingPreferencesPreference.formMetadata", ], leftJoins: [ ...views.base.leftJoins, @@ -165,7 +184,6 @@ views.detail = { { join: "listings.events", alias: "listingEvents" }, { join: "listingEvents.file", alias: "listingEventFile" }, { join: "listings.result", alias: "result" }, - { join: "listings.applicationAddress", alias: "applicationAddress" }, { join: "listings.leasingAgentAddress", alias: "leasingAgentAddress" }, { join: "listings.applicationPickUpAddress", alias: "applicationPickUpAddress" }, { join: "listings.applicationMailingAddress", alias: "applicationMailingAddress" }, @@ -179,18 +197,17 @@ views.full = { ["listings.applicationMethods", "applicationMethods"], ["applicationMethods.paperApplications", "paperApplications"], ["paperApplications.file", "paperApplicationFile"], - ["listings.image", "image"], ["listings.buildingSelectionCriteriaFile", "buildingSelectionCriteriaFile"], ["listings.events", "listingEvents"], ["listingEvents.file", "listingEventFile"], ["listings.result", "result"], - ["listings.applicationAddress", "applicationAddress"], ["listings.leasingAgentAddress", "leasingAgentAddress"], ["listings.applicationPickUpAddress", "applicationPickUpAddress"], ["listings.applicationMailingAddress", "applicationMailingAddress"], ["listings.applicationDropOffAddress", "applicationDropOffAddress"], ["listings.leasingAgents", "leasingAgents"], - ["listings.preferences", "preferences"], + ["listings.listingPreferences", "listingPreferences"], + ["listingPreferences.preference", "listingPreferencesPreference"], ["listings.property", "property"], ["property.buildingAddress", "buildingAddress"], ["property.units", "units"], @@ -201,6 +218,240 @@ views.full = { ["units.amiChart", "amiChart"], ["listings.jurisdiction", "jurisdiction"], ["listings.reservedCommunityType", "reservedCommunityType"], + ["listings.unitGroups", "unitGroups"], + ["unitGroups.unitType", "summaryUnitType"], + ["unitGroups.priorityType", "summaryPriorityType"], + ["unitGroups.amiLevels", "unitGroupsAmiLevels"], + ["listings.features", "listing_features"], + ["listings.neighborhoodAmenities", "listing_neighborhood_amenities"], + ["listings.utilities", "listing_utilities"], + ["listings.listingPrograms", "listingPrograms"], + ["listingPrograms.program", "listingProgramsProgram"], + ["listings.images", "listingImages"], + ["listingImages.image", "listingImagesImage"], + ], +} + +views.publicListings = { + select: [ + "listings.id", + "listings.name", + "listings.marketingType", + "listings.marketingDate", + "listings.marketingSeason", + "listings.isVerified", + "listings.section8Acceptance", + "listings.homeType", + "property.id", + ...getBaseAddressSelect(["buildingAddress"]), + "listingImages.ordinal", + "listingImagesImage.id", + "listingImagesImage.fileId", + "listingImagesImage.label", + "listingPrograms.ordinal", + "listingsProgramsProgram.id", + "listingsProgramsProgram.title", + "features.id", + "features.elevator", + "features.wheelchairRamp", + "features.serviceAnimalsAllowed", + "features.accessibleParking", + "features.parkingOnSite", + "features.inUnitWasherDryer", + "features.barrierFreeEntrance", + "features.rollInShower", + "features.grabBars", + "features.heatingInUnit", + "features.acInUnit", + "features.laundryInBuilding", + "utilities.id", + "utilities.water", + "utilities.gas", + "utilities.trash", + "utilities.sewer", + "utilities.electricity", + "utilities.cable", + "utilities.phone", + "utilities.internet", + ], + leftJoins: [ + { join: "listings.property", alias: "property" }, + { join: "property.buildingAddress", alias: "buildingAddress" }, + { join: "listings.images", alias: "listingImages" }, + { join: "listingImages.image", alias: "listingImagesImage" }, + { join: "listings.listingPrograms", alias: "listingPrograms" }, + { join: "listingPrograms.program", alias: "listingsProgramsProgram" }, + { join: "listings.features", alias: "features" }, + { join: "listings.utilities", alias: "utilities" }, + ], +} + +views.listingsExport = { + select: [ + "listing.id", + "listing.createdAt", + "listing.status", + "listing.publishedAt", + "listing.isVerified", + "listing.verifiedAt", + "listing.updatedAt", + "listing.name", + "property.developer", + "reservedCommunityType.id", + "reservedCommunityType.name", + "property.id", + ...getBaseAddressSelect(["buildingAddress"]), + "property.yearBuilt", + "property.neighborhood", + "property.region", + "listing.homeType", + "listing.section8Acceptance", + "unitGroups.totalCount", + "listingPrograms.ordinal", + "listingProgramsProgram.id", + "listingProgramsProgram.title", + "listing.applicationFee", + "listing.depositMin", + "listing.depositMax", + "listing.depositHelperText", + "listing.costsNotIncluded", + "utilities.id", + "utilities.water", + "utilities.gas", + "utilities.trash", + "utilities.sewer", + "utilities.electricity", + "utilities.cable", + "utilities.phone", + "utilities.internet", + "property.amenities", + "property.accessibility", + "property.unitAmenities", + "property.smokingPolicy", + "property.petPolicy", + "property.servicesOffered", + "features.id", + "features.elevator", + "features.wheelchairRamp", + "features.serviceAnimalsAllowed", + "features.accessibleParking", + "features.parkingOnSite", + "features.inUnitWasherDryer", + "features.laundryInBuilding", + "features.barrierFreeEntrance", + "features.rollInShower", + "features.grabBars", + "features.heatingInUnit", + "features.acInUnit", + "features.hearing", + "features.visual", + "features.mobility", + "features.loweredLightSwitch", + "features.barrierFreeBathroom", + "features.wideDoorways", + "features.loweredCabinets", + "neighborhoodAmenities.groceryStores", + "neighborhoodAmenities.pharmacies", + "neighborhoodAmenities.healthCareResources", + "neighborhoodAmenities.parksAndCommunityCenters", + "neighborhoodAmenities.schools", + "neighborhoodAmenities.publicTransportation", + "listing.creditHistory", + "listing.rentalHistory", + "listing.criminalBackground", + "listing.buildingSelectionCriteria", + "buildingSelectionCriteriaFile.id", + "buildingSelectionCriteriaFile.fileId", + "listing.requiredDocuments", + "listing.programRules", + "listing.specialNotes", + "listing.reviewOrderType", + "listingEvents.startTime", + "listingEvents.endTime", + "listingEvents.note", + "listing.applicationDueDate", + "listing.isWaitlistOpen", + "listing.waitlistMaxSize", + "listing.waitlistCurrentSize", + "listing.waitlistOpenSpots", + "listing.marketingType", + "listing.marketingDate", + "listing.marketingSeason", + "listing.managementCompany", + "listing.managementWebsite", + "listing.leasingAgentEmail", + "listing.leasingAgentName", + "listing.leasingAgentOfficeHours", + "listing.leasingAgentPhone", + "listing.leasingAgentTitle", + ...getBaseAddressSelect([ + "leasingAgentAddress", + "applicationPickUpAddress", + "applicationMailingAddress", + "applicationDropOffAddress", + ]), + "listing.applicationPickUpAddressOfficeHours", + "listing.postmarkedApplicationsReceivedByDate", + "listing.digitalApplication", + "applicationMethods.id", + "applicationMethods.externalReference", + "listing.paperApplication", + "paperApplications.id", + "paperApplicationFile.id", + "paperApplicationFile.fileId", + ], + leftJoins: [ + { join: "listing.buildingSelectionCriteriaFile", alias: "buildingSelectionCriteriaFile" }, + { join: "listing.reservedCommunityType", alias: "reservedCommunityType" }, + { join: "listing.neighborhoodAmenities", alias: "neighborhoodAmenities" }, + { join: "listing.property", alias: "property" }, + { join: "property.buildingAddress", alias: "buildingAddress" }, + { join: "listing.utilities", alias: "utilities" }, + { join: "listing.unitGroups", alias: "unitGroups" }, + { join: "listing.listingPrograms", alias: "listingPrograms" }, + { join: "listingPrograms.program", alias: "listingProgramsProgram" }, + { join: "listing.events", alias: "listingEvents" }, + { join: "listing.features", alias: "features" }, + { join: "listing.leasingAgentAddress", alias: "leasingAgentAddress" }, + { join: "listing.applicationPickUpAddress", alias: "applicationPickUpAddress" }, + { join: "listing.applicationMailingAddress", alias: "applicationMailingAddress" }, + { join: "listing.applicationDropOffAddress", alias: "applicationDropOffAddress" }, + { join: "listing.applicationMethods", alias: "applicationMethods" }, + { join: "applicationMethods.paperApplications", alias: "paperApplications" }, + { join: "paperApplications.file", alias: "paperApplicationFile" }, + ], +} + +views.unitsExport = { + select: [ + "listing.id", + "listing.name", + "unitGroups.id", + "unitGroups.totalCount", + "unitGroups.totalAvailable", + "unitGroups.openWaitlist", + "unitGroups.minOccupancy", + "unitGroups.maxOccupancy", + "unitGroups.sqFeetMin", + "unitGroups.sqFeetMax", + "unitGroups.floorMin", + "unitGroups.floorMax", + "unitGroups.bathroomMin", + "unitGroups.bathroomMax", + "summaryUnitType.id", + "summaryUnitType.name", + "unitGroupsAmiLevels.id", + "unitGroupsAmiLevels.amiPercentage", + "unitGroupsAmiLevels.monthlyRentDeterminationType", + "unitGroupsAmiLevels.flatRentValue", + "unitGroupsAmiLevelsCharts.id", + "unitGroupsAmiLevelsCharts.name", + ], + leftJoins: [ + { join: "listing.unitGroups", alias: "unitGroups" }, + { join: "unitGroups.amiLevels", alias: "unitGroupsAmiLevels" }, + { join: "unitGroupsAmiLevels.amiChart", alias: "unitGroupsAmiLevelsCharts" }, + { join: "unitGroups.unitType", alias: "summaryUnitType" }, ], } diff --git a/backend/core/src/listings/views/types.ts b/backend/core/src/listings/views/types.ts index 1b58858623..01aafc9c7b 100644 --- a/backend/core/src/listings/views/types.ts +++ b/backend/core/src/listings/views/types.ts @@ -1,17 +1,13 @@ +import { View } from "../../views/base.view" + export enum ListingViewEnum { base = "base", detail = "detail", full = "full", partnerList = "partnerList", -} - -export interface View { - select?: string[] - leftJoins?: { - join: string - alias: string - }[] - leftJoinAndSelect?: [string, string][] + publicListings = "publicListings", + listingsExport = "listingsExport", + unitsExport = "unitsExport", } export type Views = { diff --git a/backend/core/src/listings/views/view.spec.ts b/backend/core/src/listings/views/view.spec.ts index 40dc45bcea..ce2a5536ff 100644 --- a/backend/core/src/listings/views/view.spec.ts +++ b/backend/core/src/listings/views/view.spec.ts @@ -1,4 +1,4 @@ -import { BaseView, FullView, getView } from "./view" +import { BaseListingView, FullView, getView } from "./view" import { views } from "./config" const mockQueryBuilder = { @@ -47,29 +47,29 @@ describe("listing views", () => { describe("BaseView", () => { it("should create a new BaseView with qb view properties", () => { - const view = new BaseView(mockListingsRepo.createQueryBuilder()) + const view = new BaseListingView(mockListingsRepo.createQueryBuilder()) expect(view.qb).toEqual(mockQueryBuilder) expect(view.view).toEqual(views.base) }) it("should call getView qb select and leftJoin", () => { - const view = new BaseView(mockListingsRepo.createQueryBuilder()) + const view = new BaseListingView(mockListingsRepo.createQueryBuilder()) view.getViewQb() expect(mockQueryBuilder.select).toHaveBeenCalledTimes(1) - expect(mockQueryBuilder.leftJoin).toHaveBeenCalledTimes(9) + expect(mockQueryBuilder.leftJoin).toHaveBeenCalledTimes(12) }) it("should map unitSummary to listings", () => { - const view = new BaseView(mockListingsRepo.createQueryBuilder()) + const view = new BaseListingView(mockListingsRepo.createQueryBuilder()) const listings = view.mapUnitSummary(mockListings) listings.forEach((listing) => { - expect(listing).toHaveProperty("unitsSummarized") - expect(listing.unitsSummarized).toHaveProperty("byUnitTypeAndRent") + expect(listing).toHaveProperty("unitSummaries") + expect(listing.unitSummaries).toHaveProperty("byUnitTypeAndRent") }) }) }) @@ -80,7 +80,7 @@ describe("listing views", () => { view.getViewQb() - expect(mockQueryBuilder.leftJoinAndSelect).toHaveBeenCalledTimes(25) + expect(mockQueryBuilder.leftJoinAndSelect).toHaveBeenCalledTimes(35) }) }) diff --git a/backend/core/src/listings/views/view.ts b/backend/core/src/listings/views/view.ts index 279303a7fd..970ea86cbf 100644 --- a/backend/core/src/listings/views/view.ts +++ b/backend/core/src/listings/views/view.ts @@ -1,26 +1,34 @@ import { SelectQueryBuilder } from "typeorm" -import { summarizeUnitsByTypeAndRent } from "../../shared/units-transformations" +import { getUnitGroupSummary } from "../../shared/units-transformations" import { Listing } from "../entities/listing.entity" import { views } from "./config" -import { View } from "./types" +import { View, BaseView } from "../../views/base.view" export function getView(qb: SelectQueryBuilder, view?: string) { switch (views[view]) { case views.base: - return new BaseView(qb) + return new BaseListingView(qb) case views.detail: return new DetailView(qb) + case views.publicListings: + return new PublicListingsView(qb) + case views.partnerList: + return new PartnerListView(qb) + case views.listingsExport: + return new ListingsExportView(qb) + case views.unitsExport: + return new UnitsExportView(qb) case views.full: default: return new FullView(qb) } } -export class BaseView { +export class BaseListingView extends BaseView { qb: SelectQueryBuilder view: View constructor(qb: SelectQueryBuilder) { - this.qb = qb + super(qb) this.view = views.base } @@ -37,21 +45,48 @@ export class BaseView { mapUnitSummary(listings) { return listings.map((listing) => ({ ...listing, - unitsSummarized: { - byUnitTypeAndRent: summarizeUnitsByTypeAndRent(listing.property.units), + unitSummaries: { + byUnitTypeAndRent: getUnitGroupSummary(listing.unitGroups), }, })) } } -export class DetailView extends BaseView { +export class DetailView extends BaseListingView { constructor(qb: SelectQueryBuilder) { super(qb) this.view = views.detail } } -export class FullView extends BaseView { +export class PublicListingsView extends BaseListingView { + constructor(qb: SelectQueryBuilder) { + super(qb) + this.view = views.publicListings + } +} +export class PartnerListView extends BaseListingView { + constructor(qb: SelectQueryBuilder) { + super(qb) + this.view = views.partnerList + } +} + +export class ListingsExportView extends BaseListingView { + constructor(qb: SelectQueryBuilder) { + super(qb) + this.view = views.listingsExport + } +} + +export class UnitsExportView extends BaseListingView { + constructor(qb: SelectQueryBuilder) { + super(qb) + this.view = views.unitsExport + } +} + +export class FullView extends BaseListingView { constructor(qb: SelectQueryBuilder) { super(qb) this.view = views.full diff --git a/backend/core/src/main.ts b/backend/core/src/main.ts index d3371e58ac..f153ca5c99 100644 --- a/backend/core/src/main.ts +++ b/backend/core/src/main.ts @@ -4,11 +4,13 @@ import { Logger } from "@nestjs/common" import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger" import { getConnection } from "typeorm" import { ConfigService } from "@nestjs/config" -import dbOptions = require("../ormconfig") +import dbOptions from "../ormconfig" let app async function bootstrap() { - app = await NestFactory.create(AppModule.register(dbOptions)) + app = await NestFactory.create(AppModule.register(dbOptions), { + logger: process.env.NODE_ENV === "development" ? ["error", "warn", "log"] : ["error", "warn"], + }) // Starts listening for shutdown hooks app.enableShutdownHooks() app = applicationSetup(app) diff --git a/backend/core/src/migration/1612262618223-add-csv-formatting-type-to-listing.ts b/backend/core/src/migration/1612262618223-add-csv-formatting-type-to-listing.ts deleted file mode 100644 index ca369ae751..0000000000 --- a/backend/core/src/migration/1612262618223-add-csv-formatting-type-to-listing.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm" - -export class addCsvFormattingTypeToListing1612262618223 implements MigrationInterface { - name = "addCsvFormattingTypeToListing1612262618223" - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "listings" ADD "csv_formatting_type" character varying NOT NULL DEFAULT 'basic'` - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "csv_formatting_type"`) - } -} diff --git a/backend/core/src/migration/1620660845209-seed-translation-entries.ts b/backend/core/src/migration/1620660845209-seed-translation-entries.ts index f1ae208a19..09410cb302 100644 --- a/backend/core/src/migration/1620660845209-seed-translation-entries.ts +++ b/backend/core/src/migration/1620660845209-seed-translation-entries.ts @@ -16,7 +16,7 @@ export class seedTranslationEntries1620660845209 implements MigrationInterface { FCFS: "Applicants will be contacted by the property agent on a first come first serve basis until vacancies are filled.", lottery: - "The lottery will be held on %{lotteryDate}. Applicants will be contacted by the agent in lottery rank order until vacancies are filled.", + "Applicants will be contacted by the agent in lottery rank order until vacancies are filled.", noLottery: "Applicants will be contacted by the agent in waitlist order until vacancies are filled.", }, diff --git a/backend/core/src/migration/1624272587523-add-jurisdictions-table.ts b/backend/core/src/migration/1624272587523-add-jurisdictions-table.ts index bfde31ae7a..8d6df832b9 100644 --- a/backend/core/src/migration/1624272587523-add-jurisdictions-table.ts +++ b/backend/core/src/migration/1624272587523-add-jurisdictions-table.ts @@ -18,11 +18,7 @@ export class addJurisdictionsTable1624272587523 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "listings" ADD CONSTRAINT "FK_ba0026e02ecfe91791aed1a4818" FOREIGN KEY ("jurisdiction_id") REFERENCES "jurisdictions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` ) - for (const jurisdictionName of [ - CountyCode.alameda, - CountyCode.san_jose, - CountyCode.san_mateo, - ]) { + for (const jurisdictionName of [CountyCode.detroit, CountyCode.alameda]) { const jurisdiction = await queryRunner.query( `INSERT INTO "jurisdictions" (name) VALUES ($1)`, [jurisdictionName] diff --git a/backend/core/src/migration/1628017712683-addListingAndPropertyFields.ts b/backend/core/src/migration/1628017712683-addListingAndPropertyFields.ts new file mode 100644 index 0000000000..37997c0c56 --- /dev/null +++ b/backend/core/src/migration/1628017712683-addListingAndPropertyFields.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addListingAndPropertyFields1628017712683 implements MigrationInterface { + name = "addListingAndPropertyFields1628017712683" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "property" ADD "region" text`) + await queryRunner.query(`ALTER TABLE "property" ADD "phone_number" text`) + await queryRunner.query(`ALTER TABLE "listings" ADD "hrd_id" text`) + await queryRunner.query(`ALTER TABLE "listings" ADD "owner_company" text`) + await queryRunner.query(`ALTER TABLE "listings" ADD "management_company" text`) + await queryRunner.query(`ALTER TABLE "listings" ADD "management_website" text`) + await queryRunner.query(`ALTER TABLE "listings" ADD "ami_percentage_min" integer`) + await queryRunner.query(`ALTER TABLE "listings" ADD "ami_percentage_max" integer`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "ami_percentage_max"`) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "ami_percentage_min"`) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "management_website"`) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "management_company"`) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "owner_company"`) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "hrd_id"`) + await queryRunner.query(`ALTER TABLE "property" DROP COLUMN "phone_number"`) + await queryRunner.query(`ALTER TABLE "property" DROP COLUMN "region"`) + } +} diff --git a/backend/core/src/migration/1629306778673-migratePhoneNumberAndRegionToListing.ts b/backend/core/src/migration/1629306778673-migratePhoneNumberAndRegionToListing.ts new file mode 100644 index 0000000000..59dcd8e6b2 --- /dev/null +++ b/backend/core/src/migration/1629306778673-migratePhoneNumberAndRegionToListing.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class migratePhoneNumberAndRegionToListing1629306778673 implements MigrationInterface { + name = "migratePhoneNumberAndRegionToListing1629306778673" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "property" DROP COLUMN "region"`) + await queryRunner.query(`ALTER TABLE "property" DROP COLUMN "phone_number"`) + await queryRunner.query(`ALTER TABLE "listings" ADD "phone_number" text`) + await queryRunner.query(`ALTER TABLE "listings" ADD "region" text`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "region"`) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "phone_number"`) + await queryRunner.query(`ALTER TABLE "property" ADD "phone_number" text`) + await queryRunner.query(`ALTER TABLE "property" ADD "region" text`) + } +} diff --git a/backend/core/src/migration/1630250097191-adds-jurisdiction-relations.ts b/backend/core/src/migration/1630250097191-adds-jurisdiction-relations.ts index 2a06b56daf..6639a9601e 100644 --- a/backend/core/src/migration/1630250097191-adds-jurisdiction-relations.ts +++ b/backend/core/src/migration/1630250097191-adds-jurisdiction-relations.ts @@ -50,7 +50,9 @@ export class addsJurisdictionRelations1630250097191 implements MigrationInterfac `ALTER TABLE "user_accounts_jurisdictions_jurisdictions" ADD CONSTRAINT "FK_fe359f4430f9e0e7b278e03f0f3" FOREIGN KEY ("jurisdictions_id") REFERENCES "jurisdictions"("id") ON DELETE CASCADE ON UPDATE CASCADE` ) // get first jurisdiction_id - const [{ id }] = await queryRunner.query(`SELECT id FROM jurisdictions ORDER BY name LIMIT 1`) + const [{ id }] = await queryRunner.query( + `SELECT id FROM jurisdictions WHERE name = 'Detroit' LIMIT 1` + ) // insert into user_accounts_jurisdictions_jurisdictions // TODO: This works for Alameda, but if you have Alameda as a Jurisdiction and want to assign another, you'll want to change it, for example with Detroit, if Detroit isn't the only Jurisdiction in your DB. await queryRunner.query( diff --git a/backend/core/src/migration/1630388600246-convert-preferred-unit-to-unit-types.ts b/backend/core/src/migration/1630388600246-convert-preferred-unit-to-unit-types.ts index 2e9e565a11..3869cb5be3 100644 --- a/backend/core/src/migration/1630388600246-convert-preferred-unit-to-unit-types.ts +++ b/backend/core/src/migration/1630388600246-convert-preferred-unit-to-unit-types.ts @@ -16,7 +16,6 @@ export class convertPreferredUnitToUnitTypes1630388600246 implements MigrationIn // get applications const applications = await queryRunner.query(`SELECT id, preferred_unit FROM applications`) // insert into applications_preferred_unit_unit_types - console.log("applications = ", applications) for (const application of applications) { if (!applications?.preferred_unit) continue for (const unit of applications.preferred_unit) { diff --git a/backend/core/src/migration/1631734948743-partially-senior-reserved-community-type.ts b/backend/core/src/migration/1631734948743-partially-senior-reserved-community-type.ts new file mode 100644 index 0000000000..885c304c90 --- /dev/null +++ b/backend/core/src/migration/1631734948743-partially-senior-reserved-community-type.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class partiallySeniorReservedCommunityType1631734948743 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `INSERT INTO reserved_community_types (name) VALUES ('partiallySenior')` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DELETE FROM reserved_community_types WHERE name = 'partiallySenior'`) + } +} diff --git a/backend/core/src/migration/1632987393556-add-jurisdiction-relation-to-ami-charts.ts b/backend/core/src/migration/1632987393556-add-jurisdiction-relation-to-ami-charts.ts index 39c592724e..99e8a8adb9 100644 --- a/backend/core/src/migration/1632987393556-add-jurisdiction-relation-to-ami-charts.ts +++ b/backend/core/src/migration/1632987393556-add-jurisdiction-relation-to-ami-charts.ts @@ -6,7 +6,7 @@ export class addJurisdictionRelationToAmiCharts1632987393556 implements Migratio public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "ami_chart" ADD "jurisdiction_id" uuid`) const [{ id: jurisdictionId }] = await queryRunner.query( - `SELECT id FROM jurisdictions WHERE name = 'Alameda' LIMIT 1` + `SELECT id FROM jurisdictions WHERE name = 'Detroit' LIMIT 1` ) await queryRunner.query(`UPDATE ami_chart SET jurisdiction_id = $1`, [jurisdictionId]) await queryRunner.query(`ALTER TABLE "ami_chart" ALTER COLUMN "jurisdiction_id" SET NOT NULL`) diff --git a/backend/core/src/migration/1633359409242-add-listing-preferences-intermediate-relation.ts b/backend/core/src/migration/1633359409242-add-listing-preferences-intermediate-relation.ts new file mode 100644 index 0000000000..3831f70199 --- /dev/null +++ b/backend/core/src/migration/1633359409242-add-listing-preferences-intermediate-relation.ts @@ -0,0 +1,61 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addListingPreferencesIntermediateRelation1633359409242 implements MigrationInterface { + name = "addListingPreferencesIntermediateRelation1633359409242" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "listing_preferences" ("ordinal" integer, "page" integer, "listing_id" uuid NOT NULL, "preference_id" uuid NOT NULL, CONSTRAINT "PK_3a99e1cc861df8e2b81ab885839" PRIMARY KEY ("listing_id", "preference_id"))` + ) + await queryRunner.query( + `ALTER TABLE "listing_preferences" ADD CONSTRAINT "FK_b7fad48d744befbd6532d8a04a0" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "listing_preferences" ADD CONSTRAINT "FK_797708bfa7897f574b8eb73cdcb" FOREIGN KEY ("preference_id") REFERENCES "preferences"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + + const uniquePreferences: [ + { id: string; title: string; ordinal: number } + ] = await queryRunner.query(`SELECT DISTINCT id, title, ordinal, page FROM preferences`) + + const uniquePreferencesMap = uniquePreferences.reduce((acc, val) => { + acc[val.title] = val + return acc + }, {}) + + const listings: [{ listing_id: string; preference_title: string }] = await queryRunner.query( + `SELECT listings.id as listing_id, preferences.title as preference_title FROM listings INNER JOIN preferences preferences ON preferences.listing_id = listings.id` + ) + for (const listing of listings) { + const uniquePreference = uniquePreferencesMap[listing.preference_title] + await queryRunner.query( + `INSERT INTO listing_preferences (listing_id, preference_id, ordinal, page) VALUES ($1, $2, $3, $4)`, + [listing.listing_id, uniquePreference.id, uniquePreference.ordinal, uniquePreference.page] + ) + } + + await queryRunner.query(`DELETE FROM preferences where NOT (id = ANY($1::uuid[]))`, [ + uniquePreferences.map((pref) => pref.id), + ]) + await queryRunner.query(`ALTER TABLE "preferences" DROP COLUMN "ordinal"`) + await queryRunner.query( + `ALTER TABLE "preferences" DROP CONSTRAINT "FK_91017f2182ec7b0dcd4abe68b5a"` + ) + await queryRunner.query(`ALTER TABLE "preferences" DROP COLUMN "listing_id"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "preferences" ADD "listing_id" uuid`) + await queryRunner.query( + `ALTER TABLE "preferences" ADD CONSTRAINT "FK_91017f2182ec7b0dcd4abe68b5a" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + await queryRunner.query(`ALTER TABLE "preferences" ADD "ordinal" integer`) + await queryRunner.query( + `ALTER TABLE "listing_preferences" DROP CONSTRAINT "FK_797708bfa7897f574b8eb73cdcb"` + ) + await queryRunner.query( + `ALTER TABLE "listing_preferences" DROP CONSTRAINT "FK_b7fad48d744befbd6532d8a04a0"` + ) + await queryRunner.query(`DROP TABLE "listing_preferences"`) + } +} diff --git a/backend/core/src/migration/1633557587028-user-email-lower-case.ts b/backend/core/src/migration/1633557587028-user-email-lower-case.ts new file mode 100644 index 0000000000..186fa86ef9 --- /dev/null +++ b/backend/core/src/migration/1633557587028-user-email-lower-case.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class userEmailLowerCase1633557587028 implements MigrationInterface { + name = "userEmailLowerCase1633557587028" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE "user_accounts" SET email = lower(email); + UPDATE household_member SET email_address = lower(email_address); + UPDATE listings SET leasing_agent_email = lower(leasing_agent_email); + UPDATE applicant SET email_address = lower(email_address); + UPDATE alternate_contact SET email_address = lower(email_address); + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE "user_accounts" SET email = lower(email); + UPDATE household_member SET email_address = lower(email_address); + UPDATE listings SET leasing_agent_email = lower(leasing_agent_email); + UPDATE applicant SET email_address = lower(email_address); + UPDATE alternate_contact SET email_address = lower(email_address); + `) + } +} diff --git a/backend/core/src/migration/1633621446887-add-preference-jurisidiction-many-to-many-relation.ts b/backend/core/src/migration/1633621446887-add-preference-jurisidiction-many-to-many-relation.ts new file mode 100644 index 0000000000..dc8c3d1aba --- /dev/null +++ b/backend/core/src/migration/1633621446887-add-preference-jurisidiction-many-to-many-relation.ts @@ -0,0 +1,49 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { query } from "express" +import { CountyCode } from "../shared/types/county-code" + +export class addPreferenceJurisidictionManyToManyRelation1633621446887 + implements MigrationInterface { + name = "addPreferenceJurisidictionManyToManyRelation1633621446887" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "jurisdictions_preferences_preferences" ("jurisdictions_id" uuid NOT NULL, "preferences_id" uuid NOT NULL, CONSTRAINT "PK_e5e8a8e6f1d02a2e228444aef76" PRIMARY KEY ("jurisdictions_id", "preferences_id"))` + ) + await queryRunner.query( + `CREATE INDEX "IDX_46e20b8b62dbdabfd76955e95b" ON "jurisdictions_preferences_preferences" ("jurisdictions_id") ` + ) + await queryRunner.query( + `CREATE INDEX "IDX_7a0eef07c822800c4e9b9d4361" ON "jurisdictions_preferences_preferences" ("preferences_id") ` + ) + await queryRunner.query( + `ALTER TABLE "jurisdictions_preferences_preferences" ADD CONSTRAINT "FK_46e20b8b62dbdabfd76955e95b1" FOREIGN KEY ("jurisdictions_id") REFERENCES "jurisdictions"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + await queryRunner.query( + `ALTER TABLE "jurisdictions_preferences_preferences" ADD CONSTRAINT "FK_7a0eef07c822800c4e9b9d43619" FOREIGN KEY ("preferences_id") REFERENCES "preferences"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + // Assign all existing preferences to Alameda jurisdiction + const [{ id: alamedaJurisdictionId }] = await queryRunner.query( + `SELECT id FROM jurisdictions where name = '${CountyCode.alameda}'` + ) + const preferences: [{ id: string }] = await queryRunner.query(`SELECT id FROM preferences`) + for (const preference of preferences) { + await queryRunner.query( + `INSERT INTO jurisdictions_preferences_preferences (jurisdictions_id, preferences_id) VALUES ($1, $2)`, + [alamedaJurisdictionId, preference.id] + ) + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "jurisdictions_preferences_preferences" DROP CONSTRAINT "FK_7a0eef07c822800c4e9b9d43619"` + ) + await queryRunner.query( + `ALTER TABLE "jurisdictions_preferences_preferences" DROP CONSTRAINT "FK_46e20b8b62dbdabfd76955e95b1"` + ) + await queryRunner.query(`DROP INDEX "IDX_7a0eef07c822800c4e9b9d4361"`) + await queryRunner.query(`DROP INDEX "IDX_46e20b8b62dbdabfd76955e95b"`) + await queryRunner.query(`DROP TABLE "jurisdictions_preferences_preferences"`) + } +} diff --git a/backend/core/src/migration/1633948803537-add-jurisdictional-program-entity.ts b/backend/core/src/migration/1633948803537-add-jurisdictional-program-entity.ts new file mode 100644 index 0000000000..c248476131 --- /dev/null +++ b/backend/core/src/migration/1633948803537-add-jurisdictional-program-entity.ts @@ -0,0 +1,55 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addJurisdictionalProgramEntity1633948803537 implements MigrationInterface { + name = "addJurisdictionalProgramEntity1633948803537" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "listing_programs" ("ordinal" integer, "page" integer, "listing_id" uuid NOT NULL, "program_id" uuid NOT NULL, CONSTRAINT "PK_84171c3ea1066baeed32822b139" PRIMARY KEY ("listing_id", "program_id"))` + ) + await queryRunner.query( + `CREATE TABLE "programs" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "question" text, "description" text, "subtitle" text, "subdescription" text, CONSTRAINT "PK_d43c664bcaafc0e8a06dfd34e05" PRIMARY KEY ("id"))` + ) + await queryRunner.query( + `CREATE TABLE "jurisdictions_programs_programs" ("jurisdictions_id" uuid NOT NULL, "programs_id" uuid NOT NULL, CONSTRAINT "PK_5e2009964fd0aab1366091610d3" PRIMARY KEY ("jurisdictions_id", "programs_id"))` + ) + await queryRunner.query( + `CREATE INDEX "IDX_1ec5e2b056309e1248fb43bb08" ON "jurisdictions_programs_programs" ("jurisdictions_id") ` + ) + await queryRunner.query( + `CREATE INDEX "IDX_cc8517c9311a8e8a4bbabac30f" ON "jurisdictions_programs_programs" ("programs_id") ` + ) + await queryRunner.query( + `ALTER TABLE "listing_programs" ADD CONSTRAINT "FK_89b3daa7bbc2dbd95f2760958c2" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "listing_programs" ADD CONSTRAINT "FK_0fc46ddd2b9468b011d567740b5" FOREIGN KEY ("program_id") REFERENCES "programs"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "jurisdictions_programs_programs" ADD CONSTRAINT "FK_1ec5e2b056309e1248fb43bb08b" FOREIGN KEY ("jurisdictions_id") REFERENCES "jurisdictions"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + await queryRunner.query( + `ALTER TABLE "jurisdictions_programs_programs" ADD CONSTRAINT "FK_cc8517c9311a8e8a4bbabac30f3" FOREIGN KEY ("programs_id") REFERENCES "programs"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "jurisdictions_programs_programs" DROP CONSTRAINT "FK_cc8517c9311a8e8a4bbabac30f3"` + ) + await queryRunner.query( + `ALTER TABLE "jurisdictions_programs_programs" DROP CONSTRAINT "FK_1ec5e2b056309e1248fb43bb08b"` + ) + await queryRunner.query( + `ALTER TABLE "listing_programs" DROP CONSTRAINT "FK_0fc46ddd2b9468b011d567740b5"` + ) + await queryRunner.query( + `ALTER TABLE "listing_programs" DROP CONSTRAINT "FK_89b3daa7bbc2dbd95f2760958c2"` + ) + await queryRunner.query(`DROP INDEX "IDX_cc8517c9311a8e8a4bbabac30f"`) + await queryRunner.query(`DROP INDEX "IDX_1ec5e2b056309e1248fb43bb08"`) + await queryRunner.query(`DROP TABLE "jurisdictions_programs_programs"`) + await queryRunner.query(`DROP TABLE "programs"`) + await queryRunner.query(`DROP TABLE "listing_programs"`) + } +} diff --git a/backend/core/src/migration/1634210584036-add-cascade-to-user-roles-user-relation.ts b/backend/core/src/migration/1634210584036-add-cascade-to-user-roles-user-relation.ts new file mode 100644 index 0000000000..6c2b0b99c8 --- /dev/null +++ b/backend/core/src/migration/1634210584036-add-cascade-to-user-roles-user-relation.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addCascadeToUserRolesUserRelation1634210584036 implements MigrationInterface { + name = "addCascadeToUserRolesUserRelation1634210584036" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "user_roles" DROP CONSTRAINT IF EXISTS "FK_87b8888186ca9769c960e926870"`) + await queryRunner.query( + `ALTER TABLE "user_roles" ADD CONSTRAINT "FK_87b8888186ca9769c960e926870" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "user_roles" DROP CONSTRAINT IF EXISTS "FK_87b8888186ca9769c960e926870"`) + await queryRunner.query( + `ALTER TABLE "user_roles" ADD CONSTRAINT "FK_87b8888186ca9769c960e926870" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } +} diff --git a/backend/core/src/migration/1634213955270-add-language-to-jurisdiction.ts b/backend/core/src/migration/1634213955270-add-language-to-jurisdiction.ts new file mode 100644 index 0000000000..dbca1fb6d5 --- /dev/null +++ b/backend/core/src/migration/1634213955270-add-language-to-jurisdiction.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addLanguageToJurisdiction1634213955270 implements MigrationInterface { + name = "addLanguageToJurisdiction1634213955270" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "jurisdictions_languages_enum" AS ENUM('en', 'es', 'vi', 'zh')` + ) + await queryRunner.query( + `ALTER TABLE "jurisdictions" ADD "languages" "jurisdictions_languages_enum" array NOT NULL DEFAULT '{en}'` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "jurisdictions" DROP COLUMN "languages"`) + await queryRunner.query(`DROP TYPE "jurisdictions_languages_enum"`) + } +} diff --git a/backend/core/src/migration/1634268265134-add-indexes-to-applications-and-householdmembers.ts b/backend/core/src/migration/1634268265134-add-indexes-to-applications-and-householdmembers.ts new file mode 100644 index 0000000000..92a31b2511 --- /dev/null +++ b/backend/core/src/migration/1634268265134-add-indexes-to-applications-and-householdmembers.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addIndexesToApplicationsAndHouseholdmembers1634268265134 + implements MigrationInterface { + name = "addIndexesToApplicationsAndHouseholdmembers1634268265134" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN IF EXISTS "csv_formatting_type"`) + await queryRunner.query( + `CREATE INDEX "IDX_520996eeecf9f6fb9425dc7352" ON "household_member" ("application_id") ` + ) + await queryRunner.query( + `CREATE INDEX "IDX_cc9d65c58d8deb0ef5353e9037" ON "applications" ("listing_id") ` + ) + await queryRunner.query( + `CREATE INDEX "IDX_f2ace84eebd770f1387b47e5e4" ON "application_flagged_set" ("listing_id") ` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_f2ace84eebd770f1387b47e5e4"`) + await queryRunner.query(`DROP INDEX "IDX_cc9d65c58d8deb0ef5353e9037"`) + await queryRunner.query(`DROP INDEX "IDX_520996eeecf9f6fb9425dc7352"`) + await queryRunner.query( + `ALTER TABLE "listings" ADD "csv_formatting_type" character varying NOT NULL DEFAULT 'basic'` + ) + } +} diff --git a/backend/core/src/migration/1634316081536-remove-app-address.ts b/backend/core/src/migration/1634316081536-remove-app-address.ts new file mode 100644 index 0000000000..8327e637e6 --- /dev/null +++ b/backend/core/src/migration/1634316081536-remove-app-address.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class removeAppAddress1634316081536 implements MigrationInterface { + name = "removeAppAddress1634316081536" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listings" DROP CONSTRAINT "FK_42385e47be1780d1491f0c8c1c3"` + ) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "application_address_id"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" ADD "application_address_id" uuid`) + await queryRunner.query( + `ALTER TABLE "listings" ADD CONSTRAINT "FK_42385e47be1780d1491f0c8c1c3" FOREIGN KEY ("application_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } +} diff --git a/backend/core/src/migration/1634547352243-add-program-jsonb-to-application.ts b/backend/core/src/migration/1634547352243-add-program-jsonb-to-application.ts new file mode 100644 index 0000000000..cd885b64b8 --- /dev/null +++ b/backend/core/src/migration/1634547352243-add-program-jsonb-to-application.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addProgramJsonbToApplication1634547352243 implements MigrationInterface { + name = "addProgramJsonbToApplication1634547352243" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "applications" ADD "programs" jsonb`) + await queryRunner.query(`ALTER TABLE "programs" ADD "form_metadata" jsonb`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "programs" DROP COLUMN "form_metadata"`) + await queryRunner.query(`ALTER TABLE "applications" DROP COLUMN "programs"`) + } +} diff --git a/backend/core/src/migration/1634647281728-align-program-entity-model-with-preference.ts b/backend/core/src/migration/1634647281728-align-program-entity-model-with-preference.ts new file mode 100644 index 0000000000..e58030e801 --- /dev/null +++ b/backend/core/src/migration/1634647281728-align-program-entity-model-with-preference.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class alignProgramEntityModelWithPreference1634647281728 implements MigrationInterface { + name = "alignProgramEntityModelWithPreference1634647281728" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "programs" DROP COLUMN "question"`) + await queryRunner.query(`ALTER TABLE "programs" DROP COLUMN "subdescription"`) + await queryRunner.query(`ALTER TABLE "programs" ADD "title" text`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "programs" DROP COLUMN "title"`) + await queryRunner.query(`ALTER TABLE "programs" ADD "subdescription" text`) + await queryRunner.query(`ALTER TABLE "programs" ADD "question" text`) + } +} diff --git a/backend/core/src/migration/1634664593091-adds-jurisdiction-index-to-listings.ts b/backend/core/src/migration/1634664593091-adds-jurisdiction-index-to-listings.ts new file mode 100644 index 0000000000..84294835d1 --- /dev/null +++ b/backend/core/src/migration/1634664593091-adds-jurisdiction-index-to-listings.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addsJurisdictionIndexToListings1634664593091 implements MigrationInterface { + name = "addsJurisdictionIndexToListings1634664593091" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE INDEX "IDX_ba0026e02ecfe91791aed1a481" ON "listings" ("jurisdiction_id") ` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_ba0026e02ecfe91791aed1a481"`) + } +} diff --git a/backend/core/src/migration/1634814157491-add-on-delete-action-to-application-user-relation.ts b/backend/core/src/migration/1634814157491-add-on-delete-action-to-application-user-relation.ts new file mode 100644 index 0000000000..0e954cd001 --- /dev/null +++ b/backend/core/src/migration/1634814157491-add-on-delete-action-to-application-user-relation.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addOnDeleteActionToApplicationUserRelation1634814157491 implements MigrationInterface { + name = "addOnDeleteActionToApplicationUserRelation1634814157491" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "applications" DROP CONSTRAINT "FK_9e7594d5b474d9cbebba15c1ae7"` + ) + await queryRunner.query( + `ALTER TABLE "applications" ADD CONSTRAINT "FK_9e7594d5b474d9cbebba15c1ae7" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE SET NULL ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "applications" DROP CONSTRAINT "FK_9e7594d5b474d9cbebba15c1ae7"` + ) + await queryRunner.query( + `ALTER TABLE "applications" ADD CONSTRAINT "FK_9e7594d5b474d9cbebba15c1ae7" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } +} diff --git a/backend/core/src/migration/1634830165326-remove-page-from-listing-program-entity.ts b/backend/core/src/migration/1634830165326-remove-page-from-listing-program-entity.ts new file mode 100644 index 0000000000..347b835ed6 --- /dev/null +++ b/backend/core/src/migration/1634830165326-remove-page-from-listing-program-entity.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class removePageFromListingProgramEntity1634830165326 implements MigrationInterface { + name = "removePageFromListingProgramEntity1634830165326" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listing_programs" DROP COLUMN "page"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listing_programs" ADD "page" integer`) + } +} diff --git a/backend/core/src/migration/1634848388161-add-phone-number.ts b/backend/core/src/migration/1634848388161-add-phone-number.ts new file mode 100644 index 0000000000..34e26f2d23 --- /dev/null +++ b/backend/core/src/migration/1634848388161-add-phone-number.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addPhoneNumber1634848388161 implements MigrationInterface { + name = "addPhoneNumber1634848388161" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_accounts" ADD "phone_number" character varying`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "phone_number"`) + } +} diff --git a/backend/core/src/migration/1634912142711-remove-preferences-page.ts b/backend/core/src/migration/1634912142711-remove-preferences-page.ts new file mode 100644 index 0000000000..03d2703426 --- /dev/null +++ b/backend/core/src/migration/1634912142711-remove-preferences-page.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class removePreferencesPage1634912142711 implements MigrationInterface { + name = "removePreferencesPage1634912142711" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listing_preferences" DROP COLUMN "page"`) + await queryRunner.query(`ALTER TABLE "preferences" DROP COLUMN "page"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "preferences" ADD "page" integer`) + await queryRunner.query(`ALTER TABLE "listing_preferences" ADD "page" integer`) + } +} diff --git a/backend/core/src/migration/1635126814120-remove-app-defaults.ts b/backend/core/src/migration/1635126814120-remove-app-defaults.ts new file mode 100644 index 0000000000..8cad054545 --- /dev/null +++ b/backend/core/src/migration/1635126814120-remove-app-defaults.ts @@ -0,0 +1,53 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class removeAppDefaults1635126814120 implements MigrationInterface { + name = "removeAppDefaults1635126814120" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "digital_application" DROP NOT NULL` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "digital_application" DROP DEFAULT` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "common_digital_application" DROP NOT NULL` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "common_digital_application" DROP DEFAULT` + ) + await queryRunner.query(`ALTER TABLE "listings" ALTER COLUMN "paper_application" DROP NOT NULL`) + await queryRunner.query(`ALTER TABLE "listings" ALTER COLUMN "paper_application" DROP DEFAULT`) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "referral_opportunity" DROP NOT NULL` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "referral_opportunity" DROP DEFAULT` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "referral_opportunity" SET DEFAULT false` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "referral_opportunity" SET NOT NULL` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "paper_application" SET DEFAULT false` + ) + await queryRunner.query(`ALTER TABLE "listings" ALTER COLUMN "paper_application" SET NOT NULL`) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "common_digital_application" SET DEFAULT true` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "common_digital_application" SET NOT NULL` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "digital_application" SET DEFAULT false` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "digital_application" SET NOT NULL` + ) + } +} diff --git a/backend/core/src/migration/1635216780193-new-household-common-app-questions.ts b/backend/core/src/migration/1635216780193-new-household-common-app-questions.ts new file mode 100644 index 0000000000..7fd4837661 --- /dev/null +++ b/backend/core/src/migration/1635216780193-new-household-common-app-questions.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class newHouseholdCommonAppQuestions1635216780193 implements MigrationInterface { + name = "newHouseholdCommonAppQuestions1635216780193" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "applications" ADD "household_expecting_changes" boolean`) + await queryRunner.query(`ALTER TABLE "applications" ADD "household_student" boolean`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "applications" DROP COLUMN "household_student"`) + await queryRunner.query(`ALTER TABLE "applications" DROP COLUMN "household_expecting_changes"`) + } +} diff --git a/backend/core/src/migration/1635535204972-allow-multiple-race-checkboxes.ts b/backend/core/src/migration/1635535204972-allow-multiple-race-checkboxes.ts new file mode 100644 index 0000000000..6ba5e81914 --- /dev/null +++ b/backend/core/src/migration/1635535204972-allow-multiple-race-checkboxes.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class allowMultipleRaceCheckboxes1635535204972 implements MigrationInterface { + name = "allowMultipleRaceCheckboxes1635535204972" + + public async up(queryRunner: QueryRunner): Promise { + const existingRaceFields = await queryRunner.query(`SELECT id, race FROM demographics`) + + await queryRunner.query(`ALTER TABLE "demographics" DROP COLUMN "race"`) + await queryRunner.query(`ALTER TABLE "demographics" ADD "race" text array`) + + for (const demographic in existingRaceFields) { + await queryRunner.query(`UPDATE demographics SET race = ($1) WHERE id = ($2)`, [ + [existingRaceFields[demographic]["race"]], + existingRaceFields[demographic]["id"], + ]) + } + } + + public async down(queryRunner: QueryRunner): Promise { + const existingRaceFields = await queryRunner.query(`SELECT id, race FROM demographics`) + + await queryRunner.query(`ALTER TABLE "demographics" DROP COLUMN "race"`) + await queryRunner.query(`ALTER TABLE "demographics" ADD "race" text`) + + for (const demographic in existingRaceFields) { + await queryRunner.query(`UPDATE demographics race = ($1) WHERE id = ($2)`, [ + existingRaceFields[demographic]["race"][0], + existingRaceFields[demographic]["id"], + ]) + } + } +} diff --git a/backend/core/src/migration/1635546032998-add-jurisdictional-email-signatures.ts b/backend/core/src/migration/1635546032998-add-jurisdictional-email-signatures.ts new file mode 100644 index 0000000000..5a6bfbcf75 --- /dev/null +++ b/backend/core/src/migration/1635546032998-add-jurisdictional-email-signatures.ts @@ -0,0 +1,53 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { Language } from "../shared/types/language-enum" + +export class addJurisdictionalEmailSignatures1635546032998 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const detroitTranslation = { + footer: { + footer: "City of Detroit - Housing Connect", + }, + } + const [{ id: detroitJurisdiction }] = await queryRunner.query( + `SELECT id FROM jurisdictions WHERE name = 'Detroit' LIMIT 1` + ) + + const existingDetroitTranslations = await queryRunner.query( + `SELECT translations FROM translations WHERE jurisdiction_id = ($1)`, + [detroitJurisdiction] + ) + + const existingGeneralTranslations = await queryRunner.query( + `SELECT translations FROM translations WHERE jurisdiction_id is NULL` + ) + + let genericTranslation = {} + + if (existingDetroitTranslations?.length) { + genericTranslation = { + ...existingDetroitTranslations["0"]["translations"], + } + } + + genericTranslation = { + ...genericTranslation, + ...existingGeneralTranslations["0"]["translations"], + footer: { + footer: "", + thankYou: "Thanks!", + }, + } + + await queryRunner.query( + `UPDATE "translations" SET translations = ($1) where jurisdiction_id = ($2) and language = ($3)`, + [detroitTranslation, detroitJurisdiction, Language.en] + ) + + await queryRunner.query( + `UPDATE "translations" SET translations = ($1) where jurisdiction_id is NULL and language = ($2)`, + [genericTranslation, Language.en] + ) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1635889621244-seed-detroit-translation-entries.ts b/backend/core/src/migration/1635889621244-seed-detroit-translation-entries.ts new file mode 100644 index 0000000000..bb679932f1 --- /dev/null +++ b/backend/core/src/migration/1635889621244-seed-detroit-translation-entries.ts @@ -0,0 +1,68 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { Language } from "../shared/types/language-enum" + +export class seedDetroitTranslationEntries1635889621244 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const defaultTranslation = { + confirmation: { + yourConfirmationNumber: "Here is your confirmation number:", + shouldBeChosen: + "Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.", + subject: "Your Application Confirmation", + thankYouForApplying: "Thanks for applying. We have received your application for", + whatToExpectNext: "What to expect next:", + whatToExpect: { + FCFS: + "Applicants will be contacted by the property agent on a first come first serve basis until vacancies are filled.", + lottery: + "The lottery will be held on %{lotteryDate}. Applicants will be contacted by the agent in lottery rank order until vacancies are filled.", + noLottery: + "Applicants will be contacted by the agent in waitlist order until vacancies are filled.", + }, + }, + footer: { + callToAction: "How are we doing? We'd like to get your ", + callToActionUrl: "FEEDBACK URL UNIMPLEMENTED", + feedback: "feedback", + footer: "Detroit Home Connect", + thankYou: "Thank you", + }, + forgotPassword: { + callToAction: + "If you did make this request, please click on the link below to reset your password:", + changePassword: "Change my password", + ignoreRequest: "If you didn't request this, please ignore this email.", + passwordInfo: + "Your password won't change until you access the link above and create a new one.", + resetRequest: + "A request to reset your Bloom Housing Portal website password for %{appUrl} has recently been made.", + subject: "Forgot your password?", + }, + leasingAgent: { + contactAgentToUpdateInfo: + "If you need to update information on your application, do not apply again. Contact the agent. See below for contact information for the Agent for this listing.", + officeHours: "Office Hours:", + }, + register: { + confirmMyAccount: "Confirm my account", + toConfirmAccountMessage: "To complete your account creation, please click the link below:", + welcome: "Welcome", + welcomeMessage: + "Thank you for setting up your account on %{appUrl}. It will now be easier for you to start, save, and submit online applications for listings that appear on the site.", + }, + t: { + hello: "Hello", + }, + } + + const [{ id: detroit_jurisdiction_id }] = await queryRunner.query( + `SELECT id FROM jurisdictions where name='Detroit'` + ) + await queryRunner.query( + `INSERT into "translations" (jurisdiction_id, language, translations) VALUES ($1, $2, $3)`, + [detroit_jurisdiction_id, Language.en, defaultTranslation] + ) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1635946663862-add-change-email-translations.ts b/backend/core/src/migration/1635946663862-add-change-email-translations.ts new file mode 100644 index 0000000000..63a650b763 --- /dev/null +++ b/backend/core/src/migration/1635946663862-add-change-email-translations.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addChangeEmailTranslations1635946663862 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const changeEmail = { + message: "An email address change has been requested for your account.", + onChangeEmailMessage: + "To confirm the change to your email address, please click the link below:", + changeMyEmail: "Confirm email change", + } + const translations = await queryRunner.query(`SELECT * from translations`) + for (const t of translations) { + await queryRunner.query(`UPDATE translations SET translations = ($1) WHERE id = ($2)`, [ + { ...t.translations, changeEmail }, + t.id, + ]) + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1636490631999-add-user-preferences.ts b/backend/core/src/migration/1636490631999-add-user-preferences.ts new file mode 100644 index 0000000000..c543c9acd9 --- /dev/null +++ b/backend/core/src/migration/1636490631999-add-user-preferences.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addUserPreferences1636490631999 implements MigrationInterface { + name = "addUserPreferences1636490631999" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "user_preferences" ("send_email_notifications" boolean NOT NULL DEFAULT false, "send_sms_notifications" boolean NOT NULL DEFAULT false, "user_id" uuid NOT NULL, CONSTRAINT "REL_458057fa75b66e68a275647da2" UNIQUE ("user_id"), CONSTRAINT "PK_458057fa75b66e68a275647da2e" PRIMARY KEY ("user_id"))` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences" ADD CONSTRAINT "FK_458057fa75b66e68a275647da2e" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_preferences" DROP CONSTRAINT "FK_458057fa75b66e68a275647da2e"` + ) + await queryRunner.query(`DROP TABLE "user_preferences"`) + } +} diff --git a/backend/core/src/migration/1636499724052-deposit-helper-text-creation.ts b/backend/core/src/migration/1636499724052-deposit-helper-text-creation.ts new file mode 100644 index 0000000000..97efd5201c --- /dev/null +++ b/backend/core/src/migration/1636499724052-deposit-helper-text-creation.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class depositHelperTextCreation1636499724052 implements MigrationInterface { + name = "depositHelperTextCreation1636499724052" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" ADD "deposit_helper_text" text`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "deposit_helper_text"`) + } +} diff --git a/backend/core/src/migration/1636990024836-dev-dis-reserved-community.ts b/backend/core/src/migration/1636990024836-dev-dis-reserved-community.ts new file mode 100644 index 0000000000..86a6b4bc48 --- /dev/null +++ b/backend/core/src/migration/1636990024836-dev-dis-reserved-community.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class devDisReservedCommunity1636990024836 implements MigrationInterface { + reservedType = "developmentalDisability" + public async up(queryRunner: QueryRunner): Promise { + const jurisdictions = await queryRunner.query(`SELECT id from jurisdictions`) + for (const jurisdiction of jurisdictions) { + await queryRunner.query( + `INSERT INTO reserved_community_types (name, jurisdiction_id) VALUES ($1, $2)`, + [this.reservedType, jurisdiction.id] + ) + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DELETE FROM reserved_community_types WHERE name = $1`, [ + this.reservedType, + ]) + } +} diff --git a/backend/core/src/migration/1637680690577-add-generated-listing-translation.ts b/backend/core/src/migration/1637680690577-add-generated-listing-translation.ts new file mode 100644 index 0000000000..ad6ca34cbc --- /dev/null +++ b/backend/core/src/migration/1637680690577-add-generated-listing-translation.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addGeneratedListingTranslation1637680690577 implements MigrationInterface { + name = "addGeneratedListingTranslation1637680690577" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "generated_listing_translations" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "listing_id" character varying NOT NULL, "jurisdiction_id" character varying NOT NULL, "language" character varying NOT NULL, "translations" jsonb NOT NULL, "timestamp" TIMESTAMP NOT NULL, CONSTRAINT "PK_4059452831439aefc27c1990b20" PRIMARY KEY ("id"))` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "generated_listing_translations"`) + } +} diff --git a/backend/core/src/migration/1637700649083-alter-user-preferences.ts b/backend/core/src/migration/1637700649083-alter-user-preferences.ts new file mode 100644 index 0000000000..a833520e2b --- /dev/null +++ b/backend/core/src/migration/1637700649083-alter-user-preferences.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class alterUserPreferences1637700649083 implements MigrationInterface { + name = "alterUserPreferences1637700649083" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_preferences" DROP CONSTRAINT "FK_458057fa75b66e68a275647da2e"` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences" ADD CONSTRAINT "UQ_458057fa75b66e68a275647da2e" UNIQUE ("user_id")` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences" ADD CONSTRAINT "FK_458057fa75b66e68a275647da2e" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_preferences" DROP CONSTRAINT "FK_458057fa75b66e68a275647da2e"` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences" DROP CONSTRAINT "UQ_458057fa75b66e68a275647da2e"` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences" ADD CONSTRAINT "FK_458057fa75b66e68a275647da2e" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } +} diff --git a/backend/core/src/migration/1637710104539-add-listing-features.ts b/backend/core/src/migration/1637710104539-add-listing-features.ts new file mode 100644 index 0000000000..682c123833 --- /dev/null +++ b/backend/core/src/migration/1637710104539-add-listing-features.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addListingFeatures1637710104539 implements MigrationInterface { + name = "addListingFeatures1637710104539" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "listing_features" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "elevator" boolean, "wheelchair_ramp" boolean, "service_animals_allowed" boolean, "accessible_parking" boolean, "parking_on_site" boolean, "in_unit_washer_dryer" boolean, "laundry_in_building" boolean, "barrier_free_entrance" boolean, "roll_in_shower" boolean, "grab_bars" boolean, "heating_in_unit" boolean, "ac_in_unit" boolean, CONSTRAINT "PK_88e4fe3e46d21d8b4fdadeb7599" PRIMARY KEY ("id"))` + ) + await queryRunner.query(`ALTER TABLE "listings" ADD "features_id" uuid`) + await queryRunner.query( + `ALTER TABLE "listings" ADD CONSTRAINT "UQ_ac59a58a02199c57a588f045830" UNIQUE ("features_id")` + ) + await queryRunner.query( + `ALTER TABLE "listings" ADD CONSTRAINT "FK_ac59a58a02199c57a588f045830" FOREIGN KEY ("features_id") REFERENCES "listing_features"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listings" DROP CONSTRAINT "FK_ac59a58a02199c57a588f045830"` + ) + await queryRunner.query( + `ALTER TABLE "listings" DROP CONSTRAINT "UQ_ac59a58a02199c57a588f045830"` + ) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "features_id"`) + await queryRunner.query(`DROP TABLE "listing_features"`) + } +} diff --git a/backend/core/src/migration/1637815805105-add-sro-to-unit-types.ts b/backend/core/src/migration/1637815805105-add-sro-to-unit-types.ts new file mode 100644 index 0000000000..cfbaa949d7 --- /dev/null +++ b/backend/core/src/migration/1637815805105-add-sro-to-unit-types.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addSroToUnitTypes1637815805105 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`INSERT INTO unit_types (name, num_bedrooms) VALUES ($1, $2)`, [ + "SRO", + 0, + ]) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DELETE FROM unit_types WHERE name = $1`, ["SRO"]) + } +} diff --git a/backend/core/src/migration/1637924287461-add-user-login-metadata-to-user-entity.ts b/backend/core/src/migration/1637924287461-add-user-login-metadata-to-user-entity.ts new file mode 100644 index 0000000000..3b0503ca39 --- /dev/null +++ b/backend/core/src/migration/1637924287461-add-user-login-metadata-to-user-entity.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addUserLoginMetadataToUserEntity1637924287461 implements MigrationInterface { + name = "addUserLoginMetadataToUserEntity1637924287461" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_accounts" ADD "last_login_at" TIMESTAMP NOT NULL DEFAULT NOW()` + ) + await queryRunner.query( + `ALTER TABLE "user_accounts" ADD "failed_login_attempts_count" integer NOT NULL DEFAULT '0'` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "failed_login_attempts_count"`) + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "last_login_at"`) + } +} diff --git a/backend/core/src/migration/1638356116695-add-user-password-outdating-fields.ts b/backend/core/src/migration/1638356116695-add-user-password-outdating-fields.ts new file mode 100644 index 0000000000..9650fae6e2 --- /dev/null +++ b/backend/core/src/migration/1638356116695-add-user-password-outdating-fields.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addUserPasswordOutdatingFields1638356116695 implements MigrationInterface { + name = "addUserPasswordOutdatingFields1638356116695" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_accounts" ADD "password_updated_at" TIMESTAMP NOT NULL DEFAULT NOW()` + ) + await queryRunner.query( + `ALTER TABLE "user_accounts" ADD "password_valid_for_days" integer NOT NULL DEFAULT '180'` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "password_valid_for_days"`) + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "password_updated_at"`) + } +} diff --git a/backend/core/src/migration/1638439254453-add-activity-log-metadata.ts b/backend/core/src/migration/1638439254453-add-activity-log-metadata.ts new file mode 100644 index 0000000000..7cafcd0651 --- /dev/null +++ b/backend/core/src/migration/1638439254453-add-activity-log-metadata.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addActivityLogMetadata1638439254453 implements MigrationInterface { + name = "addActivityLogMetadata1638439254453" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "activity_logs" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "module" character varying NOT NULL, "record_id" uuid NOT NULL, "action" character varying NOT NULL, "metadata" jsonb, "user_id" uuid, CONSTRAINT "PK_f25287b6140c5ba18d38776a796" PRIMARY KEY ("id"))` + ) + await queryRunner.query( + `ALTER TABLE "activity_logs" ADD CONSTRAINT "FK_d54f841fa5478e4734590d44036" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "activity_logs" DROP CONSTRAINT "FK_d54f841fa5478e4734590d44036"` + ) + await queryRunner.query(`DROP TABLE "activity_logs"`) + } +} diff --git a/backend/core/src/migration/1638478574456-add-tagalog.ts b/backend/core/src/migration/1638478574456-add-tagalog.ts new file mode 100644 index 0000000000..1cb7b3f6ce --- /dev/null +++ b/backend/core/src/migration/1638478574456-add-tagalog.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addTagalog1638478574456 implements MigrationInterface { + name = "addTagalog1638478574456" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TYPE "jurisdictions_languages_enum" RENAME TO "jurisdictions_languages_enum_old"` + ) + await queryRunner.query( + `CREATE TYPE "jurisdictions_languages_enum" AS ENUM('en', 'es', 'vi', 'zh', 'tl')` + ) + await queryRunner.query(`ALTER TABLE "jurisdictions" ALTER COLUMN "languages" DROP DEFAULT`) + await queryRunner.query( + `ALTER TABLE "jurisdictions" ALTER COLUMN "languages" TYPE "jurisdictions_languages_enum"[] USING "languages"::"text"::"jurisdictions_languages_enum"[]` + ) + await queryRunner.query( + `ALTER TABLE "jurisdictions" ALTER COLUMN "languages" SET DEFAULT '{en}'` + ) + await queryRunner.query(`DROP TYPE "jurisdictions_languages_enum_old"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "jurisdictions_languages_enum_old" AS ENUM('en', 'es', 'vi', 'zh')` + ) + await queryRunner.query(`ALTER TABLE "jurisdictions" ALTER COLUMN "languages" DROP DEFAULT`) + await queryRunner.query( + `ALTER TABLE "jurisdictions" ALTER COLUMN "languages" TYPE "jurisdictions_languages_enum_old"[] USING "languages"::"text"::"jurisdictions_languages_enum_old"[]` + ) + await queryRunner.query( + `ALTER TABLE "jurisdictions" ALTER COLUMN "languages" SET DEFAULT '{en}'` + ) + await queryRunner.query(`DROP TYPE "jurisdictions_languages_enum"`) + await queryRunner.query( + `ALTER TYPE "jurisdictions_languages_enum_old" RENAME TO "jurisdictions_languages_enum"` + ) + } +} diff --git a/backend/core/src/migration/1639051722043-add-mfa-related-columns-to-user-entity.ts b/backend/core/src/migration/1639051722043-add-mfa-related-columns-to-user-entity.ts new file mode 100644 index 0000000000..b51dcd9565 --- /dev/null +++ b/backend/core/src/migration/1639051722043-add-mfa-related-columns-to-user-entity.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addMfaRelatedColumnsToUserEntity1639051722043 implements MigrationInterface { + name = "addMfaRelatedColumnsToUserEntity1639051722043" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_accounts" ADD "mfa_enabled" boolean NOT NULL DEFAULT FALSE` + ) + await queryRunner.query(`ALTER TABLE "user_accounts" ADD "mfa_code" character varying`) + await queryRunner.query(` + UPDATE user_accounts + SET mfa_enabled = false + WHERE id IN + (SELECT id + FROM user_accounts + LEFT JOIN user_roles on user_accounts.id = user_roles.user_id WHERE user_roles.is_partner = true OR user_roles.is_admin = true)`) + + await queryRunner.query( + `ALTER TABLE "user_accounts" ADD "mfa_code_updated_at" TIMESTAMP WITH TIME ZONE` + ) + + const mfaCodeEmail = { + message: "Access token for your account has been requested.", + mfaCode: "Your access token is: %{mfaCode}", + } + const translations = await queryRunner.query(`SELECT * from translations`) + for (const t of translations) { + await queryRunner.query(`UPDATE translations SET translations = ($1) WHERE id = ($2)`, [ + { ...t.translations, mfaCodeEmail }, + t.id, + ]) + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "mfa_code_updated_at"`) + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "mfa_code"`) + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "mfa_enabled"`) + } +} diff --git a/backend/core/src/migration/1639135587230-add-partner-terms-to-jurisdiction.ts b/backend/core/src/migration/1639135587230-add-partner-terms-to-jurisdiction.ts new file mode 100644 index 0000000000..a4d140cfbd --- /dev/null +++ b/backend/core/src/migration/1639135587230-add-partner-terms-to-jurisdiction.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addPartnerTermsToJurisdiction1639135587230 implements MigrationInterface { + name = "addPartnerTermsToJurisdiction1639135587230" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "jurisdictions" ADD "partner_terms" text`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "jurisdictions" DROP COLUMN "partner_terms"`) + } +} diff --git a/backend/core/src/migration/1639417775347-favorites_user_preference_new.ts b/backend/core/src/migration/1639417775347-favorites_user_preference_new.ts new file mode 100644 index 0000000000..8708257a3c --- /dev/null +++ b/backend/core/src/migration/1639417775347-favorites_user_preference_new.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class favoritesUserPreferenceNew1639417775347 implements MigrationInterface { + name = "favoritesUserPreferenceNew1639417775347" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "user_preferences_favorites_listings" ("user_preferences_user_id" uuid NOT NULL, "listings_id" uuid NOT NULL, CONSTRAINT "PK_a2e38b75e1a538e046de2fba364" PRIMARY KEY ("user_preferences_user_id", "listings_id"))` + ) + await queryRunner.query( + `CREATE INDEX "IDX_0115bda0994ab10a4c1a883504" ON "user_preferences_favorites_listings" ("user_preferences_user_id") ` + ) + await queryRunner.query( + `CREATE INDEX "IDX_c971c586f08b7fe93fcaf29ec0" ON "user_preferences_favorites_listings" ("listings_id") ` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences_favorites_listings" ADD CONSTRAINT "FK_0115bda0994ab10a4c1a883504e" FOREIGN KEY ("user_preferences_user_id") REFERENCES "user_preferences"("user_id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences_favorites_listings" ADD CONSTRAINT "FK_c971c586f08b7fe93fcaf29ec05" FOREIGN KEY ("listings_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_preferences_favorites_listings" DROP CONSTRAINT "FK_c971c586f08b7fe93fcaf29ec05"` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences_favorites_listings" DROP CONSTRAINT "FK_0115bda0994ab10a4c1a883504e"` + ) + await queryRunner.query(`DROP INDEX "IDX_c971c586f08b7fe93fcaf29ec0"`) + await queryRunner.query(`DROP INDEX "IDX_0115bda0994ab10a4c1a883504"`) + await queryRunner.query(`DROP TABLE "user_preferences_favorites_listings"`) + } +} diff --git a/backend/core/src/migration/1639561971201-add-public-url-to-jurisdiction.ts b/backend/core/src/migration/1639561971201-add-public-url-to-jurisdiction.ts new file mode 100644 index 0000000000..bb7710e9c6 --- /dev/null +++ b/backend/core/src/migration/1639561971201-add-public-url-to-jurisdiction.ts @@ -0,0 +1,90 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { CountyCode } from "../shared/types/county-code" + +export class addPublicUrlToJurisdiction1639561971201 implements MigrationInterface { + name = "addPublicUrlToJurisdiction1639561971201" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "jurisdictions" ADD "public_url" text NOT NULL DEFAULT ''`) + const jurisdictions: Array<{ + id: string + name: string + public_url: string + }> = await queryRunner.query(`SELECT id, name, public_url from jurisdictions`) + + const alamedaJurisdiction = jurisdictions.find((j) => j.name === CountyCode.alameda) + const sanJoseJurisdiction = jurisdictions.find((j) => j.name === CountyCode.san_jose) + const sanMateoJurisdiction = jurisdictions.find((j) => j.name === CountyCode.san_mateo) + const detroitJurisdiction = jurisdictions.find((j) => j.name === CountyCode.detroit) + + if (process.env.PARTNERS_PORTAL_URL === "https://partners.housingbayarea.bloom.exygy.dev") { + // staging + if (alamedaJurisdiction) { + alamedaJurisdiction.public_url = "https://ala.bloom.exygy.dev" + } + if (sanJoseJurisdiction) { + sanJoseJurisdiction.public_url = "https://sj.bloom.exygy.dev" + } + if (sanMateoJurisdiction) { + sanMateoJurisdiction.public_url = "https://smc.bloom.exygy.dev" + } + } else if (process.env.PARTNERS_PORTAL_URL === "https://partners.housingbayarea.org") { + // production + if (alamedaJurisdiction) { + alamedaJurisdiction.public_url = "https://housing.acgov.org" + } + if (sanJoseJurisdiction) { + sanJoseJurisdiction.public_url = "https://housing.sanjoseca.gov" + } + if (sanMateoJurisdiction) { + sanMateoJurisdiction.public_url = "https://smc.housingbayarea.org" + } + } else if (process.env.PARTNERS_PORTAL_URL === "https://dev-partners-bloom.netlify.app") { + // dev + if (alamedaJurisdiction) { + alamedaJurisdiction.public_url = "https://dev-bloom.netlify.app" + } + if (sanJoseJurisdiction) { + sanJoseJurisdiction.public_url = "https://dev-bloom.netlify.app" + } + if (sanMateoJurisdiction) { + sanMateoJurisdiction.public_url = "https://dev-bloom.netlify.app" + } + if (detroitJurisdiction) { + detroitJurisdiction.public_url = "https://detroit-public-dev.netlify.app" + } + } else if (process.env.PARTNERS_PORTAL_URL === "http://localhost:3001") { + // local + if (alamedaJurisdiction) { + alamedaJurisdiction.public_url = "http://localhost:3000" + } + if (sanJoseJurisdiction) { + sanJoseJurisdiction.public_url = "http://localhost:3000" + } + if (sanMateoJurisdiction) { + sanMateoJurisdiction.public_url = "http://localhost:3000" + } + if (detroitJurisdiction) { + detroitJurisdiction.public_url = "http://localhost:3000" + } + } + + for (const jurisdiction of [ + alamedaJurisdiction, + sanJoseJurisdiction, + sanMateoJurisdiction, + detroitJurisdiction, + ]) { + if (jurisdiction) { + await queryRunner.query(`UPDATE jurisdictions SET public_url = $1 WHERE id = $2`, [ + jurisdiction.public_url, + jurisdiction.id, + ]) + } + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "jurisdictions" DROP COLUMN "public_url"`) + } +} diff --git a/backend/core/src/migration/1640110170049-seed-detroit-translation-entries.ts b/backend/core/src/migration/1640110170049-seed-detroit-translation-entries.ts new file mode 100644 index 0000000000..59ad77e8b2 --- /dev/null +++ b/backend/core/src/migration/1640110170049-seed-detroit-translation-entries.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { Language } from "../shared/types/language-enum" + +export class seedDetroitTranslationEntries1640110170049 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const listingEmail = { + newListing: { + title: "Rental opportunity at", + applicationDue: "Application Due", + addressLabel: "Address", + unitsLabel: "Units", + rentLabel: "Rent", + seeListingLabel: "See Listing And Apply", + dhcProjectLabel: "Detroit Home Connect is a project of the", + hrdLabel: "Housing & Revitalization Department of the City of Detroit", + unsubscribeMsg: "Unsubscribe from list", + }, + updateListing: { + title: "Reminder to update your listing", + verifyMsg: "Verify the following information is correct.", + listingLabel: "Listing", + addressLabel: "Address", + unitsLabel: "Units", + rentLabel: "Rent", + seeListingLabel: "See Listing", + dhcProjectLabel: "Detroit Home Connect is a project of the", + hrdLabel: "Housing & Revitalization Department of the City of Detroit", + unsubscribeMsg: "Unsubscribe from list", + }, + } + const translations = await queryRunner.query(`SELECT * from translations`) + for (const t of translations) { + await queryRunner.query(`UPDATE "translations" SET translations = ($1) WHERE id = ($2)`, [ + { ...t.translations, listingEmail }, + t.id, + ]) + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1640878095625-favoriteID.ts b/backend/core/src/migration/1640878095625-favoriteID.ts new file mode 100644 index 0000000000..2ee63690f5 --- /dev/null +++ b/backend/core/src/migration/1640878095625-favoriteID.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class favoriteID1640878095625 implements MigrationInterface { + name = "favoriteID1640878095625" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_preferences" ADD "favorite_ids" text array NOT NULL DEFAULT '{}'` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_preferences" DROP COLUMN "favorite_ids"`) + } +} diff --git a/backend/core/src/migration/1641855882656-remove-application-due-time.ts b/backend/core/src/migration/1641855882656-remove-application-due-time.ts new file mode 100644 index 0000000000..58aa398f93 --- /dev/null +++ b/backend/core/src/migration/1641855882656-remove-application-due-time.ts @@ -0,0 +1,44 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class removeApplicationDueTime1641855882656 implements MigrationInterface { + name = "removeApplicationDueTime1641855882656" + + public async up(queryRunner: QueryRunner): Promise { + const listings = await queryRunner.query( + `SELECT id, application_due_time, application_due_date from listings` + ) + + for (const listing of listings) { + let dateTimeString = null + + // If existing due date, pull in existing time or set time as 5pm + if (listing["application_due_date"]) { + let dueDate = new Date(listing["application_due_date"]) + if (listing["application_due_time"]) { + const dueTime = new Date(listing["application_due_time"]) + dueDate.setHours(dueTime.getHours()) + } else { + dueDate.setHours(17, 0, 0, 0) + } + // Format date into db input format + const modifiedDateString = dueDate.toISOString() + const timeDelimiter = modifiedDateString.indexOf("T") + const dateString = modifiedDateString.substr(0, timeDelimiter) + const timeString = modifiedDateString.substr(timeDelimiter + 1) + dateTimeString = `${dateString} ${timeString}` + } + + await queryRunner.query("UPDATE listings SET application_due_date = ($1) WHERE id = ($2)", [ + dateTimeString, + listing["id"], + ]) + } + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "application_due_time"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listings" ADD "application_due_time" TIMESTAMP WITH TIME ZONE` + ) + } +} diff --git a/backend/core/src/migration/1641860318345-add-mailing-address-type.ts b/backend/core/src/migration/1641860318345-add-mailing-address-type.ts new file mode 100644 index 0000000000..ef5d0ab282 --- /dev/null +++ b/backend/core/src/migration/1641860318345-add-mailing-address-type.ts @@ -0,0 +1,59 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addMailingAddressType1641860318345 implements MigrationInterface { + name = "addMailingAddressType1641860318345" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "listings_application_mailing_address_type_enum" AS ENUM('leasingAgent')` + ) + await queryRunner.query( + `ALTER TABLE "listings" ADD "application_mailing_address_type" "listings_application_mailing_address_type_enum"` + ) + await queryRunner.query( + `ALTER TYPE "listings_application_pick_up_address_type_enum" RENAME TO "listings_application_pick_up_address_type_enum_old"` + ) + await queryRunner.query( + `CREATE TYPE "listings_application_pick_up_address_type_enum" AS ENUM('leasingAgent')` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "application_pick_up_address_type" TYPE "listings_application_pick_up_address_type_enum" USING "application_pick_up_address_type"::"text"::"listings_application_pick_up_address_type_enum"` + ) + await queryRunner.query(`DROP TYPE "listings_application_pick_up_address_type_enum_old"`) + await queryRunner.query( + `ALTER TYPE "listings_application_drop_off_address_type_enum" RENAME TO "listings_application_drop_off_address_type_enum_old"` + ) + await queryRunner.query( + `CREATE TYPE "listings_application_drop_off_address_type_enum" AS ENUM('leasingAgent')` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "application_drop_off_address_type" TYPE "listings_application_drop_off_address_type_enum" USING "application_drop_off_address_type"::"text"::"listings_application_drop_off_address_type_enum"` + ) + await queryRunner.query(`DROP TYPE "listings_application_drop_off_address_type_enum_old"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "listings_application_drop_off_address_type_enum_old" AS ENUM('leasingAgent', 'mailingAddress')` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "application_drop_off_address_type" TYPE "listings_application_drop_off_address_type_enum_old" USING "application_drop_off_address_type"::"text"::"listings_application_drop_off_address_type_enum_old"` + ) + await queryRunner.query(`DROP TYPE "listings_application_drop_off_address_type_enum"`) + await queryRunner.query( + `ALTER TYPE "listings_application_drop_off_address_type_enum_old" RENAME TO "listings_application_drop_off_address_type_enum"` + ) + await queryRunner.query( + `CREATE TYPE "listings_application_pick_up_address_type_enum_old" AS ENUM('leasingAgent', 'mailingAddress')` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "application_pick_up_address_type" TYPE "listings_application_pick_up_address_type_enum_old" USING "application_pick_up_address_type"::"text"::"listings_application_pick_up_address_type_enum_old"` + ) + await queryRunner.query(`DROP TYPE "listings_application_pick_up_address_type_enum"`) + await queryRunner.query( + `ALTER TYPE "listings_application_pick_up_address_type_enum_old" RENAME TO "listings_application_pick_up_address_type_enum"` + ) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "application_mailing_address_type"`) + await queryRunner.query(`DROP TYPE "listings_application_mailing_address_type_enum"`) + } +} diff --git a/backend/core/src/migration/1643191027392-add-dates-to-listing.ts b/backend/core/src/migration/1643191027392-add-dates-to-listing.ts new file mode 100644 index 0000000000..d369cc0f19 --- /dev/null +++ b/backend/core/src/migration/1643191027392-add-dates-to-listing.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addDatesToListing1643191027392 implements MigrationInterface { + name = "addDatesToListing1643191027392" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" ADD "published_at" TIMESTAMP WITH TIME ZONE`) + await queryRunner.query(`ALTER TABLE "listings" ADD "closed_at" TIMESTAMP WITH TIME ZONE`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "closed_at"`) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "published_at"`) + } +} diff --git a/backend/core/src/migration/1644325106310-add-listings-closed-at-fill.ts b/backend/core/src/migration/1644325106310-add-listings-closed-at-fill.ts new file mode 100644 index 0000000000..2c524d8a04 --- /dev/null +++ b/backend/core/src/migration/1644325106310-add-listings-closed-at-fill.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addListingsClosedAtFill1644325106310 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE listings SET closed_at = application_due_date WHERE closed_at is null` + ) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1644365733034-add-custom-email-from.ts b/backend/core/src/migration/1644365733034-add-custom-email-from.ts new file mode 100644 index 0000000000..5528f6b252 --- /dev/null +++ b/backend/core/src/migration/1644365733034-add-custom-email-from.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { CountyCode } from "../shared/types/county-code" + +export class addCustomEmailFrom1644365733034 implements MigrationInterface { + name = "addCustomEmailFrom1644365733034" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "jurisdictions" ADD "email_from_address" text`) + const jurisdictions: Array<{ + id: string + name: string + public_url: string + }> = await queryRunner.query(`SELECT id, name, public_url from jurisdictions`) + + const setEmailFromAddress = async (emailFromAddress: string, countyCode: CountyCode) => { + const jurisdiction = jurisdictions.find((j) => j.name === countyCode) + if (jurisdiction) { + await queryRunner.query(`UPDATE jurisdictions SET email_from_address = $1 WHERE id = $2`, [ + emailFromAddress, + jurisdiction.id, + ]) + } + } + + setEmailFromAddress("Alameda: Housing Bay Area ", CountyCode.alameda) + setEmailFromAddress("SJ: HousingBayArea.org ", CountyCode.san_jose) + setEmailFromAddress("SMC: HousingBayArea.org ", CountyCode.san_mateo) + setEmailFromAddress("Detroit Housing ", CountyCode.detroit) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "jurisdictions" DROP COLUMN "email_from_address"`) + } +} diff --git a/backend/core/src/migration/1644441969354-addPhoneNumberVerified.ts b/backend/core/src/migration/1644441969354-addPhoneNumberVerified.ts new file mode 100644 index 0000000000..6fc6978800 --- /dev/null +++ b/backend/core/src/migration/1644441969354-addPhoneNumberVerified.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addPhoneNumberVerified1644441969354 implements MigrationInterface { + name = "addPhoneNumberVerified1644441969354" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_accounts" ADD COLUMN "phone_number_verified" BOOLEAN DEFAULT FALSE` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "phone_number_verified"`) + } +} diff --git a/backend/core/src/migration/1644581761889-convert-listing-image-to-array.ts b/backend/core/src/migration/1644581761889-convert-listing-image-to-array.ts new file mode 100644 index 0000000000..da6cea92da --- /dev/null +++ b/backend/core/src/migration/1644581761889-convert-listing-image-to-array.ts @@ -0,0 +1,48 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class convertListingImageToArray1644581761889 implements MigrationInterface { + name = "convertListingImageToArray1644581761889" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listings" DROP CONSTRAINT "FK_ecc271b96bd18df0efe47b85186"` + ) + await queryRunner.query( + `CREATE TABLE "listing_images" ("ordinal" integer, "listing_id" uuid NOT NULL, "image_id" uuid NOT NULL, CONSTRAINT "PK_beb1c8e9f64f578908135aa6899" PRIMARY KEY ("listing_id", "image_id"))` + ) + await queryRunner.query( + `CREATE INDEX "IDX_94041359df3c1b14c4420808d1" ON "listing_images" ("listing_id") ` + ) + await queryRunner.query( + `ALTER TABLE "listing_images" ADD CONSTRAINT "FK_94041359df3c1b14c4420808d16" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "listing_images" ADD CONSTRAINT "FK_6fc0fefe11fb46d5ee863ed483a" FOREIGN KEY ("image_id") REFERENCES "assets"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + const listings: [{ id: string; image_id: string }] = await queryRunner.query( + `SELECT id, image_id FROM listings where image_id is not null` + ) + for (const l of listings) { + await queryRunner.query(`INSERT INTO listing_images (listing_id, image_id) VALUES ($1, $2)`, [ + l.id, + l.image_id, + ]) + } + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "image_id"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listing_images" DROP CONSTRAINT "FK_6fc0fefe11fb46d5ee863ed483a"` + ) + await queryRunner.query( + `ALTER TABLE "listing_images" DROP CONSTRAINT "FK_94041359df3c1b14c4420808d16"` + ) + await queryRunner.query(`ALTER TABLE "listings" ADD "image_id" uuid`) + await queryRunner.query(`DROP INDEX "IDX_94041359df3c1b14c4420808d1"`) + await queryRunner.query(`DROP TABLE "listing_images"`) + await queryRunner.query( + `ALTER TABLE "listings" ADD CONSTRAINT "FK_ecc271b96bd18df0efe47b85186" FOREIGN KEY ("image_id") REFERENCES "assets"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } +} diff --git a/backend/core/src/migration/1645001562848-make-translations-one-to-many-with-jurisdiction.ts b/backend/core/src/migration/1645001562848-make-translations-one-to-many-with-jurisdiction.ts new file mode 100644 index 0000000000..d68c5c8714 --- /dev/null +++ b/backend/core/src/migration/1645001562848-make-translations-one-to-many-with-jurisdiction.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class makeTranslationsOneToManyWithJurisdiction1645001562848 implements MigrationInterface { + name = "makeTranslationsOneToManyWithJurisdiction1645001562848" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "translations" DROP CONSTRAINT "FK_181f8168d13457f0fd00b08b359"` + ) + await queryRunner.query(`DROP INDEX "IDX_4655e7b2c26deb4b8156ea8100"`) + await queryRunner.query( + `ALTER TABLE "translations" DROP CONSTRAINT "UQ_181f8168d13457f0fd00b08b359"` + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_4655e7b2c26deb4b8156ea8100" ON "translations" ("jurisdiction_id", "language") ` + ) + await queryRunner.query( + `ALTER TABLE "translations" ADD CONSTRAINT "FK_181f8168d13457f0fd00b08b359" FOREIGN KEY ("jurisdiction_id") REFERENCES "jurisdictions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "translations" DROP CONSTRAINT "FK_181f8168d13457f0fd00b08b359"` + ) + await queryRunner.query(`DROP INDEX "IDX_4655e7b2c26deb4b8156ea8100"`) + await queryRunner.query( + `ALTER TABLE "translations" ADD CONSTRAINT "UQ_181f8168d13457f0fd00b08b359" UNIQUE ("jurisdiction_id")` + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_4655e7b2c26deb4b8156ea8100" ON "translations" ("language", "jurisdiction_id") ` + ) + await queryRunner.query( + `ALTER TABLE "translations" ADD CONSTRAINT "FK_181f8168d13457f0fd00b08b359" FOREIGN KEY ("jurisdiction_id") REFERENCES "jurisdictions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } +} diff --git a/backend/core/src/migration/1645051898243-updateSanJoseEmailTranslations.ts b/backend/core/src/migration/1645051898243-updateSanJoseEmailTranslations.ts new file mode 100644 index 0000000000..19cf4b7a92 --- /dev/null +++ b/backend/core/src/migration/1645051898243-updateSanJoseEmailTranslations.ts @@ -0,0 +1,207 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { Language } from "../shared/types/language-enum" + +export class updateSanJoseEmailTranslations1645051898243 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // First update the existing English translation for San Jose: + let sanJoseJurisdiction = await queryRunner.query( + `SELECT id FROM jurisdictions WHERE name = 'San Jose' LIMIT 1` + ) + + if (sanJoseJurisdiction.length === 0) return + + sanJoseJurisdiction = sanJoseJurisdiction[0].id + + let sanJoseTranslation = await queryRunner.query( + `SELECT translations FROM translations WHERE jurisdiction_id = ($1) AND language = ($2)`, + [sanJoseJurisdiction, Language.en] + ) + sanJoseTranslation = sanJoseTranslation["0"]["translations"] + + if (!sanJoseTranslation.confirmation) sanJoseTranslation.confirmation = {} + sanJoseTranslation.confirmation.thankYouForApplying = + "Thanks for applying for housing from the San Jose Doorway Portal. We have received your application for" + ;(sanJoseTranslation.footer.thankYou = "Thanks!"), + await queryRunner.query( + `UPDATE "translations" SET translations = ($1) where jurisdiction_id = ($2) and language = ($3)`, + [sanJoseTranslation, sanJoseJurisdiction, Language.en] + ) + + // Now add additional translations + + // Spanish + let sanJoseSpanish = await queryRunner.query( + `SELECT translations FROM translations WHERE jurisdiction_id = ($1) AND language = ($2)`, + [sanJoseJurisdiction, Language.es] + ) + if (sanJoseSpanish.length === 0) { + sanJoseSpanish = { + t: { + hello: "Hola", + }, + confirmation: { + yourConfirmationNumber: "Aquí tiene su número de confirmación:", + shouldBeChosen: + "Si su solicitud es elegida, prepárese para llenar una solicitud más detallada y proporcionar los documentos de apoyo necesarios.", + subject: "Confirmación de su solicitud", + thankYouForApplying: + "Gracias por solicitar una vivienda desde el Portal de San José. Hemos recibido su solicitud para", + whatToExpectNext: "Qué esperar a continuación:", + whatToExpect: { + FCFS: + "El agente inmobiliario se pondrá en contacto con los solicitantes por orden de llegada hasta que se cubran las vacantes.", + noLottery: + "Los solicitantes serán contactados por el agente en orden de lista de espera hasta que se cubran las vacantes.", + }, + }, + leasingAgent: { + contactAgentToUpdateInfo: + "Si necesita actualizar la información de su solicitud, no vuelva a presentarla. Póngase en contacto con el agente. Vea a continuación la información de contacto del agente para este listado.", + officeHours: "Horario de oficina:", + }, + footer: { + footer: "Ciudad de San José, Departamento de Vivienda", + thankYou: "¡Gracias!", + }, + register: { + confirmMyAccount: "Confirmar mi cuenta", + toConfirmAccountMessage: + "Para completar la creación de su cuenta, haga clic en el siguiente enlace:", + welcome: "¡Bienvenido!", + welcomeMessage: + "Gracias por crear su cuenta en %{appUrl}. Ahora le será más fácil iniciar, guardar y enviar solicitudes en línea para los listados que aparecen en el sitio.", + }, + forgotPassword: { + callToAction: + "Si usted hizo esta solicitud, haga clic en el enlace de abajo para restablecer su contraseña:", + changePassword: "Cambiar mi contraseña", + ignoreRequest: "Si usted no lo solicitó, ignore este correo electrónico.", + passwordInfo: + "Su contraseña no cambiará hasta que acceda al enlace anterior y cree una nueva.", + resetRequest: + "Recientemente se ha solicitado el restablecimiento de su contraseña del sitio web del Portal de Vivienda Bloom para %{appUrl}.", + subject: "Forgot your password?", + }, + } + await queryRunner.query( + `INSERT into "translations" (jurisdiction_id, language, translations) VALUES ($1, $2, $3)`, + [sanJoseJurisdiction, Language.es, sanJoseSpanish] + ) + } + + // Vietnamese + let sanJoseVietnamese = await queryRunner.query( + `SELECT translations FROM translations WHERE jurisdiction_id = ($1) AND language = ($2)`, + [sanJoseJurisdiction, Language.vi] + ) + if (sanJoseVietnamese.length === 0) { + sanJoseVietnamese = { + t: { + hello: "Xin chào", + }, + confirmation: { + yourConfirmationNumber: "Đây là số xác nhận của bạn:", + shouldBeChosen: + "Nếu đơn đăng ký của bạn được chọn, hãy chuẩn bị để điền vào đơn đăng ký chi tiết hơn và cung cấp các tài liệu hỗ trợ cần thiết.", + subject: "Xác Nhận Đơn Đăng Ký Của Bạn", + thankYouForApplying: + "Cảm ơn bạn đã nộp đơn xin gia cư từ Cổng thông tin San Jose Doorway. Chúng tôi đã nhận được đơn đăng ký của bạn cho", + whatToExpectNext: "Điều gì sẽ xảy ra tiếp theo:", + whatToExpect: { + FCFS: + "Các ứng viên sẽ được đại lý tài sản liên hệ trên cơ sở ai đến trước phục vụ trước cho đến khi các chỗ trống được chiếm ngụ.", + noLottery: + "Các ứng viên sẽ được đại diện liên hệ theo thứ tự trong danh sách chờ cho đến khi các chỗ trống được chiếm ngụ.", + }, + }, + leasingAgent: { + contactAgentToUpdateInfo: + "Nếu bạn cần cập nhật thông tin trên đơn đăng ký của mình, thì đừng đăng ký lại. Liên hệ với đại lý. Xem bên dưới để biết thông tin liên hệ với Đại lý cho danh sách này.", + officeHours: "Giờ Hành Chính:", + }, + footer: { + footer: "Thành Phố San José, Sở Gia Cư", + thankYou: "Cảm ơn!", + }, + register: { + confirmMyAccount: "Xác nhận tài khoản của tôi", + toConfirmAccountMessage: + "Để hoàn tất việc tạo tài khoản của bạn, vui lòng nhấp vào liên kết bên dưới:", + welcome: "Chào mừng!", + welcomeMessage: + "Cảm ơn bạn đã thiết lập tài khoản của mình trên %{appUrl}. Giờ đây bạn sẽ dễ dàng hơn khi bắt đầu, lưu và gửi đơn đăng ký trực tuyến cho các danh sách có trên trang web.", + }, + forgotPassword: { + callToAction: + "Nếu bạn thực hiện yêu cầu này, vui lòng nhấp vào liên kết bên dưới để đặt lại mật khẩu của bạn:", + changePassword: "Thay đổi mật khẩu của tôi", + ignoreRequest: "Nếu bạn không yêu cầu điều này, vui lòng bỏ qua email này.", + passwordInfo: + "Mật khẩu của bạn sẽ không thay đổi cho đến khi bạn truy cập vào liên kết ở trên và tạo một mật khẩu mới.", + resetRequest: + "Yêu cầu đặt lại mật khẩu trang web Bloom Housing Portal của bạn cho %{appUrl} gần đây đã được thực hiện.", + subject: "Forgot your password?", + }, + } + await queryRunner.query( + `INSERT into "translations" (jurisdiction_id, language, translations) VALUES ($1, $2, $3)`, + [sanJoseJurisdiction, Language.vi, sanJoseVietnamese] + ) + } + + // Chinese + let sanJoseChinese = await queryRunner.query( + `SELECT translations FROM translations WHERE jurisdiction_id = ($1) AND language = ($2)`, + [sanJoseJurisdiction, Language.zh] + ) + if (sanJoseChinese.length === 0) { + sanJoseChinese = { + t: { + hello: "您好", + }, + confirmation: { + yourConfirmationNumber: "這是您的確認號碼:", + shouldBeChosen: + "如果選中您的申請表,請準備填寫一份更詳細的申請表,並提供所需的證明文件。", + subject: "您的申請確認", + thankYouForApplying: "感謝您透過聖荷西門戶網站申請住房。我們收到了您關於 的申請", + whatToExpectNext: "後續流程:", + whatToExpect: { + FCFS: "房地產代理人會以先到先得的方式聯繫申請人,直到額滿為止。", + noLottery: "代理人會按照候補名單順序聯繫申請人,直到額滿為止。", + }, + }, + leasingAgent: { + contactAgentToUpdateInfo: + "如果您需要更新申請資訊,請勿再次申請。請聯繫租賃代理人。請參閱本物業清單所列代理人的聯繫資訊。", + officeHours: "辦公時間:", + }, + footer: { + footer: "聖荷西市住房局", + thankYou: "謝謝您!", + }, + register: { + confirmMyAccount: "聖荷西市住房局", + toConfirmAccountMessage: "要完成建立帳戶,請按以下連結:", + welcome: "歡迎!", + welcomeMessage: + "感謝您在%{appUrl}建立帳戶。現在,您可以更輕鬆地針對網站列出的物業清單建立、儲存及提交線上申請。", + }, + forgotPassword: { + callToAction: "如果您確實提出請求,請按下方連結重設密碼:", + changePassword: "變更密碼", + ignoreRequest: "如果您沒有提出請求,請勿理會本電子郵件。", + passwordInfo: "在您按上方連結並建立一個新密碼之前,您的密碼不會變更。", + resetRequest: "最近我們收到了您為%{appUrl}重設Bloom住房門戶網站密碼的請求。", + subject: "Forgot your password?", + }, + } + await queryRunner.query( + `INSERT into "translations" (jurisdiction_id, language, translations) VALUES ($1, $2, $3)`, + [sanJoseJurisdiction, Language.zh, sanJoseChinese] + ) + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1645110873807-setProdPublishDates.ts b/backend/core/src/migration/1645110873807-setProdPublishDates.ts new file mode 100644 index 0000000000..ec04ca06b2 --- /dev/null +++ b/backend/core/src/migration/1645110873807-setProdPublishDates.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class setProdPublishDates1645110873807 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const toUpdate = [ + { name: "The Mix at SoHay", date: "2021-03-10T00:16:00.000Z" }, + { name: "City Center Apartments", date: "2021-05-14T00:16:00.000Z" }, + { name: "Coliseum Place", date: "2021-06-02T00:16:00.000Z" }, + { name: "1475 167th Avenue", date: "2021-06-15T00:16:00.000Z" }, + { name: "Jordan Court", date: "2021-08-25T00:16:00.000Z" }, + { name: "Foon Lok West", date: "2021-10-04T00:16:00.000Z" }, + { name: "Alexan Webster", date: "2021-07-22T00:16:00.000Z" }, + { name: "Blake at Berkeley", date: "2021-10-18T00:16:00.000Z" }, + { name: "Nova", date: "2021-06-02T00:16:00.000Z" }, + { name: "Aurora", date: "2021-06-02T00:16:00.000Z" }, + { name: "Loro Landing", date: "2021-09-27T00:16:00.000Z" }, + { name: "The Starling", date: "2021-10-04T00:16:00.000Z" }, + { name: "Corsair Flats II for Seniors 62+", date: "2022-01-05T00:16:00.000Z" }, + { name: "Rosefield Village", date: "2022-01-07T00:16:00.000Z" }, + { name: "Berkeley Way", date: "2022-02-08T00:16:00.000Z" }, + { name: "Reilly Station", date: "2020-07-30T00:16:00.000Z" }, + { name: "Monarch Homes", date: "2020-09-24T00:16:00.000Z" }, + { name: "Atlas", date: "2020-09-08T00:16:00.000Z" }, + { name: "Jones Berkeley", date: "2020-09-15T00:16:00.000Z" }, + { name: "The Logan", date: "2020-10-15T00:16:00.000Z" }, + { name: "The Skylyne at Temescal 2", date: "2020-10-06T00:16:00.000Z" }, + { name: "Avance", date: "2021-09-30T00:16:00.000Z" }, + ] + + for (const rec of toUpdate) { + await queryRunner.query(`UPDATE listings SET published_at = $1 WHERE name = $2`, [ + rec.date, + rec.name, + ]) + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1645640266273-add-units-summary-ami-levels-entity.ts b/backend/core/src/migration/1645640266273-add-units-summary-ami-levels-entity.ts new file mode 100644 index 0000000000..b7dd43f5d2 --- /dev/null +++ b/backend/core/src/migration/1645640266273-add-units-summary-ami-levels-entity.ts @@ -0,0 +1,88 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addUnitsSummaryAmiLevelsEntity1645640266273 implements MigrationInterface { + name = "addUnitsSummaryAmiLevelsEntity1645640266273" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE units_summary RENAME TO unit_group`) + await queryRunner.query( + `CREATE TYPE "unit_group_ami_levels_monthly_rent_determination_type_enum" AS ENUM('flatRent', 'percentageOfIncome')` + ) + await queryRunner.query( + `CREATE TABLE "unit_group_ami_levels" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ami_percentage" integer NOT NULL, "monthly_rent_determination_type" "unit_group_ami_levels_monthly_rent_determination_type_enum" NOT NULL, "flat_rent_value" numeric(8,2), "percentage_of_income_value" integer, "ami_chart_id" uuid, "unit_group_id" uuid, CONSTRAINT "PK_4b540cae0d35b199c0448610378" PRIMARY KEY ("id"))` + ) + await queryRunner.query( + `CREATE TABLE "unit_group_unit_type_unit_types" ("unit_group_id" uuid NOT NULL, "unit_types_id" uuid NOT NULL, CONSTRAINT "PK_4f2d90a894495a3cb72e5f0d2c8" PRIMARY KEY ("unit_group_id", "unit_types_id"))` + ) + await queryRunner.query( + `CREATE INDEX "IDX_1951c380e8091486b980008886" ON "unit_group_unit_type_unit_types" ("unit_group_id") ` + ) + await queryRunner.query( + `CREATE INDEX "IDX_b905b8bda3171b06c7a5d4d671" ON "unit_group_unit_type_unit_types" ("unit_types_id") ` + ) + await queryRunner.query( + `ALTER TABLE "unit_group" DROP COLUMN "monthly_rent_as_percent_of_income"` + ) + await queryRunner.query(`ALTER TABLE "unit_group" DROP COLUMN "ami_percentage"`) + await queryRunner.query(`ALTER TABLE "unit_group" DROP COLUMN "minimum_income_min"`) + await queryRunner.query(`ALTER TABLE "unit_group" DROP COLUMN "minimum_income_max"`) + await queryRunner.query(`ALTER TABLE "unit_group" DROP COLUMN "unit_type_id"`) + await queryRunner.query(`ALTER TABLE "unit_group" DROP COLUMN "monthly_rent_min"`) + await queryRunner.query(`ALTER TABLE "unit_group" DROP COLUMN "monthly_rent_max"`) + await queryRunner.query(`ALTER TABLE "unit_group" ADD "bathroom_min" integer`) + await queryRunner.query(`ALTER TABLE "unit_group" ADD "bathroom_max" integer`) + await queryRunner.query( + `ALTER TABLE "unit_group" ADD "open_waitlist" boolean NOT NULL DEFAULT true` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ADD CONSTRAINT "FK_859a749beeb93898cfe3aa318e7" FOREIGN KEY ("ami_chart_id") REFERENCES "ami_chart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ADD CONSTRAINT "FK_c15eff18d0384540366861a1c9c" FOREIGN KEY ("unit_group_id") REFERENCES "unit_group"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_unit_type_unit_types" ADD CONSTRAINT "FK_1951c380e8091486b9800088865" FOREIGN KEY ("unit_group_id") REFERENCES "unit_group"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_unit_type_unit_types" ADD CONSTRAINT "FK_b905b8bda3171b06c7a5d4d6712" FOREIGN KEY ("unit_types_id") REFERENCES "unit_types"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE unit_group RENAME TO units_summary`) + await queryRunner.query( + `ALTER TABLE "units_summary_unit_type_unit_types" DROP CONSTRAINT "FK_b905b8bda3171b06c7a5d4d6712"` + ) + await queryRunner.query( + `ALTER TABLE "units_summary_unit_type_unit_types" DROP CONSTRAINT "FK_1951c380e8091486b9800088865"` + ) + await queryRunner.query( + `ALTER TABLE "units_summary_ami_levels" DROP CONSTRAINT "FK_c15eff18d0384540366861a1c9c"` + ) + await queryRunner.query( + `ALTER TABLE "units_summary_ami_levels" DROP CONSTRAINT "FK_859a749beeb93898cfe3aa318e7"` + ) + await queryRunner.query(`ALTER TABLE "units_summary" DROP COLUMN "open_waitlist"`) + await queryRunner.query(`ALTER TABLE "units_summary" DROP COLUMN "bathroom_max"`) + await queryRunner.query(`ALTER TABLE "units_summary" DROP COLUMN "bathroom_min"`) + await queryRunner.query(`ALTER TABLE "units_summary" ADD "monthly_rent_max" integer`) + await queryRunner.query(`ALTER TABLE "units_summary" ADD "monthly_rent_min" integer`) + await queryRunner.query(`ALTER TABLE "units_summary" ADD "unit_type_id" uuid`) + await queryRunner.query(`ALTER TABLE "units_summary" ADD "minimum_income_max" text`) + await queryRunner.query(`ALTER TABLE "units_summary" ADD "minimum_income_min" text`) + await queryRunner.query(`ALTER TABLE "units_summary" ADD "ami_percentage" integer`) + await queryRunner.query( + `ALTER TABLE "units_summary" ADD "monthly_rent_as_percent_of_income" numeric(8,2)` + ) + await queryRunner.query(`DROP INDEX "IDX_b905b8bda3171b06c7a5d4d671"`) + await queryRunner.query(`DROP INDEX "IDX_1951c380e8091486b980008886"`) + await queryRunner.query(`DROP TABLE "units_summary_unit_type_unit_types"`) + await queryRunner.query(`DROP TABLE "units_summary_ami_levels"`) + await queryRunner.query( + `DROP TYPE "units_summary_ami_levels_monthly_rent_determination_type_enum"` + ) + await queryRunner.query( + `ALTER TABLE "units_summary" ADD CONSTRAINT "FK_0eae6ec11a6109496d80d9a88f9" FOREIGN KEY ("unit_type_id") REFERENCES "unit_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } +} diff --git a/backend/core/src/migration/1646350628943-detroitAmiCharts.ts b/backend/core/src/migration/1646350628943-detroitAmiCharts.ts new file mode 100644 index 0000000000..3ba33a89f4 --- /dev/null +++ b/backend/core/src/migration/1646350628943-detroitAmiCharts.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { HUD2021 } from "../seeder/seeds/ami-charts/HUD2021" +import { MSHDA2021 } from "../seeder/seeds/ami-charts/MSHDA2021" + +export class detroitAmiCharts1646350628943 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const [{ id }] = await queryRunner.query(`SELECT id FROM jurisdictions WHERE name = 'Detroit'`) + + await queryRunner.query( + `INSERT INTO ami_chart + (name, items, jurisdiction_id) + VALUES ('${HUD2021.name}', '${JSON.stringify(HUD2021.items)}', '${id}') + ` + ) + + await queryRunner.query( + `INSERT INTO ami_chart + (name, items, jurisdiction_id) + VALUES ('${MSHDA2021.name}', '${JSON.stringify(MSHDA2021.items)}', '${id}') + ` + ) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1646422264825-addCommunityPrograms.ts b/backend/core/src/migration/1646422264825-addCommunityPrograms.ts new file mode 100644 index 0000000000..5c63eeec9b --- /dev/null +++ b/backend/core/src/migration/1646422264825-addCommunityPrograms.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addCommunityPrograms1646422264825 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const [{ id }] = await queryRunner.query(`SELECT id FROM jurisdictions WHERE name = 'Detroit'`) + + await queryRunner.query( + `INSERT INTO programs (title, description) + VALUES + ('Seniors 55+', 'This property offers housing for residents ages 55 and older.'), + ('Seniors 62+', 'This property offers housing for residents ages 62 and older.'), + ('Residents with Disabilities', 'This property has reserved a large portion of its units for residents with disabilities. Contact this property to see if you qualify.'), + ('Families', 'This property offers housing for families. Ask the property if there are additional requirements to qualify as a family.'), + ('Supportive Housing for the Homeless', 'This property offers housing for those experiencing homelessness, and may require additional processes that applicants need to go through in order to qualify.'), + ('Veterans', 'This property offers housing for those who have served in the military, naval, or air service.') + ` + ) + + const res = await queryRunner.query( + `SELECT id from programs WHERE title in ('Seniors 55+', 'Seniors 62+', 'Residents with Disabilities', 'Families', 'Supportive Housing for the Homeless', 'Veterans')` + ) + + for (const program of res) { + await queryRunner.query( + `INSERT INTO jurisdictions_programs_programs (jurisdictions_id, programs_id) + VALUES ($1, $2) + `, + [id, program.id] + ) + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1646642069064-add-new-listing-accessibility-features.ts b/backend/core/src/migration/1646642069064-add-new-listing-accessibility-features.ts new file mode 100644 index 0000000000..8ec2548094 --- /dev/null +++ b/backend/core/src/migration/1646642069064-add-new-listing-accessibility-features.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addNewListingAccessibilityFeatures1646642069064 implements MigrationInterface { + name = "addNewListingAccessibilityFeatures1646642069064" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listing_features" ADD "hearing" boolean`) + await queryRunner.query(`ALTER TABLE "listing_features" ADD "visual" boolean`) + await queryRunner.query(`ALTER TABLE "listing_features" ADD "mobility" boolean`) + await queryRunner.query(`ALTER TABLE "listings" ADD "temporary_listing_id" integer`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "temporary_listing_id"`) + await queryRunner.query(`ALTER TABLE "listing_features" DROP COLUMN "mobility"`) + await queryRunner.query(`ALTER TABLE "listing_features" DROP COLUMN "visual"`) + await queryRunner.query(`ALTER TABLE "listing_features" DROP COLUMN "hearing"`) + } +} diff --git a/backend/core/src/migration/1646847820396-addIsVerified.ts b/backend/core/src/migration/1646847820396-addIsVerified.ts new file mode 100644 index 0000000000..483f0b0e32 --- /dev/null +++ b/backend/core/src/migration/1646847820396-addIsVerified.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addIsVerified1646847820396 implements MigrationInterface { + name = "addIsVerified1646847820396" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" ADD "is_verified" boolean DEFAULT false`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "is_verified"`) + } +} diff --git a/backend/core/src/migration/1646866933450-sqFootToNumber.ts b/backend/core/src/migration/1646866933450-sqFootToNumber.ts new file mode 100644 index 0000000000..f10639c8fd --- /dev/null +++ b/backend/core/src/migration/1646866933450-sqFootToNumber.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class sqFootToNumber1646866933450 implements MigrationInterface { + name = "sqFootToNumber1646866933450" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "unit_group" DROP COLUMN "sq_feet_min"`) + await queryRunner.query(`ALTER TABLE "unit_group" ADD "sq_feet_min" integer`) + await queryRunner.query(`ALTER TABLE "unit_group" DROP COLUMN "sq_feet_max"`) + await queryRunner.query(`ALTER TABLE "unit_group" ADD "sq_feet_max" integer`) + await queryRunner.query(`ALTER TABLE "unit_group_ami_levels" DROP COLUMN "flat_rent_value"`) + await queryRunner.query(`ALTER TABLE "unit_group_ami_levels" ADD "flat_rent_value" integer`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "unit_group" DROP COLUMN "sq_feet_max"`) + await queryRunner.query(`ALTER TABLE "unit_group" ADD "sq_feet_max" numeric(8,2)`) + await queryRunner.query(`ALTER TABLE "unit_group" DROP COLUMN "sq_feet_min"`) + await queryRunner.query(`ALTER TABLE "unit_group" ADD "sq_feet_min" numeric(8,2)`) + await queryRunner.query(`ALTER TABLE "unit_group_ami_levels" DROP COLUMN "flat_rent_value"`) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ADD "flat_rent_value" numeric(8,2)` + ) + } +} diff --git a/backend/core/src/migration/1646872355286-catch-up.ts b/backend/core/src/migration/1646872355286-catch-up.ts new file mode 100644 index 0000000000..7943e93b46 --- /dev/null +++ b/backend/core/src/migration/1646872355286-catch-up.ts @@ -0,0 +1,99 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class catchUp1646872355286 implements MigrationInterface { + name = "catchUp1646872355286" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" DROP CONSTRAINT "FK_c15eff18d0384540366861a1c9c"` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" DROP CONSTRAINT "FK_859a749beeb93898cfe3aa318e7"` + ) + await queryRunner.query( + `ALTER TABLE "unit_group" DROP CONSTRAINT "FK_4edda29192dbc0c6a18e15437a0"` + ) + await queryRunner.query( + `ALTER TABLE "unit_group" DROP CONSTRAINT "FK_4791099ef82551aa9819a71d8f5"` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_unit_type_unit_types" DROP CONSTRAINT "FK_b905b8bda3171b06c7a5d4d6712"` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_unit_type_unit_types" DROP CONSTRAINT "FK_1951c380e8091486b9800088865"` + ) + await queryRunner.query(`DROP INDEX "IDX_1951c380e8091486b980008886"`) + await queryRunner.query(`DROP INDEX "IDX_b905b8bda3171b06c7a5d4d671"`) + await queryRunner.query( + `CREATE INDEX "IDX_1ea90313ee94f48800e9eef751" ON "unit_group_unit_type_unit_types" ("unit_group_id") ` + ) + await queryRunner.query( + `CREATE INDEX "IDX_0cf027359361dfd394f08686da" ON "unit_group_unit_type_unit_types" ("unit_types_id") ` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ADD CONSTRAINT "FK_ff3f8de67facd164607f1ef43ae" FOREIGN KEY ("ami_chart_id") REFERENCES "ami_chart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ADD CONSTRAINT "FK_ce82398e48c10dc23920c6ff05a" FOREIGN KEY ("unit_group_id") REFERENCES "unit_group"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "unit_group" ADD CONSTRAINT "FK_926790e4013043593a3976d84bd" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "unit_group" ADD CONSTRAINT "FK_e2660f5da2ff575954d765d920b" FOREIGN KEY ("priority_type_id") REFERENCES "unit_accessibility_priority_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_unit_type_unit_types" ADD CONSTRAINT "FK_1ea90313ee94f48800e9eef751e" FOREIGN KEY ("unit_group_id") REFERENCES "unit_group"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_unit_type_unit_types" ADD CONSTRAINT "FK_0cf027359361dfd394f08686da2" FOREIGN KEY ("unit_types_id") REFERENCES "unit_types"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "unit_group_unit_type_unit_types" DROP CONSTRAINT "FK_0cf027359361dfd394f08686da2"` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_unit_type_unit_types" DROP CONSTRAINT "FK_1ea90313ee94f48800e9eef751e"` + ) + await queryRunner.query( + `ALTER TABLE "unit_group" DROP CONSTRAINT "FK_e2660f5da2ff575954d765d920b"` + ) + await queryRunner.query( + `ALTER TABLE "unit_group" DROP CONSTRAINT "FK_926790e4013043593a3976d84bd"` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" DROP CONSTRAINT "FK_ce82398e48c10dc23920c6ff05a"` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" DROP CONSTRAINT "FK_ff3f8de67facd164607f1ef43ae"` + ) + await queryRunner.query(`DROP INDEX "IDX_0cf027359361dfd394f08686da"`) + await queryRunner.query(`DROP INDEX "IDX_1ea90313ee94f48800e9eef751"`) + await queryRunner.query( + `CREATE INDEX "IDX_b905b8bda3171b06c7a5d4d671" ON "unit_group_unit_type_unit_types" ("unit_types_id") ` + ) + await queryRunner.query( + `CREATE INDEX "IDX_1951c380e8091486b980008886" ON "unit_group_unit_type_unit_types" ("unit_group_id") ` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_unit_type_unit_types" ADD CONSTRAINT "FK_1951c380e8091486b9800088865" FOREIGN KEY ("unit_group_id") REFERENCES "unit_group"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_unit_type_unit_types" ADD CONSTRAINT "FK_b905b8bda3171b06c7a5d4d6712" FOREIGN KEY ("unit_types_id") REFERENCES "unit_types"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ) + await queryRunner.query( + `ALTER TABLE "unit_group" ADD CONSTRAINT "FK_4791099ef82551aa9819a71d8f5" FOREIGN KEY ("priority_type_id") REFERENCES "unit_accessibility_priority_types"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "unit_group" ADD CONSTRAINT "FK_4edda29192dbc0c6a18e15437a0" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ADD CONSTRAINT "FK_859a749beeb93898cfe3aa318e7" FOREIGN KEY ("ami_chart_id") REFERENCES "ami_chart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ADD CONSTRAINT "FK_c15eff18d0384540366861a1c9c" FOREIGN KEY ("unit_group_id") REFERENCES "unit_group"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } +} diff --git a/backend/core/src/migration/1646908736734-make-unit-group-ami-level-ami-percentage-optional.ts b/backend/core/src/migration/1646908736734-make-unit-group-ami-level-ami-percentage-optional.ts new file mode 100644 index 0000000000..783b372b3a --- /dev/null +++ b/backend/core/src/migration/1646908736734-make-unit-group-ami-level-ami-percentage-optional.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class makeUnitGroupAmiLevelAmiPercentageOptional1646908736734 implements MigrationInterface { + name = "makeUnitGroupAmiLevelAmiPercentageOptional1646908736734" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ALTER COLUMN "ami_percentage" DROP NOT NULL` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ALTER COLUMN "ami_percentage" SET NOT NULL` + ) + } +} diff --git a/backend/core/src/migration/1647917319793-adding-hit-confirmation-url.ts b/backend/core/src/migration/1647917319793-adding-hit-confirmation-url.ts new file mode 100644 index 0000000000..f51211836c --- /dev/null +++ b/backend/core/src/migration/1647917319793-adding-hit-confirmation-url.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addingHitConfirmationUrl1647917319793 implements MigrationInterface { + name = "addingHitConfirmationUrl1647917319793" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_accounts" ADD "hit_confirmation_url" TIMESTAMP WITH TIME ZONE` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "hit_confirmation_url"`) + } +} diff --git a/backend/core/src/migration/1648043352040-add-user-agreed-to-terms-of-service-column.ts b/backend/core/src/migration/1648043352040-add-user-agreed-to-terms-of-service-column.ts new file mode 100644 index 0000000000..3bc1a20b40 --- /dev/null +++ b/backend/core/src/migration/1648043352040-add-user-agreed-to-terms-of-service-column.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addUserAgreedToTermsOfServiceColumn1648043352040 implements MigrationInterface { + name = "addUserAgreedToTermsOfServiceColumn1648043352040" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_accounts" ADD "agreed_to_terms_of_service" boolean NOT NULL DEFAULT false` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "agreed_to_terms_of_service"`) + } +} diff --git a/backend/core/src/migration/1648548425458-add-listing-marketing.ts b/backend/core/src/migration/1648548425458-add-listing-marketing.ts new file mode 100644 index 0000000000..b222289411 --- /dev/null +++ b/backend/core/src/migration/1648548425458-add-listing-marketing.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addListingMarketing1648548425458 implements MigrationInterface { + name = "addListingMarketing1648548425458" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "listings_marketing_type_enum" AS ENUM('marketing', 'comingSoon')` + ) + await queryRunner.query( + `ALTER TABLE "listings" ADD "marketing_type" "listings_marketing_type_enum" NOT NULL DEFAULT 'marketing'` + ) + await queryRunner.query(`ALTER TABLE "listings" ADD "marketing_date" TIMESTAMP WITH TIME ZONE`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "marketing_date"`) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "marketing_type"`) + await queryRunner.query(`DROP TYPE "listings_marketing_type_enum"`) + } +} diff --git a/backend/core/src/migration/1649062179928-add-property-region.ts b/backend/core/src/migration/1649062179928-add-property-region.ts new file mode 100644 index 0000000000..d5f09db884 --- /dev/null +++ b/backend/core/src/migration/1649062179928-add-property-region.ts @@ -0,0 +1,74 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { Region } from "../property/types/region-enum" + +export interface Neighborhood { + name: string + region: string +} + +export class addPropertyRegion1649062179928 implements MigrationInterface { + name = "addPropertyRegion1649062179928" + + // NOTE: imported from https://github.com/CityOfDetroit/bloom/blob/main/ui-components/src/helpers/regionNeighborhoodMap.ts + // Issue comment: https://github.com/CityOfDetroit/bloom/issues/1015#issuecomment-1068056607 + neighborhoods: Neighborhood[] = [ + { name: "Airport Sub area", region: "Eastside" }, + { name: "Barton McFarland area", region: "Westside" }, + { name: "Boston-Edison/North End area", region: "Westside" }, + { name: "Boynton", region: "Southwest" }, + { name: "Campau/Banglatown", region: "Eastside" }, + { name: "Dexter Linwood", region: "Westside" }, + { name: "Farwell area", region: "Eastside" }, + { name: "Gratiot Town/Kettering area", region: "Eastside" }, + { name: "Gratiot/7 Mile area", region: "Eastside" }, + { name: "Greater Corktown area", region: "Downtown" }, + { name: "Greater Downtown area", region: "Downtown" }, + { name: "Greater Downtown area", region: "Downtown" }, + { name: "Islandview/Greater Villages area", region: "Eastside" }, + { name: "Islandview/Greater Villages area", region: "Eastside" }, + { name: "Islandview/Greater Villages area", region: "Westside" }, + { name: "Jefferson Chalmers area", region: "Eastside" }, + { name: "Livernois/McNichols area", region: "Westside" }, + { name: "Livernois/McNichols area", region: "Westside" }, + { name: "Morningside area", region: "Eastside" }, + { name: "North Campau area", region: "Eastside" }, + { name: "Northwest Grand River area", region: "Westside" }, + { name: "Northwest University District area", region: "Westside" }, + { name: "Palmer Park area", region: "Westside" }, + { name: "Russell Woods/Nardin Park area", region: "Westside" }, + { name: "Southwest/Vernor area", region: "Southwest" }, + { name: "Southwest/Vernor area", region: "Southwest" }, + { name: "Warrendale/Cody Rouge", region: "Westside" }, + { name: "West End area", region: "Eastside" }, + ] + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "property_region_enum" AS ENUM('Downtown', 'Eastside', 'Midtown - New Center', 'Southwest', 'Westside')` + ) + await queryRunner.query(`ALTER TABLE "property" ADD "region" "property_region_enum"`) + + let properties: Array<{ id: string; neighborhood?: string }> = await queryRunner.query( + `SELECT id, neighborhood FROM property` + ) + + for (let p of properties) { + const neighborhood = this.neighborhoods.find( + (neighborhood) => neighborhood.name === p.neighborhood + ) + if (!neighborhood) { + console.warn(`neighborhood ${p.neighborhood} not found in neighborhood:region map`) + continue + } + await queryRunner.query(`UPDATE property SET region = $1 WHERE id = $2`, [ + neighborhood.region, + p.id, + ]) + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "property" DROP COLUMN "region"`) + await queryRunner.query(`DROP TYPE "property_region_enum"`) + } +} diff --git a/backend/core/src/migration/1649260977278-close-overdue-listings.ts b/backend/core/src/migration/1649260977278-close-overdue-listings.ts new file mode 100644 index 0000000000..0bb519bdff --- /dev/null +++ b/backend/core/src/migration/1649260977278-close-overdue-listings.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class closeOverdueListings1649260977278 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE listings set status = 'closed' where application_due_date < NOW() AND status != 'pending'` + ) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1649273182040-updated-integer-listing-fields-to-numeric.ts b/backend/core/src/migration/1649273182040-updated-integer-listing-fields-to-numeric.ts new file mode 100644 index 0000000000..f3ee50cf0d --- /dev/null +++ b/backend/core/src/migration/1649273182040-updated-integer-listing-fields-to-numeric.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class updatedIntegerListingFieldsToNumeric1649273182040 implements MigrationInterface { + name = "updatedIntegerListingFieldsToNumeric1649273182040" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ALTER COLUMN "flat_rent_value" TYPE numeric ` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ALTER COLUMN "percentage_of_income_value" TYPE numeric ` + ) + await queryRunner.query(`ALTER TABLE "unit_group" ALTER COLUMN "sq_feet_min" TYPE numeric `) + await queryRunner.query(`ALTER TABLE "unit_group" ALTER COLUMN "sq_feet_max" TYPE numeric `) + await queryRunner.query(`ALTER TABLE "unit_group" ALTER COLUMN "bathroom_min" TYPE numeric `) + await queryRunner.query(`ALTER TABLE "unit_group" ALTER COLUMN "bathroom_max" TYPE numeric `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "unit_group" ALTER COLUMN "bathroom_max" TYPE integer `) + await queryRunner.query(`ALTER TABLE "unit_group" ALTER COLUMN "bathroom_min" TYPE integer `) + await queryRunner.query(`ALTER TABLE "unit_group" ALTER COLUMN "sq_feet_max" TYPE integer `) + await queryRunner.query(`ALTER TABLE "unit_group" ALTER COLUMN "sq_feet_min" TYPE integer `) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ALTER COLUMN "percentage_of_income_value" TYPE integer ` + ) + await queryRunner.query( + `ALTER TABLE "unit_group_ami_levels" ALTER COLUMN "flat_rent_value" TYPE integer ` + ) + } +} diff --git a/backend/core/src/migration/1649374032458-marketing-season.ts b/backend/core/src/migration/1649374032458-marketing-season.ts new file mode 100644 index 0000000000..6ef5877369 --- /dev/null +++ b/backend/core/src/migration/1649374032458-marketing-season.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class marketingSeason1649374032458 implements MigrationInterface { + name = "marketingSeason1649374032458" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "listings_marketing_season_enum" AS ENUM('spring', 'summer', 'fall', 'winter')` + ) + await queryRunner.query( + `ALTER TABLE "listings" ADD "marketing_season" "listings_marketing_season_enum"` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "marketing_season"`) + await queryRunner.query(`DROP TYPE "listings_marketing_season_enum"`) + } +} diff --git a/backend/core/src/migration/1649709708991-detroit-email-updates.ts b/backend/core/src/migration/1649709708991-detroit-email-updates.ts new file mode 100644 index 0000000000..6243400d03 --- /dev/null +++ b/backend/core/src/migration/1649709708991-detroit-email-updates.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class detroitEmailUpdates1649709708991 implements MigrationInterface { + name = "detroitEmailUpdates1649709708991" + + public async up(queryRunner: QueryRunner): Promise { + let jurisdiction = await queryRunner.query( + "SELECT id FROM jurisdictions WHERE name = 'Detroit'" + ) + jurisdiction = jurisdiction[0].id + const translation = await queryRunner.query( + `SELECT id, translations FROM translations WHERE language='en' AND jurisdiction_id = '${jurisdiction}'` + ) + const { id, translations } = translation[0] + if (!translations.footer) { + translations.footer = {} + } + translations.footer.footer = "City of Detroit Housing and Revitalization Department" + translations.footer.thankYou = "Thank you," + if (!translations.register) { + translations.register = {} + } + translations.register.welcomeMessage = + "Thank you for setting up your account on %{appUrl}. It will now be easier to save listings that you are interested in on the site." + await queryRunner.query(`UPDATE translations SET translations = ($1) WHERE id = ($2)`, [ + translations, + id, + ]) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1649893064530-add-what-to-expect-additional-text.ts b/backend/core/src/migration/1649893064530-add-what-to-expect-additional-text.ts new file mode 100644 index 0000000000..10632f9f08 --- /dev/null +++ b/backend/core/src/migration/1649893064530-add-what-to-expect-additional-text.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addWhatToExpectAdditionalText1649893064530 implements MigrationInterface { + name = "addWhatToExpectAdditionalText1649893064530" + + public async up(queryRunner: QueryRunner): Promise { + const defaultWhatToExpect = `
Vacant Units:
If you are looking to move in immediately, contact the property and ask if they have any vacant units.
Waitlists:
If none are vacant, but you are still interested in living at the property in the future, ask how you can be placed on their waitlist.
` + const defaultWhatToExpectAdditionalText = `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you will be notified as available units come up.
` + await queryRunner.query(`ALTER TABLE "listings" ADD "what_to_expect_additional_text" text`) + + await queryRunner.query(`UPDATE listings SET what_to_expect = ($1)`, [defaultWhatToExpect]) + await queryRunner.query(`UPDATE listings SET what_to_expect_additional_text = ($1)`, [ + defaultWhatToExpectAdditionalText, + ]) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "what_to_expect_additional_text"`) + } +} diff --git a/backend/core/src/migration/1651191466791-add-what-to-expect-additional-text-for-coming-soon.ts b/backend/core/src/migration/1651191466791-add-what-to-expect-additional-text-for-coming-soon.ts new file mode 100644 index 0000000000..f39a96738a --- /dev/null +++ b/backend/core/src/migration/1651191466791-add-what-to-expect-additional-text-for-coming-soon.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { ListingMarketingTypeEnum } from "../listings/types/listing-marketing-type-enum" + +export class addWhatToExpectAdditionalTextForComingSoon1651191466791 implements MigrationInterface { + name = "addWhatToExpectAdditionalTextForComingSoon1651191466791" + + public async up(queryRunner: QueryRunner): Promise { + const defaultWhatToExpect = `This property is still under construction by the property owners. If you sign up for notifications through Detroit Home Connect, we will send you updates when this property has opened up applications for residents. You can also check back later to this page for updates.` + await queryRunner.query( + `UPDATE listings SET what_to_expect = ($1) WHERE marketing_type = ($2)`, + [defaultWhatToExpect, ListingMarketingTypeEnum.ComingSoon] + ) + await queryRunner.query( + `UPDATE listings SET what_to_expect_additional_text = ($1) WHERE marketing_type = ($2)`, + [null, ListingMarketingTypeEnum.ComingSoon] + ) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1651700608419-add-new-ami-charts.ts b/backend/core/src/migration/1651700608419-add-new-ami-charts.ts new file mode 100644 index 0000000000..77cec8c7c1 --- /dev/null +++ b/backend/core/src/migration/1651700608419-add-new-ami-charts.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { HUD2021 } from "../seeder/seeds/ami-charts/HUD2021" +import { HUD2022 } from "../seeder/seeds/ami-charts/HUD2022" +import { MSHDA2022 } from "../seeder/seeds/ami-charts/MSHDA2022" + +export class addNewAmiCharts1651700608419 implements MigrationInterface { + name = "addNewAmiCharts1651700608419" + + public async up(queryRunner: QueryRunner): Promise { + const [{ id }] = await queryRunner.query(`SELECT id FROM jurisdictions WHERE name = 'Detroit'`) + + await queryRunner.query( + `INSERT INTO ami_chart + (name, items, jurisdiction_id) + VALUES ('${HUD2022.name}', '${JSON.stringify(HUD2022.items)}', '${id}') + ` + ) + + await queryRunner.query( + `INSERT INTO ami_chart + (name, items, jurisdiction_id) + VALUES ('${MSHDA2022.name}', '${JSON.stringify(MSHDA2022.items)}', '${id}') + ` + ) + + await queryRunner.query(`UPDATE ami_chart SET items = $1 WHERE name = $2`, [ + JSON.stringify(HUD2021.items), + "HUD 2021", + ]) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1651740223899-remove-region-from-listing.ts b/backend/core/src/migration/1651740223899-remove-region-from-listing.ts new file mode 100644 index 0000000000..e5aad4ffb5 --- /dev/null +++ b/backend/core/src/migration/1651740223899-remove-region-from-listing.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class removeRegionFromListing1651740223899 implements MigrationInterface { + name = "removeRegionFromListing1651740223899" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "region"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" ADD "region" text`) + } +} diff --git a/backend/core/src/migration/1652130526521-set-common-app-empty.ts b/backend/core/src/migration/1652130526521-set-common-app-empty.ts new file mode 100644 index 0000000000..f39c2dafca --- /dev/null +++ b/backend/core/src/migration/1652130526521-set-common-app-empty.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class setCommonAppEmpty1652130526521 implements MigrationInterface { + name = "setCommonAppEmpty1652130526521" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`UPDATE "listings" SET common_digital_application = ($1)`, [false]) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1654031196172-new-what-to-expect-default-values.ts b/backend/core/src/migration/1654031196172-new-what-to-expect-default-values.ts new file mode 100644 index 0000000000..e6fcce14bf --- /dev/null +++ b/backend/core/src/migration/1654031196172-new-what-to-expect-default-values.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { ListingMarketingTypeEnum } from "../listings/types/listing-marketing-type-enum" + +export class newWhatToExpectDefaultValues1654031196172 implements MigrationInterface { + name = "newWhatToExpectDefaultValues1654031196172" + + public async up(queryRunner: QueryRunner): Promise { + const defaultComingSoonWhatToExpect = `This property is still under development by the property owners. If you sign up for notifications through Detroit Home Connect, we will send you updates when this property has opened up applications for residents. You can also check back later to this page for updates.` + const defaultComingSoonAdditionalWhatToExpect = null + const defaultMarketingWhatToExpect = `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
` + const defaultMarketingAdditionalWhatToExpect = `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
` + await queryRunner.query( + `UPDATE listings SET what_to_expect = ($1) WHERE marketing_type = ($2)`, + [defaultComingSoonWhatToExpect, ListingMarketingTypeEnum.ComingSoon] + ) + await queryRunner.query( + `UPDATE listings SET what_to_expect_additional_text = ($1) WHERE marketing_type = ($2)`, + [defaultComingSoonAdditionalWhatToExpect, ListingMarketingTypeEnum.ComingSoon] + ) + await queryRunner.query( + `UPDATE listings SET what_to_expect = ($1) WHERE marketing_type = ($2)`, + [defaultMarketingWhatToExpect, ListingMarketingTypeEnum.Marketing] + ) + await queryRunner.query( + `UPDATE listings SET what_to_expect_additional_text = ($1) WHERE marketing_type = ($2)`, + [defaultMarketingAdditionalWhatToExpect, ListingMarketingTypeEnum.Marketing] + ) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1654190736229-new-accessibility-features.ts b/backend/core/src/migration/1654190736229-new-accessibility-features.ts new file mode 100644 index 0000000000..4cf32a0b37 --- /dev/null +++ b/backend/core/src/migration/1654190736229-new-accessibility-features.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class newAccessibilityFeatures1654190736229 implements MigrationInterface { + name = "newAccessibilityFeatures1654190736229" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listing_features" ADD "barrier_free_unit_entrance" boolean` + ) + await queryRunner.query(`ALTER TABLE "listing_features" ADD "lowered_light_switch" boolean`) + await queryRunner.query(`ALTER TABLE "listing_features" ADD "barrier_free_bathroom" boolean`) + await queryRunner.query(`ALTER TABLE "listing_features" ADD "wide_doorways" boolean`) + await queryRunner.query(`ALTER TABLE "listing_features" ADD "lowered_cabinets" boolean`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listing_features" DROP COLUMN "lowered_cabinets"`) + await queryRunner.query(`ALTER TABLE "listing_features" DROP COLUMN "wide_doorways"`) + await queryRunner.query(`ALTER TABLE "listing_features" DROP COLUMN "barrier_free_bathroom"`) + await queryRunner.query(`ALTER TABLE "listing_features" DROP COLUMN "lowered_light_switch"`) + await queryRunner.query( + `ALTER TABLE "listing_features" DROP COLUMN "barrier_free_unit_entrance"` + ) + } +} diff --git a/backend/core/src/migration/1654549186207-region-rename.ts b/backend/core/src/migration/1654549186207-region-rename.ts new file mode 100644 index 0000000000..4be4bc6d43 --- /dev/null +++ b/backend/core/src/migration/1654549186207-region-rename.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class regionRename1654549186207 implements MigrationInterface { + name = "regionRename1654549186207" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`BEGIN TRANSACTION`) + await queryRunner.query(`ALTER TYPE "property_region_enum" ADD VALUE 'Greater Downtown'`) + await queryRunner.query(`COMMIT TRANSACTION`) + await queryRunner.query( + `UPDATE "property" SET "region" = 'Greater Downtown' WHERE "region" = 'Downtown'` + ) + await queryRunner.query( + `ALTER TYPE "property_region_enum" RENAME TO "property_region_enum_old"` + ) + await queryRunner.query( + `CREATE TYPE "property_region_enum" AS ENUM('Greater Downtown', 'Eastside', 'Southwest', 'Westside')` + ) + await queryRunner.query( + `ALTER TABLE "property" ALTER COLUMN "region" TYPE "property_region_enum" USING "region"::"text"::"property_region_enum"` + ) + await queryRunner.query(`DROP TYPE "property_region_enum_old"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "property_region_enum_old" AS ENUM('Greater Downtown', 'Eastside', 'Midtown - New Center', 'Southwest', 'Westside')` + ) + await queryRunner.query( + `ALTER TABLE "property" ALTER COLUMN "region" TYPE "property_region_enum_old" USING "region"::"text"::"property_region_enum_old"` + ) + await queryRunner.query(`DROP TYPE "property_region_enum"`) + await queryRunner.query( + `ALTER TYPE "property_region_enum_old" RENAME TO "property_region_enum"` + ) + } +} diff --git a/backend/core/src/migration/1654884722218-adding-section-8.ts b/backend/core/src/migration/1654884722218-adding-section-8.ts new file mode 100644 index 0000000000..c3bfb532fe --- /dev/null +++ b/backend/core/src/migration/1654884722218-adding-section-8.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addingSection81654884722218 implements MigrationInterface { + name = "addingSection81654884722218" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" ADD "section8_acceptance" boolean`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "section8_acceptance"`) + } +} diff --git a/backend/core/src/migration/1655157754482-updating-forgot-password-email.ts b/backend/core/src/migration/1655157754482-updating-forgot-password-email.ts new file mode 100644 index 0000000000..78a15f690c --- /dev/null +++ b/backend/core/src/migration/1655157754482-updating-forgot-password-email.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class updatingForgotPasswordEmail1655157754482 implements MigrationInterface { + name = "updatingForgotPasswordEmail1655157754482" + + public async up(queryRunner: QueryRunner): Promise { + const [{ id }] = await queryRunner.query(` + SELECT + t.id + FROM jurisdictions j + JOIN translations t ON t.jurisdiction_id = j.id + WHERE name = 'Detroit'`) + await queryRunner.query(` + UPDATE translations + SET translations = jsonb_set(translations, '{forgotPassword, resetRequest}', '"A request to reset your Detroit Home Connect website password for %{appUrl} has recently been made."') + WHERE id = '${id}' + `) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1655243355949-add-utilities.ts b/backend/core/src/migration/1655243355949-add-utilities.ts new file mode 100644 index 0000000000..cd2a99d7bf --- /dev/null +++ b/backend/core/src/migration/1655243355949-add-utilities.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addUtilities1655243355949 implements MigrationInterface { + name = "addUtilities1655243355949" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "listing_utilities" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "water" boolean, "gas" boolean, "trash" boolean, "sewer" boolean, "electricity" boolean, "cable" boolean, "phone" boolean, "internet" boolean, CONSTRAINT "PK_8e88f883b389f7b31d331de764f" PRIMARY KEY ("id"))` + ) + await queryRunner.query(`ALTER TABLE "listings" ADD "utilities_id" uuid`) + await queryRunner.query( + `ALTER TABLE "listings" ADD CONSTRAINT "UQ_61b80a947c9db249548ba3c73a5" UNIQUE ("utilities_id")` + ) + await queryRunner.query( + `ALTER TABLE "listings" ADD CONSTRAINT "FK_61b80a947c9db249548ba3c73a5" FOREIGN KEY ("utilities_id") REFERENCES "listing_utilities"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listings" DROP CONSTRAINT "FK_61b80a947c9db249548ba3c73a5"` + ) + await queryRunner.query( + `ALTER TABLE "listings" DROP CONSTRAINT "UQ_61b80a947c9db249548ba3c73a5"` + ) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "utilities_id"`) + await queryRunner.query(`DROP TABLE "listing_utilities"`) + } +} diff --git a/backend/core/src/migration/1656457788192-remove-SRO.ts b/backend/core/src/migration/1656457788192-remove-SRO.ts new file mode 100644 index 0000000000..bcc730c258 --- /dev/null +++ b/backend/core/src/migration/1656457788192-remove-SRO.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class removeSRO1656457788192 implements MigrationInterface { + name = "removeSRO1656457788192" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "UPDATE unit_group_unit_type_unit_types SET unit_types_id = (SELECT id FROM unit_types WHERE name = 'studio') WHERE unit_types_id = (SELECT id FROM unit_types WHERE name = 'SRO')" + ) + await queryRunner.query(`DELETE FROM unit_types WHERE name = $1`, ["SRO"]) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`INSERT INTO unit_types (name, num_bedrooms) VALUES ($1, $2)`, [ + "SRO", + 0, + ]) + } +} diff --git a/backend/core/src/migration/1664814514320-add_neighborhood_amenities.ts b/backend/core/src/migration/1664814514320-add_neighborhood_amenities.ts new file mode 100644 index 0000000000..68047900f9 --- /dev/null +++ b/backend/core/src/migration/1664814514320-add_neighborhood_amenities.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addNeighborhoodAmenities1664814514320 implements MigrationInterface { + name = "addNeighborhoodAmenities1664814514320" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "listing_neighborhood_amenities" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "grocery" text, "pharmacy" text, "medical_clinic" text, "park" text, "senior_center" text, CONSTRAINT "PK_4822e277c626fd1d94cddbb9826" PRIMARY KEY ("id"))` + ) + await queryRunner.query(`ALTER TABLE "listings" ADD "neighborhood_amenities_id" uuid`) + await queryRunner.query( + `ALTER TABLE "listings" ADD CONSTRAINT "UQ_59b4618dfbe6dca2edda375b8d3" UNIQUE ("neighborhood_amenities_id")` + ) + await queryRunner.query( + `ALTER TABLE "listings" ADD CONSTRAINT "FK_59b4618dfbe6dca2edda375b8d3" FOREIGN KEY ("neighborhood_amenities_id") REFERENCES "listing_neighborhood_amenities"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listings" DROP CONSTRAINT "FK_59b4618dfbe6dca2edda375b8d3"` + ) + await queryRunner.query( + `ALTER TABLE "listings" DROP CONSTRAINT "UQ_59b4618dfbe6dca2edda375b8d3"` + ) + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "neighborhood_amenities_id"`) + await queryRunner.query(`DROP TABLE "listing_neighborhood_amenities"`) + } +} diff --git a/backend/core/src/migration/1665776578492-remove-activity-log-user-relationship-on-delete.ts b/backend/core/src/migration/1665776578492-remove-activity-log-user-relationship-on-delete.ts new file mode 100644 index 0000000000..92a5362234 --- /dev/null +++ b/backend/core/src/migration/1665776578492-remove-activity-log-user-relationship-on-delete.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class removeActivityLogUserRelationshipOnDelete1665776578492 implements MigrationInterface { + name = "removeActivityLogUserRelationshipOnDelete1665776578492" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_preferences" DROP CONSTRAINT "FK_458057fa75b66e68a275647da2e"` + ) + await queryRunner.query( + `ALTER TABLE "activity_logs" DROP CONSTRAINT "FK_d54f841fa5478e4734590d44036"` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences" ADD CONSTRAINT "FK_458057fa75b66e68a275647da2e" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "activity_logs" ADD CONSTRAINT "FK_d54f841fa5478e4734590d44036" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE SET NULL ON UPDATE NO ACTION` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "activity_logs" DROP CONSTRAINT "FK_d54f841fa5478e4734590d44036"` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences" DROP CONSTRAINT "FK_458057fa75b66e68a275647da2e"` + ) + await queryRunner.query( + `ALTER TABLE "activity_logs" ADD CONSTRAINT "FK_d54f841fa5478e4734590d44036" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "user_preferences" ADD CONSTRAINT "FK_458057fa75b66e68a275647da2e" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + } +} diff --git a/backend/core/src/migration/1667383571614-update-neighborhood-amenities-fields.ts b/backend/core/src/migration/1667383571614-update-neighborhood-amenities-fields.ts new file mode 100644 index 0000000000..5cb032d594 --- /dev/null +++ b/backend/core/src/migration/1667383571614-update-neighborhood-amenities-fields.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class updateNeighborhoodAmenitiesFields1667383571614 implements MigrationInterface { + name = "updateNeighborhoodAmenitiesFields1667383571614" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" RENAME COLUMN "grocery" TO "grocery_stores"` + ) + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" RENAME COLUMN "pharmacy" TO "pharmacies"` + ) + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" RENAME COLUMN "medical_clinic" TO "health_care_resources"` + ) + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" RENAME COLUMN "park" TO "parks_and_community_centers"` + ) + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" RENAME COLUMN "senior_center" TO "schools"` + ) + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" ADD COLUMN "public_transportation" text` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" RENAME COLUMN "grocery_stores" TO "grocery"` + ) + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" RENAME COLUMN "pharmacies" TO "pharmacy"` + ) + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" RENAME COLUMN "health_care_resources" TO "medical_clinic"` + ) + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" RENAME COLUMN "parks_and_community_centers" TO "park"` + ) + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" RENAME COLUMN "schools" TO "senior_center"` + ) + await queryRunner.query( + `ALTER TABLE "listing_neighborhood_amenities" DROP COLUMN "public_transportation"` + ) + } +} diff --git a/backend/core/src/migration/1668201101384-change-is-verified-type.ts b/backend/core/src/migration/1668201101384-change-is-verified-type.ts new file mode 100644 index 0000000000..a15eaa553b --- /dev/null +++ b/backend/core/src/migration/1668201101384-change-is-verified-type.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class changeIsVerifiedType1668201101384 implements MigrationInterface { + name = "changeIsVerifiedType1668201101384" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" ADD "verified_at" TIMESTAMP WITH TIME ZONE`) + await queryRunner.query( + `UPDATE "listings" SET "verified_at" = "updated_at" where is_verified = 'true'` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "verified_at"`) + } +} diff --git a/backend/core/src/migration/1669071600483-add-home-types.ts b/backend/core/src/migration/1669071600483-add-home-types.ts new file mode 100644 index 0000000000..2360278d79 --- /dev/null +++ b/backend/core/src/migration/1669071600483-add-home-types.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addHomeTypes1669071600483 implements MigrationInterface { + name = "addHomeTypes1669071600483" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."listings_home_type_enum" AS ENUM('apartment', 'duplex', 'house', 'townhome')` + ) + await queryRunner.query( + `ALTER TABLE "listings" ADD "home_type" "public"."listings_home_type_enum"` + ) + await queryRunner.query(`UPDATE "listings" SET "home_type" = 'apartment'`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "home_type"`) + await queryRunner.query(`DROP TYPE "public"."listings_home_type_enum"`) + } +} diff --git a/backend/core/src/migration/1680041960107-under-construction-what-to-expect.ts b/backend/core/src/migration/1680041960107-under-construction-what-to-expect.ts new file mode 100644 index 0000000000..022647d27f --- /dev/null +++ b/backend/core/src/migration/1680041960107-under-construction-what-to-expect.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { ListingMarketingTypeEnum } from "../listings/types/listing-marketing-type-enum" + +export class underConstructionWhatToExpect1680041960107 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const underConstructionWhatToExpect = `This property is under construction by the property owners. If you sign up for notifications through Detroit Home Connect, we will send you updates when this property has opened up applications for residents. You can also check back later to this page for updates.` + + await queryRunner.query( + `UPDATE listings SET what_to_expect = ($1) WHERE marketing_type = ($2)`, + [underConstructionWhatToExpect, ListingMarketingTypeEnum.ComingSoon] + ) + } + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1714773518348-mshda-ami-chart.ts b/backend/core/src/migration/1714773518348-mshda-ami-chart.ts new file mode 100644 index 0000000000..53cc64fa48 --- /dev/null +++ b/backend/core/src/migration/1714773518348-mshda-ami-chart.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { ami } from "../../scripts/script-data/MSHDA2024" + +export class mshdaAmiChart1714773518348 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const [{ id: juris }] = await queryRunner.query( + `SELECT id FROM jurisdictions WHERE name = 'Detroit'` + ) + await queryRunner.query(` + INSERT INTO ami_chart + (name, items, jurisdiction_id) + VALUES ('${ami.name}', '${JSON.stringify(ami.items)}', '${juris}') + `) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1732654757266-home-ami-chart.ts b/backend/core/src/migration/1732654757266-home-ami-chart.ts new file mode 100644 index 0000000000..59fffb7c32 --- /dev/null +++ b/backend/core/src/migration/1732654757266-home-ami-chart.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { ami } from "../../scripts/script-data/HOME2024" + +export class homeAmiChart1732654757266 implements MigrationInterface { + name = "homeAmiChart1732654757266" + + public async up(queryRunner: QueryRunner): Promise { + const [{ id: juris }] = await queryRunner.query( + `SELECT id FROM jurisdictions WHERE name = 'Detroit'` + ) + await queryRunner.query(` + INSERT INTO ami_chart + (name, items, jurisdiction_id) + VALUES ('${ami.name}', '${JSON.stringify(ami.items)}', '${juris}') + `) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1749657494376-add-2025-ami-charts.ts b/backend/core/src/migration/1749657494376-add-2025-ami-charts.ts new file mode 100644 index 0000000000..4184c8e5c3 --- /dev/null +++ b/backend/core/src/migration/1749657494376-add-2025-ami-charts.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { amiHUD2025 } from "../../scripts/script-data/HUD2025" +import { amiMSHDA2025 } from "../../scripts/script-data/MSHDA2025" + +export class homeAmiChart1749657494376 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const [{ id: juris }] = await queryRunner.query( + `SELECT id FROM jurisdictions WHERE name = 'Detroit'` + ) + + await queryRunner.query(` + INSERT INTO ami_chart + (name, items, jurisdiction_id) + VALUES ('${amiHUD2025.name}', '${JSON.stringify(amiHUD2025.items)}', '${juris}') + `) + await queryRunner.query(` + INSERT INTO ami_chart + (name, items, jurisdiction_id) + VALUES ('${amiMSHDA2025.name}', '${JSON.stringify( + amiMSHDA2025.items + )}', '${juris}') + `) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/preferences/dto/jurisdictionFilterTypeToFieldMap.ts b/backend/core/src/preferences/dto/jurisdictionFilterTypeToFieldMap.ts new file mode 100644 index 0000000000..437a01cf8a --- /dev/null +++ b/backend/core/src/preferences/dto/jurisdictionFilterTypeToFieldMap.ts @@ -0,0 +1,5 @@ +import { PreferenceFilterKeys } from "./preference-filter-keys" + +export const jurisdictionFilterTypeToFieldMap: Record = { + jurisdiction: "preferenceJurisdictions.id", +} diff --git a/backend/core/src/preferences/dto/listing-preference-update.dto.ts b/backend/core/src/preferences/dto/listing-preference-update.dto.ts new file mode 100644 index 0000000000..bd24408c91 --- /dev/null +++ b/backend/core/src/preferences/dto/listing-preference-update.dto.ts @@ -0,0 +1,17 @@ +import { OmitType } from "@nestjs/swagger" +import { Expose, Type } from "class-transformer" +import { IsDefined, IsOptional, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { IdDto } from "../../shared/dto/id.dto" +import { ListingPreferenceDto } from "./listing-preference.dto" + +export class ListingPreferenceUpdateDto extends OmitType(ListingPreferenceDto, [ + "preference", +] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => IdDto) + preference: IdDto +} diff --git a/backend/core/src/preferences/dto/listing-preference.dto.ts b/backend/core/src/preferences/dto/listing-preference.dto.ts new file mode 100644 index 0000000000..ee387a3b9f --- /dev/null +++ b/backend/core/src/preferences/dto/listing-preference.dto.ts @@ -0,0 +1,18 @@ +import { ListingPreference } from "../entities/listing-preference.entity" +import { OmitType } from "@nestjs/swagger" +import { Expose, Type } from "class-transformer" +import { IsDefined, IsOptional, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { PreferenceDto } from "./preference.dto" + +export class ListingPreferenceDto extends OmitType(ListingPreference, [ + "listing", + "preference", +] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => PreferenceDto) + preference: PreferenceDto +} diff --git a/backend/core/src/preferences/dto/preference-create.dto.ts b/backend/core/src/preferences/dto/preference-create.dto.ts new file mode 100644 index 0000000000..302ce4bee7 --- /dev/null +++ b/backend/core/src/preferences/dto/preference-create.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from "@nestjs/swagger" +import { PreferenceDto } from "./preference.dto" + +export class PreferenceCreateDto extends OmitType(PreferenceDto, [ + "id", + "createdAt", + "updatedAt", +] as const) {} diff --git a/backend/core/src/preferences/dto/preference-filter-keys.ts b/backend/core/src/preferences/dto/preference-filter-keys.ts new file mode 100644 index 0000000000..87db4c7795 --- /dev/null +++ b/backend/core/src/preferences/dto/preference-filter-keys.ts @@ -0,0 +1,3 @@ +export enum PreferenceFilterKeys { + jurisdiction = "jurisdiction", +} diff --git a/backend/core/src/preferences/dto/preference-update.dto.ts b/backend/core/src/preferences/dto/preference-update.dto.ts new file mode 100644 index 0000000000..164fe62124 --- /dev/null +++ b/backend/core/src/preferences/dto/preference-update.dto.ts @@ -0,0 +1,11 @@ +import { PreferenceCreateDto } from "./preference-create.dto" +import { Expose } from "class-transformer" +import { IsString, IsUUID } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +export class PreferenceUpdateDto extends PreferenceCreateDto { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID() + id: string +} diff --git a/backend/core/src/preferences/dto/preference.dto.ts b/backend/core/src/preferences/dto/preference.dto.ts index a3e891e225..5220bf9a8e 100644 --- a/backend/core/src/preferences/dto/preference.dto.ts +++ b/backend/core/src/preferences/dto/preference.dto.ts @@ -1,20 +1,7 @@ import { OmitType } from "@nestjs/swagger" import { Preference } from "../entities/preference.entity" -import { Expose } from "class-transformer" -import { IsString, IsUUID } from "class-validator" -import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" -export class PreferenceDto extends OmitType(Preference, ["listing"] as const) {} - -export class PreferenceCreateDto extends OmitType(PreferenceDto, [ - "id", - "createdAt", - "updatedAt", +export class PreferenceDto extends OmitType(Preference, [ + "listingPreferences", + "jurisdictions", ] as const) {} - -export class PreferenceUpdateDto extends PreferenceCreateDto { - @Expose() - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @IsUUID() - id: string -} diff --git a/backend/core/src/preferences/dto/preferences-filter-params.ts b/backend/core/src/preferences/dto/preferences-filter-params.ts new file mode 100644 index 0000000000..5492514e50 --- /dev/null +++ b/backend/core/src/preferences/dto/preferences-filter-params.ts @@ -0,0 +1,18 @@ +import { BaseFilter } from "../../shared/dto/filter.dto" +import { Expose } from "class-transformer" +import { ApiProperty } from "@nestjs/swagger" +import { IsOptional, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { PreferenceFilterKeys } from "./preference-filter-keys" + +export class PreferencesFilterParams extends BaseFilter { + @Expose() + @ApiProperty({ + type: String, + example: "uuid", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [PreferenceFilterKeys.jurisdiction]?: string +} diff --git a/backend/core/src/preferences/dto/preferences-list-query-params.ts b/backend/core/src/preferences/dto/preferences-list-query-params.ts new file mode 100644 index 0000000000..8dcc87bbdc --- /dev/null +++ b/backend/core/src/preferences/dto/preferences-list-query-params.ts @@ -0,0 +1,24 @@ +import { Expose, Type } from "class-transformer" +import { ApiProperty, getSchemaPath } from "@nestjs/swagger" +import { ArrayMaxSize, IsArray, IsOptional, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { PreferencesFilterParams } from "./preferences-filter-params" + +export class PreferencesListQueryParams { + @Expose() + @ApiProperty({ + name: "filter", + required: false, + type: [String], + items: { + $ref: getSchemaPath(PreferencesFilterParams), + }, + example: { $comparison: "=", jurisdiction: "uuid" }, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => PreferencesFilterParams) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + filter?: PreferencesFilterParams[] +} diff --git a/backend/core/src/preferences/entities/listing-preference.entity.ts b/backend/core/src/preferences/entities/listing-preference.entity.ts new file mode 100644 index 0000000000..cbc5a6c3a9 --- /dev/null +++ b/backend/core/src/preferences/entities/listing-preference.entity.ts @@ -0,0 +1,30 @@ +import { Column, Entity, ManyToOne } from "typeorm" +import { Preference } from "./preference.entity" +import { Expose, Type } from "class-transformer" +import { IsNumber, IsOptional } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { Listing } from "../../listings/entities/listing.entity" + +@Entity({ name: "listing_preferences" }) +export class ListingPreference { + @ManyToOne(() => Listing, (listing) => listing.listingPreferences, { + primary: true, + orphanedRowAction: "delete", + }) + @Type(() => Listing) + listing: Listing + + @ManyToOne(() => Preference, (preference) => preference.listingPreferences, { + primary: true, + eager: true, + }) + @Expose() + @Type(() => Preference) + preference: Preference + + @Column({ type: "integer", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + ordinal?: number | null +} diff --git a/backend/core/src/preferences/entities/preference.entity.ts b/backend/core/src/preferences/entities/preference.entity.ts index 69a3a5d189..c51cbc5bfb 100644 --- a/backend/core/src/preferences/entities/preference.entity.ts +++ b/backend/core/src/preferences/entities/preference.entity.ts @@ -2,17 +2,19 @@ import { Column, CreateDateColumn, Entity, - ManyToOne, + ManyToMany, + OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, } from "typeorm" -import { Listing } from "../../listings/entities/listing.entity" import { Expose, Type } from "class-transformer" -import { IsDate, IsNumber, IsOptional, IsString, IsUUID, ValidateNested } from "class-validator" +import { IsDate, IsOptional, IsString, IsUUID, ValidateNested } from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { FormMetadata } from "../../applications/types/form-metadata/form-metadata" import { PreferenceLink } from "../types/preference-link" import { ApiProperty } from "@nestjs/swagger" +import { ListingPreference } from "./listing-preference.entity" +import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" @Entity({ name: "preferences" }) class Preference { @@ -34,12 +36,6 @@ class Preference { @Type(() => Date) updatedAt: Date - @Column({ type: "integer", nullable: true }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - ordinal?: number | null - @Column({ type: "text", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @@ -66,11 +62,11 @@ class Preference { @ApiProperty({ type: [PreferenceLink] }) links?: PreferenceLink[] | null - @ManyToOne(() => Listing, (listing) => listing.preferences, { - onDelete: "CASCADE", - onUpdate: "CASCADE", - }) - listing: Listing + @OneToMany(() => ListingPreference, (listingPreference) => listingPreference.preference) + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingPreference) + listingPreferences: ListingPreference[] @Column({ type: "jsonb", nullable: true }) @Expose() @@ -79,11 +75,11 @@ class Preference { @Type(() => FormMetadata) formMetadata?: FormMetadata - @Column({ type: "integer", nullable: true }) + @ManyToMany(() => Jurisdiction, (jurisdiction) => jurisdiction.preferences) @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - page?: number | null + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Jurisdiction) + jurisdictions: Jurisdiction[] } export { Preference as default, Preference } diff --git a/backend/core/src/preferences/preferences.controller.ts b/backend/core/src/preferences/preferences.controller.ts index f75ba1ef92..c0f49cbebb 100644 --- a/backend/core/src/preferences/preferences.controller.ts +++ b/backend/core/src/preferences/preferences.controller.ts @@ -6,18 +6,23 @@ import { Param, Post, Put, + Query, UseGuards, UsePipes, ValidationPipe, } from "@nestjs/common" import { PreferencesService } from "../preferences/preferences.service" -import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger" -import { PreferenceCreateDto, PreferenceDto, PreferenceUpdateDto } from "./dto/preference.dto" +import { ApiBearerAuth, ApiExtraModels, ApiOperation, ApiTags } from "@nestjs/swagger" +import { PreferenceDto } from "./dto/preference.dto" import { AuthzGuard } from "../auth/guards/authz.guard" import { ResourceType } from "../auth/decorators/resource-type.decorator" import { mapTo } from "../shared/mapTo" import { OptionalAuthGuard } from "../auth/guards/optional-auth.guard" import { defaultValidationPipeOptions } from "../shared/default-validation-pipe-options" +import { PreferenceCreateDto } from "./dto/preference-create.dto" +import { PreferenceUpdateDto } from "./dto/preference-update.dto" +import { PreferencesListQueryParams } from "./dto/preferences-list-query-params" +import { PreferencesFilterParams } from "./dto/preferences-filter-params" @Controller("/preferences") @ApiTags("preferences") @@ -30,8 +35,9 @@ export class PreferencesController { @Get() @ApiOperation({ summary: "List preferences", operationId: "list" }) - async list(): Promise { - return mapTo(PreferenceDto, await this.preferencesService.list()) + @ApiExtraModels(PreferencesFilterParams) + async list(@Query() queryParams: PreferencesListQueryParams): Promise { + return mapTo(PreferenceDto, await this.preferencesService.list(queryParams)) } @Post() diff --git a/backend/core/src/preferences/preferences.module.ts b/backend/core/src/preferences/preferences.module.ts index 6488b20f72..31809d1d2f 100644 --- a/backend/core/src/preferences/preferences.module.ts +++ b/backend/core/src/preferences/preferences.module.ts @@ -2,13 +2,12 @@ import { Module } from "@nestjs/common" import { TypeOrmModule } from "@nestjs/typeorm" import { PreferencesController } from "../preferences/preferences.controller" import { PreferencesService } from "./preferences.service" -import { Listing } from "../listings/entities/listing.entity" import { Preference } from "./entities/preference.entity" import { Unit } from "../units/entities/unit.entity" import { AuthModule } from "../auth/auth.module" @Module({ - imports: [TypeOrmModule.forFeature([Listing, Preference, Unit]), AuthModule], + imports: [TypeOrmModule.forFeature([Preference, Unit]), AuthModule], providers: [PreferencesService], exports: [PreferencesService], controllers: [PreferencesController], diff --git a/backend/core/src/preferences/preferences.service.ts b/backend/core/src/preferences/preferences.service.ts index 7111f214d5..d43a9dbfb1 100644 --- a/backend/core/src/preferences/preferences.service.ts +++ b/backend/core/src/preferences/preferences.service.ts @@ -1,9 +1,61 @@ import { Preference } from "./entities/preference.entity" -import { AbstractServiceFactory } from "../shared/services/abstract-service" -import { PreferenceCreateDto, PreferenceUpdateDto } from "./dto/preference.dto" - -export class PreferencesService extends AbstractServiceFactory< - Preference, - PreferenceCreateDto, - PreferenceUpdateDto ->(Preference) {} +import { PreferenceCreateDto } from "./dto/preference-create.dto" +import { PreferenceUpdateDto } from "./dto/preference-update.dto" +import { NotFoundException } from "@nestjs/common" +import { InjectRepository } from "@nestjs/typeorm" +import { FindOneOptions, Repository } from "typeorm" +import { addFilters } from "../shared/query-filter" +import { PreferencesListQueryParams } from "./dto/preferences-list-query-params" +import { PreferencesFilterParams } from "./dto/preferences-filter-params" +import { jurisdictionFilterTypeToFieldMap } from "./dto/jurisdictionFilterTypeToFieldMap" +import { assignDefined } from "../shared/utils/assign-defined" + +export class PreferencesService { + constructor(@InjectRepository(Preference) private readonly repository: Repository) {} + + list(params?: PreferencesListQueryParams): Promise { + const qb = this.repository + .createQueryBuilder("preferences") + .leftJoin("preferences.jurisdictions", "preferenceJurisdictions") + .select(["preferences", "preferenceJurisdictions.id"]) + + if (params.filter) { + addFilters, typeof jurisdictionFilterTypeToFieldMap>( + params.filter, + jurisdictionFilterTypeToFieldMap, + qb + ) + } + return qb.getMany() + } + + async create(dto: PreferenceCreateDto): Promise { + return await this.repository.save(dto) + } + + async findOne(findOneOptions: FindOneOptions): Promise { + const obj = await this.repository.findOne(findOneOptions) + if (!obj) { + throw new NotFoundException() + } + return obj + } + + async delete(objId: string) { + await this.repository.delete(objId) + } + + async update(dto: PreferenceUpdateDto) { + const obj = await this.repository.findOne({ + where: { + id: dto.id, + }, + }) + if (!obj) { + throw new NotFoundException() + } + assignDefined(obj, dto) + await this.repository.save(obj) + return obj + } +} diff --git a/backend/core/src/program/dto/jurisdictionFilterTypeToFieldMap.ts b/backend/core/src/program/dto/jurisdictionFilterTypeToFieldMap.ts new file mode 100644 index 0000000000..1eb7bc0214 --- /dev/null +++ b/backend/core/src/program/dto/jurisdictionFilterTypeToFieldMap.ts @@ -0,0 +1,5 @@ +import { ProgramFilterKeys } from "./program-filter-keys" + +export const jurisdictionFilterTypeToFieldMap: Record = { + jurisdiction: "programJurisdictions.id", +} diff --git a/backend/core/src/program/dto/listing-program-update.dto.ts b/backend/core/src/program/dto/listing-program-update.dto.ts new file mode 100644 index 0000000000..994ee26e03 --- /dev/null +++ b/backend/core/src/program/dto/listing-program-update.dto.ts @@ -0,0 +1,15 @@ +import { Expose, Type } from "class-transformer" +import { ListingProgramDto } from "./listing-program.dto" +import { IsDefined, IsOptional, ValidateNested } from "class-validator" +import { OmitType } from "@nestjs/swagger" +import { IdDto } from "../../shared/dto/id.dto" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +export class ListingProgramUpdateDto extends OmitType(ListingProgramDto, ["program"] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => IdDto) + program: IdDto +} diff --git a/backend/core/src/program/dto/listing-program.dto.ts b/backend/core/src/program/dto/listing-program.dto.ts new file mode 100644 index 0000000000..54c37196cc --- /dev/null +++ b/backend/core/src/program/dto/listing-program.dto.ts @@ -0,0 +1,15 @@ +import { Expose, Type } from "class-transformer" +import { IsDefined, IsOptional, ValidateNested } from "class-validator" +import { OmitType } from "@nestjs/swagger" +import { ProgramDto } from "./program.dto" +import { ListingProgram } from "../entities/listing-program.entity" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +export class ListingProgramDto extends OmitType(ListingProgram, ["listing", "program"] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ProgramDto) + program: ProgramDto +} diff --git a/backend/core/src/program/dto/program-create.dto.ts b/backend/core/src/program/dto/program-create.dto.ts new file mode 100644 index 0000000000..4d4b6a04ff --- /dev/null +++ b/backend/core/src/program/dto/program-create.dto.ts @@ -0,0 +1,8 @@ +import { OmitType } from "@nestjs/swagger" +import { ProgramDto } from "./program.dto" + +export class ProgramCreateDto extends OmitType(ProgramDto, [ + "id", + "createdAt", + "updatedAt", +] as const) {} diff --git a/backend/core/src/program/dto/program-filter-keys.ts b/backend/core/src/program/dto/program-filter-keys.ts new file mode 100644 index 0000000000..332871657a --- /dev/null +++ b/backend/core/src/program/dto/program-filter-keys.ts @@ -0,0 +1,3 @@ +export enum ProgramFilterKeys { + jurisdiction = "jurisdiction", +} diff --git a/backend/core/src/program/dto/program-update.dto.ts b/backend/core/src/program/dto/program-update.dto.ts new file mode 100644 index 0000000000..1045b7e41c --- /dev/null +++ b/backend/core/src/program/dto/program-update.dto.ts @@ -0,0 +1,11 @@ +import { ProgramCreateDto } from "./program-create.dto" +import { Expose } from "class-transformer" +import { IsString, IsUUID } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +export class ProgramUpdateDto extends ProgramCreateDto { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID() + id: string +} diff --git a/backend/core/src/program/dto/program.dto.ts b/backend/core/src/program/dto/program.dto.ts new file mode 100644 index 0000000000..d9d5711a6f --- /dev/null +++ b/backend/core/src/program/dto/program.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from "@nestjs/swagger" +import { Program } from "../entities/program.entity" + +export class ProgramDto extends OmitType(Program, ["listingPrograms", "jurisdictions"] as const) {} diff --git a/backend/core/src/program/dto/programs-filter-params.ts b/backend/core/src/program/dto/programs-filter-params.ts new file mode 100644 index 0000000000..2f2952fffd --- /dev/null +++ b/backend/core/src/program/dto/programs-filter-params.ts @@ -0,0 +1,18 @@ +import { BaseFilter } from "../../shared/dto/filter.dto" +import { Expose } from "class-transformer" +import { ApiProperty } from "@nestjs/swagger" +import { IsOptional, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { ProgramFilterKeys } from "./program-filter-keys" + +export class ProgramsFilterParams extends BaseFilter { + @Expose() + @ApiProperty({ + type: String, + example: "uuid", + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + [ProgramFilterKeys.jurisdiction]?: string +} diff --git a/backend/core/src/program/dto/programs-list-query-params.ts b/backend/core/src/program/dto/programs-list-query-params.ts new file mode 100644 index 0000000000..1c8289ca44 --- /dev/null +++ b/backend/core/src/program/dto/programs-list-query-params.ts @@ -0,0 +1,24 @@ +import { Expose, Type } from "class-transformer" +import { ApiProperty, getSchemaPath } from "@nestjs/swagger" +import { ArrayMaxSize, IsArray, IsOptional, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { ProgramsFilterParams } from "./programs-filter-params" + +export class ProgramsListQueryParams { + @Expose() + @ApiProperty({ + name: "filter", + required: false, + type: [String], + items: { + $ref: getSchemaPath(ProgramsFilterParams), + }, + example: { $comparison: "=", jurisdiction: "uuid" }, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsArray({ groups: [ValidationsGroupsEnum.default] }) + @ArrayMaxSize(16, { groups: [ValidationsGroupsEnum.default] }) + @Type(() => ProgramsFilterParams) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + filter?: ProgramsFilterParams[] +} diff --git a/backend/core/src/program/entities/listing-program.entity.ts b/backend/core/src/program/entities/listing-program.entity.ts new file mode 100644 index 0000000000..16f259bcb5 --- /dev/null +++ b/backend/core/src/program/entities/listing-program.entity.ts @@ -0,0 +1,30 @@ +import { Column, Entity, ManyToOne } from "typeorm" +import { Program } from "./program.entity" +import { Expose, Type } from "class-transformer" +import { IsNumber, IsOptional } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { Listing } from "../../listings/entities/listing.entity" + +@Entity({ name: "listing_programs" }) +export class ListingProgram { + @ManyToOne(() => Listing, (listing) => listing.listingPrograms, { + primary: true, + orphanedRowAction: "delete", + }) + @Type(() => Listing) + listing: Listing + + @ManyToOne(() => Program, (program) => program.listingPrograms, { + primary: true, + eager: true, + }) + @Expose() + @Type(() => Program) + program: Program + + @Column({ type: "integer", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + ordinal?: number | null +} diff --git a/backend/core/src/program/entities/program.entity.ts b/backend/core/src/program/entities/program.entity.ts new file mode 100644 index 0000000000..8fe9d4e9df --- /dev/null +++ b/backend/core/src/program/entities/program.entity.ts @@ -0,0 +1,50 @@ +import { Column, Entity, ManyToMany, OneToMany } from "typeorm" +import { Expose, Type } from "class-transformer" +import { IsOptional, IsString, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { AbstractEntity } from "../../shared/entities/abstract.entity" +import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" +import { ListingProgram } from "./listing-program.entity" +import { FormMetadata } from "../../applications/types/form-metadata/form-metadata" + +@Entity({ name: "programs" }) +class Program extends AbstractEntity { + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + title?: string | null + + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + subtitle?: string | null + + @Column({ type: "text", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + description?: string | null + + @OneToMany(() => ListingProgram, (listingProgram) => listingProgram.program) + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => ListingProgram) + listingPrograms: ListingProgram[] + + @ManyToMany(() => Jurisdiction, (jurisdiction) => jurisdiction.programs) + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => Jurisdiction) + jurisdictions: Jurisdiction[] + + @Column({ type: "jsonb", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => FormMetadata) + formMetadata?: FormMetadata +} + +export { Program as default, Program } diff --git a/backend/core/src/program/programs.controller.ts b/backend/core/src/program/programs.controller.ts new file mode 100644 index 0000000000..6c982854d7 --- /dev/null +++ b/backend/core/src/program/programs.controller.ts @@ -0,0 +1,66 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UseGuards, + UsePipes, + ValidationPipe, +} from "@nestjs/common" +import { ApiBearerAuth, ApiOperation, ApiTags, ApiExtraModels } from "@nestjs/swagger" +import { AuthzGuard } from "../auth/guards/authz.guard" +import { ResourceType } from "../auth/decorators/resource-type.decorator" +import { mapTo } from "../shared/mapTo" +import { OptionalAuthGuard } from "../auth/guards/optional-auth.guard" +import { defaultValidationPipeOptions } from "../shared/default-validation-pipe-options" +import { ProgramsService } from "./programs.service" +import { ProgramDto } from "./dto/program.dto" +import { ProgramCreateDto } from "./dto/program-create.dto" +import { ProgramUpdateDto } from "./dto/program-update.dto" +import { ProgramsFilterParams } from "./dto/programs-filter-params" +import { ProgramsListQueryParams } from "./dto/programs-list-query-params" + +@Controller("/programs") +@ApiTags("programs") +@ApiBearerAuth() +@ResourceType("program") +@UseGuards(OptionalAuthGuard, AuthzGuard) +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +export class ProgramsController { + constructor(private readonly programsService: ProgramsService) {} + + @Get() + @ApiOperation({ summary: "List programs", operationId: "list" }) + @ApiExtraModels(ProgramsFilterParams) + async list(@Query() queryParams: ProgramsListQueryParams): Promise { + return mapTo(ProgramDto, await this.programsService.list(queryParams)) + } + + @Post() + @ApiOperation({ summary: "Create program", operationId: "create" }) + async create(@Body() program: ProgramCreateDto): Promise { + return mapTo(ProgramDto, await this.programsService.create(program)) + } + + @Put(`:programId`) + @ApiOperation({ summary: "Update program", operationId: "update" }) + async update(@Body() program: ProgramUpdateDto): Promise { + return mapTo(ProgramDto, await this.programsService.update(program)) + } + + @Get(`:programId`) + @ApiOperation({ summary: "Get program by id", operationId: "retrieve" }) + async retrieve(@Param("programId") programId: string): Promise { + return mapTo(ProgramDto, await this.programsService.findOne({ where: { id: programId } })) + } + + @Delete(`:programId`) + @ApiOperation({ summary: "Delete program by id", operationId: "delete" }) + async delete(@Param("programId") programId: string): Promise { + await this.programsService.delete(programId) + } +} diff --git a/backend/core/src/program/programs.module.ts b/backend/core/src/program/programs.module.ts new file mode 100644 index 0000000000..43fc052a67 --- /dev/null +++ b/backend/core/src/program/programs.module.ts @@ -0,0 +1,15 @@ +import { Module } from "@nestjs/common" +import { TypeOrmModule } from "@nestjs/typeorm" +import { ProgramsService } from "./programs.service" +import { Listing } from "../listings/entities/listing.entity" +import { Program } from "./entities/program.entity" +import { AuthModule } from "../auth/auth.module" +import { ProgramsController } from "./programs.controller" + +@Module({ + imports: [TypeOrmModule.forFeature([Listing, Program]), AuthModule], + providers: [ProgramsService], + exports: [ProgramsService], + controllers: [ProgramsController], +}) +export class ProgramsModule {} diff --git a/backend/core/src/program/programs.service.ts b/backend/core/src/program/programs.service.ts new file mode 100644 index 0000000000..1d6001aa5e --- /dev/null +++ b/backend/core/src/program/programs.service.ts @@ -0,0 +1,61 @@ +import { Program } from "./entities/program.entity" +import { ProgramCreateDto } from "./dto/program-create.dto" +import { ProgramUpdateDto } from "./dto/program-update.dto" +import { NotFoundException } from "@nestjs/common" +import { InjectRepository } from "@nestjs/typeorm" +import { FindOneOptions, Repository } from "typeorm" +import { addFilters } from "../shared/query-filter" +import { ProgramsListQueryParams } from "./dto/programs-list-query-params" +import { ProgramsFilterParams } from "./dto/programs-filter-params" +import { jurisdictionFilterTypeToFieldMap } from "./dto/jurisdictionFilterTypeToFieldMap" +import { assignDefined } from "../shared/utils/assign-defined" + +export class ProgramsService { + constructor(@InjectRepository(Program) private readonly repository: Repository) {} + + list(params?: ProgramsListQueryParams): Promise { + const qb = this.repository + .createQueryBuilder("programs") + .leftJoin("programs.jurisdictions", "programJurisdictions") + .select(["programs", "programJurisdictions.id"]) + + if (params.filter) { + addFilters, typeof jurisdictionFilterTypeToFieldMap>( + params.filter, + jurisdictionFilterTypeToFieldMap, + qb + ) + } + return qb.getMany() + } + + async create(dto: ProgramCreateDto): Promise { + return await this.repository.save(dto) + } + + async findOne(findOneOptions: FindOneOptions): Promise { + const obj = await this.repository.findOne(findOneOptions) + if (!obj) { + throw new NotFoundException() + } + return obj + } + + async delete(objId: string) { + await this.repository.delete(objId) + } + + async update(dto: ProgramUpdateDto) { + const obj = await this.repository.findOne({ + where: { + id: dto.id, + }, + }) + if (!obj) { + throw new NotFoundException() + } + assignDefined(obj, dto) + await this.repository.save(obj) + return obj + } +} diff --git a/backend/core/src/property/entities/property.entity.ts b/backend/core/src/property/entities/property.entity.ts index 3fcb47c732..391073da49 100644 --- a/backend/core/src/property/entities/property.entity.ts +++ b/backend/core/src/property/entities/property.entity.ts @@ -13,6 +13,7 @@ import { Expose, Type } from "class-transformer" import { IsDate, IsDefined, + IsEnum, IsNumber, IsOptional, IsString, @@ -23,6 +24,8 @@ import { Unit } from "../../units/entities/unit.entity" import { PropertyGroup } from "../../property-groups/entities/property-group.entity" import { Address } from "../../shared/entities/address.entity" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { Region } from "../types/region-enum" +import { ApiProperty } from "@nestjs/swagger" @Entity() export class Property { @@ -100,6 +103,16 @@ export class Property { @IsString({ groups: [ValidationsGroupsEnum.default] }) neighborhood?: string | null + @Column({ type: "enum", enum: Region, nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsEnum(Region, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ + enum: Region, + enumName: "Region", + }) + region?: Region | null + @Column({ type: "text", nullable: true }) @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend/core/src/property/types/region-enum.ts b/backend/core/src/property/types/region-enum.ts new file mode 100644 index 0000000000..bb5027c17e --- /dev/null +++ b/backend/core/src/property/types/region-enum.ts @@ -0,0 +1,6 @@ +export enum Region { + GreaterDowntown = "Greater Downtown", + Eastside = "Eastside", + Southwest = "Southwest", + Westside = "Westside", +} diff --git a/backend/core/src/reserved-community-type/reserved-community-types.service.ts b/backend/core/src/reserved-community-type/reserved-community-types.service.ts index e2319aca7c..ca204301e8 100644 --- a/backend/core/src/reserved-community-type/reserved-community-types.service.ts +++ b/backend/core/src/reserved-community-type/reserved-community-types.service.ts @@ -1,14 +1,13 @@ -import { QueryOneOptions } from "../shared/services/abstract-service" import { NotFoundException } from "@nestjs/common" import { ReservedCommunityType } from "./entities/reserved-community-type.entity" import { InjectRepository } from "@nestjs/typeorm" -import { Repository } from "typeorm" -import { assignDefined } from "../shared/assign-defined" +import { FindOneOptions, Repository } from "typeorm" import { ReservedCommunityTypeCreateDto, ReservedCommunityTypeUpdateDto, } from "./dto/reserved-community-type.dto" import { ReservedCommunityTypeListQueryParams } from "./dto/reserved-community-type-list-query-params" +import { assignDefined } from "../shared/utils/assign-defined" export class ReservedCommunityTypesService { constructor( @@ -35,9 +34,9 @@ export class ReservedCommunityTypesService { } async findOne( - queryOneOptions: QueryOneOptions + findOneOptions: FindOneOptions ): Promise { - const obj = await this.repository.findOne(queryOneOptions) + const obj = await this.repository.findOne(findOneOptions) if (!obj) { throw new NotFoundException() } diff --git a/backend/core/src/seed.ts b/backend/core/src/seed.ts deleted file mode 100644 index 2064c6f05a..0000000000 --- a/backend/core/src/seed.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { SeederModule } from "./seeder/seeder.module" -import { NestFactory } from "@nestjs/core" -import yargs from "yargs" -import { UserService } from "./auth/services/user.service" -import { plainToClass } from "class-transformer" -import { Repository } from "typeorm" -import { getRepositoryToken } from "@nestjs/typeorm" -import { User } from "./auth/entities/user.entity" -import { makeNewApplication } from "./seeds/applications" -import { INestApplicationContext } from "@nestjs/common" -import { ListingDefaultSeed } from "./seeds/listings/listing-default-seed" -import { defaultLeasingAgents } from "./seeds/listings/shared" -import { Listing } from "./listings/entities/listing.entity" -import { ListingColiseumSeed } from "./seeds/listings/listing-coliseum-seed" -import { ListingDefaultOpenSoonSeed } from "./seeds/listings/listing-default-open-soon" -import { ListingDefaultOnePreferenceSeed } from "./seeds/listings/listing-default-one-preference-seed" -import { ListingDefaultNoPreferenceSeed } from "./seeds/listings/listing-default-no-preference-seed" -import { ListingTritonSeed } from "./seeds/listings/listing-triton-seed" -import { ListingDefaultBmrChartSeed } from "./seeds/listings/listing-default-bmr-chart-seed" -import { ApplicationMethodsService } from "./application-methods/application-methods.service" -import { ApplicationMethodType } from "./application-methods/types/application-method-type-enum" -import { AuthContext } from "./auth/types/auth-context" -import { ListingDefaultReservedSeed } from "./seeds/listings/listing-default-reserved-seed" -import { ListingDefaultFCFSSeed } from "./seeds/listings/listing-default-fcfs-seed" -import { UserRoles } from "./auth/entities/user-roles.entity" -import { ListingDefaultMultipleAMI } from "./seeds/listings/listing-default-multiple-ami" -import { ListingDefaultMultipleAMIAndPercentages } from "./seeds/listings/listing-default-multiple-ami-and-percentages" -import { ListingDefaultMissingAMI } from "./seeds/listings/listing-default-missing-ami" -import { createJurisdictions } from "./seeds/jurisdictions" -import { Jurisdiction } from "./jurisdictions/entities/jurisdiction.entity" -import { UserCreateDto } from "./auth/dto/user-create.dto" -import { UnitTypesService } from "./unit-types/unit-types.service" - -const argv = yargs.scriptName("seed").options({ - test: { type: "boolean", default: false }, -}).argv - -// Note: if changing this list of seeds, you must also change the -// number in listings.e2e-spec.ts. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const listingSeeds: any[] = [ - ListingDefaultSeed, - ListingColiseumSeed, - ListingDefaultOpenSoonSeed, - ListingDefaultOnePreferenceSeed, - ListingDefaultNoPreferenceSeed, - ListingDefaultNoPreferenceSeed, - ListingDefaultBmrChartSeed, - ListingTritonSeed, - ListingDefaultReservedSeed, - ListingDefaultFCFSSeed, - ListingDefaultMultipleAMI, - ListingDefaultMultipleAMIAndPercentages, - ListingDefaultMissingAMI, -] - -export function getSeedListingsCount() { - return listingSeeds.length -} - -export async function createLeasingAgents( - app: INestApplicationContext, - rolesRepo: Repository, - jurisdictions: Jurisdiction[] -) { - const usersService = await app.resolve(UserService) - const leasingAgents = await Promise.all( - defaultLeasingAgents.map( - async (leasingAgent) => - await usersService.createUser( - plainToClass(UserCreateDto, { - ...leasingAgent, - jurisdictions: [jurisdictions[0]], - }), - new AuthContext(null) - ) - ) - ) - await Promise.all([ - leasingAgents.map(async (agent: User) => { - const roles: UserRoles = { user: agent, isPartner: true } - await rolesRepo.save(roles) - await usersService.confirm({ token: agent.confirmationToken }) - }), - ]) - return leasingAgents -} - -const seedListings = async ( - app: INestApplicationContext, - rolesRepo: Repository, - jurisdictions: Jurisdiction[] -) => { - const seeds = [] - const leasingAgents = await createLeasingAgents(app, rolesRepo, jurisdictions) - - const allSeeds = listingSeeds.map((listingSeed) => app.get(listingSeed)) - const listingRepository = app.get>(getRepositoryToken(Listing)) - const applicationMethodsService = await app.resolve( - ApplicationMethodsService - ) - - for (const [index, listingSeed] of allSeeds.entries()) { - const everyOtherAgent = index % 2 ? leasingAgents[0] : leasingAgents[1] - const listing = await listingSeed.seed() - listing.jurisdiction = jurisdictions[0] - listing.leasingAgents = [everyOtherAgent] - const applicationMethods = await applicationMethodsService.create({ - type: ApplicationMethodType.Internal, - acceptsPostmarkedApplications: false, - externalReference: "", - label: "Label", - paperApplications: [], - listing, - }) - listing.applicationMethods = [applicationMethods] - await listingRepository.save(listing) - - seeds.push(listing) - } - - return seeds -} - -async function seed() { - const app = await NestFactory.create(SeederModule.forRoot({ test: argv.test })) - // Starts listening for shutdown hooks - app.enableShutdownHooks() - const userService = await app.resolve(UserService) - - const userRepo = app.get>(getRepositoryToken(User)) - const rolesRepo = app.get>(getRepositoryToken(UserRoles)) - const jurisdictions = await createJurisdictions(app) - const listings = await seedListings(app, rolesRepo, jurisdictions) - - const user1 = await userService.createUser( - plainToClass(UserCreateDto, { - email: "test@example.com", - emailConfirmation: "test@example.com", - firstName: "First", - middleName: "Mid", - lastName: "Last", - dob: new Date(), - password: "abcdef", - passwordConfirmation: "Abcdef1!", - jurisdictions: [jurisdictions[0]], - }), - new AuthContext(null) - ) - await userService.confirm({ token: user1.confirmationToken }) - - const user2 = await userService.createUser( - plainToClass(UserCreateDto, { - email: "test2@example.com", - emailConfirmation: "test2@example.com", - firstName: "Second", - middleName: "Mid", - lastName: "Last", - dob: new Date(), - password: "ghijkl", - passwordConfirmation: "Ghijkl1!", - jurisdictions: [jurisdictions[0]], - }), - new AuthContext(null) - ) - await userService.confirm({ token: user2.confirmationToken }) - - const admin = await userService.createUser( - plainToClass(UserCreateDto, { - email: "admin@example.com", - emailConfirmation: "admin@example.com", - firstName: "Second", - middleName: "Mid", - lastName: "Last", - dob: new Date(), - password: "abcdef", - passwordConfirmation: "Abcdef1!", - jurisdictions, - }), - new AuthContext(null) - ) - - const unitTypesService = await app.resolve(UnitTypesService) - - const unitTypes = await unitTypesService.list() - - for (let i = 0; i < 10; i++) { - for (const listing of listings) { - await Promise.all([ - await makeNewApplication(app, listing, unitTypes, user1), - await makeNewApplication(app, listing, unitTypes, user2), - ]) - } - } - - await userRepo.save(admin) - const roles: UserRoles = { user: admin, isPartner: true, isAdmin: true } - await rolesRepo.save(roles) - - await userService.confirm({ token: admin.confirmationToken }) - await app.close() -} - -void seed() diff --git a/backend/core/src/seeder/detroit-seed.ts b/backend/core/src/seeder/detroit-seed.ts new file mode 100644 index 0000000000..ecdfc25b9d --- /dev/null +++ b/backend/core/src/seeder/detroit-seed.ts @@ -0,0 +1,194 @@ +import { SeederModule } from "./seeder.module" +import { NestFactory } from "@nestjs/core" +import yargs from "yargs" +import { UserService } from "../auth/services/user.service" +import { plainToClass } from "class-transformer" +import { UserCreateDto } from "../auth/dto/user-create.dto" +import { Repository } from "typeorm" +import { getRepositoryToken } from "@nestjs/typeorm" +import { User } from "../auth/entities/user.entity" +import { INestApplicationContext } from "@nestjs/common" +import { ListingDefaultSeed } from "./seeds/listings/listing-default-seed" +import { Listing } from "../listings/entities/listing.entity" +import { defaultLeasingAgents } from "./seeds/listings/shared" +import { AuthContext } from "../auth/types/auth-context" +import { Listing10158Seed } from "./seeds/listings/listing-detroit-10158" +import { Listing10157Seed } from "./seeds/listings/listing-detroit-10157" +import { Listing10147Seed } from "./seeds/listings/listing-detroit-10147" +import { Listing10145Seed } from "./seeds/listings/listing-detroit-10145" +import { ListingTreymoreSeed } from "./seeds/listings/listing-detroit-treymore" +import { UserRoles } from "../auth/entities/user-roles.entity" +import { AmiChart } from "../ami-charts/entities/ami-chart.entity" +import { WayneCountyMSHDA2021 } from "./seeds/ami-charts/WayneCountyMSHDA2021" +import { Listing10151Seed } from "./seeds/listings/listing-detroit-10151" +import { Listing10153Seed } from "./seeds/listings/listing-detroit-10153" +import { Listing10154Seed } from "./seeds/listings/listing-detroit-10154" +import { Listing10155Seed } from "./seeds/listings/listing-detroit-10155" +import { Listing10159Seed } from "./seeds/listings/listing-detroit-10159" +import { Listing10168Seed } from "./seeds/listings/listing-detroit-10168" +import { createJurisdictions } from "./seeds/jurisdictions" +import { Jurisdiction } from "../jurisdictions/entities/jurisdiction.entity" +import { Listing10202Seed } from "./seeds/listings/listing-detroit-10202" +import { Listing10136Seed } from "./seeds/listings/listing-detroit-10136" + +const argv = yargs.scriptName("seed").options({ + test: { type: "boolean", default: false }, +}).argv + +// Note: if changing this list of seeds, you must also change the +// number in listings.e2e-spec.ts. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const listingSeeds: any[] = [ + Listing10136Seed, + Listing10145Seed, + Listing10147Seed, + Listing10151Seed, + Listing10153Seed, + Listing10154Seed, + Listing10155Seed, + Listing10157Seed, + Listing10158Seed, + Listing10159Seed, + Listing10168Seed, + Listing10202Seed, + ListingTreymoreSeed, +] + +export function getSeedListingsCount() { + return listingSeeds.length +} + +export async function createLeasingAgents( + app: INestApplicationContext, + rolesRepo: Repository, + jurisdictions: Jurisdiction[] +) { + const usersService = await app.resolve(UserService) + const leasingAgents = await Promise.all( + defaultLeasingAgents.map( + async (leasingAgent) => + await usersService.createPublicUser( + plainToClass(UserCreateDto, { + ...leasingAgent, + jurisdictions: [jurisdictions.find((jurisdiction) => jurisdiction.name == "Detroit")], + }), + new AuthContext(null) + ) + ) + ) + await Promise.all([ + leasingAgents.map(async (agent: User) => { + const roles: UserRoles = { user: agent, isPartner: true } + await rolesRepo.save(roles) + await usersService.confirm({ token: agent.confirmationToken }) + }), + ]) + return leasingAgents +} + +const seedListings = async ( + app: INestApplicationContext, + rolesRepo: Repository, + jurisdictions: Jurisdiction[] +) => { + const seeds = [] + const leasingAgents = await createLeasingAgents(app, rolesRepo, jurisdictions) + + const allSeeds = listingSeeds.map((listingSeed) => app.get(listingSeed)) + const listingRepository = app.get>(getRepositoryToken(Listing)) + + for (const [index, listingSeed] of allSeeds.entries()) { + const everyOtherAgent = index % 2 ? leasingAgents[0] : leasingAgents[1] + const listing = await listingSeed.seed() + listing.jurisdiction = jurisdictions.find((jurisdiction) => jurisdiction.name == "Detroit") + listing.leasingAgents = [everyOtherAgent] + await listingRepository.save(listing) + + seeds.push(listing) + } + + return seeds +} + +async function seed() { + const app = await NestFactory.create(SeederModule.forRoot({ test: argv.test })) + // Starts listening for shutdown hooks + app.enableShutdownHooks() + const userService = await app.resolve(UserService) + + const userRepo = app.get>(getRepositoryToken(User)) + const rolesRepo = app.get>(getRepositoryToken(UserRoles)) + const jurisdictions = await createJurisdictions(app) + await seedListings(app, rolesRepo, jurisdictions) + + let user1 = await userService.findByEmail("test@example.com") + if (user1 === undefined) { + user1 = await userService.createPublicUser( + plainToClass(UserCreateDto, { + email: "test@example.com", + emailConfirmation: "test@example.com", + firstName: "First", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + password: "abcdef", + passwordConfirmation: "Abcdef1!", + jurisdictions: [jurisdictions.find((jurisdiction) => jurisdiction.name == "Detroit")], + }), + new AuthContext(null) + ) + await userService.confirm({ token: user1.confirmationToken }) + } + + let user2 = await userService.findByEmail("test2@example.com") + if (user2 === undefined) { + user2 = await userService.createPublicUser( + plainToClass(UserCreateDto, { + email: "test2@example.com", + emailConfirmation: "test2@example.com", + firstName: "Second", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + password: "ghijkl", + passwordConfirmation: "Ghijkl1!", + jurisdictions: [jurisdictions.find((jurisdiction) => jurisdiction.name == "Detroit")], + }), + new AuthContext(null) + ) + await userService.confirm({ token: user2.confirmationToken }) + } + + let admin = await userService.findByEmail("admin@example.com") + if (admin === undefined) { + admin = await userService.createPublicUser( + plainToClass(UserCreateDto, { + email: "admin@example.com", + emailConfirmation: "admin@example.com", + firstName: "Second", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + password: "abcdef", + passwordConfirmation: "Abcdef1!", + jurisdictions, + }), + new AuthContext(null) + ) + + await userRepo.save(admin) + const roles: UserRoles = { user: admin, isPartner: false, isAdmin: true } + await rolesRepo.save(roles) + await userService.confirm({ token: admin.confirmationToken }) + } + + // Seed the Detroit AMI data, since it's not linked to any units. + const amiChartRepo = app.get>(getRepositoryToken(AmiChart)) + await amiChartRepo.save({ + ...JSON.parse(JSON.stringify(WayneCountyMSHDA2021)), + jurisdiction: jurisdictions.find((jurisdiction) => jurisdiction.name == "Detroit"), + }) + await app.close() +} + +void seed() diff --git a/backend/core/src/seeder/seed.ts b/backend/core/src/seeder/seed.ts new file mode 100644 index 0000000000..208c9b3b0f --- /dev/null +++ b/backend/core/src/seeder/seed.ts @@ -0,0 +1,385 @@ +import { NestFactory } from "@nestjs/core" +import yargs from "yargs" +import { plainToClass } from "class-transformer" +import { Repository } from "typeorm" +import { getRepositoryToken } from "@nestjs/typeorm" +import { INestApplicationContext } from "@nestjs/common" +import { ListingDefaultSeed } from "./seeds/listings/listing-default-seed" +import { ListingColiseumSeed } from "./seeds/listings/listing-coliseum-seed" +import { ListingDefaultOpenSoonSeed } from "./seeds/listings/listing-default-open-soon" +import { ListingDefaultOnePreferenceSeed } from "./seeds/listings/listing-default-one-preference-seed" +import { ListingDefaultNoPreferenceSeed } from "./seeds/listings/listing-default-no-preference-seed" +import { ListingDefaultSummaryWithoutAndListingWith20AmiPercentageSeed } from "./seeds/listings/listing-default-summary-without-and-listing-with-20-ami-percentage-seed" +import { ListingDefaultSummaryWith30ListingWith10AmiPercentageSeed } from "./seeds/listings/listing-default-summary-with-30-listing-with-10-ami-percentage-seed" +import { ListingDefaultSummaryWith30And60AmiPercentageSeed } from "./seeds/listings/listing-default-summary-with-30-and-60-ami-percentage-seed" +import { ListingDefaultSummaryWith10ListingWith30AmiPercentageSeed } from "./seeds/listings/listing-default-summary-with-10-listing-with-30-ami-percentage-seed" +import { ListingDefaultNeighborhoodAmenitiesSeed } from "./seeds/listings/listing-default-neighbor-amenities" +import { Listing10158Seed } from "./seeds/listings/listing-detroit-10158" +import { Listing10157Seed } from "./seeds/listings/listing-detroit-10157" +import { Listing10147Seed } from "./seeds/listings/listing-detroit-10147" +import { Listing10145Seed } from "./seeds/listings/listing-detroit-10145" +import { CountyCode } from "../shared/types/county-code" +import { ListingTreymoreSeed } from "./seeds/listings/listing-detroit-treymore" +import { AmiChart } from "../ami-charts/entities/ami-chart.entity" +import { WayneCountyMSHDA2021 } from "./seeds/ami-charts/WayneCountyMSHDA2021" +import { ListingDefaultBmrChartSeed } from "./seeds/listings/listing-default-bmr-chart-seed" +import { ListingTritonSeed, ListingTritonSeedDetroit } from "./seeds/listings/listing-triton-seed" +import { ListingDefaultReservedSeed } from "./seeds/listings/listing-default-reserved-seed" +import { ListingDefaultFCFSSeed } from "./seeds/listings/listing-default-fcfs-seed" +import { ListingDefaultMultipleAMI } from "./seeds/listings/listing-default-multiple-ami" +import { ListingDefaultLottery } from "./seeds/listings/listing-default-lottery-results" +import { ListingDefaultLotteryPending } from "./seeds/listings/listing-default-lottery-pending" +import { ListingDefaultMultipleAMIAndPercentages } from "./seeds/listings/listing-default-multiple-ami-and-percentages" +import { ListingDefaultMissingAMI } from "./seeds/listings/listing-default-missing-ami" +import { AmiChartDefaultSeed } from "./seeds/ami-charts/default-ami-chart" +import { + defaultLeasingAgents, + getDisabilityOrMentalIllnessProgram, + getDisplaceePreference, + getHopwaPreference, + getHousingSituationProgram, + getLiveWorkPreference, + getPbvPreference, + getServedInMilitaryProgram, + getTayProgram, + getFlatRentAndRentBasedOnIncomeProgram, +} from "./seeds/listings/shared" +import { UserCreateDto } from "../auth/dto/user-create.dto" +import { AuthContext } from "../auth/types/auth-context" +import { createJurisdictions } from "./seeds/jurisdictions" +import { AmiDefaultMissingAMI } from "./seeds/ami-charts/missing-household-ami-levels" +import { SeederModule } from "./seeder.module" +import { AmiDefaultTriton } from "./seeds/ami-charts/triton-ami-chart" +import { AmiDefaultTritonDetroit } from "./seeds/ami-charts/triton-ami-chart-detroit" +import { makeNewApplication } from "./seeds/applications" +import { UserRoles } from "../auth/entities/user-roles.entity" +import { Jurisdiction } from "../jurisdictions/entities/jurisdiction.entity" +import { UserService } from "../auth/services/user.service" +import { User } from "../auth/entities/user.entity" +import { Preference } from "../preferences/entities/preference.entity" +import { Program } from "../program/entities/program.entity" +import { Listing } from "../listings/entities/listing.entity" +import { UnitTypesService } from "../unit-types/unit-types.service" +import dayjs from "dayjs" + +const argv = yargs.scriptName("seed").options({ + test: { type: "boolean", default: false }, +}).argv + +// Note: if changing this list of seeds, you must also change the +// number in listings.e2e-spec.ts. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const listingSeeds: any[] = [ + ListingDefaultSeed, + ListingColiseumSeed, + ListingDefaultOpenSoonSeed, + ListingDefaultOnePreferenceSeed, + ListingDefaultNoPreferenceSeed, + ListingDefaultBmrChartSeed, + ListingTritonSeed, + ListingDefaultReservedSeed, + ListingDefaultFCFSSeed, + ListingDefaultSummaryWith30And60AmiPercentageSeed, + ListingDefaultSummaryWithoutAndListingWith20AmiPercentageSeed, + ListingDefaultSummaryWith30ListingWith10AmiPercentageSeed, + ListingDefaultSummaryWith10ListingWith30AmiPercentageSeed, + ListingDefaultNeighborhoodAmenitiesSeed, + Listing10145Seed, + Listing10147Seed, + Listing10157Seed, + Listing10158Seed, + ListingTreymoreSeed, + ListingDefaultMultipleAMI, + ListingDefaultMultipleAMIAndPercentages, + ListingDefaultMissingAMI, + ListingDefaultLottery, + ListingDefaultLotteryPending, + ListingTritonSeedDetroit, + ListingDefaultFCFSSeed, +] + +const amiSeeds: any[] = [ + AmiChartDefaultSeed, + AmiDefaultMissingAMI, + AmiDefaultTriton, + AmiDefaultTritonDetroit, +] + +export function getSeedListingsCount() { + return listingSeeds.length +} + +export async function createLeasingAgents( + app: INestApplicationContext, + rolesRepo: Repository, + jurisdictions: Jurisdiction[] +) { + const usersService = await app.resolve(UserService) + const leasingAgents = await Promise.all( + defaultLeasingAgents.map( + async (leasingAgent) => + await usersService.createPublicUser( + plainToClass(UserCreateDto, { + ...leasingAgent, + jurisdictions: [jurisdictions[0]], + }), + new AuthContext(null) + ) + ) + ) + await Promise.all([ + leasingAgents.map(async (agent: User) => { + const roles: UserRoles = { user: agent, isPartner: true } + await rolesRepo.save(roles) + await usersService.confirm({ token: agent.confirmationToken }) + }), + ]) + return leasingAgents +} + +export async function createPreferences( + app: INestApplicationContext, + jurisdictions: Jurisdiction[] +) { + const preferencesRepository = app.get>(getRepositoryToken(Preference)) + const preferencesToSave = [] + + jurisdictions.forEach((jurisdiction) => { + preferencesToSave.push( + getLiveWorkPreference(jurisdiction.name), + getPbvPreference(jurisdiction.name), + getHopwaPreference(jurisdiction.name), + getDisplaceePreference(jurisdiction.name) + ) + }) + + const preferences = await preferencesRepository.save(preferencesToSave) + + for (const jurisdiction of jurisdictions) { + jurisdiction.preferences = preferences.filter((preference) => { + const jurisdictionName = preference.title.split("-").pop() + return jurisdictionName === ` ${jurisdiction.name}` + }) + } + const jurisdictionsRepository = app.get>( + getRepositoryToken(Jurisdiction) + ) + await jurisdictionsRepository.save(jurisdictions) + return preferences +} + +export async function createPrograms(app: INestApplicationContext, jurisdictions: Jurisdiction[]) { + const programsRepository = app.get>(getRepositoryToken(Program)) + const programs = await programsRepository.save([ + getServedInMilitaryProgram(), + getTayProgram(), + getDisabilityOrMentalIllnessProgram(), + getHousingSituationProgram(), + getFlatRentAndRentBasedOnIncomeProgram(), + ]) + + for (const jurisdiction of jurisdictions) { + jurisdiction.programs = programs + } + const jurisdictionsRepository = app.get>( + getRepositoryToken(Jurisdiction) + ) + await jurisdictionsRepository.save(jurisdictions) + + return programs +} + +const seedAmiCharts = async (app: INestApplicationContext) => { + const allSeeds = amiSeeds.map((amiSeed) => app.get(amiSeed)) + const amiCharts = [] + for (const chart of allSeeds) { + const amiChart = await chart.seed() + amiCharts.push(amiChart) + } + return amiCharts +} + +const seedListings = async ( + app: INestApplicationContext, + rolesRepo: Repository, + jurisdictions: Jurisdiction[] +) => { + const seeds = [] + const leasingAgents = await createLeasingAgents(app, rolesRepo, jurisdictions) + await createPreferences(app, jurisdictions) + const allSeeds = listingSeeds.map((listingSeed) => app.get(listingSeed)) + const listingRepository = app.get>(getRepositoryToken(Listing)) + + for (const [index, listingSeed] of allSeeds.entries()) { + const everyOtherAgent = index % 2 ? leasingAgents[0] : leasingAgents[1] + const listing: Listing & { jurisdictionName?: string } = await listingSeed.seed() + // set jurisdiction based off of the name provided on the seed + listing.jurisdiction = jurisdictions.find( + (jurisdiction) => jurisdiction.name === listing.jurisdictionName + ) + listing.leasingAgents = [everyOtherAgent] + await listingRepository.save(listing) + + seeds.push(listing) + } + + return seeds +} + +async function seed() { + const app = await NestFactory.create(SeederModule.forRoot({ test: argv.test })) + // Starts listening for shutdown hooks + app.enableShutdownHooks() + const userService = await app.resolve(UserService) + + const userRepo = app.get>(getRepositoryToken(User)) + const rolesRepo = app.get>(getRepositoryToken(UserRoles)) + const jurisdictions = await createJurisdictions(app) + await createPrograms(app, jurisdictions) + await seedAmiCharts(app) + const listings = await seedListings(app, rolesRepo, jurisdictions) + + const user1 = await userService.createPublicUser( + plainToClass(UserCreateDto, { + email: "test@example.com", + emailConfirmation: "test@example.com", + firstName: "First", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + password: "abcdef", + passwordConfirmation: "abcdef", + jurisdictions: [jurisdictions[0]], + }), + new AuthContext(null) + ) + + await userService.confirm({ token: user1.confirmationToken }) + + const user2 = await userService.createPublicUser( + plainToClass(UserCreateDto, { + email: "test2@example.com", + emailConfirmation: "test2@example.com", + firstName: "Second", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + password: "ghijkl", + passwordConfirmation: "ghijkl", + jurisdictions: [jurisdictions[0]], + }), + new AuthContext(null) + ) + await userService.confirm({ token: user2.confirmationToken }) + + // create not confirmed user + await userService.createPublicUser( + plainToClass(UserCreateDto, { + email: "user+notconfirmed@example.com", + emailConfirmation: "user+notconfirmed@example.com", + firstName: "First", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + password: "abcdef", + passwordConfirmation: "abcdef", + jurisdictions: [jurisdictions[0]], + }), + new AuthContext(null) + ) + + // create user with expired password + const userExpiredPassword = await userService.createPublicUser( + plainToClass(UserCreateDto, { + email: "user+expired@example.com", + emailConfirmation: "user+expired@example.com", + firstName: "Second", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + password: "abcdef", + passwordConfirmation: "abcdef", + jurisdictions: [jurisdictions[0]], + roles: { isAdmin: false, isPartner: true }, + }), + new AuthContext(null) + ) + + await userService.confirm({ token: userExpiredPassword.confirmationToken }) + + userExpiredPassword.passwordValidForDays = 180 + userExpiredPassword.passwordUpdatedAt = new Date("2020-01-01") + userExpiredPassword.confirmedAt = new Date() + + await userRepo.save(userExpiredPassword) + + const admin = await userService.createPublicUser( + plainToClass(UserCreateDto, { + email: "admin@example.com", + emailConfirmation: "admin@example.com", + firstName: "Second", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + password: "abcdef", + passwordConfirmation: "abcdef", + jurisdictions, + }), + new AuthContext(null) + ) + + const mfaUser = await userService.createPublicUser( + plainToClass(UserCreateDto, { + email: "mfaUser@bloom.com", + emailConfirmation: "mfaUser@bloom.com", + firstName: "I", + middleName: "Use", + lastName: "MFA", + dob: new Date(), + password: "abcdef12", + passwordConfirmation: "abcdef12", + jurisdictions, + }), + new AuthContext(null) + ) + + const unitTypesService = await app.resolve(UnitTypesService) + + const unitTypes = await unitTypesService.list() + + for (let i = 0; i < 10; i++) { + for (const listing of listings) { + if (listing.countyCode !== CountyCode.detroit) { + await Promise.all([ + await makeNewApplication(app, listing, unitTypes, user1), + await makeNewApplication(app, listing, unitTypes, user2), + ]) + } + } + } + + // Seed the Detroit AMI data, since it's not linked to any units. + const amiChartRepo = app.get>(getRepositoryToken(AmiChart)) + await amiChartRepo.save({ + ...JSON.parse(JSON.stringify(WayneCountyMSHDA2021)), + jurisdiction: jurisdictions.find((jurisdiction) => jurisdiction.name == "Detroit"), + }) + + await userRepo.save(admin) + await userRepo.save({ + ...mfaUser, + mfaEnabled: false, + mfaCode: "123456", + mfaCodeUpdatedAt: dayjs(new Date()).add(1, "day"), + }) + const roles: UserRoles = { user: admin, isPartner: true, isAdmin: true } + const mfaRoles: UserRoles = { user: mfaUser, isPartner: true, isAdmin: true } + await rolesRepo.save(roles) + await rolesRepo.save(mfaRoles) + + await userService.confirm({ token: admin.confirmationToken }) + await userService.confirm({ token: mfaUser.confirmationToken }) + await app.close() +} + +void seed() diff --git a/backend/core/src/seeder/seeder.module.ts b/backend/core/src/seeder/seeder.module.ts index dbd85eb1f0..701806e8d6 100644 --- a/backend/core/src/seeder/seeder.module.ts +++ b/backend/core/src/seeder/seeder.module.ts @@ -1,15 +1,15 @@ import { DynamicModule, Module } from "@nestjs/common" import { TypeOrmModule } from "@nestjs/typeorm" -import dbOptions = require("../../ormconfig") -import testDbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig" +import testDbOptions from "../../ormconfig.test" import { ThrottlerModule } from "@nestjs/throttler" import { SharedModule } from "../shared/shared.module" import { AuthModule } from "../auth/auth.module" import { ApplicationsModule } from "../applications/applications.module" import { ListingsModule } from "../listings/listings.module" import { AmiChartsModule } from "../ami-charts/ami-charts.module" -import { ListingDefaultSeed } from "../seeds/listings/listing-default-seed" +import { ListingDefaultSeed } from "../seeder/seeds/listings/listing-default-seed" import { Listing } from "../listings/entities/listing.entity" import { UnitAccessibilityPriorityType } from "../unit-accessbility-priority-types/entities/unit-accessibility-priority-type.entity" import { ReservedCommunityType } from "../reserved-community-type/entities/reserved-community-type.entity" @@ -20,25 +20,57 @@ import { Property } from "../property/entities/property.entity" import { Unit } from "../units/entities/unit.entity" import { User } from "../auth/entities/user.entity" import { UserRoles } from "../auth/entities/user-roles.entity" -import { ListingColiseumSeed } from "../seeds/listings/listing-coliseum-seed" -import { ListingDefaultOnePreferenceSeed } from "../seeds/listings/listing-default-one-preference-seed" -import { ListingDefaultNoPreferenceSeed } from "../seeds/listings/listing-default-no-preference-seed" +import { ListingColiseumSeed } from "../seeder/seeds/listings/listing-coliseum-seed" +import { ListingDefaultOnePreferenceSeed } from "../seeder/seeds/listings/listing-default-one-preference-seed" +import { ListingDefaultNoPreferenceSeed } from "../seeder/seeds/listings/listing-default-no-preference-seed" import { Preference } from "../preferences/entities/preference.entity" -import { ListingDefaultFCFSSeed } from "../seeds/listings/listing-default-fcfs-seed" -import { ListingDefaultOpenSoonSeed } from "../seeds/listings/listing-default-open-soon" -import { ListingTritonSeed } from "../seeds/listings/listing-triton-seed" -import { ListingDefaultBmrChartSeed } from "../seeds/listings/listing-default-bmr-chart-seed" +import { ListingDefaultFCFSSeed } from "../seeder/seeds/listings/listing-default-fcfs-seed" +import { ListingDefaultOpenSoonSeed } from "../seeder/seeds/listings/listing-default-open-soon" +import { + ListingTritonSeed, + ListingTritonSeedDetroit, +} from "../seeder/seeds/listings/listing-triton-seed" +import { ListingDefaultBmrChartSeed } from "../seeder/seeds/listings/listing-default-bmr-chart-seed" import { ApplicationMethod } from "../application-methods/entities/application-method.entity" import { PaperApplication } from "../paper-applications/entities/paper-application.entity" import { ApplicationMethodsModule } from "../application-methods/applications-methods.module" import { PaperApplicationsModule } from "../paper-applications/paper-applications.module" import { AssetsModule } from "../assets/assets.module" -import { ListingDefaultReservedSeed } from "../seeds/listings/listing-default-reserved-seed" -import { ListingDefaultMultipleAMI } from "../seeds/listings/listing-default-multiple-ami" -import { ListingDefaultMultipleAMIAndPercentages } from "../seeds/listings/listing-default-multiple-ami-and-percentages" -import { ListingDefaultMissingAMI } from "../seeds/listings/listing-default-missing-ami" +import { Listing10158Seed } from "./seeds/listings/listing-detroit-10158" +import { Listing10157Seed } from "./seeds/listings/listing-detroit-10157" +import { Listing10147Seed } from "./seeds/listings/listing-detroit-10147" +import { Listing10145Seed } from "./seeds/listings/listing-detroit-10145" +import { Listing10202Seed } from "./seeds/listings/listing-detroit-10202" +import { ListingTreymoreSeed } from "./seeds/listings/listing-detroit-treymore" +import { UnitGroup } from "../units-summary/entities/unit-group.entity" +import { Listing10151Seed } from "./seeds/listings/listing-detroit-10151" +import { Listing10153Seed } from "./seeds/listings/listing-detroit-10153" +import { Listing10154Seed } from "./seeds/listings/listing-detroit-10154" +import { Listing10155Seed } from "./seeds/listings/listing-detroit-10155" +import { Listing10159Seed } from "./seeds/listings/listing-detroit-10159" +import { Listing10168Seed } from "./seeds/listings/listing-detroit-10168" +import { ListingDefaultSummaryWith30And60AmiPercentageSeed } from "./seeds/listings/listing-default-summary-with-30-and-60-ami-percentage-seed" +import { ListingDefaultSummaryWithoutAndListingWith20AmiPercentageSeed } from "./seeds/listings/listing-default-summary-without-and-listing-with-20-ami-percentage-seed" +import { ListingDefaultSummaryWith30ListingWith10AmiPercentageSeed } from "./seeds/listings/listing-default-summary-with-30-listing-with-10-ami-percentage-seed" +import { ListingDefaultSummaryWith10ListingWith30AmiPercentageSeed } from "./seeds/listings/listing-default-summary-with-10-listing-with-30-ami-percentage-seed" +import { Listing10136Seed } from "./seeds/listings/listing-detroit-10136" +import { ListingDefaultReservedSeed } from "../seeder/seeds/listings/listing-default-reserved-seed" +import { ListingDefaultMultipleAMI } from "../seeder/seeds/listings/listing-default-multiple-ami" +import { ListingDefaultMultipleAMIAndPercentages } from "../seeder/seeds/listings/listing-default-multiple-ami-and-percentages" +import { ListingDefaultLottery } from "./seeds/listings/listing-default-lottery-results" +import { ListingDefaultLotteryPending } from "./seeds/listings/listing-default-lottery-pending" +import { ListingDefaultMissingAMI } from "../seeder/seeds/listings/listing-default-missing-ami" import { UnitTypesModule } from "../unit-types/unit-types.module" import { Jurisdiction } from "../jurisdictions/entities/jurisdiction.entity" +import { Program } from "../program/entities/program.entity" +import { AmiChartDefaultSeed } from "../seeder/seeds/ami-charts/default-ami-chart" +import { AmiDefaultMissingAMI } from "../seeder/seeds/ami-charts/missing-household-ami-levels" +import { AmiDefaultTriton } from "../seeder/seeds/ami-charts/triton-ami-chart" +import { AmiDefaultTritonDetroit } from "../seeder/seeds/ami-charts/triton-ami-chart-detroit" +import { AmiDefaultSanJose } from "../seeder/seeds/ami-charts/default-ami-chart-san-jose" +import { AmiDefaultSanMateo } from "../seeder/seeds/ami-charts/default-ami-chart-san-mateo" +import { Asset } from "../assets/entities/asset.entity" +import { ListingDefaultNeighborhoodAmenitiesSeed } from "./seeds/listings/listing-default-neighbor-amenities" @Module({}) export class SeederModule { @@ -53,6 +85,7 @@ export class SeederModule { ...dbConfig, }), TypeOrmModule.forFeature([ + Asset, Listing, Preference, UnitAccessibilityPriorityType, @@ -62,11 +95,13 @@ export class SeederModule { AmiChart, Property, Unit, + UnitGroup, User, UserRoles, ApplicationMethod, PaperApplication, Jurisdiction, + Program, ]), ThrottlerModule.forRoot({ ttl: 60, @@ -92,9 +127,36 @@ export class SeederModule { ListingTritonSeed, ListingDefaultReservedSeed, ListingDefaultFCFSSeed, + Listing10136Seed, + Listing10158Seed, + Listing10157Seed, + Listing10147Seed, + Listing10145Seed, + Listing10151Seed, + Listing10153Seed, + Listing10154Seed, + Listing10155Seed, + Listing10159Seed, + Listing10168Seed, + Listing10202Seed, + ListingTreymoreSeed, ListingDefaultMultipleAMI, ListingDefaultMultipleAMIAndPercentages, ListingDefaultMissingAMI, + ListingDefaultLottery, + ListingDefaultLotteryPending, + ListingDefaultSummaryWith30And60AmiPercentageSeed, + ListingDefaultSummaryWithoutAndListingWith20AmiPercentageSeed, + ListingDefaultSummaryWith30ListingWith10AmiPercentageSeed, + ListingDefaultSummaryWith10ListingWith30AmiPercentageSeed, + ListingTritonSeedDetroit, + ListingDefaultNeighborhoodAmenitiesSeed, + AmiChartDefaultSeed, + AmiDefaultMissingAMI, + AmiDefaultTriton, + AmiDefaultTritonDetroit, + AmiDefaultSanJose, + AmiDefaultSanMateo, ], } } diff --git a/backend/core/src/seeder/seeds/ami-charts/HUD-MSHDA2021.ts b/backend/core/src/seeder/seeds/ami-charts/HUD-MSHDA2021.ts new file mode 100644 index 0000000000..a5fc4b01aa --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/HUD-MSHDA2021.ts @@ -0,0 +1,609 @@ +import { AmiChartCreateDto } from "../../../ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM HUD-MSHDA2021.txt. +export const HUDMSHDA2021: Omit = { + name: "HUD-MSHDA2021", + items: [ + { + percentOfAmi: 20, + householdSize: 1, + income: 11200, + }, + { + percentOfAmi: 20, + householdSize: 2, + income: 12800, + }, + { + percentOfAmi: 20, + householdSize: 3, + income: 14400, + }, + { + percentOfAmi: 20, + householdSize: 4, + income: 16000, + }, + { + percentOfAmi: 20, + householdSize: 5, + income: 17280, + }, + { + percentOfAmi: 20, + householdSize: 6, + income: 18560, + }, + { + percentOfAmi: 20, + householdSize: 7, + income: 19840, + }, + { + percentOfAmi: 20, + householdSize: 8, + income: 21120, + }, + { + percentOfAmi: 25, + householdSize: 1, + income: 14000, + }, + { + percentOfAmi: 25, + householdSize: 2, + income: 16000, + }, + { + percentOfAmi: 25, + householdSize: 3, + income: 18000, + }, + { + percentOfAmi: 25, + householdSize: 4, + income: 20000, + }, + { + percentOfAmi: 25, + householdSize: 5, + income: 21600, + }, + { + percentOfAmi: 25, + householdSize: 6, + income: 23200, + }, + { + percentOfAmi: 25, + householdSize: 7, + income: 24800, + }, + { + percentOfAmi: 25, + householdSize: 8, + income: 26400, + }, + { + percentOfAmi: 30, + householdSize: 1, + income: 16800, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 19200, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 21600, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 24000, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 25950, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 27850, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 29800, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 31700, + }, + { + percentOfAmi: 35, + householdSize: 1, + income: 19600, + }, + { + percentOfAmi: 35, + householdSize: 2, + income: 22400, + }, + { + percentOfAmi: 35, + householdSize: 3, + income: 25200, + }, + { + percentOfAmi: 35, + householdSize: 4, + income: 28000, + }, + { + percentOfAmi: 35, + householdSize: 5, + income: 30240, + }, + { + percentOfAmi: 35, + householdSize: 6, + income: 32480, + }, + { + percentOfAmi: 35, + householdSize: 7, + income: 34720, + }, + { + percentOfAmi: 35, + householdSize: 8, + income: 36960, + }, + { + percentOfAmi: 40, + householdSize: 1, + income: 22400, + }, + { + percentOfAmi: 40, + householdSize: 2, + income: 25600, + }, + { + percentOfAmi: 40, + householdSize: 3, + income: 28800, + }, + { + percentOfAmi: 40, + householdSize: 4, + income: 32000, + }, + { + percentOfAmi: 40, + householdSize: 5, + income: 34560, + }, + { + percentOfAmi: 40, + householdSize: 6, + income: 37120, + }, + { + percentOfAmi: 40, + householdSize: 7, + income: 39680, + }, + { + percentOfAmi: 40, + householdSize: 8, + income: 42240, + }, + { + percentOfAmi: 45, + householdSize: 1, + income: 25200, + }, + { + percentOfAmi: 45, + householdSize: 2, + income: 28800, + }, + { + percentOfAmi: 45, + householdSize: 3, + income: 32400, + }, + { + percentOfAmi: 45, + householdSize: 4, + income: 36000, + }, + { + percentOfAmi: 45, + householdSize: 5, + income: 38880, + }, + { + percentOfAmi: 45, + householdSize: 6, + income: 41760, + }, + { + percentOfAmi: 45, + householdSize: 7, + income: 44640, + }, + { + percentOfAmi: 45, + householdSize: 8, + income: 47520, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 28000, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 32000, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 36000, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 40000, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 43200, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 46400, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 49600, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 52800, + }, + { + percentOfAmi: 55, + householdSize: 1, + income: 30800, + }, + { + percentOfAmi: 55, + householdSize: 2, + income: 35200, + }, + { + percentOfAmi: 55, + householdSize: 3, + income: 39600, + }, + { + percentOfAmi: 55, + householdSize: 4, + income: 44000, + }, + { + percentOfAmi: 55, + householdSize: 5, + income: 47520, + }, + { + percentOfAmi: 55, + householdSize: 6, + income: 51040, + }, + { + percentOfAmi: 55, + householdSize: 7, + income: 54560, + }, + { + percentOfAmi: 55, + householdSize: 8, + income: 58080, + }, + { + percentOfAmi: 60, + householdSize: 1, + income: 33600, + }, + { + percentOfAmi: 60, + householdSize: 2, + income: 38400, + }, + { + percentOfAmi: 60, + householdSize: 3, + income: 43200, + }, + { + percentOfAmi: 60, + householdSize: 4, + income: 48000, + }, + { + percentOfAmi: 60, + householdSize: 5, + income: 51840, + }, + { + percentOfAmi: 60, + householdSize: 6, + income: 55680, + }, + { + percentOfAmi: 60, + householdSize: 7, + income: 59520, + }, + { + percentOfAmi: 60, + householdSize: 8, + income: 63360, + }, + { + percentOfAmi: 70, + householdSize: 1, + income: 39200, + }, + { + percentOfAmi: 70, + householdSize: 2, + income: 44800, + }, + { + percentOfAmi: 70, + householdSize: 3, + income: 50400, + }, + { + percentOfAmi: 70, + householdSize: 4, + income: 56000, + }, + { + percentOfAmi: 70, + householdSize: 5, + income: 60480, + }, + { + percentOfAmi: 70, + householdSize: 6, + income: 64960, + }, + { + percentOfAmi: 70, + householdSize: 7, + income: 69440, + }, + { + percentOfAmi: 70, + householdSize: 8, + income: 73920, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 44800, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 51200, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 57600, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 64000, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 69150, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 74250, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 79400, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 84500, + }, + { + percentOfAmi: 100, + householdSize: 1, + income: 56000, + }, + { + percentOfAmi: 100, + householdSize: 2, + income: 64000, + }, + { + percentOfAmi: 100, + householdSize: 3, + income: 72000, + }, + { + percentOfAmi: 100, + householdSize: 4, + income: 80000, + }, + { + percentOfAmi: 100, + householdSize: 5, + income: 86400, + }, + { + percentOfAmi: 100, + householdSize: 6, + income: 92800, + }, + { + percentOfAmi: 100, + householdSize: 7, + income: 99200, + }, + { + percentOfAmi: 100, + householdSize: 8, + income: 105600, + }, + { + percentOfAmi: 120, + householdSize: 1, + income: 67200, + }, + { + percentOfAmi: 120, + householdSize: 2, + income: 76800, + }, + { + percentOfAmi: 120, + householdSize: 3, + income: 86400, + }, + { + percentOfAmi: 120, + householdSize: 4, + income: 96000, + }, + { + percentOfAmi: 120, + householdSize: 5, + income: 103680, + }, + { + percentOfAmi: 120, + householdSize: 6, + income: 111360, + }, + { + percentOfAmi: 120, + householdSize: 7, + income: 119040, + }, + { + percentOfAmi: 120, + householdSize: 8, + income: 126720, + }, + { + percentOfAmi: 125, + householdSize: 1, + income: 70000, + }, + { + percentOfAmi: 125, + householdSize: 2, + income: 80000, + }, + { + percentOfAmi: 125, + householdSize: 3, + income: 90000, + }, + { + percentOfAmi: 125, + householdSize: 4, + income: 100000, + }, + { + percentOfAmi: 125, + householdSize: 5, + income: 108000, + }, + { + percentOfAmi: 125, + householdSize: 6, + income: 116000, + }, + { + percentOfAmi: 125, + householdSize: 7, + income: 124000, + }, + { + percentOfAmi: 125, + householdSize: 8, + income: 132000, + }, + { + percentOfAmi: 140, + householdSize: 1, + income: 78400, + }, + { + percentOfAmi: 140, + householdSize: 2, + income: 89600, + }, + { + percentOfAmi: 140, + householdSize: 3, + income: 100800, + }, + { + percentOfAmi: 140, + householdSize: 4, + income: 112000, + }, + { + percentOfAmi: 140, + householdSize: 5, + income: 120960, + }, + { + percentOfAmi: 140, + householdSize: 6, + income: 129920, + }, + { + percentOfAmi: 140, + householdSize: 7, + income: 138880, + }, + { + percentOfAmi: 140, + householdSize: 8, + income: 147840, + }, + ], +} diff --git a/backend/core/src/seeder/seeds/ami-charts/HUD-MSHDA2021.txt b/backend/core/src/seeder/seeds/ami-charts/HUD-MSHDA2021.txt new file mode 100644 index 0000000000..4253521116 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/HUD-MSHDA2021.txt @@ -0,0 +1,16 @@ +20% 11,200 12,800 14,400 16,000 17,280 18,560 19,840 21,120 +25% 14,000 16,000 18,000 20,000 21,600 23,200 24,800 26,400 +30% 16,800 19,200 21,600 24,000 25,950 27,850 29,800 31,700 +35% 19,600 22,400 25,200 28,000 30,240 32,480 34,720 36,960 +40% 22,400 25,600 28,800 32,000 34,560 37,120 39,680 42,240 +45% 25,200 28,800 32,400 36,000 38,880 41,760 44,640 47,520 +50% 28,000 32,000 36,000 40,000 43,200 46,400 49,600 52,800 +55% 30,800 35,200 39,600 44,000 47,520 51,040 54,560 58,080 +60% 33,600 38,400 43,200 48,000 51,840 55,680 59,520 63,360 +70% 39,200 44,800 50,400 56,000 60,480 64,960 69,440 73,920 +80% 44,800 51,200 57,600 64,000 69,150 74,250 79,400 84,500 +100% 56,000 64,000 72,000 80,000 86,400 92,800 99,200 105,600 +120% 67,200 76,800 86,400 96,000 103,680 111,360 119,040 126,720 +125% 70,000 80,000 90,000 100,000 108,000 116,000 124,000 132,000 +140% 78,400 89,600 100,800 112,000 120,960 129,920 138,880 147,840 +150% 84,000 96,000 108,000 120,000 129,600 139,200 148,800 158,400 \ No newline at end of file diff --git a/backend/core/src/seeder/seeds/ami-charts/HUD2021.ts b/backend/core/src/seeder/seeds/ami-charts/HUD2021.ts new file mode 100644 index 0000000000..3a688800da --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/HUD2021.ts @@ -0,0 +1,129 @@ +import { AmiChartCreateDto } from "../../../ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM HUD2021.txt. +export const HUD2021: Omit = { + name: "HUD 2021", + items: [ + { + percentOfAmi: 30, + householdSize: 1, + income: 16800, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 19200, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 21960, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 26500, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 31040, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 35580, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 40120, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 44660, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 28000, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 32000, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 36000, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 40000, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 43200, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 46400, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 49600, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 52800, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 44800, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 51200, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 57600, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 64000, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 69150, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 74250, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 79400, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 84500, + }, + ], +} diff --git a/backend/core/src/seeder/seeds/ami-charts/HUD2021.txt b/backend/core/src/seeder/seeds/ami-charts/HUD2021.txt new file mode 100644 index 0000000000..7f5e4cdb69 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/HUD2021.txt @@ -0,0 +1,3 @@ +30% 16,800 19,200 21,960 26,500 31,040 35,580 40,120 44,660 +50% 28,000 32,000 36,000 40,000 43,200 46,400 49,600 52,800 +80% 44,800 51,200 57,600 64,000 69,150 74,250 79,400 84,500 diff --git a/backend/core/src/seeder/seeds/ami-charts/HUD2022.ts b/backend/core/src/seeder/seeds/ami-charts/HUD2022.ts new file mode 100644 index 0000000000..966860f6ab --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/HUD2022.ts @@ -0,0 +1,129 @@ +import { AmiChartCreateDto } from "../../../ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM HUD2022.txt. +export const HUD2022: Omit = { + name: "HUD 2022", + items: [ + { + percentOfAmi: 30, + householdSize: 1, + income: 18800, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 21500, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 24200, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 27750, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 32470, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 37190, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 41910, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 46630, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 31350, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 35800, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 40300, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 44750, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 48350, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 51950, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 55500, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 59100, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 50150, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 57300, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 64450, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 71600, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 77350, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 83100, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 88800, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 94550, + }, + ], +} diff --git a/backend/core/src/seeder/seeds/ami-charts/HUD2022.txt b/backend/core/src/seeder/seeds/ami-charts/HUD2022.txt new file mode 100644 index 0000000000..c9b73250a7 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/HUD2022.txt @@ -0,0 +1,3 @@ +30% 18,800 21,500 24,200 27,750 32,470 37,190 41,910 46,630 +50% 31,350 35,800 40,300 44,750 48,350 51,950 55,500 59,100 +80% 50,150 57,300 64,450 71,600 77,350 83,100 88,800 94,550 diff --git a/backend/core/src/seeder/seeds/ami-charts/MSHDA2021.ts b/backend/core/src/seeder/seeds/ami-charts/MSHDA2021.ts new file mode 100644 index 0000000000..0479448cb9 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/MSHDA2021.ts @@ -0,0 +1,609 @@ +import { AmiChartCreateDto } from "../../../ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM MSHDA2021.txt. +export const MSHDA2021: Omit = { + name: "MSHDA 2021", + items: [ + { + percentOfAmi: 20, + householdSize: 1, + income: 11200, + }, + { + percentOfAmi: 20, + householdSize: 2, + income: 12800, + }, + { + percentOfAmi: 20, + householdSize: 3, + income: 14400, + }, + { + percentOfAmi: 20, + householdSize: 4, + income: 16000, + }, + { + percentOfAmi: 20, + householdSize: 5, + income: 17280, + }, + { + percentOfAmi: 20, + householdSize: 6, + income: 18560, + }, + { + percentOfAmi: 20, + householdSize: 7, + income: 19840, + }, + { + percentOfAmi: 20, + householdSize: 8, + income: 21120, + }, + { + percentOfAmi: 25, + householdSize: 1, + income: 14000, + }, + { + percentOfAmi: 25, + householdSize: 2, + income: 16000, + }, + { + percentOfAmi: 25, + householdSize: 3, + income: 18000, + }, + { + percentOfAmi: 25, + householdSize: 4, + income: 20000, + }, + { + percentOfAmi: 25, + householdSize: 5, + income: 21600, + }, + { + percentOfAmi: 25, + householdSize: 6, + income: 23200, + }, + { + percentOfAmi: 25, + householdSize: 7, + income: 24800, + }, + { + percentOfAmi: 25, + householdSize: 8, + income: 26400, + }, + { + percentOfAmi: 30, + householdSize: 1, + income: 16800, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 19200, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 21600, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 24000, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 25920, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 27840, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 29760, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 31680, + }, + { + percentOfAmi: 35, + householdSize: 1, + income: 19600, + }, + { + percentOfAmi: 35, + householdSize: 2, + income: 22400, + }, + { + percentOfAmi: 35, + householdSize: 3, + income: 25200, + }, + { + percentOfAmi: 35, + householdSize: 4, + income: 28000, + }, + { + percentOfAmi: 35, + householdSize: 5, + income: 30240, + }, + { + percentOfAmi: 35, + householdSize: 6, + income: 32480, + }, + { + percentOfAmi: 35, + householdSize: 7, + income: 34720, + }, + { + percentOfAmi: 35, + householdSize: 8, + income: 36960, + }, + { + percentOfAmi: 40, + householdSize: 1, + income: 22400, + }, + { + percentOfAmi: 40, + householdSize: 2, + income: 25600, + }, + { + percentOfAmi: 40, + householdSize: 3, + income: 28800, + }, + { + percentOfAmi: 40, + householdSize: 4, + income: 32000, + }, + { + percentOfAmi: 40, + householdSize: 5, + income: 34560, + }, + { + percentOfAmi: 40, + householdSize: 6, + income: 37120, + }, + { + percentOfAmi: 40, + householdSize: 7, + income: 39680, + }, + { + percentOfAmi: 40, + householdSize: 8, + income: 42240, + }, + { + percentOfAmi: 45, + householdSize: 1, + income: 25200, + }, + { + percentOfAmi: 45, + householdSize: 2, + income: 28800, + }, + { + percentOfAmi: 45, + householdSize: 3, + income: 32400, + }, + { + percentOfAmi: 45, + householdSize: 4, + income: 36000, + }, + { + percentOfAmi: 45, + householdSize: 5, + income: 38880, + }, + { + percentOfAmi: 45, + householdSize: 6, + income: 41760, + }, + { + percentOfAmi: 45, + householdSize: 7, + income: 44640, + }, + { + percentOfAmi: 45, + householdSize: 8, + income: 47520, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 28000, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 32000, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 36000, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 40000, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 43200, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 46400, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 49600, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 52800, + }, + { + percentOfAmi: 55, + householdSize: 1, + income: 30800, + }, + { + percentOfAmi: 55, + householdSize: 2, + income: 35200, + }, + { + percentOfAmi: 55, + householdSize: 3, + income: 39600, + }, + { + percentOfAmi: 55, + householdSize: 4, + income: 44000, + }, + { + percentOfAmi: 55, + householdSize: 5, + income: 47520, + }, + { + percentOfAmi: 55, + householdSize: 6, + income: 51040, + }, + { + percentOfAmi: 55, + householdSize: 7, + income: 54560, + }, + { + percentOfAmi: 55, + householdSize: 8, + income: 58080, + }, + { + percentOfAmi: 60, + householdSize: 1, + income: 33600, + }, + { + percentOfAmi: 60, + householdSize: 2, + income: 38400, + }, + { + percentOfAmi: 60, + householdSize: 3, + income: 43200, + }, + { + percentOfAmi: 60, + householdSize: 4, + income: 48000, + }, + { + percentOfAmi: 60, + householdSize: 5, + income: 51840, + }, + { + percentOfAmi: 60, + householdSize: 6, + income: 55680, + }, + { + percentOfAmi: 60, + householdSize: 7, + income: 59520, + }, + { + percentOfAmi: 60, + householdSize: 8, + income: 63360, + }, + { + percentOfAmi: 70, + householdSize: 1, + income: 39200, + }, + { + percentOfAmi: 70, + householdSize: 2, + income: 44800, + }, + { + percentOfAmi: 70, + householdSize: 3, + income: 50400, + }, + { + percentOfAmi: 70, + householdSize: 4, + income: 56000, + }, + { + percentOfAmi: 70, + householdSize: 5, + income: 60480, + }, + { + percentOfAmi: 70, + householdSize: 6, + income: 64960, + }, + { + percentOfAmi: 70, + householdSize: 7, + income: 69440, + }, + { + percentOfAmi: 70, + householdSize: 8, + income: 73920, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 44800, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 51200, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 57600, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 64000, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 69120, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 74240, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 79360, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 84480, + }, + { + percentOfAmi: 100, + householdSize: 1, + income: 56000, + }, + { + percentOfAmi: 100, + householdSize: 2, + income: 64000, + }, + { + percentOfAmi: 100, + householdSize: 3, + income: 72000, + }, + { + percentOfAmi: 100, + householdSize: 4, + income: 80000, + }, + { + percentOfAmi: 100, + householdSize: 5, + income: 86400, + }, + { + percentOfAmi: 100, + householdSize: 6, + income: 92800, + }, + { + percentOfAmi: 100, + householdSize: 7, + income: 99200, + }, + { + percentOfAmi: 100, + householdSize: 8, + income: 105600, + }, + { + percentOfAmi: 120, + householdSize: 1, + income: 67200, + }, + { + percentOfAmi: 120, + householdSize: 2, + income: 76800, + }, + { + percentOfAmi: 120, + householdSize: 3, + income: 86400, + }, + { + percentOfAmi: 120, + householdSize: 4, + income: 96000, + }, + { + percentOfAmi: 120, + householdSize: 5, + income: 103680, + }, + { + percentOfAmi: 120, + householdSize: 6, + income: 111360, + }, + { + percentOfAmi: 120, + householdSize: 7, + income: 119040, + }, + { + percentOfAmi: 120, + householdSize: 8, + income: 126720, + }, + { + percentOfAmi: 125, + householdSize: 1, + income: 70000, + }, + { + percentOfAmi: 125, + householdSize: 2, + income: 80000, + }, + { + percentOfAmi: 125, + householdSize: 3, + income: 90000, + }, + { + percentOfAmi: 125, + householdSize: 4, + income: 100000, + }, + { + percentOfAmi: 125, + householdSize: 5, + income: 108000, + }, + { + percentOfAmi: 125, + householdSize: 6, + income: 116000, + }, + { + percentOfAmi: 125, + householdSize: 7, + income: 124000, + }, + { + percentOfAmi: 125, + householdSize: 8, + income: 132000, + }, + { + percentOfAmi: 140, + householdSize: 1, + income: 78400, + }, + { + percentOfAmi: 140, + householdSize: 2, + income: 89600, + }, + { + percentOfAmi: 140, + householdSize: 3, + income: 100800, + }, + { + percentOfAmi: 140, + householdSize: 4, + income: 112000, + }, + { + percentOfAmi: 140, + householdSize: 5, + income: 120960, + }, + { + percentOfAmi: 140, + householdSize: 6, + income: 129920, + }, + { + percentOfAmi: 140, + householdSize: 7, + income: 138880, + }, + { + percentOfAmi: 140, + householdSize: 8, + income: 147840, + }, + ], +} diff --git a/backend/core/src/seeder/seeds/ami-charts/MSHDA2021.txt b/backend/core/src/seeder/seeds/ami-charts/MSHDA2021.txt new file mode 100644 index 0000000000..184e40548f --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/MSHDA2021.txt @@ -0,0 +1,16 @@ +20% 11,200 12,800 14,400 16,000 17,280 18,560 19,840 21,120 +25% 14,000 16,000 18,000 20,000 21,600 23,200 24,800 26,400 +30% 16,800 19,200 21,600 24,000 25,920 27,840 29,760 31,680 +35% 19,600 22,400 25,200 28,000 30,240 32,480 34,720 36,960 +40% 22,400 25,600 28,800 32,000 34,560 37,120 39,680 42,240 +45% 25,200 28,800 32,400 36,000 38,880 41,760 44,640 47,520 +50% 28,000 32,000 36,000 40,000 43,200 46,400 49,600 52,800 +55% 30,800 35,200 39,600 44,000 47,520 51,040 54,560 58,080 +60% 33,600 38,400 43,200 48,000 51,840 55,680 59,520 63,360 +70% 39,200 44,800 50,400 56,000 60,480 64,960 69,440 73,920 +80% 44,800 51,200 57,600 64,000 69,120 74,240 79,360 84,480 +100% 56,000 64,000 72,000 80,000 86,400 92,800 99,200 105,600 +120% 67,200 76,800 86,400 96,000 103,680 111,360 119,040 126,720 +125% 70,000 80,000 90,000 100,000 108,000 116,000 124,000 132,000 +140% 78,400 89,600 100,800 112,000 120,960 129,920 138,880 147,840 +150% 84,000 96,000 108,000 120,000 129,600 139,200 148,800 158,400 \ No newline at end of file diff --git a/backend/core/src/seeder/seeds/ami-charts/MSHDA2022.ts b/backend/core/src/seeder/seeds/ami-charts/MSHDA2022.ts new file mode 100644 index 0000000000..a6dbb08576 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/MSHDA2022.ts @@ -0,0 +1,649 @@ +import { AmiChartCreateDto } from "../../../ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM MSHDA2022.txt. +export const MSHDA2022: Omit = { + name: "MSHDA 2022", + items: [ + { + percentOfAmi: 20, + householdSize: 1, + income: 12860, + }, + { + percentOfAmi: 20, + householdSize: 2, + income: 14700, + }, + { + percentOfAmi: 20, + householdSize: 3, + income: 16540, + }, + { + percentOfAmi: 20, + householdSize: 4, + income: 18360, + }, + { + percentOfAmi: 20, + householdSize: 5, + income: 19840, + }, + { + percentOfAmi: 20, + householdSize: 6, + income: 21300, + }, + { + percentOfAmi: 20, + householdSize: 7, + income: 22780, + }, + { + percentOfAmi: 20, + householdSize: 8, + income: 24240, + }, + { + percentOfAmi: 25, + householdSize: 1, + income: 16075, + }, + { + percentOfAmi: 25, + householdSize: 2, + income: 18375, + }, + { + percentOfAmi: 25, + householdSize: 3, + income: 20675, + }, + { + percentOfAmi: 25, + householdSize: 4, + income: 22950, + }, + { + percentOfAmi: 25, + householdSize: 5, + income: 24800, + }, + { + percentOfAmi: 25, + householdSize: 6, + income: 26625, + }, + { + percentOfAmi: 25, + householdSize: 7, + income: 28475, + }, + { + percentOfAmi: 25, + householdSize: 8, + income: 30300, + }, + { + percentOfAmi: 30, + householdSize: 1, + income: 19290, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 22050, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 24810, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 27540, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 29760, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 31950, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 34170, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 36360, + }, + { + percentOfAmi: 35, + householdSize: 1, + income: 22505, + }, + { + percentOfAmi: 35, + householdSize: 2, + income: 25725, + }, + { + percentOfAmi: 35, + householdSize: 3, + income: 28945, + }, + { + percentOfAmi: 35, + householdSize: 4, + income: 32130, + }, + { + percentOfAmi: 35, + householdSize: 5, + income: 34720, + }, + { + percentOfAmi: 35, + householdSize: 6, + income: 37275, + }, + { + percentOfAmi: 35, + householdSize: 7, + income: 39865, + }, + { + percentOfAmi: 35, + householdSize: 8, + income: 42420, + }, + { + percentOfAmi: 40, + householdSize: 1, + income: 25720, + }, + { + percentOfAmi: 40, + householdSize: 2, + income: 29400, + }, + { + percentOfAmi: 40, + householdSize: 3, + income: 33080, + }, + { + percentOfAmi: 40, + householdSize: 4, + income: 36720, + }, + { + percentOfAmi: 40, + householdSize: 5, + income: 39680, + }, + { + percentOfAmi: 40, + householdSize: 6, + income: 42600, + }, + { + percentOfAmi: 40, + householdSize: 7, + income: 45560, + }, + { + percentOfAmi: 40, + householdSize: 8, + income: 48480, + }, + { + percentOfAmi: 45, + householdSize: 1, + income: 28935, + }, + { + percentOfAmi: 45, + householdSize: 2, + income: 33075, + }, + { + percentOfAmi: 45, + householdSize: 3, + income: 37215, + }, + { + percentOfAmi: 45, + householdSize: 4, + income: 41310, + }, + { + percentOfAmi: 45, + householdSize: 5, + income: 44640, + }, + { + percentOfAmi: 45, + householdSize: 6, + income: 47925, + }, + { + percentOfAmi: 45, + householdSize: 7, + income: 51255, + }, + { + percentOfAmi: 45, + householdSize: 8, + income: 54540, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 32150, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 36750, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 41350, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 45900, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 49600, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 53250, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 56950, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 60600, + }, + { + percentOfAmi: 55, + householdSize: 1, + income: 35365, + }, + { + percentOfAmi: 55, + householdSize: 2, + income: 40425, + }, + { + percentOfAmi: 55, + householdSize: 3, + income: 45485, + }, + { + percentOfAmi: 55, + householdSize: 4, + income: 50490, + }, + { + percentOfAmi: 55, + householdSize: 5, + income: 54560, + }, + { + percentOfAmi: 55, + householdSize: 6, + income: 58575, + }, + { + percentOfAmi: 55, + householdSize: 7, + income: 62645, + }, + { + percentOfAmi: 55, + householdSize: 8, + income: 66660, + }, + { + percentOfAmi: 60, + householdSize: 1, + income: 38580, + }, + { + percentOfAmi: 60, + householdSize: 2, + income: 44100, + }, + { + percentOfAmi: 60, + householdSize: 3, + income: 49620, + }, + { + percentOfAmi: 60, + householdSize: 4, + income: 55080, + }, + { + percentOfAmi: 60, + householdSize: 5, + income: 59520, + }, + { + percentOfAmi: 60, + householdSize: 6, + income: 63900, + }, + { + percentOfAmi: 60, + householdSize: 7, + income: 68340, + }, + { + percentOfAmi: 60, + householdSize: 8, + income: 72720, + }, + { + percentOfAmi: 70, + householdSize: 1, + income: 45010, + }, + { + percentOfAmi: 70, + householdSize: 2, + income: 51450, + }, + { + percentOfAmi: 70, + householdSize: 3, + income: 57890, + }, + { + percentOfAmi: 70, + householdSize: 4, + income: 64260, + }, + { + percentOfAmi: 70, + householdSize: 5, + income: 69440, + }, + { + percentOfAmi: 70, + householdSize: 6, + income: 74550, + }, + { + percentOfAmi: 70, + householdSize: 7, + income: 79730, + }, + { + percentOfAmi: 70, + householdSize: 8, + income: 84840, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 51440, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 58800, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 66160, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 73440, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 79360, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 85200, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 91120, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 96960, + }, + { + percentOfAmi: 100, + householdSize: 1, + income: 64300, + }, + { + percentOfAmi: 100, + householdSize: 2, + income: 73500, + }, + { + percentOfAmi: 100, + householdSize: 3, + income: 82700, + }, + { + percentOfAmi: 100, + householdSize: 4, + income: 91800, + }, + { + percentOfAmi: 100, + householdSize: 5, + income: 99200, + }, + { + percentOfAmi: 100, + householdSize: 6, + income: 106500, + }, + { + percentOfAmi: 100, + householdSize: 7, + income: 113900, + }, + { + percentOfAmi: 100, + householdSize: 8, + income: 121200, + }, + { + percentOfAmi: 120, + householdSize: 1, + income: 77160, + }, + { + percentOfAmi: 120, + householdSize: 2, + income: 88200, + }, + { + percentOfAmi: 120, + householdSize: 3, + income: 99240, + }, + { + percentOfAmi: 120, + householdSize: 4, + income: 110160, + }, + { + percentOfAmi: 120, + householdSize: 5, + income: 119040, + }, + { + percentOfAmi: 120, + householdSize: 6, + income: 127800, + }, + { + percentOfAmi: 120, + householdSize: 7, + income: 136680, + }, + { + percentOfAmi: 120, + householdSize: 8, + income: 145440, + }, + { + percentOfAmi: 125, + householdSize: 1, + income: 80375, + }, + { + percentOfAmi: 125, + householdSize: 2, + income: 91875, + }, + { + percentOfAmi: 125, + householdSize: 3, + income: 103375, + }, + { + percentOfAmi: 125, + householdSize: 4, + income: 114750, + }, + { + percentOfAmi: 125, + householdSize: 5, + income: 124000, + }, + { + percentOfAmi: 125, + householdSize: 6, + income: 133125, + }, + { + percentOfAmi: 125, + householdSize: 7, + income: 142375, + }, + { + percentOfAmi: 125, + householdSize: 8, + income: 151500, + }, + { + percentOfAmi: 140, + householdSize: 1, + income: 90020, + }, + { + percentOfAmi: 140, + householdSize: 2, + income: 102900, + }, + { + percentOfAmi: 140, + householdSize: 3, + income: 115780, + }, + { + percentOfAmi: 140, + householdSize: 4, + income: 128520, + }, + { + percentOfAmi: 140, + householdSize: 5, + income: 138880, + }, + { + percentOfAmi: 140, + householdSize: 6, + income: 149100, + }, + { + percentOfAmi: 140, + householdSize: 7, + income: 159460, + }, + { + percentOfAmi: 140, + householdSize: 8, + income: 169680, + }, + { + percentOfAmi: 150, + householdSize: 1, + income: 96450, + }, + { + percentOfAmi: 150, + householdSize: 2, + income: 110250, + }, + { + percentOfAmi: 150, + householdSize: 3, + income: 124050, + }, + { + percentOfAmi: 150, + householdSize: 4, + income: 137700, + }, + { + percentOfAmi: 150, + householdSize: 5, + income: 148800, + }, + { + percentOfAmi: 150, + householdSize: 6, + income: 159750, + }, + { + percentOfAmi: 150, + householdSize: 7, + income: 170850, + }, + { + percentOfAmi: 150, + householdSize: 8, + income: 181800, + }, + ], +} diff --git a/backend/core/src/seeder/seeds/ami-charts/MSHDA2022.txt b/backend/core/src/seeder/seeds/ami-charts/MSHDA2022.txt new file mode 100644 index 0000000000..cbc2869e77 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/MSHDA2022.txt @@ -0,0 +1,16 @@ +20% 12,860 14,700 16,540 18,360 19,840 21,300 22,780 24,240 +25% 16,075 18,375 20,675 22,950 24,800 26,625 28,475 30,300 +30% 19,290 22,050 24,810 27,540 29,760 31,950 34,170 36,360 +35% 22,505 25,725 28,945 32,130 34,720 37,275 39,865 42,420 +40% 25,720 29,400 33,080 36,720 39,680 42,600 45,560 48,480 +45% 28,935 33,075 37,215 41,310 44,640 47,925 51,255 54,540 +50% 32,150 36,750 41,350 45,900 49,600 53,250 56,950 60,600 +55% 35,365 40,425 45,485 50,490 54,560 58,575 62,645 66,660 +60% 38,580 44,100 49,620 55,080 59,520 63,900 68,340 72,720 +70% 45,010 51,450 57,890 64,260 69,440 74,550 79,730 84,840 +80% 51,440 58,800 66,160 73,440 79,360 85,200 91,120 96,960 +100% 64,300 73,500 82,700 91,800 99,200 106,500 113,900 121,200 +120% 77,160 88,200 99,240 110,160 119,040 127,800 136,680 145,440 +125% 80,375 91,875 103,375 114,750 124,000 133,125 142,375 151,500 +140% 90,020 102,900 115,780 128,520 138,880 149,100 159,460 169,680 +150% 96,450 110,250 124,050 137,700 148,800 159,750 170,850 181,800 diff --git a/backend/core/src/seeder/seeds/ami-charts/WayneCountyMSHDA2021.ts b/backend/core/src/seeder/seeds/ami-charts/WayneCountyMSHDA2021.ts new file mode 100644 index 0000000000..d58232bfd8 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/WayneCountyMSHDA2021.ts @@ -0,0 +1,649 @@ +import { AmiChartCreateDto } from "../../../ami-charts/dto/ami-chart.dto" +import { BaseEntity } from "typeorm" + +// THIS FILE WAS AUTOMATICALLY GENERATED FROM WayneCountyMSHDA2021.txt. +export const WayneCountyMSHDA2021: Omit = { + name: "WayneCountyMSHDA2021", + items: [ + { + percentOfAmi: 20, + householdSize: 1, + income: 11200, + }, + { + percentOfAmi: 20, + householdSize: 2, + income: 12800, + }, + { + percentOfAmi: 20, + householdSize: 3, + income: 14400, + }, + { + percentOfAmi: 20, + householdSize: 4, + income: 16000, + }, + { + percentOfAmi: 20, + householdSize: 5, + income: 17280, + }, + { + percentOfAmi: 20, + householdSize: 6, + income: 18560, + }, + { + percentOfAmi: 20, + householdSize: 7, + income: 19840, + }, + { + percentOfAmi: 20, + householdSize: 8, + income: 21120, + }, + { + percentOfAmi: 25, + householdSize: 1, + income: 14000, + }, + { + percentOfAmi: 25, + householdSize: 2, + income: 16000, + }, + { + percentOfAmi: 25, + householdSize: 3, + income: 18000, + }, + { + percentOfAmi: 25, + householdSize: 4, + income: 20000, + }, + { + percentOfAmi: 25, + householdSize: 5, + income: 21600, + }, + { + percentOfAmi: 25, + householdSize: 6, + income: 23200, + }, + { + percentOfAmi: 25, + householdSize: 7, + income: 24800, + }, + { + percentOfAmi: 25, + householdSize: 8, + income: 26400, + }, + { + percentOfAmi: 30, + householdSize: 1, + income: 16800, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 19200, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 21600, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 24000, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 25920, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 27840, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 29760, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 31680, + }, + { + percentOfAmi: 35, + householdSize: 1, + income: 19600, + }, + { + percentOfAmi: 35, + householdSize: 2, + income: 22400, + }, + { + percentOfAmi: 35, + householdSize: 3, + income: 25200, + }, + { + percentOfAmi: 35, + householdSize: 4, + income: 28000, + }, + { + percentOfAmi: 35, + householdSize: 5, + income: 30240, + }, + { + percentOfAmi: 35, + householdSize: 6, + income: 32480, + }, + { + percentOfAmi: 35, + householdSize: 7, + income: 34720, + }, + { + percentOfAmi: 35, + householdSize: 8, + income: 36960, + }, + { + percentOfAmi: 40, + householdSize: 1, + income: 22400, + }, + { + percentOfAmi: 40, + householdSize: 2, + income: 25600, + }, + { + percentOfAmi: 40, + householdSize: 3, + income: 28800, + }, + { + percentOfAmi: 40, + householdSize: 4, + income: 32000, + }, + { + percentOfAmi: 40, + householdSize: 5, + income: 34560, + }, + { + percentOfAmi: 40, + householdSize: 6, + income: 37120, + }, + { + percentOfAmi: 40, + householdSize: 7, + income: 39680, + }, + { + percentOfAmi: 40, + householdSize: 8, + income: 42240, + }, + { + percentOfAmi: 45, + householdSize: 1, + income: 25200, + }, + { + percentOfAmi: 45, + householdSize: 2, + income: 28800, + }, + { + percentOfAmi: 45, + householdSize: 3, + income: 32400, + }, + { + percentOfAmi: 45, + householdSize: 4, + income: 36000, + }, + { + percentOfAmi: 45, + householdSize: 5, + income: 38880, + }, + { + percentOfAmi: 45, + householdSize: 6, + income: 41760, + }, + { + percentOfAmi: 45, + householdSize: 7, + income: 44640, + }, + { + percentOfAmi: 45, + householdSize: 8, + income: 47520, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 28000, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 32000, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 36000, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 40000, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 43200, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 46400, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 49600, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 52800, + }, + { + percentOfAmi: 55, + householdSize: 1, + income: 30800, + }, + { + percentOfAmi: 55, + householdSize: 2, + income: 35200, + }, + { + percentOfAmi: 55, + householdSize: 3, + income: 39600, + }, + { + percentOfAmi: 55, + householdSize: 4, + income: 44000, + }, + { + percentOfAmi: 55, + householdSize: 5, + income: 47520, + }, + { + percentOfAmi: 55, + householdSize: 6, + income: 51040, + }, + { + percentOfAmi: 55, + householdSize: 7, + income: 54560, + }, + { + percentOfAmi: 55, + householdSize: 8, + income: 58080, + }, + { + percentOfAmi: 60, + householdSize: 1, + income: 33600, + }, + { + percentOfAmi: 60, + householdSize: 2, + income: 38400, + }, + { + percentOfAmi: 60, + householdSize: 3, + income: 43200, + }, + { + percentOfAmi: 60, + householdSize: 4, + income: 48000, + }, + { + percentOfAmi: 60, + householdSize: 5, + income: 51840, + }, + { + percentOfAmi: 60, + householdSize: 6, + income: 55680, + }, + { + percentOfAmi: 60, + householdSize: 7, + income: 59520, + }, + { + percentOfAmi: 60, + householdSize: 8, + income: 63360, + }, + { + percentOfAmi: 70, + householdSize: 1, + income: 39200, + }, + { + percentOfAmi: 70, + householdSize: 2, + income: 44800, + }, + { + percentOfAmi: 70, + householdSize: 3, + income: 50400, + }, + { + percentOfAmi: 70, + householdSize: 4, + income: 56000, + }, + { + percentOfAmi: 70, + householdSize: 5, + income: 60480, + }, + { + percentOfAmi: 70, + householdSize: 6, + income: 64960, + }, + { + percentOfAmi: 70, + householdSize: 7, + income: 69440, + }, + { + percentOfAmi: 70, + householdSize: 8, + income: 73920, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 44800, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 51200, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 57600, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 64000, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 69120, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 74240, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 79360, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 84480, + }, + { + percentOfAmi: 100, + householdSize: 1, + income: 56000, + }, + { + percentOfAmi: 100, + householdSize: 2, + income: 64000, + }, + { + percentOfAmi: 100, + householdSize: 3, + income: 72000, + }, + { + percentOfAmi: 100, + householdSize: 4, + income: 80000, + }, + { + percentOfAmi: 100, + householdSize: 5, + income: 86400, + }, + { + percentOfAmi: 100, + householdSize: 6, + income: 92800, + }, + { + percentOfAmi: 100, + householdSize: 7, + income: 99200, + }, + { + percentOfAmi: 100, + householdSize: 8, + income: 105600, + }, + { + percentOfAmi: 120, + householdSize: 1, + income: 67200, + }, + { + percentOfAmi: 120, + householdSize: 2, + income: 76800, + }, + { + percentOfAmi: 120, + householdSize: 3, + income: 86400, + }, + { + percentOfAmi: 120, + householdSize: 4, + income: 96000, + }, + { + percentOfAmi: 120, + householdSize: 5, + income: 103680, + }, + { + percentOfAmi: 120, + householdSize: 6, + income: 111360, + }, + { + percentOfAmi: 120, + householdSize: 7, + income: 119040, + }, + { + percentOfAmi: 120, + householdSize: 8, + income: 126720, + }, + { + percentOfAmi: 125, + householdSize: 1, + income: 70000, + }, + { + percentOfAmi: 125, + householdSize: 2, + income: 80000, + }, + { + percentOfAmi: 125, + householdSize: 3, + income: 90000, + }, + { + percentOfAmi: 125, + householdSize: 4, + income: 100000, + }, + { + percentOfAmi: 125, + householdSize: 5, + income: 108000, + }, + { + percentOfAmi: 125, + householdSize: 6, + income: 116000, + }, + { + percentOfAmi: 125, + householdSize: 7, + income: 124000, + }, + { + percentOfAmi: 125, + householdSize: 8, + income: 132000, + }, + { + percentOfAmi: 140, + householdSize: 1, + income: 78400, + }, + { + percentOfAmi: 140, + householdSize: 2, + income: 89600, + }, + { + percentOfAmi: 140, + householdSize: 3, + income: 100800, + }, + { + percentOfAmi: 140, + householdSize: 4, + income: 112000, + }, + { + percentOfAmi: 140, + householdSize: 5, + income: 120960, + }, + { + percentOfAmi: 140, + householdSize: 6, + income: 129920, + }, + { + percentOfAmi: 140, + householdSize: 7, + income: 138880, + }, + { + percentOfAmi: 140, + householdSize: 8, + income: 147840, + }, + { + percentOfAmi: 150, + householdSize: 1, + income: 84000, + }, + { + percentOfAmi: 150, + householdSize: 2, + income: 96000, + }, + { + percentOfAmi: 150, + householdSize: 3, + income: 108000, + }, + { + percentOfAmi: 150, + householdSize: 4, + income: 120000, + }, + { + percentOfAmi: 150, + householdSize: 5, + income: 129600, + }, + { + percentOfAmi: 150, + householdSize: 6, + income: 139200, + }, + { + percentOfAmi: 150, + householdSize: 7, + income: 148800, + }, + { + percentOfAmi: 150, + householdSize: 8, + income: 158400, + }, + ], +} diff --git a/backend/core/src/seeder/seeds/ami-charts/WayneCountyMSHDA2021.txt b/backend/core/src/seeder/seeds/ami-charts/WayneCountyMSHDA2021.txt new file mode 100644 index 0000000000..e3157e3071 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/WayneCountyMSHDA2021.txt @@ -0,0 +1,16 @@ +20% 11,200 12,800 14,400 16,000 17,280 18,560 19,840 21,120 +25% 14,000 16,000 18,000 20,000 21,600 23,200 24,800 26,400 +30% 16,800 19,200 21,600 24,000 25,920 27,840 29,760 31,680 +35% 19,600 22,400 25,200 28,000 30,240 32,480 34,720 36,960 +40% 22,400 25,600 28,800 32,000 34,560 37,120 39,680 42,240 +45% 25,200 28,800 32,400 36,000 38,880 41,760 44,640 47,520 +50% 28,000 32,000 36,000 40,000 43,200 46,400 49,600 52,800 +55% 30,800 35,200 39,600 44,000 47,520 51,040 54,560 58,080 +60% 33,600 38,400 43,200 48,000 51,840 55,680 59,520 63,360 +70% 39,200 44,800 50,400 56,000 60,480 64,960 69,440 73,920 +80% 44,800 51,200 57,600 64,000 69,120 74,240 79,360 84,480 +100% 56,000 64,000 72,000 80,000 86,400 92,800 99,200 105,600 +120% 67,200 76,800 86,400 96,000 103,680 111,360 119,040 126,720 +125% 70,000 80,000 90,000 100,000 108,000 116,000 124,000 132,000 +140% 78,400 89,600 100,800 112,000 120,960 129,920 138,880 147,840 +150% 84,000 96,000 108,000 120,000 129,600 139,200 148,800 158,400 diff --git a/backend/core/src/seeder/seeds/ami-charts/default-ami-chart-san-jose.ts b/backend/core/src/seeder/seeds/ami-charts/default-ami-chart-san-jose.ts new file mode 100644 index 0000000000..c30e396c35 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/default-ami-chart-san-jose.ts @@ -0,0 +1,15 @@ +import { AmiChartDefaultSeed, getDefaultAmiChart } from "./default-ami-chart" +import { CountyCode } from "../../../shared/types/county-code" + +export class AmiDefaultSanJose extends AmiChartDefaultSeed { + async seed() { + const sanjoseJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.san_jose, + }) + return await this.amiChartRepository.save({ + ...getDefaultAmiChart(), + name: "San Jose TCAC 2021", + jurisdiction: sanjoseJurisdiction, + }) + } +} diff --git a/backend/core/src/seeder/seeds/ami-charts/default-ami-chart-san-mateo.ts b/backend/core/src/seeder/seeds/ami-charts/default-ami-chart-san-mateo.ts new file mode 100644 index 0000000000..5849db2da7 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/default-ami-chart-san-mateo.ts @@ -0,0 +1,15 @@ +import { AmiChartDefaultSeed, getDefaultAmiChart } from "./default-ami-chart" +import { CountyCode } from "../../../shared/types/county-code" + +export class AmiDefaultSanMateo extends AmiChartDefaultSeed { + async seed() { + const sanMateoJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.san_mateo, + }) + return await this.amiChartRepository.save({ + ...getDefaultAmiChart(), + name: "San mateo TCAC 2021", + jurisdiction: sanMateoJurisdiction, + }) + } +} diff --git a/backend/core/src/seeder/seeds/ami-charts/default-ami-chart.ts b/backend/core/src/seeder/seeds/ami-charts/default-ami-chart.ts new file mode 100644 index 0000000000..ef6f1a12bd --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/default-ami-chart.ts @@ -0,0 +1,318 @@ +import { InjectRepository } from "@nestjs/typeorm" +import { Repository } from "typeorm" +import { AmiChartCreateDto } from "../../../ami-charts/dto/ami-chart.dto" +import { AmiChart } from "../../../ami-charts/entities/ami-chart.entity" +import { Jurisdiction } from "../../../jurisdictions/entities/jurisdiction.entity" +import { CountyCode } from "../../../shared/types/county-code" + +export function getDefaultAmiChart() { + return JSON.parse(JSON.stringify(defaultAmiChart)) +} + +export const defaultAmiChart: Omit = { + name: "AlamedaCountyTCAC2021", + items: [ + { income: 140900, percentOfAmi: 120, householdSize: 3 }, + { income: 156600, percentOfAmi: 120, householdSize: 4 }, + { income: 169140, percentOfAmi: 120, householdSize: 5 }, + { + percentOfAmi: 80, + householdSize: 1, + income: 76720, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 87680, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 98640, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 109600, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 11840, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 127200, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 135920, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 144720, + }, + { + percentOfAmi: 60, + householdSize: 1, + income: 57540, + }, + { + percentOfAmi: 60, + householdSize: 2, + income: 65760, + }, + { + percentOfAmi: 60, + householdSize: 3, + income: 73980, + }, + { + percentOfAmi: 60, + householdSize: 4, + income: 82200, + }, + { + percentOfAmi: 60, + householdSize: 5, + income: 88800, + }, + { + percentOfAmi: 60, + householdSize: 6, + income: 95400, + }, + { + percentOfAmi: 60, + householdSize: 7, + income: 101940, + }, + { + percentOfAmi: 60, + householdSize: 8, + income: 108540, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 47950, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 54800, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 61650, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 68500, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 74000, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 79500, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 84950, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 90450, + }, + { + percentOfAmi: 45, + householdSize: 1, + income: 43155, + }, + { + percentOfAmi: 45, + householdSize: 2, + income: 49320, + }, + { + percentOfAmi: 45, + householdSize: 3, + income: 55485, + }, + { + percentOfAmi: 45, + householdSize: 4, + income: 61650, + }, + { + percentOfAmi: 45, + householdSize: 5, + income: 66600, + }, + { + percentOfAmi: 45, + householdSize: 6, + income: 71550, + }, + { + percentOfAmi: 45, + householdSize: 7, + income: 76455, + }, + { + percentOfAmi: 45, + householdSize: 8, + income: 81405, + }, + { + percentOfAmi: 40, + householdSize: 1, + income: 38360, + }, + { + percentOfAmi: 40, + householdSize: 2, + income: 43840, + }, + { + percentOfAmi: 40, + householdSize: 3, + income: 49320, + }, + { + percentOfAmi: 40, + householdSize: 4, + income: 54800, + }, + { + percentOfAmi: 40, + householdSize: 5, + income: 59200, + }, + { + percentOfAmi: 40, + householdSize: 6, + income: 63600, + }, + { + percentOfAmi: 40, + householdSize: 7, + income: 67960, + }, + { + percentOfAmi: 40, + householdSize: 8, + income: 72360, + }, + { + percentOfAmi: 30, + householdSize: 1, + income: 28770, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 32880, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 36990, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 41100, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 44400, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 47700, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 50970, + }, + { + percentOfAmi: 30, + householdSize: 8, + income: 54270, + }, + { + percentOfAmi: 20, + householdSize: 1, + income: 19180, + }, + { + percentOfAmi: 20, + householdSize: 2, + income: 21920, + }, + { + percentOfAmi: 20, + householdSize: 3, + income: 24660, + }, + { + percentOfAmi: 20, + householdSize: 4, + income: 27400, + }, + { + percentOfAmi: 20, + householdSize: 5, + income: 29600, + }, + { + percentOfAmi: 20, + householdSize: 6, + income: 31800, + }, + { + percentOfAmi: 20, + householdSize: 7, + income: 33980, + }, + { + percentOfAmi: 20, + householdSize: 8, + income: 36180, + }, + ], +} + +export class AmiChartDefaultSeed { + constructor( + @InjectRepository(AmiChart) + protected readonly amiChartRepository: Repository, + @InjectRepository(Jurisdiction) + protected readonly jurisdictionRepository: Repository + ) {} + + async seed() { + const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.alameda, + }) + return await this.amiChartRepository.save({ + ...getDefaultAmiChart(), + jurisdiction: alamedaJurisdiction, + }) + } +} diff --git a/backend/core/src/seeder/seeds/ami-charts/missing-household-ami-levels.ts b/backend/core/src/seeder/seeds/ami-charts/missing-household-ami-levels.ts new file mode 100644 index 0000000000..6c5b460e83 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/missing-household-ami-levels.ts @@ -0,0 +1,46 @@ +import { AmiChartDefaultSeed } from "./default-ami-chart" +import { CountyCode } from "../../../shared/types/county-code" + +export class AmiDefaultMissingAMI extends AmiChartDefaultSeed { + async seed() { + const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.alameda, + }) + return await this.amiChartRepository.save({ + name: "Missing Household Ami Levels", + items: [ + { + percentOfAmi: 50, + householdSize: 3, + income: 65850, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 73150, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 79050, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 84900, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 90750, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 96600, + }, + ], + jurisdiction: alamedaJurisdiction, + }) + } +} diff --git a/backend/core/src/seeder/seeds/ami-charts/triton-ami-chart-detroit.ts b/backend/core/src/seeder/seeds/ami-charts/triton-ami-chart-detroit.ts new file mode 100644 index 0000000000..6fd344a5a4 --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/triton-ami-chart-detroit.ts @@ -0,0 +1,16 @@ +import { AmiChartDefaultSeed } from "./default-ami-chart" +import { itemInfo } from "./triton-ami-chart" +import { CountyCode } from "../../../shared/types/county-code" + +export class AmiDefaultTritonDetroit extends AmiChartDefaultSeed { + async seed() { + const detroitJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.detroit, + }) + return await this.amiChartRepository.save({ + name: "Detroit TCAC 2019", + items: itemInfo, + jurisdiction: detroitJurisdiction, + }) + } +} diff --git a/backend/core/src/seeder/seeds/ami-charts/triton-ami-chart.ts b/backend/core/src/seeder/seeds/ami-charts/triton-ami-chart.ts new file mode 100644 index 0000000000..cd61fc9e8c --- /dev/null +++ b/backend/core/src/seeder/seeds/ami-charts/triton-ami-chart.ts @@ -0,0 +1,568 @@ +import { AmiChartDefaultSeed } from "./default-ami-chart" +import { CountyCode } from "../../../shared/types/county-code" + +export const itemInfo = [ + { + percentOfAmi: 120, + householdSize: 1, + income: 110400, + }, + { + percentOfAmi: 120, + householdSize: 2, + income: 126150, + }, + { + percentOfAmi: 120, + householdSize: 3, + income: 141950, + }, + { + percentOfAmi: 120, + householdSize: 4, + income: 157700, + }, + { + percentOfAmi: 120, + householdSize: 5, + income: 170300, + }, + { + percentOfAmi: 120, + householdSize: 6, + income: 182950, + }, + { + percentOfAmi: 120, + householdSize: 7, + income: 195550, + }, + { + percentOfAmi: 120, + householdSize: 8, + income: 208150, + }, + { + percentOfAmi: 110, + householdSize: 1, + income: 101200, + }, + { + percentOfAmi: 110, + householdSize: 2, + income: 115610, + }, + { + percentOfAmi: 110, + householdSize: 3, + income: 130075, + }, + { + percentOfAmi: 110, + householdSize: 4, + income: 144540, + }, + { + percentOfAmi: 110, + householdSize: 5, + income: 156090, + }, + { + percentOfAmi: 110, + householdSize: 6, + income: 167640, + }, + { + percentOfAmi: 110, + householdSize: 7, + income: 179245, + }, + { + percentOfAmi: 110, + householdSize: 8, + income: 190795, + }, + { + percentOfAmi: 100, + householdSize: 1, + income: 92000, + }, + { + percentOfAmi: 100, + householdSize: 2, + income: 105100, + }, + { + percentOfAmi: 100, + householdSize: 3, + income: 118250, + }, + { + percentOfAmi: 100, + householdSize: 4, + income: 131400, + }, + { + percentOfAmi: 100, + householdSize: 5, + income: 141900, + }, + { + percentOfAmi: 100, + householdSize: 6, + income: 152400, + }, + { + percentOfAmi: 100, + householdSize: 7, + income: 162950, + }, + { + percentOfAmi: 100, + householdSize: 8, + income: 173450, + }, + { + percentOfAmi: 80, + householdSize: 1, + income: 72750, + }, + { + percentOfAmi: 80, + householdSize: 2, + income: 83150, + }, + { + percentOfAmi: 80, + householdSize: 3, + income: 93550, + }, + { + percentOfAmi: 80, + householdSize: 4, + income: 103900, + }, + { + percentOfAmi: 80, + householdSize: 5, + income: 112250, + }, + { + percentOfAmi: 80, + householdSize: 6, + income: 120550, + }, + { + percentOfAmi: 80, + householdSize: 7, + income: 128850, + }, + { + percentOfAmi: 80, + householdSize: 8, + income: 137150, + }, + { + percentOfAmi: 60, + householdSize: 1, + income: 61500, + }, + { + percentOfAmi: 60, + householdSize: 2, + income: 70260, + }, + { + percentOfAmi: 60, + householdSize: 3, + income: 79020, + }, + { + percentOfAmi: 60, + householdSize: 4, + income: 87780, + }, + { + percentOfAmi: 60, + householdSize: 5, + income: 94860, + }, + { + percentOfAmi: 60, + householdSize: 6, + income: 101880, + }, + { + percentOfAmi: 60, + householdSize: 7, + income: 108900, + }, + { + percentOfAmi: 60, + householdSize: 8, + income: 115920, + }, + { + percentOfAmi: 55, + householdSize: 1, + income: 56375, + }, + { + percentOfAmi: 55, + householdSize: 2, + income: 64405, + }, + { + percentOfAmi: 55, + householdSize: 3, + income: 72435, + }, + { + percentOfAmi: 55, + householdSize: 4, + income: 80465, + }, + { + percentOfAmi: 55, + householdSize: 5, + income: 86955, + }, + { + percentOfAmi: 55, + householdSize: 6, + income: 93390, + }, + { + percentOfAmi: 55, + householdSize: 7, + income: 99825, + }, + { + percentOfAmi: 55, + householdSize: 8, + income: 106260, + }, + { + percentOfAmi: 50, + householdSize: 1, + income: 51250, + }, + { + percentOfAmi: 50, + householdSize: 2, + income: 58550, + }, + { + percentOfAmi: 50, + householdSize: 3, + income: 65850, + }, + { + percentOfAmi: 50, + householdSize: 4, + income: 73150, + }, + { + percentOfAmi: 50, + householdSize: 5, + income: 79050, + }, + { + percentOfAmi: 50, + householdSize: 6, + income: 84900, + }, + { + percentOfAmi: 50, + householdSize: 7, + income: 90750, + }, + { + percentOfAmi: 50, + householdSize: 8, + income: 96600, + }, + { + percentOfAmi: 45, + householdSize: 1, + income: 46125, + }, + { + percentOfAmi: 45, + householdSize: 2, + income: 52695, + }, + { + percentOfAmi: 45, + householdSize: 3, + income: 59265, + }, + { + percentOfAmi: 45, + householdSize: 4, + income: 65835, + }, + { + percentOfAmi: 45, + householdSize: 5, + income: 71145, + }, + { + percentOfAmi: 45, + householdSize: 6, + income: 76410, + }, + { + percentOfAmi: 45, + householdSize: 7, + income: 81675, + }, + { + percentOfAmi: 40, + householdSize: 1, + income: 41000, + }, + { + percentOfAmi: 40, + householdSize: 2, + income: 46840, + }, + { + percentOfAmi: 40, + householdSize: 3, + income: 52680, + }, + { + percentOfAmi: 40, + householdSize: 4, + income: 58520, + }, + { + percentOfAmi: 40, + householdSize: 5, + income: 63240, + }, + { + percentOfAmi: 40, + householdSize: 6, + income: 67920, + }, + { + percentOfAmi: 40, + householdSize: 7, + income: 72600, + }, + { + percentOfAmi: 40, + householdSize: 8, + income: 77280, + }, + { + percentOfAmi: 35, + householdSize: 1, + income: 35875, + }, + { + percentOfAmi: 35, + householdSize: 2, + income: 40985, + }, + { + percentOfAmi: 35, + householdSize: 3, + income: 46095, + }, + { + percentOfAmi: 35, + householdSize: 4, + income: 51205, + }, + { + percentOfAmi: 35, + householdSize: 5, + income: 55335, + }, + { + percentOfAmi: 35, + householdSize: 6, + income: 59430, + }, + { + percentOfAmi: 35, + householdSize: 7, + income: 63525, + }, + { + percentOfAmi: 35, + householdSize: 8, + income: 67620, + }, + { + percentOfAmi: 30, + householdSize: 1, + income: 30750, + }, + { + percentOfAmi: 30, + householdSize: 2, + income: 35130, + }, + { + percentOfAmi: 30, + householdSize: 3, + income: 39510, + }, + { + percentOfAmi: 30, + householdSize: 4, + income: 43890, + }, + { + percentOfAmi: 30, + householdSize: 5, + income: 47430, + }, + { + percentOfAmi: 30, + householdSize: 6, + income: 50940, + }, + { + percentOfAmi: 30, + householdSize: 7, + income: 54450, + }, + { + percentOfAmi: 25, + householdSize: 1, + income: 25625, + }, + { + percentOfAmi: 25, + householdSize: 2, + income: 29275, + }, + { + percentOfAmi: 25, + householdSize: 3, + income: 32925, + }, + { + percentOfAmi: 25, + householdSize: 4, + income: 36575, + }, + { + percentOfAmi: 25, + householdSize: 5, + income: 39525, + }, + { + percentOfAmi: 25, + householdSize: 6, + income: 42450, + }, + { + percentOfAmi: 25, + householdSize: 7, + income: 45375, + }, + { + percentOfAmi: 25, + householdSize: 8, + income: 48300, + }, + { + percentOfAmi: 20, + householdSize: 1, + income: 20500, + }, + { + percentOfAmi: 20, + householdSize: 2, + income: 23420, + }, + { + percentOfAmi: 20, + householdSize: 3, + income: 26340, + }, + { + percentOfAmi: 20, + householdSize: 4, + income: 29260, + }, + { + percentOfAmi: 20, + householdSize: 5, + income: 31620, + }, + { + percentOfAmi: 20, + householdSize: 6, + income: 33960, + }, + { + percentOfAmi: 20, + householdSize: 7, + income: 36300, + }, + { + percentOfAmi: 20, + householdSize: 8, + income: 38640, + }, + { + percentOfAmi: 15, + householdSize: 1, + income: 15375, + }, + { + percentOfAmi: 15, + householdSize: 2, + income: 17565, + }, + { + percentOfAmi: 15, + householdSize: 3, + income: 19755, + }, + { + percentOfAmi: 15, + householdSize: 4, + income: 21945, + }, + { + percentOfAmi: 15, + householdSize: 5, + income: 23715, + }, + { + percentOfAmi: 15, + householdSize: 6, + income: 25470, + }, + { + percentOfAmi: 15, + householdSize: 7, + income: 27225, + }, + { + percentOfAmi: 15, + householdSize: 8, + income: 28980, + }, +] + +export class AmiDefaultTriton extends AmiChartDefaultSeed { + async seed() { + const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.alameda, + }) + return await this.amiChartRepository.save({ + name: "San Jose TCAC 2019", + items: itemInfo, + jurisdiction: alamedaJurisdiction, + }) + } +} diff --git a/backend/core/src/seeds/applications.ts b/backend/core/src/seeder/seeds/applications.ts similarity index 84% rename from backend/core/src/seeds/applications.ts rename to backend/core/src/seeder/seeds/applications.ts index a23bddd12d..a637ec199c 100644 --- a/backend/core/src/seeds/applications.ts +++ b/backend/core/src/seeder/seeds/applications.ts @@ -1,17 +1,17 @@ import { INestApplicationContext } from "@nestjs/common" -import { ApplicationCreateDto } from "../applications/dto/application.dto" import { Repository } from "typeorm" -import { Application } from "../applications/entities/application.entity" import { getRepositoryToken } from "@nestjs/typeorm" -import { InputType } from "../shared/types/input-type" -import { Language } from "../shared/types/language-enum" -import { ApplicationStatus } from "../applications/types/application-status-enum" -import { ApplicationSubmissionType } from "../applications/types/application-submission-type-enum" -import { IncomePeriod } from "../applications/types/income-period-enum" -import { Listing } from "../listings/entities/listing.entity" -import { User } from "../auth/entities/user.entity" -import { UnitType } from "../unit-types/entities/unit-type.entity" -import { ApplicationsService } from "../applications/applications.service" +import { IncomePeriod } from "../../applications/types/income-period-enum" +import { Language } from "../../shared/types/language-enum" +import { InputType } from "../../shared/types/input-type" +import { ApplicationStatus } from "../../applications/types/application-status-enum" +import { ApplicationSubmissionType } from "../../applications/types/application-submission-type-enum" +import { Listing } from "../../listings/entities/listing.entity" +import { UnitType } from "../../unit-types/entities/unit-type.entity" +import { User } from "../../auth/entities/user.entity" +import { Application } from "../../applications/entities/application.entity" +import { ApplicationsService } from "../../applications/services/applications.service" +import { ApplicationCreateDto } from "../../applications/dto/application-create.dto" const applicationCreateDtoTemplate: Omit< ApplicationCreateDto, @@ -96,11 +96,11 @@ const applicationCreateDtoTemplate: Omit< }, contactPreferences: [], demographics: { - ethnicity: "ethnicity", - gender: "gender", + ethnicity: null, + gender: null, howDidYouHear: ["email", "facebook"], - race: "race", - sexualOrientation: "orientation", + race: ["asian", "filipino"], + sexualOrientation: null, }, householdMembers: [ { @@ -148,6 +148,8 @@ const applicationCreateDtoTemplate: Omit< income: "5000.00", incomePeriod: IncomePeriod.perMonth, incomeVouchers: false, + householdExpectingChanges: false, + householdStudent: false, language: Language.en, mailingAddress: { city: "city", diff --git a/backend/core/src/seeder/seeds/jurisdictions.ts b/backend/core/src/seeder/seeds/jurisdictions.ts new file mode 100644 index 0000000000..803b238b92 --- /dev/null +++ b/backend/core/src/seeder/seeds/jurisdictions.ts @@ -0,0 +1,29 @@ +import { INestApplicationContext } from "@nestjs/common" +import { JurisdictionCreateDto } from "../../jurisdictions/dto/jurisdiction-create.dto" +import { Language } from "../../shared/types/language-enum" +import { JurisdictionsService } from "../../jurisdictions/services/jurisdictions.service" + +export const defaultJurisdictions: JurisdictionCreateDto[] = [ + { + name: "Detroit", + preferences: [], + languages: [Language.en], + programs: [], + publicUrl: "", + emailFromAddress: "Detroit Housing", + }, +] + +export async function createJurisdictions(app: INestApplicationContext) { + const jurisdictionService = await app.resolve(JurisdictionsService) + // some jurisdictions are added via previous migrations + const jurisdictions = await jurisdictionService.list() + const toInsert = defaultJurisdictions.filter( + (rec) => jurisdictions.findIndex((item) => item.name === rec.name) === -1 + ) + const inserted = await Promise.all( + toInsert.map(async (jurisdiction) => await jurisdictionService.create(jurisdiction)) + ) + // names are unique + return jurisdictions.concat(inserted).sort((a, b) => (a.name < b.name ? -1 : 1)) +} diff --git a/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts b/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts new file mode 100644 index 0000000000..257fa3b67f --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts @@ -0,0 +1,1071 @@ +import { ListingSeedType, PropertySeedType, UnitSeedType } from "./listings" +import { + getDate, + getDefaultAssets, + getHopwaPreference, + getLiveWorkPreference, + getPbvPreference, + getServedInMilitaryProgram, + getTayProgram, + PriorityTypes, +} from "./shared" +import { BaseEntity, DeepPartial } from "typeorm" +import { ListingDefaultSeed } from "./listing-default-seed" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingReviewOrder } from "../../../listings/types/listing-review-order-enum" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { UnitStatus } from "../../../units/types/unit-status-enum" +import { UnitCreateDto } from "../../../units/dto/unit-create.dto" +import { Listing } from "../../../listings/entities/listing.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" + +const coliseumProperty: PropertySeedType = { + accessibility: + "Fifteen (15) units are designed for residents with mobility impairments per HUD/U.F.A.S. guidelines with one (1) of these units further designed for residents with auditory or visual impairments. There are two (2) additional units with features for those with auditory or visual impairments. All the other units are adaptable. Accessible features in the property include: * 36” wide entries and doorways * Kitchens built to the accessibility standards of the California Building Code, including appliance controls and switch outlets within reach, and work surfaces and storage at accessible heights * Bathrooms built to the accessibility standards of the California Building Code, including grab bars, flexible shower spray hose, switch outlets within reach, and in-tub seats. * Closet rods and shelves at mobility height. * Window blinds/shades able to be used without grasping or twisting * Units for the Hearing & Visually Impaired will have a horn & strobe for fire alarm and a flashing light doorbell. The 44 non-ADA units are built to Adaptable standards.", + amenities: "Community room, bike parking, courtyard off the community room, 2nd floor courtyard.", + buildingAddress: { + county: "Alameda", + city: "Oakland", + street: "3300 Hawley Street", + zipCode: "94621", + state: "CA", + latitude: 37.7549632, + longitude: -122.1968792, + }, + buildingTotalUnits: 58, + developer: "Resources for Community Development", + neighborhood: "Coliseum", + petPolicy: "Permitted", + servicesOffered: + "Residential supportive services are provided to all residents on a volunteer basis.", + smokingPolicy: "No Smoking", + unitAmenities: null, + unitsAvailable: 46, + yearBuilt: 2021, +} + +const coliseumListing: ListingSeedType = { + jurisdictionName: "Alameda", + digitalApplication: false, + commonDigitalApplication: false, + paperApplication: false, + section8Acceptance: true, + referralOpportunity: false, + countyCode: CountyCode.alameda, + applicationDropOffAddress: null, + applicationDropOffAddressOfficeHours: null, + applicationMailingAddress: null, + applicationDueDate: new Date(getDate(1).setHours(17, 0, 0, 0)), + applicationFee: "12", + applicationOpenDate: getDate(-10), + applicationOrganization: "John Stewart Company", + applicationPickUpAddress: { + county: "Alameda", + city: "Oakland", + street: "1701 Martin Luther King Way", + zipCode: "94621", + state: "CA", + latitude: 37.7549632, + longitude: -122.1968792, + }, + images: [], + applicationPickUpAddressOfficeHours: null, + buildingSelectionCriteria: null, + costsNotIncluded: + "Electricity, phone, TV, internet, and cable not included. For the PBV units, deposit is one month of the tenant-paid portion of rent (30% of income).", + creditHistory: + "Management staff will request credit histories on each adult member of each applicant household. It is the applicant’s responsibility that at least one household member can demonstrate utilities can be put in their name. For this to be demonstrated, at least one household member must have a credit report that shows no utility accounts in default. Applicants who cannot have utilities put in their name will be considered ineligible. Any currently open bankruptcy proceeding of any of the household members will be considered a disqualifying condition. Applicants will not be considered to have a poor credit history when they were delinquent in rent because they were withholding rent due to substandard housing conditions in a manner consistent with local ordinance; or had a poor rent paying history clearly related to an excessive rent relative to their income, and responsible efforts were made to resolve the non-payment problem. If there is a finding of any kind which would negatively impact an application, the applicant will be notified in writing. The applicant then shall have 14 calendar days in which such a finding may be appealed to staff for consideration.", + criminalBackground: null, + depositMax: "200", + depositMin: "100", + disableUnitsAccordion: true, + displayWaitlistSize: false, + leasingAgentAddress: { + county: "Alameda", + city: "Oakland", + street: "1701 Martin Luther King Way", + zipCode: "94621", + state: "CA", + latitude: 37.7549632, + longitude: -122.1968792, + }, + leasingAgentEmail: "coliseum@jsco.net", + leasingAgentName: "Leasing agent name", + leasingAgentOfficeHours: + "Tuesdays & Thursdays, 9:00am to 5:00pm | Persons with disabilities who are unable to access the on-line application may request a Reasonable Accommodation by calling (510) 649-5739 for assistance. A TDD line is available at (415) 345-4470.", + leasingAgentPhone: "(510) 625-1632", + leasingAgentTitle: "Property Manager", + listingPreferences: [], + listingPrograms: [], + name: "Test: Coliseum", + postmarkedApplicationsReceivedByDate: null, + programRules: null, + rentalAssistance: "Rental assistance", + rentalHistory: "Two years' landlord history or homeless verification", + requiredDocuments: + "Application Document Checklist: https://org-housingbayarea-public-assets.s3-us-west-1.amazonaws.com/Tax+Credit+Application+Interview+Checklist.pdf", + reviewOrderType: "firstComeFirstServe" as ListingReviewOrder, + specialNotes: + "Priority Units: 3 apartments are set-aside for households eligible for the HOPWA program (Housing Opportunities for Persons with AIDS), which are households where a person has been medically diagnosed with HIV/AIDS. These 3 apartments also have Project-Based Section rental subsidies (tenant pays 30% of household income). 15 apartments are for those with mobility impairments and one of these units also has features for the hearing/visually impaired. Two additional apartments have features for the hearing/visually impaired. All units require eligibility requirements beyond income qualification: The waiting list will be ordered by incorporating the Alameda County preference for eligible households in which at least one member lives or works in the County. Three (3) apartments are restricted to households eligible under the HOPWA (Housing Opportunities for Persons with AIDS), which are households where a person has been medically diagnosed with HIV/AIDS. These apartments also receive PBV’s from OHA. For the twenty-five (25) apartments that have Project-Based Section 8 Vouchers from OHA, applicants will be called for an interview in the order according to the site-based waiting list compiled from the initial application and lotter process specifically for the PBV units. The waiting list order for these apartments will also incorporate the local preferences required by OHA. These preferences are: * A Residency preference (Applicants who live or work in the City of Oakland at the time of the application interview and/or applicants that lived or worked in the City of Oakland at the time of submitting their initial application and can verify their previous residency/employment at the applicant interview, qualify for this preference). * A Family preference (Applicant families with two or more persons, or a single person applicant that is 62 years of age or older, or a single person applicant with a disability, qualify for this preference). * A Veteran and active members of the military preference. Per OHA policy, a Veteran is a person who served in the active military, naval, or air service and who was discharged or released from such service under conditions other than dishonorable. * A Homeless preference. Applicant families who meet the McKinney-Vento Act definition of homeless qualify for this preference (see definition below). Each PBV applicant will receive one point for each preference for which it is eligible and the site-based PBV waiting list will be prioritized by the number of points applicants have from these preferences. Applicants for the PBV units must comply with OHA’s policy regarding Social Security Numbers. The applicant and all members of the applicant’s household must disclose the complete and accurate social security number (SSN) assigned to each household member, and they must provide the documentation necessary to verify each SSN. As an EveryOne Home partner, each applicant’s individual circumstances will be evaluated, alternative forms of verification and additional information submitted by the applicant will considered, and reasonable accommodations will be provided when requested and if verified and necessary. Persons with disabilities are encouraged to apply.", + status: ListingStatus.active, + waitlistCurrentSize: 0, + waitlistMaxSize: 3000, + waitlistOpenSpots: 3000, + isWaitlistOpen: true, + whatToExpect: null, + marketingType: ListingMarketingTypeEnum.Marketing, +} + +export class ListingColiseumSeed extends ListingDefaultSeed { + async seed() { + const priorityTypeMobilityAndHearingWithVisual = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( + { + name: PriorityTypes.mobilityHearingVisual, + } + ) + const priorityTypeMobilityAndMobilityWithHearingAndVisual = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( + { + name: PriorityTypes.mobilityHearingVisual, + } + ) + const priorityTypeMobilityAndHearing = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( + { + name: PriorityTypes.mobilityHearing, + } + ) + const priorityMobility = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail({ + name: PriorityTypes.mobility, + }) + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + const unitTypeThreeBdrm = await this.unitTypeRepository.findOneOrFail({ name: "threeBdrm" }) + + const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.alameda, + }) + const amiChart = await this.amiChartRepository.findOneOrFail({ + name: "AlamedaCountyTCAC2021", + jurisdiction: alamedaJurisdiction, + }) + + const property = await this.propertyRepository.save({ + ...coliseumProperty, + }) + + const coliseumUnits: Array = [ + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "36990", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 3, + minOccupancy: 1, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 1, + number: null, + sqFeet: "486", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "36990", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 3, + minOccupancy: 1, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 1, + number: null, + sqFeet: "491", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "36990", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 3, + minOccupancy: 1, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 1, + number: null, + sqFeet: "491", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "61650", + annualIncomeMin: "38520", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 3, + minOccupancy: 1, + monthlyIncomeMin: "3210", + monthlyRent: "1284", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 1, + number: null, + sqFeet: "491", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "44400", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 4, + minOccupancy: 2, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "44400", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 4, + minOccupancy: 2, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "785", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "44400", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 4, + minOccupancy: 2, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "785", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "44400", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 4, + minOccupancy: 2, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "785", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "44400", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 4, + minOccupancy: 2, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "785", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "44400", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 4, + minOccupancy: 2, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "785", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "44400", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 4, + minOccupancy: 2, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "785", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "44400", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 4, + minOccupancy: 2, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "785", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "44400", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 4, + minOccupancy: 2, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "785", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "30", + annualIncomeMax: "44400", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 4, + minOccupancy: 2, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "785", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "45", + annualIncomeMax: "66600", + annualIncomeMin: "41616", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3468", + monthlyRent: "1387", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "45", + annualIncomeMax: "66600", + annualIncomeMin: "41616", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3468", + monthlyRent: "1387", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "74000", + annualIncomeMin: "46236", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3853", + monthlyRent: "1541", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "20", + annualIncomeMax: "31800", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "20", + annualIncomeMax: "31800", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 6, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1080", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "20", + annualIncomeMax: "31800", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "45", + annualIncomeMax: "71550", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "45", + annualIncomeMax: "71550", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "45", + annualIncomeMax: "71550", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "79500", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "79500", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "79500", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "79500", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "79500", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "79500", + annualIncomeMin: "0", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 6, + minOccupancy: 4, + monthlyIncomeMin: "0", + monthlyRent: null, + monthlyRentAsPercentOfIncome: "30", + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "84950", + annualIncomeMin: "53436", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 7, + minOccupancy: 4, + monthlyIncomeMin: "4453", + monthlyRent: "1781", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "84950", + annualIncomeMin: "53436", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 7, + minOccupancy: 4, + monthlyIncomeMin: "4453", + monthlyRent: "1781", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "84950", + annualIncomeMin: "53436", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 7, + minOccupancy: 4, + monthlyIncomeMin: "4453", + monthlyRent: "1781", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "84950", + annualIncomeMin: "53436", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 7, + minOccupancy: 4, + monthlyIncomeMin: "4453", + monthlyRent: "1781", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "84950", + annualIncomeMin: "53436", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 7, + minOccupancy: 4, + monthlyIncomeMin: "4453", + monthlyRent: "1781", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "84950", + annualIncomeMin: "53436", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 7, + minOccupancy: 4, + monthlyIncomeMin: "4453", + monthlyRent: "1781", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "84950", + annualIncomeMin: "53436", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 7, + minOccupancy: 4, + monthlyIncomeMin: "4453", + monthlyRent: "1781", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 2, + numBedrooms: 3, + number: null, + sqFeet: "1029", + status: UnitStatus.available, + }, + ] + + const unitsToBeCreated: Array> = coliseumUnits.map( + (unit) => { + return { + ...unit, + property: { + id: property.id, + }, + amiChart, + } + } + ) + + // Assign priorityTypes + for (let i = 0; i < 3; i++) { + unitsToBeCreated[i].priorityType = priorityTypeMobilityAndMobilityWithHearingAndVisual + } + for (let i = 3; i < 14; i++) { + unitsToBeCreated[i].priorityType = priorityTypeMobilityAndHearingWithVisual + } + for (let i = 14; i < 27; i++) { + unitsToBeCreated[i].priorityType = priorityTypeMobilityAndHearing + } + for (let i = 27; i < 46; i++) { + unitsToBeCreated[i].priorityType = priorityMobility + } + + // Assign unit types + for (let i = 0; i < 4; i++) { + unitsToBeCreated[i].unitType = unitTypeOneBdrm + } + for (let i = 4; i < 27; i++) { + unitsToBeCreated[i].unitType = unitTypeTwoBdrm + } + for (let i = 27; i < 46; i++) { + unitsToBeCreated[i].unitType = unitTypeThreeBdrm + } + + await this.unitsRepository.save(unitsToBeCreated) + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...coliseumListing, + property: property, + assets: getDefaultAssets(), + listingPreferences: [ + { + preference: await this.preferencesRepository.findOneOrFail({ + title: getLiveWorkPreference(alamedaJurisdiction.name).title, + }), + ordinal: 1, + }, + { + preference: await this.preferencesRepository.findOneOrFail({ + title: getPbvPreference(alamedaJurisdiction.name).title, + }), + ordinal: 2, + }, + { + preference: await this.preferencesRepository.findOneOrFail({ + title: getHopwaPreference(alamedaJurisdiction.name).title, + }), + ordinal: 3, + }, + ], + events: [], + listingPrograms: [ + { + program: await this.programsRepository.findOneOrFail({ + title: getServedInMilitaryProgram().title, + }), + ordinal: 1, + }, + { + program: await this.programsRepository.findOneOrFail({ + title: getTayProgram().title, + }), + ordinal: 2, + }, + ], + } + + return await this.listingRepository.save(listingCreateDto) + } +} diff --git a/backend/core/src/seeds/listings/listing-default-bmr-chart-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-bmr-chart-seed.ts similarity index 80% rename from backend/core/src/seeds/listings/listing-default-bmr-chart-seed.ts rename to backend/core/src/seeder/seeds/listings/listing-default-bmr-chart-seed.ts index f471415d76..957ec2eefc 100644 --- a/backend/core/src/seeds/listings/listing-default-bmr-chart-seed.ts +++ b/backend/core/src/seeder/seeds/listings/listing-default-bmr-chart-seed.ts @@ -1,8 +1,9 @@ import { ListingDefaultSeed } from "./listing-default-seed" -import { getDefaultUnits, getDefaultProperty, getDefaultAmiChart } from "./shared" +import { getDefaultUnits, getDefaultProperty } from "./shared" import { BaseEntity } from "typeorm" -import { UnitCreateDto } from "../../units/dto/unit-create.dto" -import { CountyCode } from "../../shared/types/county-code" +import { defaultAmiChart } from "../ami-charts/default-ami-chart" +import { UnitCreateDto } from "../../../units/dto/unit-create.dto" +import { CountyCode } from "../../../shared/types/county-code" export class ListingDefaultBmrChartSeed extends ListingDefaultSeed { async seed() { @@ -15,8 +16,8 @@ export class ListingDefaultBmrChartSeed extends ListingDefaultSeed { const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ name: CountyCode.alameda, }) - const amiChart = await this.amiChartRepository.save({ - ...getDefaultAmiChart(), + const amiChart = await this.amiChartRepository.findOneOrFail({ + name: defaultAmiChart.name, jurisdiction: alamedaJurisdiction, }) diff --git a/backend/core/src/seeds/listings/listing-default-fcfs-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-fcfs-seed.ts similarity index 82% rename from backend/core/src/seeds/listings/listing-default-fcfs-seed.ts rename to backend/core/src/seeder/seeds/listings/listing-default-fcfs-seed.ts index 2b374840c1..33a31d3020 100644 --- a/backend/core/src/seeds/listings/listing-default-fcfs-seed.ts +++ b/backend/core/src/seeder/seeds/listings/listing-default-fcfs-seed.ts @@ -1,5 +1,5 @@ import { ListingDefaultSeed } from "./listing-default-seed" -import { ListingReviewOrder } from "../../listings/types/listing-review-order-enum" +import { ListingReviewOrder } from "../../../listings/types/listing-review-order-enum" export class ListingDefaultFCFSSeed extends ListingDefaultSeed { async seed() { diff --git a/backend/core/src/seeder/seeds/listings/listing-default-lottery-pending.ts b/backend/core/src/seeder/seeds/listings/listing-default-lottery-pending.ts new file mode 100644 index 0000000000..b27cfbe91a --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-lottery-pending.ts @@ -0,0 +1,52 @@ +import { ListingEventType } from "../../../../types/src/backend-swagger" +import { ListingDefaultSeed } from "./listing-default-seed" +import { getDate } from "./shared" + +export class ListingDefaultLotteryPending extends ListingDefaultSeed { + async seed() { + const listing = await super.seed() + return await this.listingRepository.save({ + ...listing, + name: "Test: Default, Lottery Results Pending", + applicationOpenDate: getDate(30), + applicationDueDate: getDate(60), + events: [ + { + startTime: getDate(10), + endTime: getDate(10), + note: + "Custom public lottery event note. This is a long note and should take up more space.", + type: ListingEventType.openHouse, + url: "https://www.example.com", + label: "Custom Event URL Label", + }, + { + startTime: getDate(15), + endTime: getDate(15), + type: ListingEventType.openHouse, + }, + { + startTime: getDate(20), + endTime: getDate(20), + note: "Custom open house event note", + type: ListingEventType.openHouse, + url: "https://www.example.com", + label: "Custom Event URL Label", + }, + { + startTime: getDate(-10), + endTime: getDate(-10), + type: ListingEventType.publicLottery, + url: "https://www.example2.com", + label: "Custom Event URL Label", + }, + { + startTime: getDate(15), + endTime: getDate(15), + type: ListingEventType.lotteryResults, + label: "Custom Event URL Label", + }, + ], + }) + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-default-lottery-results.ts b/backend/core/src/seeder/seeds/listings/listing-default-lottery-results.ts new file mode 100644 index 0000000000..8aab9fe5f0 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-lottery-results.ts @@ -0,0 +1,53 @@ +import { ListingEventType } from "../../../../types/src/backend-swagger" +import { ListingDefaultSeed } from "./listing-default-seed" +import { getDate } from "./shared" + +export class ListingDefaultLottery extends ListingDefaultSeed { + async seed() { + const listing = await super.seed() + return await this.listingRepository.save({ + ...listing, + name: "Test: Default, Lottery Results", + applicationOpenDate: getDate(30), + applicationDueDate: getDate(60), + events: [ + { + startTime: getDate(10), + endTime: getDate(10), + note: + "Custom public lottery event note. This is a long note and should take up more space.", + type: ListingEventType.openHouse, + url: "https://www.example.com", + label: "Custom Event URL Label", + }, + { + startTime: getDate(15), + endTime: getDate(15), + type: ListingEventType.openHouse, + }, + { + startTime: getDate(20), + endTime: getDate(20), + note: "Custom open house event note", + type: ListingEventType.openHouse, + url: "https://www.example.com", + label: "Custom Event URL Label", + }, + { + startTime: getDate(10), + endTime: getDate(10), + type: ListingEventType.publicLottery, + url: "https://www.example2.com", + label: "Custom Event URL Label", + }, + { + startTime: getDate(15), + endTime: getDate(15), + type: ListingEventType.lotteryResults, + url: "https://www.example2.com", + label: "Custom Event URL Label", + }, + ], + }) + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-default-missing-ami.ts b/backend/core/src/seeder/seeds/listings/listing-default-missing-ami.ts new file mode 100644 index 0000000000..0a924d619d --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-missing-ami.ts @@ -0,0 +1,146 @@ +import { ListingDefaultSeed } from "./listing-default-seed" +import { getDefaultProperty } from "./shared" +import { BaseEntity } from "typeorm" +import { UnitSeedType } from "./listings" +import { CountyCode } from "../../../shared/types/county-code" +import { UnitStatus } from "../../../units/types/unit-status-enum" +import { UnitCreateDto } from "../../../units/dto/unit-create.dto" + +export class ListingDefaultMissingAMI extends ListingDefaultSeed { + async seed() { + const listing = await super.seed() + + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + + const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.alameda, + }) + + const amiChart = await this.amiChartRepository.findOneOrFail({ + name: "Missing Household Ami Levels", + jurisdiction: alamedaJurisdiction, + }) + + const property = await this.propertyRepository.save({ + ...getDefaultProperty(), + }) + + const missingAmiLevelsUnits: Array = [ + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "177300.0", + annualIncomeMin: "84696.0", + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "7058.0", + monthlyRent: "3340.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 2, + number: null, + priorityType: null, + sqFeet: "1100", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "103350.0", + annualIncomeMin: "58152.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "4858.0", + monthlyRent: "2624.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "103350.0", + annualIncomeMin: "58152.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "4858.0", + monthlyRent: "2624.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "103350.0", + annualIncomeMin: "58152.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "4858.0", + monthlyRent: "2624.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "50", + annualIncomeMax: "103350.0", + annualIncomeMin: "38952.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "3246.0", + monthlyRent: "1575.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + ] + + const unitsToBeCreated: Array> = missingAmiLevelsUnits.map((unit) => { + return { + ...unit, + property: { + id: property.id, + }, + amiChart, + } + }) + + unitsToBeCreated.forEach((unit) => { + unit.unitType = unitTypeOneBdrm + }) + + await this.unitsRepository.save(unitsToBeCreated) + + return await this.listingRepository.save({ + ...listing, + property: property, + name: "Test: Default, Missing Household Levels in AMI", + }) + } +} diff --git a/backend/core/src/seeds/listings/listing-default-multiple-ami-and-percentages.ts b/backend/core/src/seeder/seeds/listings/listing-default-multiple-ami-and-percentages.ts similarity index 87% rename from backend/core/src/seeds/listings/listing-default-multiple-ami-and-percentages.ts rename to backend/core/src/seeder/seeds/listings/listing-default-multiple-ami-and-percentages.ts index a77d2bfbf3..b3aa396419 100644 --- a/backend/core/src/seeds/listings/listing-default-multiple-ami-and-percentages.ts +++ b/backend/core/src/seeder/seeds/listings/listing-default-multiple-ami-and-percentages.ts @@ -1,11 +1,10 @@ import { ListingDefaultSeed } from "./listing-default-seed" -import { getDefaultAmiChart, getDefaultProperty } from "./shared" -import { tritonAmiChart } from "./listing-triton-seed" +import { getDefaultProperty } from "./shared" import { BaseEntity } from "typeorm" import { UnitSeedType } from "./listings" -import { UnitStatus } from "../../units/types/unit-status-enum" -import { UnitCreateDto } from "../../units/dto/unit-create.dto" -import { CountyCode } from "../../shared/types/county-code" +import { CountyCode } from "../../../shared/types/county-code" +import { UnitStatus } from "../../../units/types/unit-status-enum" +import { UnitCreateDto } from "../../../units/dto/unit-create.dto" export class ListingDefaultMultipleAMIAndPercentages extends ListingDefaultSeed { async seed() { @@ -16,12 +15,12 @@ export class ListingDefaultMultipleAMIAndPercentages extends ListingDefaultSeed const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ name: CountyCode.alameda, }) - const amiChartOne = await this.amiChartRepository.save({ - ...tritonAmiChart, + const amiChartOne = await this.amiChartRepository.findOneOrFail({ + name: "San Jose TCAC 2019", jurisdiction: alamedaJurisdiction, }) - const amiChartTwo = await this.amiChartRepository.save({ - ...getDefaultAmiChart(), + const amiChartTwo = await this.amiChartRepository.findOneOrFail({ + name: "AlamedaCountyTCAC2021", jurisdiction: alamedaJurisdiction, }) diff --git a/backend/core/src/seeder/seeds/listings/listing-default-multiple-ami.ts b/backend/core/src/seeder/seeds/listings/listing-default-multiple-ami.ts new file mode 100644 index 0000000000..c3443dbfdd --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-multiple-ami.ts @@ -0,0 +1,52 @@ +import { ListingDefaultSeed } from "./listing-default-seed" +import { getDefaultUnits, getDefaultProperty } from "./shared" +import { BaseEntity } from "typeorm" +import { CountyCode } from "../../../shared/types/county-code" +import { UnitCreateDto } from "../../../units/dto/unit-create.dto" + +export class ListingDefaultMultipleAMI extends ListingDefaultSeed { + async seed() { + const listing = await super.seed() + + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + + const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.alameda, + }) + const amiChartOne = await this.amiChartRepository.findOneOrFail({ + name: "San Jose TCAC 2019", + jurisdiction: alamedaJurisdiction, + }) + const amiChartTwo = await this.amiChartRepository.findOneOrFail({ + name: "AlamedaCountyTCAC2021", + jurisdiction: alamedaJurisdiction, + }) + + const property = await this.propertyRepository.save({ + ...getDefaultProperty(), + }) + + const unitsToBeCreated: Array> = getDefaultUnits().map( + (unit, index) => { + return { + ...unit, + property: { + id: property.id, + }, + amiChart: index % 2 === 0 ? amiChartOne : amiChartTwo, + } + } + ) + + unitsToBeCreated[0].unitType = unitTypeOneBdrm + unitsToBeCreated[1].unitType = unitTypeOneBdrm + + await this.unitsRepository.save(unitsToBeCreated) + + return await this.listingRepository.save({ + ...listing, + property: property, + name: "Test: Default, Multiple AMI", + }) + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-default-neighbor-amenities.ts b/backend/core/src/seeder/seeds/listings/listing-default-neighbor-amenities.ts new file mode 100644 index 0000000000..8c535e371d --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-neighbor-amenities.ts @@ -0,0 +1,18 @@ +import { ListingDefaultSeed } from "./listing-default-seed" + +export class ListingDefaultNeighborhoodAmenitiesSeed extends ListingDefaultSeed { + async seed() { + const listing = await super.seed() + return await this.listingRepository.save({ + ...listing, + name: "Test: Neighborhood Amenities", + neighborhoodAmenities: { + groceryStores: "Food and Stuff", + pharmacies: "Oedipus Rx pharmacy", + parksAndCommunityCenters: + "Acadia National Park, Arches National Park, Badlands National Park, Big Bend National Park, Biscayne National Park, Black Canyon of the Gunnison National Park, Bryce Canyon National Park, Canyonlands National Park, Capitol Reef National Park, Carlsbad Caverns National Park, Channel Islands National Park, Congaree National Park, Crater Lake National Park, Cuyahoga Valley National Park, Death Valley National Park, Denali National Park and Preserve, Dry Tortugas National Park, Everglades National Park, Gates of the Arctic National Park, Gateway Arch National Park, Glacier National Park, Glacier Bay National Park, Grand Canyon National Park, Grand Teton National Park, Great Basin National Park, Great Sand Dunes National Park and Preserve, Great Smoky Mountains National Park, Guadalupe Mountains National Park, Haleakala National Park, Hawaii Volcanoes National Park, Hot Springs National Park, Indiana Dunes National Park, Isle Royale National Park, Joshua Tree National Park, Katmai National Park and Preserve, Kenai Fjords National Park, Kings Canyon National Park, Kobuk Valley National Park, Lake Clark National Park, Lassen Volcanic National Park, Mammoth Cave National Park, Mesa Verde National Park, Mount Rainier National Park, National Park of American Samoa, New River Gorge National Park, North Cascades National Park, Olympic National Park, Petrified Forest National Park, Pinnacles National Park, Redwood National Park, Rocky Mountain National Park, Saguaro National Park, Sequoia National Park, Shenandoah National Park, Theodore Roosevelt National Park, Virgin Islands National Park, Voyageurs National Park, White Sands National Park, Wind Cave National Park, Wrangell-St. Elias National Park and Preserve, Yellowstone National Park, Yosemite National Park, Zion National Park", + healthCareResources: "-", + }, + }) + } +} diff --git a/backend/core/src/seeds/listings/listing-default-no-preference-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-no-preference-seed.ts similarity index 93% rename from backend/core/src/seeds/listings/listing-default-no-preference-seed.ts rename to backend/core/src/seeder/seeds/listings/listing-default-no-preference-seed.ts index ef2b42e12f..cdd09ea43b 100644 --- a/backend/core/src/seeds/listings/listing-default-no-preference-seed.ts +++ b/backend/core/src/seeder/seeds/listings/listing-default-no-preference-seed.ts @@ -7,7 +7,7 @@ export class ListingDefaultNoPreferenceSeed extends ListingDefaultSeed { return await this.listingRepository.save({ ...listing, name: "Test: Default, No Preferences", - preferences: [], + listingPreferences: [], applicationDueDate: getDate(5), applicationOpenDate: getDate(-5), }) diff --git a/backend/core/src/seeder/seeds/listings/listing-default-one-preference-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-one-preference-seed.ts new file mode 100644 index 0000000000..720fd9c926 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-one-preference-seed.ts @@ -0,0 +1,21 @@ +import { getLiveWorkPreference } from "./shared" +import { ListingDefaultSeed } from "./listing-default-seed" + +export class ListingDefaultOnePreferenceSeed extends ListingDefaultSeed { + async seed() { + const listing = await super.seed() + return await this.listingRepository.save({ + ...listing, + name: "Test: Default, One Preference", + listingPreferences: [ + { + preference: await this.preferencesRepository.findOneOrFail({ + title: getLiveWorkPreference(listing.jurisdiction.name).title, + }), + ordinal: 1, + page: 1, + }, + ], + }) + } +} diff --git a/backend/core/src/seeds/listings/listing-default-open-soon.ts b/backend/core/src/seeder/seeds/listings/listing-default-open-soon.ts similarity index 100% rename from backend/core/src/seeds/listings/listing-default-open-soon.ts rename to backend/core/src/seeder/seeds/listings/listing-default-open-soon.ts diff --git a/backend/core/src/seeds/listings/listing-default-reserved-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-reserved-seed.ts similarity index 100% rename from backend/core/src/seeds/listings/listing-default-reserved-seed.ts rename to backend/core/src/seeder/seeds/listings/listing-default-reserved-seed.ts diff --git a/backend/core/src/seeder/seeds/listings/listing-default-sanjose-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-sanjose-seed.ts new file mode 100644 index 0000000000..213bd33700 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-sanjose-seed.ts @@ -0,0 +1,113 @@ +import { InjectRepository } from "@nestjs/typeorm" +import { BaseEntity, DeepPartial, Repository } from "typeorm" + +import { + getDefaultAssets, + getDefaultListing, + getDefaultListingEvents, + getDefaultProperty, + getDefaultUnits, + getDisplaceePreference, + getLiveWorkPreference, + PriorityTypes, +} from "./shared" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitAccessibilityPriorityType } from "../../../unit-accessbility-priority-types/entities/unit-accessibility-priority-type.entity" +import { UnitType } from "../../../unit-types/entities/unit-type.entity" +import { ReservedCommunityType } from "../../../reserved-community-type/entities/reserved-community-type.entity" +import { AmiChart } from "../../../ami-charts/entities/ami-chart.entity" +import { Property } from "../../../property/entities/property.entity" +import { Unit } from "../../../units/entities/unit.entity" +import { User } from "../../../auth/entities/user.entity" +import { ApplicationMethod } from "../../../application-methods/entities/application-method.entity" +import { Jurisdiction } from "../../../jurisdictions/entities/jurisdiction.entity" +import { CountyCode } from "../../../shared/types/county-code" +import { UnitCreateDto } from "../../../units/dto/unit-create.dto" + +export class ListingDefaultSanJoseSeed { + constructor( + @InjectRepository(Listing) protected readonly listingRepository: Repository, + @InjectRepository(UnitAccessibilityPriorityType) + protected readonly unitAccessibilityPriorityTypeRepository: Repository< + UnitAccessibilityPriorityType + >, + @InjectRepository(UnitType) protected readonly unitTypeRepository: Repository, + @InjectRepository(ReservedCommunityType) + protected readonly reservedTypeRepository: Repository, + @InjectRepository(AmiChart) protected readonly amiChartRepository: Repository, + @InjectRepository(Property) protected readonly propertyRepository: Repository, + @InjectRepository(Unit) protected readonly unitsRepository: Repository, + @InjectRepository(User) protected readonly userRepository: Repository, + @InjectRepository(ApplicationMethod) + protected readonly applicationMethodRepository: Repository, + @InjectRepository(Jurisdiction) + protected readonly jurisdictionRepository: Repository + ) {} + + async seed() { + const priorityTypeMobilityAndHearing = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( + { name: PriorityTypes.mobilityHearing } + ) + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.alameda, + }) + const amiChart = await this.amiChartRepository.findOneOrFail({ + name: "AlamedaCountyTCAC2021", + jurisdiction: alamedaJurisdiction, + }) + + const property = await this.propertyRepository.save({ + ...getDefaultProperty(), + }) + + const unitsToBeCreated: Array> = getDefaultUnits().map( + (unit) => { + return { + ...unit, + property: { + id: property.id, + }, + amiChart, + } + } + ) + + unitsToBeCreated[0].priorityType = priorityTypeMobilityAndHearing + unitsToBeCreated[1].priorityType = priorityTypeMobilityAndHearing + unitsToBeCreated[0].unitType = unitTypeOneBdrm + unitsToBeCreated[1].unitType = unitTypeTwoBdrm + const newUnits = await this.unitsRepository.save(unitsToBeCreated) + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...getDefaultListing(), + amiChartOverrides: [ + { + unit: { id: newUnits[0].id }, + items: [ + { + percentOfAmi: 80, + householdSize: 1, + income: 777777, + }, + ], + }, + ], + name: "Test: Default, Two Preferences (San Jose)", + property: property, + assets: getDefaultAssets(), + preferences: [ + getLiveWorkPreference(alamedaJurisdiction.name), + { ...getDisplaceePreference(alamedaJurisdiction.name), ordinal: 2 }, + ], + events: getDefaultListingEvents(), + jurisdictionName: "San Jose", + } + + return await this.listingRepository.save(listingCreateDto) + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-default-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-seed.ts new file mode 100644 index 0000000000..e1503667f1 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-seed.ts @@ -0,0 +1,185 @@ +import { InjectRepository } from "@nestjs/typeorm" +import { BaseEntity, DeepPartial, Repository } from "typeorm" + +import { + getDefaultAssets, + getDefaultListing, + getDefaultListingEvents, + getDefaultProperty, + getDefaultUnits, + getDisabilityOrMentalIllnessProgram, + getDisplaceePreference, + getHousingSituationProgram, + getLiveWorkPreference, + getServedInMilitaryProgram, + getTayProgram, + PriorityTypes, + getFlatRentAndRentBasedOnIncomeProgram, +} from "./shared" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitAccessibilityPriorityType } from "../../../unit-accessbility-priority-types/entities/unit-accessibility-priority-type.entity" +import { UnitType } from "../../../unit-types/entities/unit-type.entity" +import { ReservedCommunityType } from "../../../reserved-community-type/entities/reserved-community-type.entity" +import { AmiChart } from "../../../ami-charts/entities/ami-chart.entity" +import { Property } from "../../../property/entities/property.entity" +import { Unit } from "../../../units/entities/unit.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { User } from "../../../auth/entities/user.entity" +import { ApplicationMethod } from "../../../application-methods/entities/application-method.entity" +import { Jurisdiction } from "../../../jurisdictions/entities/jurisdiction.entity" +import { Preference } from "../../../preferences/entities/preference.entity" +import { Program } from "../../../program/entities/program.entity" +import { CountyCode } from "../../../shared/types/county-code" +import { UnitCreateDto } from "../../../units/dto/unit-create.dto" +import { Asset } from "../../../assets/entities/asset.entity" +import { UnitGroupAmiLevel } from "../../../units-summary/entities/unit-group-ami-level.entity" + +export class ListingDefaultSeed { + constructor( + @InjectRepository(Listing) protected readonly listingRepository: Repository, + @InjectRepository(UnitAccessibilityPriorityType) + protected readonly unitAccessibilityPriorityTypeRepository: Repository< + UnitAccessibilityPriorityType + >, + @InjectRepository(UnitType) protected readonly unitTypeRepository: Repository, + @InjectRepository(ReservedCommunityType) + protected readonly reservedTypeRepository: Repository, + @InjectRepository(AmiChart) protected readonly amiChartRepository: Repository, + @InjectRepository(Property) protected readonly propertyRepository: Repository, + @InjectRepository(Unit) protected readonly unitsRepository: Repository, + @InjectRepository(UnitGroup) + protected readonly unitGroupRepository: Repository, + @InjectRepository(UnitGroup) + protected readonly unitGroupAmiLevelRepository: Repository, + @InjectRepository(User) protected readonly userRepository: Repository, + @InjectRepository(ApplicationMethod) + protected readonly applicationMethodRepository: Repository, + @InjectRepository(Jurisdiction) + protected readonly jurisdictionRepository: Repository, + @InjectRepository(Preference) + protected readonly preferencesRepository: Repository, + @InjectRepository(Program) + protected readonly programsRepository: Repository, + @InjectRepository(Asset) protected readonly assetsRepository: Repository + ) {} + + async seed() { + const priorityTypeMobilityAndHearing = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( + { name: PriorityTypes.mobilityHearing } + ) + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.alameda, + }) + const amiChart = await this.amiChartRepository.findOneOrFail({ + name: "AlamedaCountyTCAC2021", + jurisdiction: alamedaJurisdiction, + }) + + const property = await this.propertyRepository.save({ + ...getDefaultProperty(), + }) + + const unitsToBeCreated: Array> = getDefaultUnits().map( + (unit) => { + return { + ...unit, + property: { + id: property.id, + }, + amiChart, + } + } + ) + + unitsToBeCreated[0].priorityType = priorityTypeMobilityAndHearing + unitsToBeCreated[1].priorityType = priorityTypeMobilityAndHearing + unitsToBeCreated[0].unitType = unitTypeOneBdrm + unitsToBeCreated[1].unitType = unitTypeTwoBdrm + const newUnits = await this.unitsRepository.save(unitsToBeCreated) + + const defaultImage = await this.assetsRepository.save(getDefaultAssets()[0]) + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...getDefaultListing(), + amiChartOverrides: [ + { + unit: { id: newUnits[0].id }, + items: [ + { + percentOfAmi: 80, + householdSize: 1, + income: 777777, + }, + ], + }, + ], + name: "Test: Default, Two Preferences", + property: property, + assets: getDefaultAssets(), + listingPreferences: [ + { + preference: await this.preferencesRepository.findOneOrFail({ + title: getLiveWorkPreference(alamedaJurisdiction.name).title, + }), + ordinal: 1, + page: 1, + }, + { + preference: await this.preferencesRepository.findOneOrFail({ + title: getDisplaceePreference(alamedaJurisdiction.name).title, + }), + ordinal: 2, + page: 1, + }, + ], + events: getDefaultListingEvents(), + listingPrograms: [ + { + program: await this.programsRepository.findOneOrFail({ + title: getServedInMilitaryProgram().title, + }), + ordinal: 1, + }, + { + program: await this.programsRepository.findOneOrFail({ + title: getTayProgram().title, + }), + ordinal: 2, + }, + { + program: await this.programsRepository.findOneOrFail({ + title: getDisabilityOrMentalIllnessProgram().title, + }), + ordinal: 3, + }, + { + program: await this.programsRepository.findOneOrFail({ + title: getHousingSituationProgram().title, + }), + ordinal: 4, + }, + { + program: await this.programsRepository.findOneOrFail({ + title: getFlatRentAndRentBasedOnIncomeProgram().title, + }), + ordinal: 5, + }, + ], + images: [ + { + image: defaultImage, + ordinal: 1, + }, + ], + jurisdictionName: "Alameda", + jurisdiction: alamedaJurisdiction, + } + + return await this.listingRepository.save(listingCreateDto) + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-default-summary-with-10-listing-with-30-ami-percentage-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-summary-with-10-listing-with-30-ami-percentage-seed.ts new file mode 100644 index 0000000000..293c4ff09c --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-summary-with-10-listing-with-30-ami-percentage-seed.ts @@ -0,0 +1,38 @@ +import { ListingDefaultSeed } from "./listing-default-seed" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { DeepPartial } from "typeorm" +import { MonthlyRentDeterminationType } from "../../../units-summary/types/monthly-rent-determination.enum" + +export class ListingDefaultSummaryWith10ListingWith30AmiPercentageSeed extends ListingDefaultSeed { + async seed() { + const listing = await super.seed() + + const newListing = await this.listingRepository.save({ + ...listing, + name: "Test: Default, Summary With 10 Listing With 30 Ami Percentage", + amiPercentageMax: 30, + }) + + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const unitGroupToBeCreated: Array> = [] + + const twoBdrm30AmiUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 8, + listing: listing, + amiLevels: [ + { + amiPercentage: 10, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 1000, + }, + ], + } + unitGroupToBeCreated.push(twoBdrm30AmiUnitGroup) + + await this.unitGroupRepository.save(unitGroupToBeCreated) + + return newListing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-default-summary-with-30-and-60-ami-percentage-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-summary-with-30-and-60-ami-percentage-seed.ts new file mode 100644 index 0000000000..14354bce10 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-summary-with-30-and-60-ami-percentage-seed.ts @@ -0,0 +1,50 @@ +import { ListingDefaultSeed } from "./listing-default-seed" +import { DeepPartial } from "typeorm" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { MonthlyRentDeterminationType } from "../../../units-summary/types/monthly-rent-determination.enum" + +export class ListingDefaultSummaryWith30And60AmiPercentageSeed extends ListingDefaultSeed { + async seed() { + const listing = await super.seed() + + const newListing = await this.listingRepository.save({ + ...listing, + name: "Test: Default, Summary With 30 and 60 Ami Percentage", + }) + + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const unitGroupToBeCreated: Array> = [] + + const twoBdrm30AmiUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 8, + amiLevels: [ + { + amiPercentage: 30, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 1000, + }, + ], + } + unitGroupToBeCreated.push(twoBdrm30AmiUnitGroup) + + const twoBdrm60AmiUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 8, + listing: listing, + amiLevels: [ + { + amiPercentage: 60, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 1000, + }, + ], + } + unitGroupToBeCreated.push(twoBdrm60AmiUnitGroup) + + await this.unitGroupRepository.save(unitGroupToBeCreated) + + return newListing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-default-summary-with-30-listing-with-10-ami-percentage-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-summary-with-30-listing-with-10-ami-percentage-seed.ts new file mode 100644 index 0000000000..487314dfb8 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-summary-with-30-listing-with-10-ami-percentage-seed.ts @@ -0,0 +1,38 @@ +import { ListingDefaultSeed } from "./listing-default-seed" +import { DeepPartial } from "typeorm" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { MonthlyRentDeterminationType } from "../../../units-summary/types/monthly-rent-determination.enum" + +export class ListingDefaultSummaryWith30ListingWith10AmiPercentageSeed extends ListingDefaultSeed { + async seed() { + const listing = await super.seed() + + const newListing = await this.listingRepository.save({ + ...listing, + name: "Test: Default, Summary With 30 Listing With 10 Ami Percentage", + amiPercentageMax: 10, + }) + + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const unitGroupToBeCreated: Array> = [] + + const twoBdrm30AmiUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 8, + listing: listing, + amiLevels: [ + { + amiPercentage: 30, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 1000, + }, + ], + } + unitGroupToBeCreated.push(twoBdrm30AmiUnitGroup) + + await this.unitGroupRepository.save(unitGroupToBeCreated) + + return newListing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-default-summary-without-and-listing-with-20-ami-percentage-seed.ts b/backend/core/src/seeder/seeds/listings/listing-default-summary-without-and-listing-with-20-ami-percentage-seed.ts new file mode 100644 index 0000000000..76af32bf43 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-default-summary-without-and-listing-with-20-ami-percentage-seed.ts @@ -0,0 +1,37 @@ +import { ListingDefaultSeed } from "./listing-default-seed" +import { DeepPartial } from "typeorm" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" + +export class ListingDefaultSummaryWithoutAndListingWith20AmiPercentageSeed extends ListingDefaultSeed { + async seed() { + const listing = await super.seed() + + const newListing = await this.listingRepository.save({ + ...listing, + name: "Test: Default, Summary Without And Listing With 20 Ami Percentage", + amiPercentageMax: 20, + }) + + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const unitGroupToBeCreated: Array> = [] + + const twoBdrm30AmiUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 8, + listing: listing, + } + unitGroupToBeCreated.push(twoBdrm30AmiUnitGroup) + + const twoBdrm60AmiUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 8, + listing: listing, + } + unitGroupToBeCreated.push(twoBdrm60AmiUnitGroup) + + await this.unitGroupRepository.save(unitGroupToBeCreated) + + return newListing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10136.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10136.ts new file mode 100644 index 0000000000..0300fa45c3 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10136.ts @@ -0,0 +1,357 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { MonthlyRentDeterminationType } from "../../../units-summary/types/monthly-rent-determination.enum" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const propertySeed: PropertySeedType = { + buildingAddress: { + city: "Detroit", + state: "MI", + street: "1854 Lafayette", + zipCode: "48207", + latitude: 42.339165, + longitude: -83.030315, + }, + buildingTotalUnits: 312, + neighborhood: "Elmwood Park", +} + +const listingSeed: ListingSeedType = { + amiPercentageMax: 60, + amiPercentageMin: 30, + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10136", + leasingAgentName: "James Harrigan", + leasingAgentPhone: "810-750-7000", + managementCompany: "Independent Management Service", + managementWebsite: "https://www.imsproperties.net/michigan", + name: "Martin Luther King II", + status: ListingStatus.pending, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: true, + homeType: HomeTypeEnum.apartment, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: false, + wheelchairRamp: false, + serviceAnimalsAllowed: false, + accessibleParking: false, + parkingOnSite: false, + inUnitWasherDryer: false, + laundryInBuilding: false, + barrierFreeEntrance: false, + rollInShower: false, + grabBars: false, + heatingInUnit: false, + acInUnit: false, + }, + utilities: { + water: null, + gas: null, + trash: null, + sewer: true, + electricity: false, + cable: null, + phone: null, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10136Seed extends ListingDefaultSeed { + async seed() { + const unitTypeStudio = await this.unitTypeRepository.findOneOrFail({ name: "studio" }) + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + const unitTypeThreeBdrm = await this.unitTypeRepository.findOneOrFail({ name: "threeBdrm" }) + const unitTypeFourBdrm = await this.unitTypeRepository.findOneOrFail({ name: "fourBdrm" }) + + const property = await this.propertyRepository.save({ + ...propertySeed, + }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...listingSeed, + applicationMethods: [], + assets: assets, + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const detroitJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.detroit, + }) + + const unitGroups: Omit[] = [ + { + amiLevels: [], + unitType: [unitTypeStudio, unitTypeOneBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: 1, + maxOccupancy: 2, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 500, + sqFeetMax: 550, + openWaitlist: true, + listing, + totalAvailable: 2, + }, + { + amiLevels: [], + unitType: [unitTypeOneBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: 1, + maxOccupancy: 3, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 600, + sqFeetMax: 600, + openWaitlist: true, + listing, + }, + { + amiLevels: [], + unitType: [unitTypeThreeBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: 1, + maxOccupancy: 3, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 600, + sqFeetMax: 600, + openWaitlist: false, + listing, + }, + { + amiLevels: [], + unitType: [unitTypeFourBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: 1, + maxOccupancy: 3, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 600, + sqFeetMax: 600, + openWaitlist: true, + listing, + }, + { + amiLevels: [], + unitType: [unitTypeTwoBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: 2, + maxOccupancy: 6, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 600, + sqFeetMax: 600, + openWaitlist: true, + listing, + }, + { + amiLevels: [], + unitType: [unitTypeTwoBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: 2, + maxOccupancy: null, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 600, + sqFeetMax: 600, + openWaitlist: true, + listing, + }, + { + amiLevels: [], + unitType: [unitTypeTwoBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: null, + maxOccupancy: 2, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 600, + sqFeetMax: 600, + openWaitlist: true, + listing, + }, + { + amiLevels: [], + unitType: [unitTypeTwoBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: 1, + maxOccupancy: 1, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 600, + sqFeetMax: 600, + openWaitlist: true, + listing, + }, + { + amiLevels: [], + unitType: [unitTypeThreeBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: 3, + maxOccupancy: 3, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 600, + sqFeetMax: 600, + openWaitlist: true, + listing, + }, + { + amiLevels: [], + unitType: [unitTypeFourBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: null, + maxOccupancy: null, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 600, + sqFeetMax: 600, + openWaitlist: true, + listing, + }, + { + amiLevels: [], + unitType: [unitTypeTwoBdrm, unitTypeOneBdrm], + floorMin: 1, + floorMax: 5, + minOccupancy: 1, + maxOccupancy: 7, + bathroomMin: 1, + bathroomMax: 1, + sqFeetMin: 600, + sqFeetMax: 600, + openWaitlist: true, + listing, + }, + ] + + const savedUnitGroups = await this.unitGroupRepository.save(unitGroups) + + const MSHDA = await this.amiChartRepository.findOneOrFail({ + name: "MSHDA 2021", + jurisdiction: detroitJurisdiction, + }) + const HUD = await this.amiChartRepository.findOneOrFail({ + name: "HUD 2021", + jurisdiction: detroitJurisdiction, + }) + + await this.unitGroupRepository.save({ + ...savedUnitGroups[0], + amiLevels: [ + { + amiChart: MSHDA, + amiChartId: MSHDA.id, + amiPercentage: 30, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 2500, + unitGroup: savedUnitGroups[0], + }, + { + amiChart: HUD, + amiChartId: HUD.id, + amiPercentage: 40, + monthlyRentDeterminationType: MonthlyRentDeterminationType.percentageOfIncome, + percentageOfIncomeValue: 30, + unitGroup: savedUnitGroups[0], + }, + ], + }) + + await this.unitGroupRepository.save({ + ...savedUnitGroups[1], + amiLevels: [ + { + amiChart: MSHDA, + amiChartId: MSHDA.id, + amiPercentage: 30, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 2500, + unitGroup: savedUnitGroups[1], + }, + { + amiChart: MSHDA, + amiChartId: MSHDA.id, + amiPercentage: 40, + monthlyRentDeterminationType: MonthlyRentDeterminationType.percentageOfIncome, + percentageOfIncomeValue: 30, + unitGroup: savedUnitGroups[1], + }, + ], + }) + + await this.unitGroupRepository.save({ + ...savedUnitGroups[2], + amiLevels: [ + { + amiChart: MSHDA, + amiChartId: MSHDA.id, + amiPercentage: 55, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 1200, + unitGroup: savedUnitGroups[2], + }, + ], + }) + + await this.unitGroupRepository.save({ + ...savedUnitGroups[3], + amiLevels: [ + { + amiChart: MSHDA, + amiChartId: MSHDA.id, + amiPercentage: 55, + monthlyRentDeterminationType: MonthlyRentDeterminationType.percentageOfIncome, + percentageOfIncomeValue: 25, + unitGroup: savedUnitGroups[3], + }, + ], + }) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10145.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10145.ts new file mode 100644 index 0000000000..420fbdd17f --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10145.ts @@ -0,0 +1,139 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const mcvProperty: PropertySeedType = { + buildingAddress: { + city: "Detroit", + state: "MI", + street: "4701 Chrysler Drive", + zipCode: "48201", + latitude: 42.35923, + longitude: -83.054134, + }, + buildingTotalUnits: 194, + neighborhood: "Forest Park", +} + +const mcvListing: ListingSeedType = { + amiPercentageMax: 60, + amiPercentageMin: null, + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10145", + leasingAgentName: "Janelle Henderson", + leasingAgentPhone: "313-831-1725", + managementCompany: "Associated Management Co", + managementWebsite: "https://associated-management.rentlinx.com/listings", + name: "Medical Center Village", + status: ListingStatus.active, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: true, + homeType: HomeTypeEnum.apartment, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: true, + wheelchairRamp: true, + serviceAnimalsAllowed: false, + accessibleParking: false, + parkingOnSite: false, + inUnitWasherDryer: false, + laundryInBuilding: true, + barrierFreeEntrance: true, + rollInShower: false, + grabBars: false, + heatingInUnit: false, + acInUnit: true, + }, + utilities: { + water: null, + gas: true, + trash: null, + sewer: null, + electricity: null, + cable: null, + phone: true, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10145Seed extends ListingDefaultSeed { + async seed() { + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + const unitTypeThreeBdrm = await this.unitTypeRepository.findOneOrFail({ name: "threeBdrm" }) + + const property = await this.propertyRepository.save({ + ...mcvProperty, + }) + + const reservedType = await this.reservedTypeRepository.findOneOrFail({ name: "senior62" }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...mcvListing, + applicationMethods: [], + assets: assets, + events: [], + property: property, + reservedCommunityType: reservedType, + // If a reservedCommunityType is specified, a reservedCommunityDescription MUST also be specified + reservedCommunityDescription: "", + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const mcvUnitGroupToBeCreated: Array> = [] + + const oneBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeOneBdrm], + totalCount: 28, + listing: listing, + } + mcvUnitGroupToBeCreated.push(oneBdrmUnitGroup) + + const twoBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 142, + listing: listing, + } + mcvUnitGroupToBeCreated.push(twoBdrmUnitGroup) + + const threeBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeThreeBdrm], + totalCount: 24, + listing: listing, + } + mcvUnitGroupToBeCreated.push(threeBdrmUnitGroup) + + await this.unitGroupRepository.save(mcvUnitGroupToBeCreated) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10147.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10147.ts new file mode 100644 index 0000000000..64c97c35ce --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10147.ts @@ -0,0 +1,122 @@ +import { ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const mshProperty: PropertySeedType = { + buildingAddress: { + city: "Detroit", + state: "MI", + street: "7335 Melrose St", + zipCode: "48211", + latitude: 42.37442, + longitude: -83.06363, + }, + buildingTotalUnits: 24, + neighborhood: "North End", +} + +const mshListing: ListingSeedType = { + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10147", + leasingAgentName: "Kim Hagood", + leasingAgentPhone: "248-228-1340", + managementCompany: "Elite Property Management LLC", + managementWebsite: "https://www.elitep-m.com", + name: "Melrose Square Homes", + status: ListingStatus.active, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: true, + homeType: HomeTypeEnum.apartment, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: false, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: true, + laundryInBuilding: false, + barrierFreeEntrance: true, + rollInShower: false, + grabBars: false, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: null, + gas: null, + trash: true, + sewer: null, + electricity: null, + cable: true, + phone: null, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10147Seed extends ListingDefaultSeed { + async seed() { + const unitTypeThreeBdrm = await this.unitTypeRepository.findOneOrFail({ name: "threeBdrm" }) + const unitTypeFourBdrm = await this.unitTypeRepository.findOneOrFail({ name: "fourBdrm" }) + + const property = await this.propertyRepository.save({ + ...mshProperty, + }) + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...mshListing, + applicationMethods: [], + assets: [], + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const mshUnitGroupToBeCreated: Array> = [] + + const fourBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeFourBdrm], + totalCount: 15, + listing: listing, + } + mshUnitGroupToBeCreated.push(fourBdrmUnitGroup) + + const threeBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeThreeBdrm], + totalCount: 9, + listing: listing, + } + mshUnitGroupToBeCreated.push(threeBdrmUnitGroup) + + await this.unitGroupRepository.save(mshUnitGroupToBeCreated) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10151.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10151.ts new file mode 100644 index 0000000000..fcbb922698 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10151.ts @@ -0,0 +1,132 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const propertySeed: PropertySeedType = { + buildingAddress: { + city: "Detroit", + state: "MI", + street: "2515 W Forest Ave", + zipCode: "48208", + latitude: 42.34547, + longitude: -83.08877, + }, + buildingTotalUnits: 45, + neighborhood: "Core City", +} + +const listingSeed: ListingSeedType = { + amiPercentageMax: 60, + amiPercentageMin: 30, + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10151", + leasingAgentName: "Natasha Gaston", + leasingAgentPhone: "313-926-8509", + managementCompany: "NRP Group", + managementWebsite: "https://www.nrpgroup.com/Home/Communities", + name: "MLK Homes", + status: ListingStatus.active, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: true, + homeType: HomeTypeEnum.apartment, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: false, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: true, + laundryInBuilding: false, + barrierFreeEntrance: true, + rollInShower: false, + grabBars: false, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: null, + gas: false, + trash: null, + sewer: false, + electricity: null, + cable: null, + phone: null, + internet: null, + }, + + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10151Seed extends ListingDefaultSeed { + async seed() { + const unitTypeThreeBdrm = await this.unitTypeRepository.findOneOrFail({ name: "threeBdrm" }) + const unitTypeFourBdrm = await this.unitTypeRepository.findOneOrFail({ name: "fourBdrm" }) + + const property = await this.propertyRepository.save({ + ...propertySeed, + }) + + const reservedType = await this.reservedTypeRepository.findOneOrFail({ name: "specialNeeds" }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...listingSeed, + applicationMethods: [], + assets: assets, + events: [], + property: property, + reservedCommunityType: reservedType, + // If a reservedCommunityType is specified, a reservedCommunityDescription MUST also be specified + reservedCommunityDescription: "", + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const unitGroupToBeCreated: DeepPartial[] = [] + + const threeBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeThreeBdrm], + totalCount: 16, + listing: listing, + } + unitGroupToBeCreated.push(threeBdrmUnitGroup) + + const fourBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeFourBdrm], + totalCount: 29, + listing: listing, + } + unitGroupToBeCreated.push(fourBdrmUnitGroup) + + await this.unitGroupRepository.save(unitGroupToBeCreated) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10153.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10153.ts new file mode 100644 index 0000000000..3130695488 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10153.ts @@ -0,0 +1,114 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const propertySeed: PropertySeedType = { + buildingAddress: { + city: "Detroit", + state: "MI", + street: "12026 Morang", + zipCode: "48224", + latitude: 42.42673, + longitude: -82.95126, + }, + buildingTotalUnits: 40, + neighborhood: "Moross-Morang", +} + +const listingSeed: ListingSeedType = { + amiPercentageMax: 60, + amiPercentageMin: 30, + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10153", + leasingAgentPhone: "313-999-1268", + managementCompany: "Smiley Management", + name: "Morang Apartments", + status: ListingStatus.active, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: true, + homeType: HomeTypeEnum.apartment, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + leasingAgentName: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: true, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: false, + laundryInBuilding: false, + barrierFreeEntrance: true, + rollInShower: true, + grabBars: true, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: null, + gas: null, + trash: null, + sewer: null, + electricity: null, + cable: null, + phone: null, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10153Seed extends ListingDefaultSeed { + async seed() { + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + + const property = await this.propertyRepository.save({ + ...propertySeed, + }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...listingSeed, + applicationMethods: [], + assets: assets, + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const oneBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeOneBdrm], + totalCount: 40, + listing: listing, + } + + await this.unitGroupRepository.save([oneBdrmUnitGroup]) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10154.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10154.ts new file mode 100644 index 0000000000..c837831a8a --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10154.ts @@ -0,0 +1,134 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const propertySeed: PropertySeedType = { + buildingAddress: { + city: "Detroit", + state: "MI", + street: "4000-4100 Blocks Alter Rd & Wayburn St.", + zipCode: "48224", + latitude: 42.39175, + longitude: -82.95057, + }, + buildingTotalUnits: 64, + neighborhood: "Morningside", +} + +const listingSeed: ListingSeedType = { + amiPercentageMax: 50, + amiPercentageMin: 30, + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10154", + leasingAgentName: "Kristy Schornak", + leasingAgentPhone: "313-821-0469", + managementCompany: "Continental Management", + managementWebsite: "https://www.continentalmgt.com", + name: "Morningside Commons Multi", + status: ListingStatus.active, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: true, + homeType: HomeTypeEnum.apartment, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: false, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: true, + laundryInBuilding: false, + barrierFreeEntrance: true, + rollInShower: false, + grabBars: false, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: false, + gas: null, + trash: false, + sewer: null, + electricity: null, + cable: null, + phone: null, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10154Seed extends ListingDefaultSeed { + async seed() { + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + const unitTypeThreeBdrm = await this.unitTypeRepository.findOneOrFail({ name: "threeBdrm" }) + const unitTypeFourBdrm = await this.unitTypeRepository.findOneOrFail({ name: "fourBdrm" }) + + const property = await this.propertyRepository.save({ + ...propertySeed, + }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...listingSeed, + applicationMethods: [], + assets: assets, + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const unitGroupToBeCreated: DeepPartial[] = [] + + const twoBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 8, + listing: listing, + } + unitGroupToBeCreated.push(twoBdrmUnitGroup) + + const threeBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeThreeBdrm], + totalCount: 38, + listing: listing, + } + unitGroupToBeCreated.push(threeBdrmUnitGroup) + + const fourBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeFourBdrm], + totalCount: 18, + listing: listing, + } + unitGroupToBeCreated.push(fourBdrmUnitGroup) + + await this.unitGroupRepository.save(unitGroupToBeCreated) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10155.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10155.ts new file mode 100644 index 0000000000..cd4ef725dc --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10155.ts @@ -0,0 +1,142 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const propertySeed: PropertySeedType = { + buildingAddress: { + city: "Detroit", + state: "MI", + street: "20000 Dequindre St", + zipCode: "48234", + latitude: 42.44133, + longitude: -83.08308, + }, + buildingTotalUnits: 151, + neighborhood: "Nolan", +} + +const listingSeed: ListingSeedType = { + amiPercentageMax: 50, + amiPercentageMin: 30, + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10155", + leasingAgentName: "Ryan Beale", + leasingAgentPhone: "313-366-1616", + managementCompany: "Premier Property Management", + name: "Morton Manor", + status: ListingStatus.active, + images: [ + { + image: { + label: "building", + fileId: + "https://regional-dahlia-staging.s3-us-west-1.amazonaws.com/listings/triton/thetriton.png", + }, + }, + { + image: { + label: "building", + fileId: + "https://res.cloudinary.com/exygy/image/upload/w_1302,c_limit,q_65/dev/oakhouse_cgdqmx.jpg", + }, + }, + { + image: { + label: "building", + fileId: + "https://res.cloudinary.com/exygy/image/upload/w_1302,c_limit,q_65/dev/house_goo3cp.jpg", + }, + }, + { + image: { + label: "building", + fileId: + "https://regional-dahlia-staging.s3-us-west-1.amazonaws.com/listings/triton/thetriton.png", + }, + }, + ], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: false, + homeType: HomeTypeEnum.apartment, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: true, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: false, + laundryInBuilding: false, + barrierFreeEntrance: true, + rollInShower: true, + grabBars: true, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: null, + gas: null, + trash: null, + sewer: null, + electricity: null, + cable: null, + phone: null, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.ComingSoon, + whatToExpect: `This property is still under construction by the property owners. If you sign up for notifications through Detroit Home Connect, we will send you updates when this property has opened up applications for residents. You can also check back later to this page for updates.`, + whatToExpectAdditionalText: null, +} + +export class Listing10155Seed extends ListingDefaultSeed { + async seed() { + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + + const property = await this.propertyRepository.save({ + ...propertySeed, + }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...listingSeed, + applicationMethods: [], + assets: assets, + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const oneBdrmUnitsSummary: DeepPartial = { + unitType: [unitTypeOneBdrm], + totalCount: 150, + listing: listing, + } + await this.unitGroupRepository.save([oneBdrmUnitsSummary]) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10157.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10157.ts new file mode 100644 index 0000000000..32838e2c2f --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10157.ts @@ -0,0 +1,168 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { MonthlyRentDeterminationType } from "../../../units-summary/types/monthly-rent-determination.enum" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const nccProperty: PropertySeedType = { + // See http://rentlinx.kmgprestige.com/640-Delaware-Street-Detroit-MI-48202 + amenities: "Parking, Elevator in Building", + buildingAddress: { + city: "Detroit", + state: "MI", + street: "640 Delaware St", + zipCode: "48202", + latitude: 42.37273, + longitude: -83.07981, + }, + buildingTotalUnits: 71, + neighborhood: "New Center Commons", + petPolicy: "No Pets Allowed", + unitAmenities: "Air Conditioning, Dishwasher, Garbage Disposal, Range, Refrigerator", + unitsAvailable: 5, + yearBuilt: 1929, +} + +const nccListing: ListingSeedType = { + applicationDropOffAddress: null, + applicationFee: "25", + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + costsNotIncluded: + "Water Included, Resident Pays Electricity, Resident Pays Gas, Resident Pays Heat(Heat is gas.)", + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10157", + leasingAgentPhone: "313-873-1022", + managementCompany: "KMG Prestige", + managementWebsite: "https://www.kmgprestige.com/communities/", + name: "New Center Commons", + status: ListingStatus.active, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: false, + homeType: HomeTypeEnum.apartment, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + leasingAgentName: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: true, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: false, + laundryInBuilding: false, + barrierFreeEntrance: true, + rollInShower: true, + grabBars: true, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: null, + gas: null, + trash: null, + sewer: null, + electricity: null, + cable: null, + phone: null, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10157Seed extends ListingDefaultSeed { + async seed() { + const unitTypeStudio = await this.unitTypeRepository.findOneOrFail({ name: "studio" }) + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const property = await this.propertyRepository.save({ + ...nccProperty, + }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...nccListing, + applicationMethods: [], + assets: JSON.parse(JSON.stringify(assets)), + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const nccUnitGroupToBeCreated: DeepPartial[] = [] + + const zeroBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeStudio], + totalCount: 1, + amiLevels: [ + { + amiPercentage: 10, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 650, + }, + ], + listing: listing, + sqFeetMax: 550, + } + nccUnitGroupToBeCreated.push(zeroBdrmUnitGroup) + + const oneBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeOneBdrm], + totalCount: 2, + amiLevels: [ + { + amiPercentage: 10, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 650, + }, + ], + listing: listing, + sqFeetMin: 800, + sqFeetMax: 1000, + } + nccUnitGroupToBeCreated.push(oneBdrmUnitGroup) + + const twoBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 2, + amiLevels: [ + { + amiPercentage: 10, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 750, + }, + ], + listing: listing, + sqFeetMin: 900, + sqFeetMax: 1100, + } + nccUnitGroupToBeCreated.push(twoBdrmUnitGroup) + + await this.unitGroupRepository.save(nccUnitGroupToBeCreated) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10158.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10158.ts new file mode 100644 index 0000000000..1d2b2f3d34 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10158.ts @@ -0,0 +1,136 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const ncpProperty: PropertySeedType = { + amenities: "Parking, Elevator in Building", + buildingAddress: { + city: "Detroit", + state: "MI", + street: "666 W Bethune St", + zipCode: "48202", + latitude: 42.37056, + longitude: -83.07968, + }, + buildingTotalUnits: 76, + neighborhood: "New Center Commons", + unitAmenities: "Air Conditioning (Wall unit), Garbage Disposal, Range, Refrigerator", + yearBuilt: 1971, +} + +const ncpListing: ListingSeedType = { + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + costsNotIncluded: "Electricity Included Gas Included Water Included", + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10158", + isWaitlistOpen: false, + leasingAgentPhone: "313-872-7717", + managementCompany: "KMG Prestige", + managementWebsite: "https://www.kmgprestige.com/communities/", + name: "New Center Pavilion", + status: ListingStatus.active, + images: [ + { + image: { + label: "building", + fileId: + "https://res.cloudinary.com/exygy/image/upload/w_1302,c_limit,q_65/dev/house_goo3cp.jpg", + }, + }, + ], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: false, + homeType: HomeTypeEnum.apartment, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + leasingAgentName: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + features: { + elevator: true, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: false, + laundryInBuilding: false, + barrierFreeEntrance: true, + rollInShower: true, + grabBars: true, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: null, + gas: null, + trash: null, + sewer: null, + electricity: null, + cable: null, + phone: null, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10158Seed extends ListingDefaultSeed { + async seed() { + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const property = await this.propertyRepository.save({ + ...ncpProperty, + }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...ncpListing, + applicationMethods: [], + assets: assets, + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const ncpUnitGroupToBeCreated: DeepPartial[] = [] + + const oneBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeOneBdrm], + totalCount: 40, + listing: listing, + } + ncpUnitGroupToBeCreated.push(oneBdrmUnitGroup) + + const twoBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 36, + listing: listing, + } + ncpUnitGroupToBeCreated.push(twoBdrmUnitGroup) + + await this.unitGroupRepository.save(ncpUnitGroupToBeCreated) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10159.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10159.ts new file mode 100644 index 0000000000..4de64df0f5 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10159.ts @@ -0,0 +1,115 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" + +const propertySeed: PropertySeedType = { + buildingAddress: { + city: "Detroit", + state: "MI", + street: "112 Seward Avenue", + zipCode: "48202", + latitude: 42.373219, + longitude: -83.079147, + }, + buildingTotalUnits: 49, + neighborhood: "New Center Commons", +} + +const listingSeed: ListingSeedType = { + amiPercentageMax: 60, + amiPercentageMin: 30, + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + disableUnitsAccordion: true, + displayWaitlistSize: false, + isWaitlistOpen: true, + waitlistCurrentSize: 20, + waitlistMaxSize: 50, + hrdId: "HRD10159", + leasingAgentName: "Kim Hagood", + leasingAgentPhone: "313-656-4146", + managementCompany: "Elite Property Management LLC", + managementWebsite: "https://www.elitep-m.com", + name: "New Center Square", + status: ListingStatus.active, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: null, + homeType: null, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + features: { + elevator: false, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: true, + laundryInBuilding: false, + barrierFreeEntrance: true, + rollInShower: false, + grabBars: false, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: null, + gas: null, + trash: null, + sewer: null, + electricity: null, + cable: null, + phone: null, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10159Seed extends ListingDefaultSeed { + async seed() { + const unitTypeThreeBdrm = await this.unitTypeRepository.findOneOrFail({ name: "threeBdrm" }) + + const property = await this.propertyRepository.save({ + ...propertySeed, + }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...listingSeed, + applicationMethods: [], + assets: JSON.parse(JSON.stringify(assets)), + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const threeBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeThreeBdrm], + totalCount: 49, + listing: listing, + } + await this.unitGroupRepository.save([threeBdrmUnitGroup]) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10168.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10168.ts new file mode 100644 index 0000000000..ab756d8eee --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10168.ts @@ -0,0 +1,152 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ApplicationMethod } from "../../../application-methods/entities/application-method.entity" +import { ApplicationMethodType } from "../../../application-methods/types/application-method-type-enum" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const propertySeed: PropertySeedType = { + buildingAddress: { + city: "Detroit", + state: "MI", + street: "4401 Burlingame St", + zipCode: "48204", + latitude: 42.37704, + longitude: -83.12847, + }, + buildingTotalUnits: 10, + neighborhood: "Nardin Park", + unitAmenities: + "Professional Management Team, Smoke-free building, Gated community, Entry control system, Community room, Nicely appointed lobby area, On-site laundry with fully accessible washers and dryers, Lovely patio area to relax, 24-hour emergency maintenance, Cable-ready", +} + +const listingSeed: ListingSeedType = { + amiPercentageMax: 30, + amiPercentageMin: 30, + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10168", + leasingAgentName: "Chris Garland", + leasingAgentPhone: "313-934-0010", + leasingAgentEmail: "OakVillageIndependenceHouse@voami.org", + managementCompany: "Detroit Voa Elderly Nonprofit Housing Corporation", + managementWebsite: "https://www.voa.org/housing_properties/oak-village-independence-house", + name: "Oak Village Independence", + status: ListingStatus.active, + images: [ + { + image: { + label: "building", + fileId: + "https://res.cloudinary.com/exygy/image/upload/w_1302,c_limit,q_65/dev/house_goo3cp.jpg", + }, + }, + { + image: { + label: "building", + fileId: + "https://regional-dahlia-staging.s3-us-west-1.amazonaws.com/listings/triton/thetriton.png", + }, + }, + { + image: { + label: "building", + fileId: + "https://res.cloudinary.com/exygy/image/upload/w_1302,c_limit,q_65/dev/oakhouse_cgdqmx.jpg", + }, + }, + ], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: null, + homeType: HomeTypeEnum.duplex, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: true, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: true, + laundryInBuilding: true, + barrierFreeEntrance: true, + rollInShower: true, + grabBars: true, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: null, + gas: null, + trash: null, + sewer: true, + electricity: null, + cable: null, + phone: null, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10168Seed extends ListingDefaultSeed { + async seed() { + const applicationMethod: ApplicationMethod = await this.applicationMethodRepository.save({ + type: ApplicationMethodType.ExternalLink, + acceptsPostmarkedApplications: false, + externalReference: + "https://voa-production.s3.amazonaws.com/uploads/pdf_file/file/1118/Oak_Village_Independence_House_Resident_Selection_Guidelines.pdf", + }) + + const assets: Array = [] + + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + + const property = await this.propertyRepository.save({ + ...propertySeed, + }) + const reservedType = await this.reservedTypeRepository.findOneOrFail({ name: "specialNeeds" }) + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...listingSeed, + applicationMethods: [applicationMethod], + assets: JSON.parse(JSON.stringify(assets)), + events: [], + property: property, + reservedCommunityType: reservedType, + // If a reservedCommunityType is specified, a reservedCommunityDescription MUST also be specified + reservedCommunityDescription: "Persons with Disabilities", + digitalApplication: true, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const oneBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeOneBdrm], + totalCount: 10, + listing: listing, + } + await this.unitGroupRepository.save([oneBdrmUnitGroup]) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10169.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10169.ts new file mode 100644 index 0000000000..9cdb00c868 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10169.ts @@ -0,0 +1,171 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { getDate } from "./shared" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { MonthlyRentDeterminationType } from "../../../units-summary/types/monthly-rent-determination.enum" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +const grandRivProperty: PropertySeedType = { + // See http://rentlinx.kmgprestige.com/640-Delaware-Street-Detroit-MI-48202 + amenities: "Parking, Elevator in Building", + buildingAddress: { + city: "Detroit", + state: "MI", + street: "28 W. Grand River", + zipCode: "48226", + latitude: 42.334007, + longitude: -83.04893, + }, + buildingTotalUnits: 175, + neighborhood: "Downtown", + petPolicy: "No Pets Allowed", + unitAmenities: "Air Conditioning, Dishwasher, Garbage Disposal, Range, Refrigerator", + unitsAvailable: 5, + yearBuilt: 1929, +} + +const grandRivListing: ListingSeedType = { + applicationDropOffAddress: null, + applicationOpenDate: getDate(1000), + applicationDueDate: getDate(1500), + applicationFee: "25", + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + costsNotIncluded: + "Water Included, Resident Pays Electricity, Resident Pays Gas, Resident Pays Heat(Heat is gas.)", + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10157", + leasingAgentPhone: "313-545-8720", + managementCompany: "Rock Management Company", + managementWebsite: "https://www.28granddetroit.com", + name: "Capitol Park Micro Units", + status: ListingStatus.active, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: null, + homeType: HomeTypeEnum.townhome, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + leasingAgentName: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: true, + wheelchairRamp: true, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: false, + laundryInBuilding: false, + barrierFreeEntrance: true, + rollInShower: true, + grabBars: true, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: null, + gas: null, + trash: true, + sewer: null, + electricity: null, + cable: true, + phone: null, + internet: null, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10157Seed extends ListingDefaultSeed { + async seed() { + const unitTypeStudio = await this.unitTypeRepository.findOneOrFail({ name: "studio" }) + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const property = await this.propertyRepository.save({ + ...grandRivProperty, + }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...grandRivListing, + applicationMethods: [], + assets: JSON.parse(JSON.stringify(assets)), + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const nccUnitGroupToBeCreated: DeepPartial[] = [] + + const zeroBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeStudio], + totalCount: 1, + amiLevels: [ + { + amiPercentage: 30, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 470, + }, + ], + listing: listing, + sqFeetMax: 550, + } + nccUnitGroupToBeCreated.push(zeroBdrmUnitGroup) + + const oneBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeOneBdrm], + totalCount: 2, + amiLevels: [ + { + amiPercentage: 30, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 650, + }, + ], + listing: listing, + sqFeetMin: 800, + sqFeetMax: 1000, + } + nccUnitGroupToBeCreated.push(oneBdrmUnitGroup) + + const twoBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 2, + amiLevels: [ + { + amiPercentage: 30, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 750, + }, + ], + listing: listing, + sqFeetMin: 900, + sqFeetMax: 1100, + } + nccUnitGroupToBeCreated.push(twoBdrmUnitGroup) + + await this.unitGroupRepository.save(nccUnitGroupToBeCreated) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-10202.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-10202.ts new file mode 100644 index 0000000000..864d9aa34c --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-10202.ts @@ -0,0 +1,127 @@ +import { AssetDtoSeedType, ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +import { HomeTypeEnum } from "../../../listings/types/home-type-enum" + +// +const mcvProperty: PropertySeedType = { + buildingAddress: { + city: "Detroit", + state: "MI", + street: "7800 E Jefferson Ave", + zipCode: "48214", + latitude: 42.35046, + longitude: -82.99615, + }, + buildingTotalUnits: 469, + neighborhood: "Gold Coast", +} + +const mcvListing: ListingSeedType = { + amiPercentageMax: 60, + amiPercentageMin: null, + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + disableUnitsAccordion: true, + displayWaitlistSize: false, + hrdId: "HRD10202", + leasingAgentName: "Janelle Henderson", + leasingAgentPhone: "313-824-2244", + managementCompany: "Associated Management Co", + managementWebsite: "https://associated-management.rentlinx.com/listings", + name: "River Towers", + status: ListingStatus.pending, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: null, + homeType: HomeTypeEnum.house, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + isWaitlistOpen: undefined, + features: { + elevator: true, + wheelchairRamp: false, + serviceAnimalsAllowed: true, + accessibleParking: true, + parkingOnSite: true, + inUnitWasherDryer: true, + laundryInBuilding: false, + barrierFreeEntrance: true, + rollInShower: false, + grabBars: false, + heatingInUnit: true, + acInUnit: true, + }, + utilities: { + water: null, + gas: null, + trash: true, + sewer: null, + electricity: null, + cable: null, + phone: null, + internet: true, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class Listing10202Seed extends ListingDefaultSeed { + async seed() { + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const property = await this.propertyRepository.save({ + ...mcvProperty, + }) + + const assets: Array = [] + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...mcvListing, + applicationMethods: [], + assets: assets, + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const mcvUnitGroupToBeCreated: DeepPartial[] = [] + + const oneBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeOneBdrm], + totalCount: 376, + listing: listing, + } + mcvUnitGroupToBeCreated.push(oneBdrmUnitGroup) + + const twoBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 96, + listing: listing, + } + mcvUnitGroupToBeCreated.push(twoBdrmUnitGroup) + + await this.unitGroupRepository.save(mcvUnitGroupToBeCreated) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-detroit-treymore.ts b/backend/core/src/seeder/seeds/listings/listing-detroit-treymore.ts new file mode 100644 index 0000000000..d79956de49 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-detroit-treymore.ts @@ -0,0 +1,128 @@ +import { ListingSeedType, PropertySeedType } from "./listings" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { Listing } from "../../../listings/entities/listing.entity" +import { UnitGroup } from "../../../units-summary/entities/unit-group.entity" +import { MonthlyRentDeterminationType } from "../../../units-summary/types/monthly-rent-determination.enum" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" + +const treymoreProperty: PropertySeedType = { + // See http://rentlinx.kmgprestige.com/457-Brainard-Street-Detroit-MI-48201 + amenities: "Parking, Elevator in Building, Community Room", + buildingAddress: { + city: "Detroit", + state: "MI", + street: "457 Brainard St", + zipCode: "48201", + latitude: 42.3461357, + longitude: -83.0645436, + }, + petPolicy: "No Pets Allowed", + unitAmenities: + "Air Conditioning (Central Air Conditioning), Garbage Disposal, Range, Refrigerator, Coin Laundry Room in building", + unitsAvailable: 4, + yearBuilt: 1916, + accessibility: "2 units are barrier free; 2 units are bi-level 1.5 bath", +} + +const treymoreListing: ListingSeedType = { + applicationDropOffAddress: null, + applicationMailingAddress: null, + countyCode: CountyCode.detroit, + costsNotIncluded: "Water Included Resident Pays Electricity Resident Pays Gas Resident Pays Heat", + disableUnitsAccordion: true, + displayWaitlistSize: false, + isWaitlistOpen: false, + leasingAgentPhone: "313-462-4123", + managementCompany: "KMG Prestige", + managementWebsite: "http://rentlinx.kmgprestige.com/Company.aspx?CompanyID=107", + name: "Treymore Apartments", + status: ListingStatus.pending, + images: [], + digitalApplication: undefined, + paperApplication: undefined, + section8Acceptance: null, + referralOpportunity: undefined, + depositMin: undefined, + depositMax: undefined, + leasingAgentEmail: undefined, + leasingAgentName: undefined, + rentalAssistance: undefined, + reviewOrderType: undefined, + features: { + elevator: false, + wheelchairRamp: false, + serviceAnimalsAllowed: false, + accessibleParking: false, + parkingOnSite: false, + inUnitWasherDryer: false, + laundryInBuilding: false, + barrierFreeEntrance: false, + rollInShower: false, + grabBars: false, + heatingInUnit: false, + acInUnit: false, + }, + listingPreferences: [], + jurisdictionName: "Detroit", + marketingType: ListingMarketingTypeEnum.Marketing, + whatToExpect: `
If you are interested in applying for this property, please get in touch in one of these ways:
  • Phone
  • Email
  • In-person
  • In some instances, the property has a link directly to an application
Once you contact a property, ask if they have any available units if you are looking to move in immediately.
Waitlists:
If none are available, but you are still interested in eventually living at the property, ask how you can be placed on their waitlist.
`, + whatToExpectAdditionalText: `
  • Property staff should walk you through the process to get on their waitlist.
  • You can be on waitlists for multiple properties, but you will need to contact each one of them to begin that process.
  • Even if you are on a waitlist, it can take months or over a year to get an available unit for that building.
  • Many properties that are affordable because of government funding or agreements have long waitlists. If you're on a waitlist for a property, you should contact the property on a regular basis to see if any units are available.
`, +} + +export class ListingTreymoreSeed extends ListingDefaultSeed { + async seed() { + const unitTypeStudio = await this.unitTypeRepository.findOneOrFail({ name: "studio" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const property = await this.propertyRepository.save({ + ...treymoreProperty, + }) + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...treymoreListing, + applicationMethods: [], + assets: [], + events: [], + property: property, + } + + const listing = await this.listingRepository.save(listingCreateDto) + + const treymoreUnitGroupToBeCreated: DeepPartial[] = [] + + const studioUnitGroup: DeepPartial = { + unitType: [unitTypeStudio], + totalCount: 2, + listing: listing, + totalAvailable: 0, + } + treymoreUnitGroupToBeCreated.push(studioUnitGroup) + + const twoBdrmUnitGroup: DeepPartial = { + unitType: [unitTypeTwoBdrm], + totalCount: 4, + amiLevels: [ + { + amiPercentage: 10, + monthlyRentDeterminationType: MonthlyRentDeterminationType.flatRent, + flatRentValue: 707, + }, + ], + listing: listing, + sqFeetMin: 720, + sqFeetMax: 1003, + totalAvailable: 4, + } + treymoreUnitGroupToBeCreated.push(twoBdrmUnitGroup) + + await this.unitGroupRepository.save(treymoreUnitGroupToBeCreated) + + return listing + } +} diff --git a/backend/core/src/seeder/seeds/listings/listing-triton-seed.ts b/backend/core/src/seeder/seeds/listings/listing-triton-seed.ts new file mode 100644 index 0000000000..843607442d --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listing-triton-seed.ts @@ -0,0 +1,407 @@ +import { ListingSeedType, PropertySeedType, UnitSeedType } from "./listings" +import { getDate, getDefaultAssets, getLiveWorkPreference } from "./shared" +import { ListingDefaultSeed } from "./listing-default-seed" +import { BaseEntity, DeepPartial } from "typeorm" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingReviewOrder } from "../../../listings/types/listing-review-order-enum" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { UnitStatus } from "../../../units/types/unit-status-enum" +import { UnitCreateDto } from "../../../units/dto/unit-create.dto" +import { Listing } from "../../../listings/entities/listing.entity" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" + +const tritonProperty: PropertySeedType = { + accessibility: + "Accessibility features in common areas like lobby – wheelchair ramps, wheelchair accessible bathrooms and elevators.", + amenities: "Gym, Clubhouse, Business Lounge, View Lounge, Pool, Spa", + buildingAddress: { + city: "Foster City", + county: "San Mateo", + state: "CA", + street: "55 Triton Park Lane", + zipCode: "94404", + latitude: 37.5658152, + longitude: -122.2704286, + }, + buildingTotalUnits: 48, + developer: "Thompson Dorfman, LLC", + neighborhood: "Foster City", + petPolicy: + "Pets allowed except the following; pit bull, malamute, akita, rottweiler, doberman, staffordshire terrier, presa canario, chowchow, american bull dog, karelian bear dog, st bernard, german shepherd, husky, great dane, any hybrid or mixed breed of the aforementioned breeds. 50 pound weight limit. 2 pets per household limit. $500 pet deposit per pet. $60 pet rent per pet.", + servicesOffered: null, + smokingPolicy: "Non-Smoking", + unitAmenities: "Washer and dryer, AC and Heater, Gas Stove", + unitsAvailable: 4, + yearBuilt: 2021, +} + +const tritonListing: ListingSeedType = { + jurisdictionName: "Alameda", + digitalApplication: false, + commonDigitalApplication: false, + paperApplication: false, + section8Acceptance: null, + referralOpportunity: false, + countyCode: CountyCode.alameda, + applicationDropOffAddress: null, + applicationDropOffAddressOfficeHours: null, + applicationMailingAddress: null, + applicationDueDate: getDate(5), + applicationFee: "38.0", + applicationOpenDate: getDate(-10), + applicationOrganization: "Triton", + applicationPickUpAddress: { + city: "Foster City", + state: "CA", + street: "55 Triton Park Lane", + zipCode: "94404", + latitude: 37.5658152, + longitude: -122.2704286, + }, + images: [], + applicationPickUpAddressOfficeHours: null, + buildingSelectionCriteria: + "https://regional-dahlia-staging.s3-us-west-1.amazonaws.com/listings/triton/The_Triton_BMR_rental_information.pdf", + costsNotIncluded: + "Residents responsible for PG&E, Internet, Utilities - water, sewer, trash, admin fee. Pet Deposit is $500 with a $60 monthly pet rent. Residents required to maintain a renter's insurance policy as outlined in the lease agreement. Rent is due by the 3rd of each month. Late fee is $50.00. Resident to pay $25 for each returned check or rejected electronic payment. For additional returned checks, resident will pay a charge of $50.00.", + creditHistory: + "No collections, no bankruptcy, income is twice monthly rent A credit report will be completed on all applicants to verify credit ratings.\n\nIncome plus verified credit history will be entered into a credit scoring model to determine rental eligibility and security deposit levels. All decisions for residency are based on a system which considers credit history, rent history, income qualifications, and employment history. An approved decision based on the system does not automatically constittute an approval of residency. Applicant(s) and occupant(s) aged 18 years or older MUST also pass the criminal background check based on the criteria contained herein to be approved for residency. \n\nCredit recommendations other than an accept decision, will require a rental verification. Applications for residency will automatically be denied for the following reasons:\n\n- a. An outstanding debt to a previous landlord or an outstanding NSF check must be paid in full\n- b. An unsatisfied breach of a prior lease or a prior eviction of any applicant or occupant\n- c. More than four (4) late pays and two (2) NSF's in the last twenty-four (24) months", + criminalBackground: null, + depositMax: "800", + depositMin: "500", + disableUnitsAccordion: true, + displayWaitlistSize: false, + leasingAgentAddress: { + city: "Foster City", + state: "CA", + street: "55 Triton Park Lane", + zipCode: "94404", + latitude: 37.5658152, + longitude: -122.2704286, + }, + leasingAgentEmail: "thetriton@legacypartners.com", + leasingAgentName: "Francis Santos", + leasingAgentOfficeHours: "Monday - Friday, 9:00 am - 5:00 pm", + leasingAgentPhone: "650-437-2039", + leasingAgentTitle: "Business Manager", + listingPreferences: [], + listingPrograms: [], + name: "Test: Triton", + postmarkedApplicationsReceivedByDate: null, + programRules: null, + rentalAssistance: "Rental assistance", + rentalHistory: "No evictions", + requiredDocuments: + "Due at interview - Paystubs, 3 months’ bank statements, recent tax returns or non-tax affidavit, recent retirement statement, application to lease, application qualifying criteria, social security card, state or nation ID. For self-employed, copy of IRS Tax Return including schedule C and current or most recent clients. Unemployment if applicable. Child support/Alimony; current notice from DA office, a court order or a letter from the provider with copies of last two checks. Any other income etc", + reviewOrderType: "firstComeFirstServe" as ListingReviewOrder, + specialNotes: null, + status: ListingStatus.active, + waitlistCurrentSize: 400, + waitlistMaxSize: 600, + waitlistOpenSpots: 200, + isWaitlistOpen: true, + whatToExpect: null, + marketingType: ListingMarketingTypeEnum.Marketing, +} + +export class ListingTritonSeed extends ListingDefaultSeed { + async seed() { + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.alameda, + }) + + const amiChart = await this.amiChartRepository.findOneOrFail({ + name: "San Jose TCAC 2019", + jurisdiction: alamedaJurisdiction, + }) + + const property = await this.propertyRepository.save({ + ...tritonProperty, + }) + + const tritonUnits: Array = [ + { + amiChart: amiChart, + amiPercentage: "120.0", + annualIncomeMax: "177300.0", + annualIncomeMin: "84696.0", + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "7058.0", + monthlyRent: "3340.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 2, + number: null, + priorityType: null, + sqFeet: "1100", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "80.0", + annualIncomeMax: "103350.0", + annualIncomeMin: "58152.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "4858.0", + monthlyRent: "2624.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "80.0", + annualIncomeMax: "103350.0", + annualIncomeMin: "58152.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "4858.0", + monthlyRent: "2624.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "80.0", + annualIncomeMax: "103350.0", + annualIncomeMin: "58152.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "4858.0", + monthlyRent: "2624.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "50.0", + annualIncomeMax: "103350.0", + annualIncomeMin: "38952.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "3246.0", + monthlyRent: "1575.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + ] + + const unitsToBeCreated: Array> = tritonUnits.map( + (unit) => { + return { + ...unit, + property: { + id: property.id, + }, + amiChart, + } + } + ) + + unitsToBeCreated[0].unitType = unitTypeTwoBdrm + unitsToBeCreated[1].unitType = unitTypeOneBdrm + unitsToBeCreated[2].unitType = unitTypeOneBdrm + unitsToBeCreated[3].unitType = unitTypeOneBdrm + unitsToBeCreated[4].unitType = unitTypeOneBdrm + + await this.unitsRepository.save(unitsToBeCreated) + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...tritonListing, + name: "Test: Triton 2", + property: property, + assets: getDefaultAssets(), + listingPreferences: [ + { + preference: await this.preferencesRepository.findOneOrFail({ + title: getLiveWorkPreference(alamedaJurisdiction.name).title, + }), + ordinal: 2, + }, + ], + events: [], + } + + return await this.listingRepository.save(listingCreateDto) + } +} + +export class ListingTritonSeedDetroit extends ListingDefaultSeed { + async seed() { + const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) + const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) + + const detroitJurisdiction = await this.jurisdictionRepository.findOneOrFail({ + name: CountyCode.detroit, + }) + const amiChart = await this.amiChartRepository.findOneOrFail({ + name: "Detroit TCAC 2019", + jurisdiction: detroitJurisdiction, + }) + + const property = await this.propertyRepository.findOneOrFail({ + developer: "Thompson Dorfman, LLC", + neighborhood: "Foster City", + smokingPolicy: "Non-Smoking", + }) + + const tritonUnits: Array = [ + { + amiChart: amiChart, + amiPercentage: "120.0", + annualIncomeMax: "177300.0", + annualIncomeMin: "84696.0", + floor: 1, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "7058.0", + monthlyRent: "3340.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 2, + number: null, + priorityType: null, + sqFeet: "1100", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "80.0", + annualIncomeMax: "103350.0", + annualIncomeMin: "58152.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "4858.0", + monthlyRent: "2624.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "80.0", + annualIncomeMax: "103350.0", + annualIncomeMin: "58152.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "4858.0", + monthlyRent: "2624.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "80.0", + annualIncomeMax: "103350.0", + annualIncomeMin: "58152.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "4858.0", + monthlyRent: "2624.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + { + amiChart: amiChart, + amiPercentage: "50.0", + annualIncomeMax: "103350.0", + annualIncomeMin: "38952.0", + floor: 1, + maxOccupancy: 2, + minOccupancy: 1, + monthlyIncomeMin: "3246.0", + monthlyRent: "1575.0", + monthlyRentAsPercentOfIncome: null, + numBathrooms: null, + numBedrooms: 1, + number: null, + priorityType: null, + sqFeet: "750", + status: UnitStatus.occupied, + }, + ] + + const unitsToBeCreated: Array> = tritonUnits.map( + (unit) => { + return { + ...unit, + property: { + id: property.id, + }, + amiChart, + } + } + ) + + unitsToBeCreated[0].unitType = unitTypeTwoBdrm + unitsToBeCreated[1].unitType = unitTypeOneBdrm + unitsToBeCreated[2].unitType = unitTypeOneBdrm + unitsToBeCreated[3].unitType = unitTypeOneBdrm + unitsToBeCreated[4].unitType = unitTypeOneBdrm + + await this.unitsRepository.save(unitsToBeCreated) + + const listingCreateDto: Omit< + DeepPartial, + keyof BaseEntity | "urlSlug" | "showWaitlist" + > = { + ...tritonListing, + name: "Test: Triton 1", + property: property, + applicationOpenDate: getDate(-5), + assets: getDefaultAssets(), + events: [], + } + + return await this.listingRepository.save(listingCreateDto) + } +} diff --git a/backend/core/src/seeder/seeds/listings/listings.ts b/backend/core/src/seeder/seeds/listings/listings.ts new file mode 100644 index 0000000000..a8fb1ff5a5 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/listings.ts @@ -0,0 +1,78 @@ +import { BaseEntity } from "typeorm" +import { PropertyCreateDto } from "../../../property/dto/property.dto" +import { UnitCreateDto } from "../../../units/dto/unit-create.dto" +import { ApplicationMethodCreateDto } from "../../../application-methods/dto/application-method.dto" +import { ListingPublishedCreateDto } from "../../../listings/dto/listing-published-create.dto" +import { PreferenceCreateDto } from "../../../preferences/dto/preference-create.dto" +import { ProgramCreateDto } from "../../../program/dto/program-create.dto" +import { AssetCreateDto } from "../../../assets/dto/asset.dto" +import { AmiChartCreateDto } from "../../../ami-charts/dto/ami-chart.dto" +import { ListingEventCreateDto } from "../../../listings/dto/listing-event.dto" +import { UserCreateDto } from "../../../auth/dto/user-create.dto" + +export type PropertySeedType = Omit< + PropertyCreateDto, + | "propertyGroups" + | "listings" + | "units" + | "unitSummaries" + | "householdSizeMin" + | "householdSizeMax" +> + +export type UnitSeedType = Omit + +export type ApplicationMethodSeedType = ApplicationMethodCreateDto + +export type ListingSeedType = Omit< + ListingPublishedCreateDto, + | keyof BaseEntity + | "property" + | "urlSlug" + | "applicationMethods" + | "events" + | "assets" + | "preferences" + | "leasingAgents" + | "showWaitlist" + | "units" + | "propertyGroups" + | "accessibility" + | "amenities" + | "buildingAddress" + | "buildingTotalUnits" + | "developer" + | "householdSizeMax" + | "householdSizeMin" + | "neighborhood" + | "petPolicy" + | "smokingPolicy" + | "unitsAvailable" + | "unitAmenities" + | "servicesOffered" + | "yearBuilt" + | "unitGroups" + | "unitSummaries" + | "amiChartOverrides" + | "jurisdiction" +> & { + jurisdictionName: string +} + +export type PreferenceSeedType = PreferenceCreateDto +export type ProgramSeedType = Omit + +export type AssetDtoSeedType = Omit + +// Properties that are ommited in DTOS derived types are relations and getters +export interface ListingSeed { + amiChart: AmiChartCreateDto + units: Array + applicationMethods: Array + property: PropertySeedType + preferences: Array + listingEvents: Array + assets: Array + listing: ListingSeedType + leasingAgents: UserCreateDto[] +} diff --git a/backend/core/src/seeder/seeds/listings/shared.ts b/backend/core/src/seeder/seeds/listings/shared.ts new file mode 100644 index 0000000000..514c968256 --- /dev/null +++ b/backend/core/src/seeder/seeds/listings/shared.ts @@ -0,0 +1,556 @@ +// AMI Charts +import { + AssetDtoSeedType, + ListingSeedType, + PreferenceSeedType, + ProgramSeedType, + PropertySeedType, + UnitSeedType, +} from "./listings" +import { defaultAmiChart } from "../ami-charts/default-ami-chart" +import { ListingEventCreateDto } from "../../../listings/dto/listing-event.dto" +import { ListingEventType } from "../../../listings/types/listing-event-type-enum" +import { AmiChart } from "../../../ami-charts/entities/ami-chart.entity" +import { UnitStatus } from "../../../units/types/unit-status-enum" +import { UserCreateDto } from "../../../auth/dto/user-create.dto" +import { CountyCode } from "../../../shared/types/county-code" +import { ListingReviewOrder } from "../../../listings/types/listing-review-order-enum" +import { ListingStatus } from "../../../listings/types/listing-status-enum" +import { InputType } from "../../../shared/types/input-type" +import { FormMetaDataType } from "../../../applications/types/form-metadata/form-metadata" +import { ListingMarketingTypeEnum } from "../../../listings/types/listing-marketing-type-enum" +export const getDate = (days: number) => { + const someDate = new Date() + someDate.setDate(someDate.getDate() + days) + return someDate +} + +export enum PriorityTypes { + mobility = "Mobility", + hearing = "Hearing", + visual = "Visual", + hearingVisual = "Hearing and Visual", + mobilityHearing = "Mobility and Hearing", + mobilityVisual = "Mobility and Visual", + mobilityHearingVisual = "Mobility, Hearing and Visual", +} + +// Events +export function getDefaultListingEvents() { + return JSON.parse(JSON.stringify(defaultListingEvents)) +} + +export const defaultListingEvents: Array = [ + { + startTime: getDate(10), + endTime: getDate(10), + note: "Custom open house event note", + type: ListingEventType.openHouse, + url: "https://www.example.com", + label: "Custom Event URL Label", + }, + { + startTime: getDate(10), + endTime: getDate(10), + note: "Custom public lottery event note", + type: ListingEventType.publicLottery, + url: "https://www.example2.com", + label: "Custom Event URL Label", + }, +] + +// Assets +export function getDefaultAssets() { + return JSON.parse(JSON.stringify(defaultAssets)) +} + +export const defaultAssets: Array = [ + { + label: "building", + fileId: + "https://regional-dahlia-staging.s3-us-west-1.amazonaws.com/listings/triton/thetriton.png", + }, +] +// Properties +export function getDefaultProperty() { + return JSON.parse(JSON.stringify(defaultProperty)) +} + +export const defaultProperty: PropertySeedType = { + accessibility: "Custom accessibility text", + amenities: "Custom property amenities text", + buildingAddress: { + city: "San Francisco", + state: "CA", + street: "548 Market Street", + street2: "Suite #59930", + zipCode: "94104", + latitude: 37.789673, + longitude: -122.40151, + }, + buildingTotalUnits: 100, + developer: "Developer", + neighborhood: "Custom neighborhood text", + petPolicy: "Custom pet text", + servicesOffered: "Custom services offered text", + smokingPolicy: "Custom smoking text", + unitAmenities: "Custom unit amenities text", + unitsAvailable: 2, + yearBuilt: 2021, +} + +// Unit Sets +export function getDefaultUnits() { + return JSON.parse(JSON.stringify(defaultUnits)) +} + +export const defaultUnits: Array = [ + { + amiChart: defaultAmiChart as AmiChart, + amiPercentage: "30", + annualIncomeMax: "45600", + annualIncomeMin: "36168", + bmrProgramChart: false, + floor: 1, + maxOccupancy: 3, + minOccupancy: 1, + monthlyIncomeMin: "3014", + monthlyRent: "1219", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 1, + number: null, + sqFeet: "635", + status: UnitStatus.available, + }, + { + amiChart: defaultAmiChart as AmiChart, + amiPercentage: "30", + annualIncomeMax: "66600", + annualIncomeMin: "41616", + bmrProgramChart: false, + floor: 2, + maxOccupancy: 5, + minOccupancy: 2, + monthlyIncomeMin: "3468", + monthlyRent: "1387", + monthlyRentAsPercentOfIncome: null, + numBathrooms: 1, + numBedrooms: 2, + number: null, + sqFeet: "748", + status: UnitStatus.available, + }, +] + +export const defaultLeasingAgents: Omit[] = [ + { + firstName: "First", + lastName: "Last", + middleName: "Middle", + email: "leasing-agent-1@example.com", + emailConfirmation: "leasing-agent-1@example.com", + password: "abcdef", + passwordConfirmation: "Abcdef1", + dob: new Date(), + }, + { + firstName: "First", + lastName: "Last", + middleName: "Middle", + email: "leasing-agent-2@example.com", + emailConfirmation: "leasing-agent-2@example.com", + password: "abcdef", + passwordConfirmation: "Abcdef1", + dob: new Date(), + }, +] + +// Listings +export function getDefaultListing() { + return JSON.parse(JSON.stringify(defaultListing)) +} + +export const defaultListing: ListingSeedType = { + jurisdictionName: "Alameda", + countyCode: CountyCode.alameda, + applicationDropOffAddress: null, + applicationDropOffAddressOfficeHours: null, + applicationMailingAddress: null, + digitalApplication: false, + commonDigitalApplication: false, + paperApplication: false, + referralOpportunity: false, + section8Acceptance: null, + homeType: null, + applicationDueDate: getDate(10), + applicationFee: "20", + applicationOpenDate: getDate(-10), + applicationOrganization: "Application Organization", + applicationPickUpAddress: { + city: "San Francisco", + state: "CA", + street: "548 Market Street", + street2: "Suite #59930", + zipCode: "94104", + latitude: 37.789673, + longitude: -122.40151, + }, + applicationPickUpAddressOfficeHours: "Custom pick up address office hours text", + buildingSelectionCriteria: "https://www.example.com", + costsNotIncluded: "Custom costs not included text", + creditHistory: "Custom credit history text", + criminalBackground: "Custom criminal background text", + depositMax: "500", + depositMin: "500", + disableUnitsAccordion: true, + displayWaitlistSize: false, + images: [], + leasingAgentAddress: { + city: "San Francisco", + state: "CA", + street: "548 Market Street", + street2: "Suite #59930", + zipCode: "94104", + latitude: 37.789673, + longitude: -122.40151, + }, + leasingAgentEmail: "hello@exygy.com", + leasingAgentName: "Leasing Agent Name", + leasingAgentOfficeHours: "Custom leasing agent office hours", + leasingAgentPhone: "(415) 992-7251", + leasingAgentTitle: "Leasing Agent Title", + listingPreferences: [], + listingPrograms: [], + name: "Default Listing Seed", + postmarkedApplicationsReceivedByDate: null, + programRules: "Custom program rules text", + rentalAssistance: "Custom rental assistance text", + rentalHistory: "Custom rental history text", + requiredDocuments: "Custom required documents text", + reviewOrderType: "lottery" as ListingReviewOrder, + specialNotes: "Custom special notes text", + status: ListingStatus.active, + waitlistCurrentSize: null, + waitlistOpenSpots: null, + isWaitlistOpen: false, + waitlistMaxSize: null, + whatToExpect: "Custom what to expect text", + marketingType: ListingMarketingTypeEnum.Marketing, +} + +// Preferences +export function getLiveWorkPreference(jurisdictionName) { + const preference = { ...liveWorkPreference } + preference.title += ` - ${jurisdictionName}` + return preference +} + +export const liveWorkPreference: PreferenceSeedType = { + title: "Live/Work in County", + subtitle: "Live/Work in County subtitle", + description: "At least one household member lives or works in County", + links: [ + { + title: "Link Title", + url: "https://www.example.com", + }, + ], + formMetadata: { + key: "liveWork", + options: [ + { + key: "live", + extraData: [], + }, + { + key: "work", + extraData: [], + }, + ], + }, +} +export function getDisplaceePreference(jurisdictionName) { + const preference = { ...displaceePreference } + preference.title += ` - ${jurisdictionName}` + return preference +} + +export const displaceePreference: PreferenceSeedType = { + title: "Displacee Tenant Housing", + subtitle: "Displacee Tenant Housing subtitle", + description: + "At least one member of my household was displaced from a residential property due to redevelopment activity by Housing Authority or City.", + links: [], + formMetadata: { + key: "displacedTenant", + options: [ + { + key: "general", + extraData: [ + { + key: "name", + type: InputType.text, + }, + { + key: "address", + type: InputType.address, + }, + ], + }, + { + key: "missionCorridor", + extraData: [ + { + key: "name", + type: InputType.text, + }, + { + key: "address", + type: InputType.address, + }, + ], + }, + ], + }, +} + +export function getPbvPreference(jurisdictionName) { + const preference = { ...pbvPreference } + preference.title += ` - ${jurisdictionName}` + return preference +} + +export const pbvPreference: PreferenceSeedType = { + title: "Housing Authority Project-Based Voucher", + subtitle: "", + description: + "You are currently applying to be in a general applicant waiting list. Of the total apartments available in this application process, several have Project-Based Vouchers for rental subsidy assistance from the Housing Authority. With that subsidy, tenant households pay 30% of their income as rent. These tenants are required to verify their income annually with the property manager as well as the Housing Authority.", + links: [], + formMetadata: { + key: "PBV", + customSelectText: "Please select any of the following that apply to you", + hideGenericDecline: true, + hideFromListing: true, + options: [ + { + key: "residency", + extraData: [], + }, + { + key: "family", + extraData: [], + }, + { + key: "veteran", + extraData: [], + }, + { + key: "homeless", + extraData: [], + }, + { + key: "noneApplyButConsider", + exclusive: true, + description: false, + extraData: [], + }, + { + key: "doNotConsider", + exclusive: true, + description: false, + extraData: [], + }, + ], + }, +} + +export function getHopwaPreference(jurisdictionName) { + const preference = { ...hopwaPreference } + preference.title += ` - ${jurisdictionName}` + return preference +} + +export const hopwaPreference: PreferenceSeedType = { + title: "Housing Opportunities for Persons with AIDS", + subtitle: "", + description: + "There are apartments set-aside for households eligible for the HOPWA program (Housing Opportunities for Persons with AIDS), which are households where a person has been medically diagnosed with HIV/AIDS. These apartments also have Project-Based Section rental subsidies (tenant pays 30% of household income).", + links: [], + formMetadata: { + key: "HOPWA", + customSelectText: + "Please indicate if you are interested in applying for one of these HOPWA apartments", + hideGenericDecline: true, + hideFromListing: true, + options: [ + { + key: "hopwa", + extraData: [], + }, + { + key: "doNotConsider", + exclusive: true, + description: false, + extraData: [], + }, + ], + }, +} + +// programs + +export function getServedInMilitaryProgram() { + return JSON.parse(JSON.stringify(servedInMilitaryProgram)) +} + +export function getFlatRentAndRentBasedOnIncomeProgram() { + return JSON.parse(JSON.stringify(flatRentAndRentBasedOnIncomeProgram)) +} + +export const servedInMilitaryProgram: ProgramSeedType = { + title: "Veteran", + subtitle: "Should your application be chosen, be prepared to provide supporting documentation.", + description: "Have you or anyone in your household served in the US military?", + formMetadata: { + key: "servedInMilitary", + options: [ + { + key: "servedInMilitary", + description: false, + extraData: [], + }, + { + key: "doNotConsider", + description: false, + extraData: [], + }, + { + key: "preferNotToSay", + description: false, + extraData: [], + }, + ], + }, +} + +export const flatRentAndRentBasedOnIncomeProgram: ProgramSeedType = { + title: "Flat Rent & Rent Based on Income", + subtitle: + "This property includes two types of affordable housing programs. You can choose to apply for one or both programs. Each program will have its own applicant list. Your choice will tell us which list(s) to put your name on. Additional information on each of the two types of housing opportunities are below.", + description: "Do you want to apply for apartments with flat rent and rent based on income?", + formMetadata: { + key: "rentBasedOnIncome", + type: FormMetaDataType.checkbox, + options: [ + { + key: "flatRent", + description: true, + extraData: [], + }, + { + key: "30Percent", + description: true, + extraData: [], + }, + ], + }, +} + +export function getTayProgram() { + return JSON.parse(JSON.stringify(tayProgram)) +} + +export const tayProgram: ProgramSeedType = { + title: "Transition Age Youth", + subtitle: "Should your application be chosen, be prepared to provide supporting documentation.", + description: + "Are you or anyone in your household a transition age youth (TAY) aging out of foster care?", + formMetadata: { + key: "tay", + options: [ + { + key: "tay", + description: false, + extraData: [], + }, + { + key: "doNotConsider", + description: false, + extraData: [], + }, + { + key: "preferNotToSay", + description: false, + extraData: [], + }, + ], + }, +} + +export function getDisabilityOrMentalIllnessProgram() { + return JSON.parse(JSON.stringify(disabilityOrMentalIllnessProgram)) +} + +export const disabilityOrMentalIllnessProgram: ProgramSeedType = { + title: "Developmental Disability", + subtitle: "Should your application be chosen, be prepared to provide supporting documentation.", + description: + "Do you or anyone in your household have a developmental disability or mental illness?", + formMetadata: { + key: "disabilityOrMentalIllness", + options: [ + { + key: "disabilityOrMentalIllness", + description: false, + extraData: [], + }, + { + key: "doNotConsider", + description: false, + extraData: [], + }, + { + key: "preferNotToSay", + description: false, + extraData: [], + }, + ], + }, +} + +export function getHousingSituationProgram() { + return JSON.parse(JSON.stringify(housingSituationProgram)) +} + +export const housingSituationProgram: ProgramSeedType = { + title: "Housing Situation", + subtitle: "", + description: + "Thinking about the past 30 days, do either of these describe your housing situation?", + formMetadata: { + key: "housingSituation", + options: [ + { + key: "notPermanent", + description: true, + extraData: [], + }, + { + key: "homeless", + description: true, + extraData: [], + }, + { + key: "doNotConsider", + description: false, + extraData: [], + }, + { + key: "preferNotToSay", + description: false, + extraData: [], + }, + ], + }, +} diff --git a/backend/core/src/seeds/ami-charts/AlamedaCountyLIHTC2020.ts b/backend/core/src/seeds/ami-charts/AlamedaCountyLIHTC2020.ts deleted file mode 100644 index 863bc11819..0000000000 --- a/backend/core/src/seeds/ami-charts/AlamedaCountyLIHTC2020.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { BaseEntity } from "typeorm" -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" - -export const AlamedaCountyLIHTC2020: Omit = { - name: "AlamedaCountyLIHTC2020", - items: [ - { - householdSize: 1, - income: 27420, - percentOfAmi: 30, - }, - { - householdSize: 2, - income: 31320, - percentOfAmi: 30, - }, - { - householdSize: 3, - income: 35250, - percentOfAmi: 30, - }, - { - householdSize: 4, - income: 39150, - percentOfAmi: 30, - }, - { - householdSize: 5, - income: 42300, - percentOfAmi: 30, - }, - { - householdSize: 6, - income: 45420, - percentOfAmi: 30, - }, - { - householdSize: 7, - income: 48570, - percentOfAmi: 30, - }, - { - householdSize: 8, - income: 51700, - percentOfAmi: 30, - }, - { - householdSize: 1, - income: 45700, - percentOfAmi: 50, - }, - { - householdSize: 2, - income: 52200, - percentOfAmi: 50, - }, - { - householdSize: 3, - income: 58750, - percentOfAmi: 50, - }, - { - householdSize: 4, - income: 65250, - percentOfAmi: 50, - }, - { - householdSize: 5, - income: 70500, - percentOfAmi: 50, - }, - { - householdSize: 6, - income: 75700, - percentOfAmi: 50, - }, - { - householdSize: 7, - income: 80950, - percentOfAmi: 50, - }, - { - householdSize: 8, - income: 86150, - percentOfAmi: 50, - }, - { - householdSize: 1, - income: 54840, - percentOfAmi: 60, - }, - { - householdSize: 2, - income: 62640, - percentOfAmi: 60, - }, - { - householdSize: 3, - income: 70500, - percentOfAmi: 60, - }, - { - householdSize: 4, - income: 78300, - percentOfAmi: 60, - }, - { - householdSize: 5, - income: 84600, - percentOfAmi: 60, - }, - { - householdSize: 6, - income: 90840, - percentOfAmi: 60, - }, - { - householdSize: 7, - income: 97140, - percentOfAmi: 60, - }, - { - householdSize: 8, - income: 103380, - percentOfAmi: 60, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/AlamedaCountyTCAC2019.ts b/backend/core/src/seeds/ami-charts/AlamedaCountyTCAC2019.ts deleted file mode 100644 index 0877602d23..0000000000 --- a/backend/core/src/seeds/ami-charts/AlamedaCountyTCAC2019.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" - -export const AlamedaCountyTCAC2019: Omit = { - name: "AlamedaCountyTCAC2019", - items: [ - { - percentOfAmi: 80, - householdSize: 1, - income: 69000, - }, - { - percentOfAmi: 80, - householdSize: 2, - income: 78850, - }, - { - percentOfAmi: 80, - householdSize: 3, - income: 88700, - }, - { - percentOfAmi: 80, - householdSize: 4, - income: 98550, - }, - { - percentOfAmi: 80, - householdSize: 5, - income: 106450, - }, - { - percentOfAmi: 80, - householdSize: 6, - income: 115040, - }, - { - percentOfAmi: 80, - householdSize: 7, - income: 122960, - }, - { - percentOfAmi: 80, - householdSize: 8, - income: 130800, - }, - { - percentOfAmi: 60, - householdSize: 1, - income: 52080, - }, - { - percentOfAmi: 60, - householdSize: 2, - income: 59520, - }, - { - percentOfAmi: 60, - householdSize: 3, - income: 66960, - }, - { - percentOfAmi: 60, - householdSize: 4, - income: 74340, - }, - { - percentOfAmi: 60, - householdSize: 5, - income: 80340, - }, - { - percentOfAmi: 60, - householdSize: 6, - income: 86280, - }, - { - percentOfAmi: 60, - householdSize: 7, - income: 92220, - }, - { - percentOfAmi: 60, - householdSize: 8, - income: 98160, - }, - { - percentOfAmi: 30, - householdSize: 1, - income: 26040, - }, - { - percentOfAmi: 30, - householdSize: 2, - income: 29760, - }, - { - percentOfAmi: 30, - householdSize: 3, - income: 33480, - }, - { - percentOfAmi: 30, - householdSize: 4, - income: 37170, - }, - { - percentOfAmi: 30, - householdSize: 5, - income: 40170, - }, - { - percentOfAmi: 30, - householdSize: 6, - income: 43140, - }, - { - percentOfAmi: 30, - householdSize: 7, - income: 46110, - }, - { - percentOfAmi: 30, - householdSize: 8, - income: 49080, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/AlamedaCountyTCAC2020.ts b/backend/core/src/seeds/ami-charts/AlamedaCountyTCAC2020.ts deleted file mode 100644 index f66b773745..0000000000 --- a/backend/core/src/seeds/ami-charts/AlamedaCountyTCAC2020.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" - -export const AlamedaCountyTCAC2020: Omit = { - name: "AlamedaCountyTCAC2020", - items: [ - { - percentOfAmi: 80, - householdSize: 1, - income: 73100, - }, - { - percentOfAmi: 80, - householdSize: 2, - income: 83550, - }, - { - percentOfAmi: 80, - householdSize: 3, - income: 94000, - }, - { - percentOfAmi: 80, - householdSize: 4, - income: 104400, - }, - { - percentOfAmi: 80, - householdSize: 5, - income: 112800, - }, - { - percentOfAmi: 80, - householdSize: 6, - income: 121150, - }, - { - percentOfAmi: 80, - householdSize: 7, - income: 129500, - }, - { - percentOfAmi: 80, - householdSize: 8, - income: 137850, - }, - { - percentOfAmi: 60, - householdSize: 1, - income: 54840, - }, - { - percentOfAmi: 60, - householdSize: 2, - income: 62640, - }, - { - percentOfAmi: 60, - householdSize: 3, - income: 70500, - }, - { - percentOfAmi: 60, - householdSize: 4, - income: 78300, - }, - { - percentOfAmi: 60, - householdSize: 5, - income: 84600, - }, - { - percentOfAmi: 60, - householdSize: 6, - income: 90840, - }, - { - percentOfAmi: 60, - householdSize: 7, - income: 97140, - }, - { - percentOfAmi: 60, - householdSize: 8, - income: 103380, - }, - { - percentOfAmi: 50, - householdSize: 1, - income: 45700, - }, - { - percentOfAmi: 50, - householdSize: 2, - income: 52200, - }, - { - percentOfAmi: 50, - householdSize: 3, - income: 58750, - }, - { - percentOfAmi: 50, - householdSize: 4, - income: 65250, - }, - { - percentOfAmi: 50, - householdSize: 5, - income: 70500, - }, - { - percentOfAmi: 50, - householdSize: 6, - income: 75700, - }, - { - percentOfAmi: 50, - householdSize: 7, - income: 80950, - }, - { - percentOfAmi: 50, - householdSize: 8, - income: 86500, - }, - { - percentOfAmi: 30, - householdSize: 1, - income: 27450, - }, - { - percentOfAmi: 30, - householdSize: 2, - income: 31350, - }, - { - percentOfAmi: 30, - householdSize: 3, - income: 35250, - }, - { - percentOfAmi: 30, - householdSize: 4, - income: 39150, - }, - { - percentOfAmi: 30, - householdSize: 5, - income: 42300, - }, - { - percentOfAmi: 30, - householdSize: 6, - income: 45450, - }, - { - percentOfAmi: 30, - householdSize: 7, - income: 48550, - }, - { - percentOfAmi: 30, - householdSize: 8, - income: 51700, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/OaklandFremontHUD2020.ts b/backend/core/src/seeds/ami-charts/OaklandFremontHUD2020.ts deleted file mode 100644 index 770fbd22e2..0000000000 --- a/backend/core/src/seeds/ami-charts/OaklandFremontHUD2020.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" - -export const OaklandFremontHUD2020: Omit = { - name: "OaklandFremontHUD2020", - items: [ - { - percentOfAmi: 50, - householdSize: 1, - income: 45700, - }, - { - percentOfAmi: 50, - householdSize: 2, - income: 52200, - }, - { - percentOfAmi: 50, - householdSize: 3, - income: 58750, - }, - { - percentOfAmi: 50, - householdSize: 4, - income: 65250, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/SanJoseTCAC2019.ts b/backend/core/src/seeds/ami-charts/SanJoseTCAC2019.ts deleted file mode 100644 index e155912ad4..0000000000 --- a/backend/core/src/seeds/ami-charts/SanJoseTCAC2019.ts +++ /dev/null @@ -1,558 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" - -export const SanJoseTCAC2019: Omit = { - name: "SanJoseTCAC2019", - items: [ - { - percentOfAmi: 120, - householdSize: 1, - income: 110400, - }, - { - percentOfAmi: 120, - householdSize: 2, - income: 126150, - }, - { - percentOfAmi: 120, - householdSize: 3, - income: 141950, - }, - { - percentOfAmi: 120, - householdSize: 4, - income: 157700, - }, - { - percentOfAmi: 120, - householdSize: 5, - income: 170300, - }, - { - percentOfAmi: 120, - householdSize: 6, - income: 182950, - }, - { - percentOfAmi: 120, - householdSize: 7, - income: 195550, - }, - { - percentOfAmi: 120, - householdSize: 8, - income: 208150, - }, - { - percentOfAmi: 110, - householdSize: 1, - income: 101200, - }, - { - percentOfAmi: 110, - householdSize: 2, - income: 115610, - }, - { - percentOfAmi: 110, - householdSize: 3, - income: 130075, - }, - { - percentOfAmi: 110, - householdSize: 4, - income: 144540, - }, - { - percentOfAmi: 110, - householdSize: 5, - income: 156090, - }, - { - percentOfAmi: 110, - householdSize: 6, - income: 167640, - }, - { - percentOfAmi: 110, - householdSize: 7, - income: 179245, - }, - { - percentOfAmi: 110, - householdSize: 8, - income: 190795, - }, - { - percentOfAmi: 100, - householdSize: 1, - income: 92000, - }, - { - percentOfAmi: 100, - householdSize: 2, - income: 105100, - }, - { - percentOfAmi: 100, - householdSize: 3, - income: 118250, - }, - { - percentOfAmi: 100, - householdSize: 4, - income: 131400, - }, - { - percentOfAmi: 100, - householdSize: 5, - income: 141900, - }, - { - percentOfAmi: 100, - householdSize: 6, - income: 152400, - }, - { - percentOfAmi: 100, - householdSize: 7, - income: 162950, - }, - { - percentOfAmi: 100, - householdSize: 8, - income: 173450, - }, - { - percentOfAmi: 80, - householdSize: 1, - income: 72750, - }, - { - percentOfAmi: 80, - householdSize: 2, - income: 83150, - }, - { - percentOfAmi: 80, - householdSize: 3, - income: 93550, - }, - { - percentOfAmi: 80, - householdSize: 4, - income: 103900, - }, - { - percentOfAmi: 80, - householdSize: 5, - income: 112250, - }, - { - percentOfAmi: 80, - householdSize: 6, - income: 120550, - }, - { - percentOfAmi: 80, - householdSize: 7, - income: 128850, - }, - { - percentOfAmi: 80, - householdSize: 8, - income: 137150, - }, - { - percentOfAmi: 60, - householdSize: 1, - income: 61500, - }, - { - percentOfAmi: 60, - householdSize: 2, - income: 70260, - }, - { - percentOfAmi: 60, - householdSize: 3, - income: 79020, - }, - { - percentOfAmi: 60, - householdSize: 4, - income: 87780, - }, - { - percentOfAmi: 60, - householdSize: 5, - income: 94860, - }, - { - percentOfAmi: 60, - householdSize: 6, - income: 101880, - }, - { - percentOfAmi: 60, - householdSize: 7, - income: 108900, - }, - { - percentOfAmi: 60, - householdSize: 8, - income: 115920, - }, - { - percentOfAmi: 55, - householdSize: 1, - income: 56375, - }, - { - percentOfAmi: 55, - householdSize: 2, - income: 64405, - }, - { - percentOfAmi: 55, - householdSize: 3, - income: 72435, - }, - { - percentOfAmi: 55, - householdSize: 4, - income: 80465, - }, - { - percentOfAmi: 55, - householdSize: 5, - income: 86955, - }, - { - percentOfAmi: 55, - householdSize: 6, - income: 93390, - }, - { - percentOfAmi: 55, - householdSize: 7, - income: 99825, - }, - { - percentOfAmi: 55, - householdSize: 8, - income: 106260, - }, - { - percentOfAmi: 50, - householdSize: 1, - income: 51250, - }, - { - percentOfAmi: 50, - householdSize: 2, - income: 58550, - }, - { - percentOfAmi: 50, - householdSize: 3, - income: 65850, - }, - { - percentOfAmi: 50, - householdSize: 4, - income: 73150, - }, - { - percentOfAmi: 50, - householdSize: 5, - income: 79050, - }, - { - percentOfAmi: 50, - householdSize: 6, - income: 84900, - }, - { - percentOfAmi: 50, - householdSize: 7, - income: 90750, - }, - { - percentOfAmi: 50, - householdSize: 8, - income: 96600, - }, - { - percentOfAmi: 45, - householdSize: 1, - income: 46125, - }, - { - percentOfAmi: 45, - householdSize: 2, - income: 52695, - }, - { - percentOfAmi: 45, - householdSize: 3, - income: 59265, - }, - { - percentOfAmi: 45, - householdSize: 4, - income: 65835, - }, - { - percentOfAmi: 45, - householdSize: 5, - income: 71145, - }, - { - percentOfAmi: 45, - householdSize: 6, - income: 76410, - }, - { - percentOfAmi: 45, - householdSize: 7, - income: 81675, - }, - { - percentOfAmi: 40, - householdSize: 1, - income: 41000, - }, - { - percentOfAmi: 40, - householdSize: 2, - income: 46840, - }, - { - percentOfAmi: 40, - householdSize: 3, - income: 52680, - }, - { - percentOfAmi: 40, - householdSize: 4, - income: 58520, - }, - { - percentOfAmi: 40, - householdSize: 5, - income: 63240, - }, - { - percentOfAmi: 40, - householdSize: 6, - income: 67920, - }, - { - percentOfAmi: 40, - householdSize: 7, - income: 72600, - }, - { - percentOfAmi: 40, - householdSize: 8, - income: 77280, - }, - { - percentOfAmi: 35, - householdSize: 1, - income: 35875, - }, - { - percentOfAmi: 35, - householdSize: 2, - income: 40985, - }, - { - percentOfAmi: 35, - householdSize: 3, - income: 46095, - }, - { - percentOfAmi: 35, - householdSize: 4, - income: 51205, - }, - { - percentOfAmi: 35, - householdSize: 5, - income: 55335, - }, - { - percentOfAmi: 35, - householdSize: 6, - income: 59430, - }, - { - percentOfAmi: 35, - householdSize: 7, - income: 63525, - }, - { - percentOfAmi: 35, - householdSize: 8, - income: 67620, - }, - { - percentOfAmi: 30, - householdSize: 1, - income: 30750, - }, - { - percentOfAmi: 30, - householdSize: 2, - income: 35130, - }, - { - percentOfAmi: 30, - householdSize: 3, - income: 39510, - }, - { - percentOfAmi: 30, - householdSize: 4, - income: 43890, - }, - { - percentOfAmi: 30, - householdSize: 5, - income: 47430, - }, - { - percentOfAmi: 30, - householdSize: 6, - income: 50940, - }, - { - percentOfAmi: 30, - householdSize: 7, - income: 54450, - }, - { - percentOfAmi: 25, - householdSize: 1, - income: 25625, - }, - { - percentOfAmi: 25, - householdSize: 2, - income: 29275, - }, - { - percentOfAmi: 25, - householdSize: 3, - income: 32925, - }, - { - percentOfAmi: 25, - householdSize: 4, - income: 36575, - }, - { - percentOfAmi: 25, - householdSize: 5, - income: 39525, - }, - { - percentOfAmi: 25, - householdSize: 6, - income: 42450, - }, - { - percentOfAmi: 25, - householdSize: 7, - income: 45375, - }, - { - percentOfAmi: 25, - householdSize: 8, - income: 48300, - }, - { - percentOfAmi: 20, - householdSize: 1, - income: 20500, - }, - { - percentOfAmi: 20, - householdSize: 2, - income: 23420, - }, - { - percentOfAmi: 20, - householdSize: 3, - income: 26340, - }, - { - percentOfAmi: 20, - householdSize: 4, - income: 29260, - }, - { - percentOfAmi: 20, - householdSize: 5, - income: 31620, - }, - { - percentOfAmi: 20, - householdSize: 6, - income: 33960, - }, - { - percentOfAmi: 20, - householdSize: 7, - income: 36300, - }, - { - percentOfAmi: 20, - householdSize: 8, - income: 38640, - }, - { - percentOfAmi: 15, - householdSize: 1, - income: 15375, - }, - { - percentOfAmi: 15, - householdSize: 2, - income: 17565, - }, - { - percentOfAmi: 15, - householdSize: 3, - income: 19755, - }, - { - percentOfAmi: 15, - householdSize: 4, - income: 21945, - }, - { - percentOfAmi: 15, - householdSize: 5, - income: 23715, - }, - { - percentOfAmi: 15, - householdSize: 6, - income: 25470, - }, - { - percentOfAmi: 15, - householdSize: 7, - income: 27225, - }, - { - percentOfAmi: 15, - householdSize: 8, - income: 28980, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/SanMateoCountyTCAC2019.ts b/backend/core/src/seeds/ami-charts/SanMateoCountyTCAC2019.ts deleted file mode 100644 index 9ed43730da..0000000000 --- a/backend/core/src/seeds/ami-charts/SanMateoCountyTCAC2019.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" - -export const SanMateoCountyTCAC2019: Omit = { - name: "SanMateoCountyTCAC2019", - items: [ - { - percentOfAmi: 60, - householdSize: 1, - income: 71170, - }, - { - percentOfAmi: 50, - householdSize: 1, - income: 56450, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/SanMateoCountyTCAC2020.ts b/backend/core/src/seeds/ami-charts/SanMateoCountyTCAC2020.ts deleted file mode 100644 index db0a3bcaeb..0000000000 --- a/backend/core/src/seeds/ami-charts/SanMateoCountyTCAC2020.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" - -export const SanMateoCounty2020: Omit = { - name: "SanMateoCounty2020", - items: [ - { - percentOfAmi: 30, - householdSize: 1, - income: 36540, - }, - { - percentOfAmi: 30, - householdSize: 2, - income: 41760, - }, - { - percentOfAmi: 30, - householdSize: 3, - income: 46980, - }, - { - percentOfAmi: 30, - householdSize: 4, - income: 52200, - }, - { - percentOfAmi: 30, - householdSize: 5, - income: 56400, - }, - { - percentOfAmi: 30, - householdSize: 6, - income: 60570, - }, - { - percentOfAmi: 30, - householdSize: 7, - income: 64740, - }, - { - percentOfAmi: 30, - householdSize: 8, - income: 68910, - }, - { - percentOfAmi: 50, - householdSize: 1, - income: 60900, - }, - { - percentOfAmi: 50, - householdSize: 2, - income: 69600, - }, - { - percentOfAmi: 50, - householdSize: 3, - income: 78300, - }, - { - percentOfAmi: 50, - householdSize: 4, - income: 87000, - }, - { - percentOfAmi: 50, - householdSize: 5, - income: 94000, - }, - { - percentOfAmi: 50, - householdSize: 6, - income: 100950, - }, - { - percentOfAmi: 50, - householdSize: 7, - income: 107900, - }, - { - percentOfAmi: 50, - householdSize: 8, - income: 114850, - }, - { - percentOfAmi: 60, - householdSize: 1, - income: 73080, - }, - { - percentOfAmi: 60, - householdSize: 2, - income: 83520, - }, - { - percentOfAmi: 60, - householdSize: 3, - income: 93960, - }, - { - percentOfAmi: 60, - householdSize: 4, - income: 104400, - }, - { - percentOfAmi: 60, - householdSize: 5, - income: 112800, - }, - { - percentOfAmi: 60, - householdSize: 6, - income: 121140, - }, - { - percentOfAmi: 60, - householdSize: 7, - income: 129480, - }, - { - percentOfAmi: 80, - householdSize: 1, - income: 97440, - }, - { - percentOfAmi: 80, - householdSize: 2, - income: 111360, - }, - { - percentOfAmi: 80, - householdSize: 3, - income: 125280, - }, - { - percentOfAmi: 80, - householdSize: 4, - income: 139200, - }, - { - percentOfAmi: 80, - householdSize: 5, - income: 150400, - }, - { - percentOfAmi: 80, - householdSize: 6, - income: 161520, - }, - { - percentOfAmi: 80, - householdSize: 7, - income: 172640, - }, - { - percentOfAmi: 80, - householdSize: 8, - income: 183760, - }, - { - percentOfAmi: 100, - householdSize: 1, - income: 121800, - }, - { - percentOfAmi: 100, - householdSize: 2, - income: 139200, - }, - { - percentOfAmi: 100, - householdSize: 3, - income: 156600, - }, - { - percentOfAmi: 100, - householdSize: 4, - income: 174000, - }, - { - percentOfAmi: 100, - householdSize: 5, - income: 188000, - }, - { - percentOfAmi: 100, - householdSize: 6, - income: 201900, - }, - { - percentOfAmi: 100, - householdSize: 7, - income: 215800, - }, - { - percentOfAmi: 100, - householdSize: 8, - income: 229700, - }, - { - percentOfAmi: 120, - householdSize: 1, - income: 146160, - }, - { - percentOfAmi: 120, - householdSize: 2, - income: 167040, - }, - { - percentOfAmi: 120, - householdSize: 3, - income: 187920, - }, - { - percentOfAmi: 120, - householdSize: 4, - income: 208800, - }, - { - percentOfAmi: 120, - householdSize: 5, - income: 225600, - }, - { - percentOfAmi: 120, - householdSize: 6, - income: 242280, - }, - { - percentOfAmi: 120, - householdSize: 7, - income: 258960, - }, - { - percentOfAmi: 120, - householdSize: 8, - income: 275640, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/SanMateoHERASpecial2019.ts b/backend/core/src/seeds/ami-charts/SanMateoHERASpecial2019.ts deleted file mode 100644 index da83052da8..0000000000 --- a/backend/core/src/seeds/ami-charts/SanMateoHERASpecial2019.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" - -export const SanMateoHERASpecial2019: Omit = { - name: "SanMateoHERASpecial2019", - items: [ - { - percentOfAmi: 50, - householdSize: 1, - income: 56450, - }, - { - percentOfAmi: 50, - householdSize: 2, - income: 64500, - }, - { - percentOfAmi: 50, - householdSize: 3, - income: 72550, - }, - { - percentOfAmi: 50, - householdSize: 4, - income: 80600, - }, - { - percentOfAmi: 50, - householdSize: 5, - income: 87050, - }, - { - percentOfAmi: 50, - householdSize: 6, - income: 93500, - }, - { - percentOfAmi: 50, - householdSize: 7, - income: 99950, - }, - { - percentOfAmi: 50, - householdSize: 8, - income: 106400, - }, - { - percentOfAmi: 60, - householdSize: 1, - income: 71170, - }, - { - percentOfAmi: 60, - householdSize: 2, - income: 81340, - }, - { - percentOfAmi: 60, - householdSize: 3, - income: 91502, - }, - { - percentOfAmi: 60, - householdSize: 4, - income: 101630, - }, - { - percentOfAmi: 60, - householdSize: 5, - income: 109833, - }, - { - percentOfAmi: 60, - householdSize: 6, - income: 117924, - }, - { - percentOfAmi: 60, - householdSize: 7, - income: 126059, - }, - { - percentOfAmi: 60, - householdSize: 8, - income: 134219, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/SanMateoHOME2019.ts b/backend/core/src/seeds/ami-charts/SanMateoHOME2019.ts deleted file mode 100644 index 445d311dbe..0000000000 --- a/backend/core/src/seeds/ami-charts/SanMateoHOME2019.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" - -export const SanMateoHOME2019: Omit = { - name: "SanMateoHOME2019", - items: [ - { - percentOfAmi: 60, - householdSize: 1, - income: 71170, - }, - { - percentOfAmi: 60, - householdSize: 2, - income: 81340, - }, - { - percentOfAmi: 60, - householdSize: 3, - income: 91500, - }, - { - percentOfAmi: 60, - householdSize: 4, - income: 101630, - }, - { - percentOfAmi: 60, - householdSize: 5, - income: 109830, - }, - { - percentOfAmi: 60, - householdSize: 6, - income: 117920, - }, - { - percentOfAmi: 60, - householdSize: 7, - income: 126060, - }, - { - percentOfAmi: 60, - householdSize: 8, - income: 134219, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/SanMateoHUD2019.ts b/backend/core/src/seeds/ami-charts/SanMateoHUD2019.ts deleted file mode 100644 index c092997a3c..0000000000 --- a/backend/core/src/seeds/ami-charts/SanMateoHUD2019.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" - -export const SanMateoHUD2019: Omit = { - name: "SanMateoHUD2019", - items: [ - { - percentOfAmi: 120, - householdSize: 1, - income: 114900, - }, - { - percentOfAmi: 120, - householdSize: 2, - income: 131300, - }, - { - percentOfAmi: 120, - householdSize: 3, - income: 147750, - }, - { - percentOfAmi: 120, - householdSize: 4, - income: 164150, - }, - { - percentOfAmi: 120, - householdSize: 5, - income: 177300, - }, - { - percentOfAmi: 100, - householdSize: 1, - income: 95750, - }, - { - percentOfAmi: 100, - householdSize: 2, - income: 109450, - }, - { - percentOfAmi: 100, - householdSize: 3, - income: 123100, - }, - { - percentOfAmi: 100, - householdSize: 4, - income: 136800, - }, - { - percentOfAmi: 100, - householdSize: 5, - income: 147750, - }, - { - percentOfAmi: 100, - householdSize: 6, - income: 158700, - }, - { - percentOfAmi: 100, - householdSize: 7, - income: 169650, - }, - { - percentOfAmi: 100, - householdSize: 8, - income: 180600, - }, - { - percentOfAmi: 80, - householdSize: 1, - income: 90450, - }, - { - percentOfAmi: 80, - householdSize: 2, - income: 103350, - }, - { - percentOfAmi: 80, - householdSize: 3, - income: 116250, - }, - { - percentOfAmi: 80, - householdSize: 4, - income: 129150, - }, - { - percentOfAmi: 80, - householdSize: 5, - income: 139500, - }, - { - percentOfAmi: 80, - householdSize: 6, - income: 149850, - }, - { - percentOfAmi: 80, - householdSize: 7, - income: 160150, - }, - { - percentOfAmi: 80, - householdSize: 8, - income: 170500, - }, - { - percentOfAmi: 60, - householdSize: 1, - income: 71170, - }, - { - percentOfAmi: 60, - householdSize: 2, - income: 81340, - }, - { - percentOfAmi: 60, - householdSize: 3, - income: 91502, - }, - { - percentOfAmi: 60, - householdSize: 4, - income: 101629, - }, - { - percentOfAmi: 60, - householdSize: 5, - income: 109833, - }, - { - percentOfAmi: 60, - householdSize: 6, - income: 117924, - }, - { - percentOfAmi: 60, - householdSize: 7, - income: 126059, - }, - { - percentOfAmi: 60, - householdSize: 8, - income: 134219, - }, - { - percentOfAmi: 50, - householdSize: 1, - income: 56450, - }, - { - percentOfAmi: 50, - householdSize: 2, - income: 64500, - }, - { - percentOfAmi: 50, - householdSize: 3, - income: 72550, - }, - { - percentOfAmi: 50, - householdSize: 4, - income: 80600, - }, - { - percentOfAmi: 50, - householdSize: 5, - income: 87050, - }, - { - percentOfAmi: 50, - householdSize: 6, - income: 93500, - }, - { - percentOfAmi: 50, - householdSize: 7, - income: 99950, - }, - { - percentOfAmi: 50, - householdSize: 8, - income: 106400, - }, - { - percentOfAmi: 30, - householdSize: 1, - income: 33850, - }, - { - percentOfAmi: 30, - householdSize: 2, - income: 38700, - }, - { - percentOfAmi: 30, - householdSize: 3, - income: 43550, - }, - { - percentOfAmi: 30, - householdSize: 4, - income: 48350, - }, - { - percentOfAmi: 30, - householdSize: 5, - income: 52250, - }, - { - percentOfAmi: 30, - householdSize: 6, - income: 56100, - }, - { - percentOfAmi: 30, - householdSize: 7, - income: 60000, - }, - { - percentOfAmi: 30, - householdSize: 8, - income: 63850, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/SanMateoHUD2020.ts b/backend/core/src/seeds/ami-charts/SanMateoHUD2020.ts deleted file mode 100644 index 2ce05b0c86..0000000000 --- a/backend/core/src/seeds/ami-charts/SanMateoHUD2020.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" - -export const SanMateoHUD2020: Omit = { - name: "SanMateoHUD2020", - items: [ - { - percentOfAmi: 120, - householdSize: 1, - income: 120200, - }, - { - percentOfAmi: 120, - householdSize: 2, - income: 137350, - }, - { - percentOfAmi: 120, - householdSize: 3, - income: 154550, - }, - { - percentOfAmi: 120, - householdSize: 4, - income: 171700, - }, - { - percentOfAmi: 120, - householdSize: 5, - income: 185450, - }, - { - percentOfAmi: 120, - householdSize: 6, - income: 199150, - }, - { - percentOfAmi: 120, - householdSize: 7, - income: 212900, - }, - { - percentOfAmi: 80, - householdSize: 1, - income: 97600, - }, - { - percentOfAmi: 80, - householdSize: 2, - income: 111550, - }, - { - percentOfAmi: 80, - householdSize: 3, - income: 125500, - }, - { - percentOfAmi: 80, - householdSize: 4, - income: 139400, - }, - { - percentOfAmi: 80, - householdSize: 5, - income: 150600, - }, - { - percentOfAmi: 80, - householdSize: 6, - income: 161750, - }, - { - percentOfAmi: 80, - householdSize: 7, - income: 172900, - }, - { - percentOfAmi: 50, - householdSize: 1, - income: 60900, - }, - { - percentOfAmi: 50, - householdSize: 2, - income: 69600, - }, - { - percentOfAmi: 50, - householdSize: 3, - income: 78300, - }, - { - percentOfAmi: 50, - householdSize: 4, - income: 87000, - }, - { - percentOfAmi: 50, - householdSize: 5, - income: 94000, - }, - { - percentOfAmi: 50, - householdSize: 6, - income: 100950, - }, - { - percentOfAmi: 50, - householdSize: 7, - income: 107900, - }, - ], -} diff --git a/backend/core/src/seeds/ami-charts/index.ts b/backend/core/src/seeds/ami-charts/index.ts deleted file mode 100644 index c59748c4b2..0000000000 --- a/backend/core/src/seeds/ami-charts/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./AlamedaCountyLIHTC2020" -export * from "./AlamedaCountyTCAC2019" -export * from "./AlamedaCountyTCAC2020" -export * from "./OaklandFremontHUD2020" -export * from "./SanJoseTCAC2019" -export * from "./SanMateoCountyTCAC2019" -export * from "./SanMateoCountyTCAC2020" -export * from "./SanMateoHERASpecial2019" -export * from "./SanMateoHOME2019" -export * from "./SanMateoHUD2019" -export * from "./SanMateoHUD2020" diff --git a/backend/core/src/seeds/jurisdictions.ts b/backend/core/src/seeds/jurisdictions.ts deleted file mode 100644 index a06b4c43ed..0000000000 --- a/backend/core/src/seeds/jurisdictions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { INestApplicationContext } from "@nestjs/common" -import { JurisdictionsService } from "../jurisdictions/services/jurisdictions.service" -import { JurisdictionCreateDto } from "../jurisdictions/dto/jurisdiction.dto" - -export const defaultJurisdictions: JurisdictionCreateDto[] = [ - { name: "Alameda" }, - { name: "San Jose" }, - { name: "San Mateo" }, - { name: "Detroit" }, -] - -export async function createJurisdictions(app: INestApplicationContext) { - const jurisdictionService = await app.resolve(JurisdictionsService) - // some jurisdictions are added via previous migrations - const jurisdictions = await jurisdictionService.list() - const toInsert = defaultJurisdictions.filter( - (rec) => jurisdictions.findIndex((item) => item.name === rec.name) === -1 - ) - const inserted = await Promise.all( - toInsert.map(async (jurisdiction) => await jurisdictionService.create(jurisdiction)) - ) - // names are unique - return jurisdictions.concat(inserted).sort((a, b) => (a.name < b.name ? -1 : 1)) -} diff --git a/backend/core/src/seeds/listings/listing-coliseum-seed.ts b/backend/core/src/seeds/listings/listing-coliseum-seed.ts deleted file mode 100644 index 5cec5797e1..0000000000 --- a/backend/core/src/seeds/listings/listing-coliseum-seed.ts +++ /dev/null @@ -1,1046 +0,0 @@ -import { ListingSeedType, PropertySeedType, UnitSeedType } from "./listings" -import { - getDate, - getDefaultAmiChart, - getDefaultAssets, - getHopwaPreference, - getLiveWorkPreference, - getPbvPreference, - PriorityTypes, -} from "./shared" -import { AmiChart } from "../../ami-charts/entities/ami-chart.entity" -import { CSVFormattingType } from "../../csv/types/csv-formatting-type-enum" -import { ListingStatus } from "../../listings/types/listing-status-enum" -import { Listing } from "../../listings/entities/listing.entity" -import { BaseEntity, DeepPartial } from "typeorm" -import { ListingDefaultSeed } from "./listing-default-seed" -import { UnitStatus } from "../../units/types/unit-status-enum" -import { ListingReviewOrder } from "../../listings/types/listing-review-order-enum" -import { CountyCode } from "../../shared/types/county-code" -import { UnitCreateDto } from "../../units/dto/unit-create.dto" - -const coliseumProperty: PropertySeedType = { - accessibility: - "Fifteen (15) units are designed for residents with mobility impairments per HUD/U.F.A.S. guidelines with one (1) of these units further designed for residents with auditory or visual impairments. There are two (2) additional units with features for those with auditory or visual impairments. All the other units are adaptable. Accessible features in the property include: * 36” wide entries and doorways * Kitchens built to the accessibility standards of the California Building Code, including appliance controls and switch outlets within reach, and work surfaces and storage at accessible heights * Bathrooms built to the accessibility standards of the California Building Code, including grab bars, flexible shower spray hose, switch outlets within reach, and in-tub seats. * Closet rods and shelves at mobility height. * Window blinds/shades able to be used without grasping or twisting * Units for the Hearing & Visually Impaired will have a horn & strobe for fire alarm and a flashing light doorbell. The 44 non-ADA units are built to Adaptable standards.", - amenities: "Community room, bike parking, courtyard off the community room, 2nd floor courtyard.", - buildingAddress: { - county: "Alameda", - city: "Oakland", - street: "3300 Hawley Street", - zipCode: "94621", - state: "CA", - latitude: 37.7549632, - longitude: -122.1968792, - }, - buildingTotalUnits: 58, - developer: "Resources for Community Development", - neighborhood: "Coliseum", - petPolicy: "Permitted", - servicesOffered: - "Residential supportive services are provided to all residents on a volunteer basis.", - smokingPolicy: "No Smoking", - unitAmenities: null, - unitsAvailable: 46, - yearBuilt: 2021, -} -const coliseumUnits: Array = [ - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "36990", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 3, - minOccupancy: 1, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 1, - number: null, - sqFeet: "486", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "36990", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 3, - minOccupancy: 1, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 1, - number: null, - sqFeet: "491", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "36990", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 3, - minOccupancy: 1, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 1, - number: null, - sqFeet: "491", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "61650", - annualIncomeMin: "38520", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 3, - minOccupancy: 1, - monthlyIncomeMin: "3210", - monthlyRent: "1284", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 1, - number: null, - sqFeet: "491", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "44400", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 4, - minOccupancy: 2, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "44400", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 4, - minOccupancy: 2, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "785", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "44400", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 4, - minOccupancy: 2, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "785", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "44400", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 4, - minOccupancy: 2, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "785", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "44400", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 4, - minOccupancy: 2, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "785", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "44400", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 4, - minOccupancy: 2, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "785", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "44400", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 4, - minOccupancy: 2, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "785", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "44400", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 4, - minOccupancy: 2, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "785", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "44400", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 4, - minOccupancy: 2, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "785", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "30", - annualIncomeMax: "44400", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 4, - minOccupancy: 2, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "785", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "45", - annualIncomeMax: "66600", - annualIncomeMin: "41616", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3468", - monthlyRent: "1387", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "45", - annualIncomeMax: "66600", - annualIncomeMin: "41616", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3468", - monthlyRent: "1387", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "74000", - annualIncomeMin: "46236", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3853", - monthlyRent: "1541", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "20", - annualIncomeMax: "31800", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "20", - annualIncomeMax: "31800", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 6, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1080", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "20", - annualIncomeMax: "31800", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "45", - annualIncomeMax: "71550", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "45", - annualIncomeMax: "71550", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "45", - annualIncomeMax: "71550", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "79500", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "79500", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "79500", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "79500", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "79500", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "79500", - annualIncomeMin: "0", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 6, - minOccupancy: 4, - monthlyIncomeMin: "0", - monthlyRent: null, - monthlyRentAsPercentOfIncome: "30", - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "84950", - annualIncomeMin: "53436", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 7, - minOccupancy: 4, - monthlyIncomeMin: "4453", - monthlyRent: "1781", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "84950", - annualIncomeMin: "53436", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 7, - minOccupancy: 4, - monthlyIncomeMin: "4453", - monthlyRent: "1781", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "84950", - annualIncomeMin: "53436", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 7, - minOccupancy: 4, - monthlyIncomeMin: "4453", - monthlyRent: "1781", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "84950", - annualIncomeMin: "53436", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 7, - minOccupancy: 4, - monthlyIncomeMin: "4453", - monthlyRent: "1781", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "84950", - annualIncomeMin: "53436", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 7, - minOccupancy: 4, - monthlyIncomeMin: "4453", - monthlyRent: "1781", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "84950", - annualIncomeMin: "53436", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 7, - minOccupancy: 4, - monthlyIncomeMin: "4453", - monthlyRent: "1781", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50", - annualIncomeMax: "84950", - annualIncomeMin: "53436", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 7, - minOccupancy: 4, - monthlyIncomeMin: "4453", - monthlyRent: "1781", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 2, - numBedrooms: 3, - number: null, - sqFeet: "1029", - status: UnitStatus.available, - }, -] - -const coliseumListing: ListingSeedType = { - applicationAddress: { - county: "Alameda", - city: "Oakland", - street: "1701 Martin Luther King Way", - zipCode: "94621", - state: "CA", - latitude: 37.7549632, - longitude: -122.1968792, - }, - countyCode: CountyCode.alameda, - applicationDropOffAddress: null, - applicationDropOffAddressOfficeHours: null, - applicationMailingAddress: null, - applicationDueDate: getDate(1), - applicationDueTime: null, - applicationFee: "12", - applicationOpenDate: getDate(-10), - applicationOrganization: "John Stewart Company", - applicationPickUpAddress: { - county: "Alameda", - city: "Oakland", - street: "1701 Martin Luther King Way", - zipCode: "94621", - state: "CA", - latitude: 37.7549632, - longitude: -122.1968792, - }, - image: { - label: "test_label", - fileId: "fileid", - }, - applicationPickUpAddressOfficeHours: null, - buildingSelectionCriteria: null, - costsNotIncluded: - "Electricity, phone, TV, internet, and cable not included. For the PBV units, deposit is one month of the tenant-paid portion of rent (30% of income).", - creditHistory: - "Management staff will request credit histories on each adult member of each applicant household. It is the applicant’s responsibility that at least one household member can demonstrate utilities can be put in their name. For this to be demonstrated, at least one household member must have a credit report that shows no utility accounts in default. Applicants who cannot have utilities put in their name will be considered ineligible. Any currently open bankruptcy proceeding of any of the household members will be considered a disqualifying condition. Applicants will not be considered to have a poor credit history when they were delinquent in rent because they were withholding rent due to substandard housing conditions in a manner consistent with local ordinance; or had a poor rent paying history clearly related to an excessive rent relative to their income, and responsible efforts were made to resolve the non-payment problem. If there is a finding of any kind which would negatively impact an application, the applicant will be notified in writing. The applicant then shall have 14 calendar days in which such a finding may be appealed to staff for consideration.", - criminalBackground: null, - CSVFormattingType: CSVFormattingType.basic, - depositMax: "1,781", - depositMin: "1,284", - disableUnitsAccordion: true, - displayWaitlistSize: false, - leasingAgentAddress: { - county: "Alameda", - city: "Oakland", - street: "1701 Martin Luther King Way", - zipCode: "94621", - state: "CA", - latitude: 37.7549632, - longitude: -122.1968792, - }, - leasingAgentEmail: "coliseum@jsco.net", - leasingAgentName: "", - leasingAgentOfficeHours: - "Tuesdays & Thursdays, 9:00am to 5:00pm | Persons with disabilities who are unable to access the on-line application may request a Reasonable Accommodation by calling (510) 649-5739 for assistance. A TDD line is available at (415) 345-4470.", - leasingAgentPhone: "(510) 625-1632", - leasingAgentTitle: "Property Manager", - name: "Test: Coliseum", - postmarkedApplicationsReceivedByDate: null, - programRules: null, - rentalAssistance: "", - rentalHistory: "Two years' landlord history or homeless verification", - requiredDocuments: - "Application Document Checklist: https://org-housingbayarea-public-assets.s3-us-west-1.amazonaws.com/Tax+Credit+Application+Interview+Checklist.pdf", - reviewOrderType: "firstComeFirstServe" as ListingReviewOrder, - specialNotes: - "Priority Units: 3 apartments are set-aside for households eligible for the HOPWA program (Housing Opportunities for Persons with AIDS), which are households where a person has been medically diagnosed with HIV/AIDS. These 3 apartments also have Project-Based Section rental subsidies (tenant pays 30% of household income). 15 apartments are for those with mobility impairments and one of these units also has features for the hearing/visually impaired. Two additional apartments have features for the hearing/visually impaired. All units require eligibility requirements beyond income qualification: The waiting list will be ordered by incorporating the Alameda County preference for eligible households in which at least one member lives or works in the County. Three (3) apartments are restricted to households eligible under the HOPWA (Housing Opportunities for Persons with AIDS), which are households where a person has been medically diagnosed with HIV/AIDS. These apartments also receive PBV’s from OHA. For the twenty-five (25) apartments that have Project-Based Section 8 Vouchers from OHA, applicants will be called for an interview in the order according to the site-based waiting list compiled from the initial application and lotter process specifically for the PBV units. The waiting list order for these apartments will also incorporate the local preferences required by OHA. These preferences are: * A Residency preference (Applicants who live or work in the City of Oakland at the time of the application interview and/or applicants that lived or worked in the City of Oakland at the time of submitting their initial application and can verify their previous residency/employment at the applicant interview, qualify for this preference). * A Family preference (Applicant families with two or more persons, or a single person applicant that is 62 years of age or older, or a single person applicant with a disability, qualify for this preference). * A Veteran and active members of the military preference. Per OHA policy, a Veteran is a person who served in the active military, naval, or air service and who was discharged or released from such service under conditions other than dishonorable. * A Homeless preference. Applicant families who meet the McKinney-Vento Act definition of homeless qualify for this preference (see definition below). Each PBV applicant will receive one point for each preference for which it is eligible and the site-based PBV waiting list will be prioritized by the number of points applicants have from these preferences. Applicants for the PBV units must comply with OHA’s policy regarding Social Security Numbers. The applicant and all members of the applicant’s household must disclose the complete and accurate social security number (SSN) assigned to each household member, and they must provide the documentation necessary to verify each SSN. As an EveryOne Home partner, each applicant’s individual circumstances will be evaluated, alternative forms of verification and additional information submitted by the applicant will considered, and reasonable accommodations will be provided when requested and if verified and necessary. Persons with disabilities are encouraged to apply.", - status: ListingStatus.active, - waitlistCurrentSize: 0, - waitlistMaxSize: 3000, - waitlistOpenSpots: 3000, - isWaitlistOpen: true, - whatToExpect: null, -} - -export class ListingColiseumSeed extends ListingDefaultSeed { - async seed() { - const priorityTypeMobilityAndHearingWithVisual = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( - { - name: PriorityTypes.mobilityHearingVisual, - } - ) - const priorityTypeMobilityAndMobilityWithHearingAndVisual = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( - { - name: PriorityTypes.mobilityHearingVisual, - } - ) - const priorityTypeMobilityAndHearing = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( - { - name: PriorityTypes.mobilityHearing, - } - ) - const priorityMobility = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail({ - name: PriorityTypes.mobility, - }) - const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) - const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) - const unitTypeThreeBdrm = await this.unitTypeRepository.findOneOrFail({ name: "threeBdrm" }) - - const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ - name: CountyCode.alameda, - }) - const amiChart = await this.amiChartRepository.save({ - ...getDefaultAmiChart(), - jurisdiction: alamedaJurisdiction, - }) - - const property = await this.propertyRepository.save({ - ...coliseumProperty, - }) - - const unitsToBeCreated: Array> = coliseumUnits.map( - (unit) => { - return { - ...unit, - property: { - id: property.id, - }, - amiChart, - } - } - ) - - // Assign priorityTypes - for (let i = 0; i < 3; i++) { - unitsToBeCreated[i].priorityType = priorityTypeMobilityAndMobilityWithHearingAndVisual - } - for (let i = 3; i < 14; i++) { - unitsToBeCreated[i].priorityType = priorityTypeMobilityAndHearingWithVisual - } - for (let i = 14; i < 27; i++) { - unitsToBeCreated[i].priorityType = priorityTypeMobilityAndHearing - } - for (let i = 27; i < 46; i++) { - unitsToBeCreated[i].priorityType = priorityMobility - } - - // Assign unit types - for (let i = 0; i < 4; i++) { - unitsToBeCreated[i].unitType = unitTypeOneBdrm - } - for (let i = 4; i < 27; i++) { - unitsToBeCreated[i].unitType = unitTypeTwoBdrm - } - for (let i = 27; i < 46; i++) { - unitsToBeCreated[i].unitType = unitTypeThreeBdrm - } - - await this.unitsRepository.save(unitsToBeCreated) - - const listingCreateDto: Omit< - DeepPartial, - keyof BaseEntity | "urlSlug" | "showWaitlist" - > = { - ...coliseumListing, - property: property, - assets: getDefaultAssets(), - preferences: [ - getLiveWorkPreference(), - { ...getPbvPreference(), ordinal: 2, page: 2 }, - { ...getHopwaPreference(), ordinal: 3, page: 3 }, - ], - events: [], - } - - return await this.listingRepository.save(listingCreateDto) - } -} diff --git a/backend/core/src/seeds/listings/listing-default-missing-ami.ts b/backend/core/src/seeds/listings/listing-default-missing-ami.ts deleted file mode 100644 index 03bfed6921..0000000000 --- a/backend/core/src/seeds/listings/listing-default-missing-ami.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { ListingDefaultSeed } from "./listing-default-seed" -import { getDefaultProperty } from "./shared" -import { BaseEntity } from "typeorm" -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { UnitSeedType } from "./listings" -import { AmiChart } from "../../ami-charts/entities/ami-chart.entity" -import { UnitStatus } from "../../units/types/unit-status-enum" -import { UnitCreateDto } from "../../units/dto/unit-create.dto" -import { CountyCode } from "../../shared/types/county-code" - -export const missingAmiLevelsChart: Omit = { - name: "Missing Household Ami Levels", - items: [ - { - percentOfAmi: 50, - householdSize: 3, - income: 65850, - }, - { - percentOfAmi: 50, - householdSize: 4, - income: 73150, - }, - { - percentOfAmi: 50, - householdSize: 5, - income: 79050, - }, - { - percentOfAmi: 50, - householdSize: 6, - income: 84900, - }, - { - percentOfAmi: 50, - householdSize: 7, - income: 90750, - }, - { - percentOfAmi: 50, - householdSize: 8, - income: 96600, - }, - ], -} - -const missingAmiLevelsUnits: Array = [ - { - amiChart: missingAmiLevelsChart as AmiChart, - amiPercentage: "50", - annualIncomeMax: "177300.0", - annualIncomeMin: "84696.0", - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "7058.0", - monthlyRent: "3340.0", - monthlyRentAsPercentOfIncome: null, - numBathrooms: null, - numBedrooms: 2, - number: null, - priorityType: null, - sqFeet: "1100", - status: UnitStatus.occupied, - }, - { - amiChart: missingAmiLevelsChart as AmiChart, - amiPercentage: "50", - annualIncomeMax: "103350.0", - annualIncomeMin: "58152.0", - floor: 1, - maxOccupancy: 2, - minOccupancy: 1, - monthlyIncomeMin: "4858.0", - monthlyRent: "2624.0", - monthlyRentAsPercentOfIncome: null, - numBathrooms: null, - numBedrooms: 1, - number: null, - priorityType: null, - sqFeet: "750", - status: UnitStatus.occupied, - }, - { - amiChart: missingAmiLevelsChart as AmiChart, - amiPercentage: "50", - annualIncomeMax: "103350.0", - annualIncomeMin: "58152.0", - floor: 1, - maxOccupancy: 2, - minOccupancy: 1, - monthlyIncomeMin: "4858.0", - monthlyRent: "2624.0", - monthlyRentAsPercentOfIncome: null, - numBathrooms: null, - numBedrooms: 1, - number: null, - priorityType: null, - sqFeet: "750", - status: UnitStatus.occupied, - }, - { - amiChart: missingAmiLevelsChart as AmiChart, - amiPercentage: "50", - annualIncomeMax: "103350.0", - annualIncomeMin: "58152.0", - floor: 1, - maxOccupancy: 2, - minOccupancy: 1, - monthlyIncomeMin: "4858.0", - monthlyRent: "2624.0", - monthlyRentAsPercentOfIncome: null, - numBathrooms: null, - numBedrooms: 1, - number: null, - priorityType: null, - sqFeet: "750", - status: UnitStatus.occupied, - }, - { - amiChart: missingAmiLevelsChart as AmiChart, - amiPercentage: "50", - annualIncomeMax: "103350.0", - annualIncomeMin: "38952.0", - floor: 1, - maxOccupancy: 2, - minOccupancy: 1, - monthlyIncomeMin: "3246.0", - monthlyRent: "1575.0", - monthlyRentAsPercentOfIncome: null, - numBathrooms: null, - numBedrooms: 1, - number: null, - priorityType: null, - sqFeet: "750", - status: UnitStatus.occupied, - }, -] -export class ListingDefaultMissingAMI extends ListingDefaultSeed { - async seed() { - const listing = await super.seed() - - const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) - - const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ - name: CountyCode.alameda, - }) - const amiChart = await this.amiChartRepository.save({ - ...missingAmiLevelsChart, - jurisdiction: alamedaJurisdiction, - }) - - const property = await this.propertyRepository.save({ - ...getDefaultProperty(), - }) - - const unitsToBeCreated: Array> = missingAmiLevelsUnits.map((unit) => { - return { - ...unit, - property: { - id: property.id, - }, - amiChart, - } - }) - - unitsToBeCreated.forEach((unit) => { - unit.unitType = unitTypeOneBdrm - }) - - await this.unitsRepository.save(unitsToBeCreated) - - return await this.listingRepository.save({ - ...listing, - property: property, - name: "Test: Default, Missing Household Levels in AMI", - }) - } -} diff --git a/backend/core/src/seeds/listings/listing-default-multiple-ami.ts b/backend/core/src/seeds/listings/listing-default-multiple-ami.ts deleted file mode 100644 index b37f916ed0..0000000000 --- a/backend/core/src/seeds/listings/listing-default-multiple-ami.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ListingDefaultSeed } from "./listing-default-seed" -import { getDefaultAmiChart, getDefaultUnits, getDefaultProperty } from "./shared" -import { tritonAmiChart } from "./listing-triton-seed" -import { BaseEntity } from "typeorm" -import { UnitCreateDto } from "../../units/dto/unit-create.dto" -import { CountyCode } from "../../shared/types/county-code" - -export class ListingDefaultMultipleAMI extends ListingDefaultSeed { - async seed() { - const listing = await super.seed() - - const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) - - const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ - name: CountyCode.alameda, - }) - const amiChartOne = await this.amiChartRepository.save({ - ...tritonAmiChart, - jurisdiction: alamedaJurisdiction, - }) - const amiChartTwo = await this.amiChartRepository.save({ - ...getDefaultAmiChart(), - jurisdiction: alamedaJurisdiction, - }) - - const property = await this.propertyRepository.save({ - ...getDefaultProperty(), - }) - - const unitsToBeCreated: Array> = getDefaultUnits().map( - (unit, index) => { - return { - ...unit, - property: { - id: property.id, - }, - amiChart: index % 2 === 0 ? amiChartOne : amiChartTwo, - } - } - ) - - unitsToBeCreated[0].unitType = unitTypeOneBdrm - unitsToBeCreated[1].unitType = unitTypeOneBdrm - - await this.unitsRepository.save(unitsToBeCreated) - - return await this.listingRepository.save({ - ...listing, - property: property, - name: "Test: Default, Multiple AMI", - }) - } -} diff --git a/backend/core/src/seeds/listings/listing-default-one-preference-seed.ts b/backend/core/src/seeds/listings/listing-default-one-preference-seed.ts deleted file mode 100644 index 47536539f0..0000000000 --- a/backend/core/src/seeds/listings/listing-default-one-preference-seed.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getLiveWorkPreference } from "./shared" -import { ListingDefaultSeed } from "./listing-default-seed" -import { Preference } from "../../preferences/entities/preference.entity" - -export class ListingDefaultOnePreferenceSeed extends ListingDefaultSeed { - async seed() { - const listing = await super.seed() - return await this.listingRepository.save({ - ...listing, - name: "Test: Default, One Preference", - preferences: [getLiveWorkPreference() as Preference], - }) - } -} diff --git a/backend/core/src/seeds/listings/listing-default-seed.ts b/backend/core/src/seeds/listings/listing-default-seed.ts deleted file mode 100644 index ab6675ffe9..0000000000 --- a/backend/core/src/seeds/listings/listing-default-seed.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { InjectRepository } from "@nestjs/typeorm" -import { BaseEntity, DeepPartial, Repository } from "typeorm" - -import { Listing } from "../../listings/entities/listing.entity" -import { UnitAccessibilityPriorityType } from "../../unit-accessbility-priority-types/entities/unit-accessibility-priority-type.entity" -import { UnitType } from "../../unit-types/entities/unit-type.entity" -import { ReservedCommunityType } from "../../reserved-community-type/entities/reserved-community-type.entity" -import { AmiChart } from "../../ami-charts/entities/ami-chart.entity" -import { Property } from "../../property/entities/property.entity" -import { Unit } from "../../units/entities/unit.entity" -import { User } from "../../auth/entities/user.entity" -import { - getDefaultAmiChart, - getDefaultAssets, - getDefaultListing, - getDefaultListingEvents, - getDefaultProperty, - getDefaultUnits, - getDisplaceePreference, - getLiveWorkPreference, - PriorityTypes, -} from "./shared" -import { ApplicationMethod } from "../../application-methods/entities/application-method.entity" -import { UnitCreateDto } from "../../units/dto/unit-create.dto" -import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" -import { CountyCode } from "../../shared/types/county-code" - -export class ListingDefaultSeed { - constructor( - @InjectRepository(Listing) protected readonly listingRepository: Repository, - @InjectRepository(UnitAccessibilityPriorityType) - protected readonly unitAccessibilityPriorityTypeRepository: Repository< - UnitAccessibilityPriorityType - >, - @InjectRepository(UnitType) protected readonly unitTypeRepository: Repository, - @InjectRepository(ReservedCommunityType) - protected readonly reservedTypeRepository: Repository, - @InjectRepository(AmiChart) protected readonly amiChartRepository: Repository, - @InjectRepository(Property) protected readonly propertyRepository: Repository, - @InjectRepository(Unit) protected readonly unitsRepository: Repository, - @InjectRepository(User) protected readonly userRepository: Repository, - @InjectRepository(ApplicationMethod) - protected readonly applicationMethodRepository: Repository, - @InjectRepository(Jurisdiction) - protected readonly jurisdictionRepository: Repository - ) {} - - async seed() { - const priorityTypeMobilityAndHearing = await this.unitAccessibilityPriorityTypeRepository.findOneOrFail( - { name: PriorityTypes.mobilityHearing } - ) - const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) - const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) - const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ - name: CountyCode.alameda, - }) - const amiChart = await this.amiChartRepository.save({ - ...getDefaultAmiChart(), - jurisdiction: alamedaJurisdiction, - }) - - const property = await this.propertyRepository.save({ - ...getDefaultProperty(), - }) - - const unitsToBeCreated: Array> = getDefaultUnits().map( - (unit) => { - return { - ...unit, - property: { - id: property.id, - }, - amiChart, - } - } - ) - - unitsToBeCreated[0].priorityType = priorityTypeMobilityAndHearing - unitsToBeCreated[1].priorityType = priorityTypeMobilityAndHearing - unitsToBeCreated[0].unitType = unitTypeOneBdrm - unitsToBeCreated[1].unitType = unitTypeTwoBdrm - const newUnits = await this.unitsRepository.save(unitsToBeCreated) - - const listingCreateDto: Omit< - DeepPartial, - keyof BaseEntity | "urlSlug" | "showWaitlist" - > = { - ...getDefaultListing(), - amiChartOverrides: [ - { - unit: { id: newUnits[0].id }, - items: [ - { - percentOfAmi: 80, - householdSize: 1, - income: 777777, - }, - ], - }, - ], - name: "Test: Default, Two Preferences", - property: property, - assets: getDefaultAssets(), - preferences: [getLiveWorkPreference(), { ...getDisplaceePreference(), ordinal: 2 }], - events: getDefaultListingEvents(), - } - - return await this.listingRepository.save(listingCreateDto) - } -} diff --git a/backend/core/src/seeds/listings/listing-triton-seed.ts b/backend/core/src/seeds/listings/listing-triton-seed.ts deleted file mode 100644 index 0e3d134afb..0000000000 --- a/backend/core/src/seeds/listings/listing-triton-seed.ts +++ /dev/null @@ -1,810 +0,0 @@ -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { ListingSeedType, PropertySeedType, UnitSeedType } from "./listings" -import { getDefaultAmiChart, getDate, getDefaultAssets, getLiveWorkPreference } from "./shared" -import { ListingStatus } from "../../listings/types/listing-status-enum" -import { CSVFormattingType } from "../../csv/types/csv-formatting-type-enum" -import { AmiChart } from "../../ami-charts/entities/ami-chart.entity" -import { ListingDefaultSeed } from "./listing-default-seed" -import { BaseEntity, DeepPartial } from "typeorm" -import { Listing } from "../../listings/entities/listing.entity" -import { UnitStatus } from "../../units/types/unit-status-enum" -import { ListingReviewOrder } from "../../listings/types/listing-review-order-enum" -import { CountyCode } from "../../shared/types/county-code" -import { UnitCreateDto } from "../../units/dto/unit-create.dto" - -export const tritonAmiChart: Omit = { - name: "San Jose TCAC 2019", - items: [ - { - percentOfAmi: 120, - householdSize: 1, - income: 110400, - }, - { - percentOfAmi: 120, - householdSize: 2, - income: 126150, - }, - { - percentOfAmi: 120, - householdSize: 3, - income: 141950, - }, - { - percentOfAmi: 120, - householdSize: 4, - income: 157700, - }, - { - percentOfAmi: 120, - householdSize: 5, - income: 170300, - }, - { - percentOfAmi: 120, - householdSize: 6, - income: 182950, - }, - { - percentOfAmi: 120, - householdSize: 7, - income: 195550, - }, - { - percentOfAmi: 120, - householdSize: 8, - income: 208150, - }, - { - percentOfAmi: 110, - householdSize: 1, - income: 101200, - }, - { - percentOfAmi: 110, - householdSize: 2, - income: 115610, - }, - { - percentOfAmi: 110, - householdSize: 3, - income: 130075, - }, - { - percentOfAmi: 110, - householdSize: 4, - income: 144540, - }, - { - percentOfAmi: 110, - householdSize: 5, - income: 156090, - }, - { - percentOfAmi: 110, - householdSize: 6, - income: 167640, - }, - { - percentOfAmi: 110, - householdSize: 7, - income: 179245, - }, - { - percentOfAmi: 110, - householdSize: 8, - income: 190795, - }, - { - percentOfAmi: 100, - householdSize: 1, - income: 92000, - }, - { - percentOfAmi: 100, - householdSize: 2, - income: 105100, - }, - { - percentOfAmi: 100, - householdSize: 3, - income: 118250, - }, - { - percentOfAmi: 100, - householdSize: 4, - income: 131400, - }, - { - percentOfAmi: 100, - householdSize: 5, - income: 141900, - }, - { - percentOfAmi: 100, - householdSize: 6, - income: 152400, - }, - { - percentOfAmi: 100, - householdSize: 7, - income: 162950, - }, - { - percentOfAmi: 100, - householdSize: 8, - income: 173450, - }, - { - percentOfAmi: 80, - householdSize: 1, - income: 72750, - }, - { - percentOfAmi: 80, - householdSize: 2, - income: 83150, - }, - { - percentOfAmi: 80, - householdSize: 3, - income: 93550, - }, - { - percentOfAmi: 80, - householdSize: 4, - income: 103900, - }, - { - percentOfAmi: 80, - householdSize: 5, - income: 112250, - }, - { - percentOfAmi: 80, - householdSize: 6, - income: 120550, - }, - { - percentOfAmi: 80, - householdSize: 7, - income: 128850, - }, - { - percentOfAmi: 80, - householdSize: 8, - income: 137150, - }, - { - percentOfAmi: 60, - householdSize: 1, - income: 61500, - }, - { - percentOfAmi: 60, - householdSize: 2, - income: 70260, - }, - { - percentOfAmi: 60, - householdSize: 3, - income: 79020, - }, - { - percentOfAmi: 60, - householdSize: 4, - income: 87780, - }, - { - percentOfAmi: 60, - householdSize: 5, - income: 94860, - }, - { - percentOfAmi: 60, - householdSize: 6, - income: 101880, - }, - { - percentOfAmi: 60, - householdSize: 7, - income: 108900, - }, - { - percentOfAmi: 60, - householdSize: 8, - income: 115920, - }, - { - percentOfAmi: 55, - householdSize: 1, - income: 56375, - }, - { - percentOfAmi: 55, - householdSize: 2, - income: 64405, - }, - { - percentOfAmi: 55, - householdSize: 3, - income: 72435, - }, - { - percentOfAmi: 55, - householdSize: 4, - income: 80465, - }, - { - percentOfAmi: 55, - householdSize: 5, - income: 86955, - }, - { - percentOfAmi: 55, - householdSize: 6, - income: 93390, - }, - { - percentOfAmi: 55, - householdSize: 7, - income: 99825, - }, - { - percentOfAmi: 55, - householdSize: 8, - income: 106260, - }, - { - percentOfAmi: 50, - householdSize: 1, - income: 51250, - }, - { - percentOfAmi: 50, - householdSize: 2, - income: 58550, - }, - { - percentOfAmi: 50, - householdSize: 3, - income: 65850, - }, - { - percentOfAmi: 50, - householdSize: 4, - income: 73150, - }, - { - percentOfAmi: 50, - householdSize: 5, - income: 79050, - }, - { - percentOfAmi: 50, - householdSize: 6, - income: 84900, - }, - { - percentOfAmi: 50, - householdSize: 7, - income: 90750, - }, - { - percentOfAmi: 50, - householdSize: 8, - income: 96600, - }, - { - percentOfAmi: 45, - householdSize: 1, - income: 46125, - }, - { - percentOfAmi: 45, - householdSize: 2, - income: 52695, - }, - { - percentOfAmi: 45, - householdSize: 3, - income: 59265, - }, - { - percentOfAmi: 45, - householdSize: 4, - income: 65835, - }, - { - percentOfAmi: 45, - householdSize: 5, - income: 71145, - }, - { - percentOfAmi: 45, - householdSize: 6, - income: 76410, - }, - { - percentOfAmi: 45, - householdSize: 7, - income: 81675, - }, - { - percentOfAmi: 40, - householdSize: 1, - income: 41000, - }, - { - percentOfAmi: 40, - householdSize: 2, - income: 46840, - }, - { - percentOfAmi: 40, - householdSize: 3, - income: 52680, - }, - { - percentOfAmi: 40, - householdSize: 4, - income: 58520, - }, - { - percentOfAmi: 40, - householdSize: 5, - income: 63240, - }, - { - percentOfAmi: 40, - householdSize: 6, - income: 67920, - }, - { - percentOfAmi: 40, - householdSize: 7, - income: 72600, - }, - { - percentOfAmi: 40, - householdSize: 8, - income: 77280, - }, - { - percentOfAmi: 35, - householdSize: 1, - income: 35875, - }, - { - percentOfAmi: 35, - householdSize: 2, - income: 40985, - }, - { - percentOfAmi: 35, - householdSize: 3, - income: 46095, - }, - { - percentOfAmi: 35, - householdSize: 4, - income: 51205, - }, - { - percentOfAmi: 35, - householdSize: 5, - income: 55335, - }, - { - percentOfAmi: 35, - householdSize: 6, - income: 59430, - }, - { - percentOfAmi: 35, - householdSize: 7, - income: 63525, - }, - { - percentOfAmi: 35, - householdSize: 8, - income: 67620, - }, - { - percentOfAmi: 30, - householdSize: 1, - income: 30750, - }, - { - percentOfAmi: 30, - householdSize: 2, - income: 35130, - }, - { - percentOfAmi: 30, - householdSize: 3, - income: 39510, - }, - { - percentOfAmi: 30, - householdSize: 4, - income: 43890, - }, - { - percentOfAmi: 30, - householdSize: 5, - income: 47430, - }, - { - percentOfAmi: 30, - householdSize: 6, - income: 50940, - }, - { - percentOfAmi: 30, - householdSize: 7, - income: 54450, - }, - { - percentOfAmi: 25, - householdSize: 1, - income: 25625, - }, - { - percentOfAmi: 25, - householdSize: 2, - income: 29275, - }, - { - percentOfAmi: 25, - householdSize: 3, - income: 32925, - }, - { - percentOfAmi: 25, - householdSize: 4, - income: 36575, - }, - { - percentOfAmi: 25, - householdSize: 5, - income: 39525, - }, - { - percentOfAmi: 25, - householdSize: 6, - income: 42450, - }, - { - percentOfAmi: 25, - householdSize: 7, - income: 45375, - }, - { - percentOfAmi: 25, - householdSize: 8, - income: 48300, - }, - { - percentOfAmi: 20, - householdSize: 1, - income: 20500, - }, - { - percentOfAmi: 20, - householdSize: 2, - income: 23420, - }, - { - percentOfAmi: 20, - householdSize: 3, - income: 26340, - }, - { - percentOfAmi: 20, - householdSize: 4, - income: 29260, - }, - { - percentOfAmi: 20, - householdSize: 5, - income: 31620, - }, - { - percentOfAmi: 20, - householdSize: 6, - income: 33960, - }, - { - percentOfAmi: 20, - householdSize: 7, - income: 36300, - }, - { - percentOfAmi: 20, - householdSize: 8, - income: 38640, - }, - { - percentOfAmi: 15, - householdSize: 1, - income: 15375, - }, - { - percentOfAmi: 15, - householdSize: 2, - income: 17565, - }, - { - percentOfAmi: 15, - householdSize: 3, - income: 19755, - }, - { - percentOfAmi: 15, - householdSize: 4, - income: 21945, - }, - { - percentOfAmi: 15, - householdSize: 5, - income: 23715, - }, - { - percentOfAmi: 15, - householdSize: 6, - income: 25470, - }, - { - percentOfAmi: 15, - householdSize: 7, - income: 27225, - }, - { - percentOfAmi: 15, - householdSize: 8, - income: 28980, - }, - ], -} -const tritonProperty: PropertySeedType = { - accessibility: - "Accessibility features in common areas like lobby – wheelchair ramps, wheelchair accessible bathrooms and elevators.", - amenities: "Gym, Clubhouse, Business Lounge, View Lounge, Pool, Spa", - buildingAddress: { - city: "Foster City", - county: "San Mateo", - state: "CA", - street: "55 Triton Park Lane", - zipCode: "94404", - latitude: 37.5658152, - longitude: -122.2704286, - }, - buildingTotalUnits: 48, - developer: "Thompson Dorfman, LLC", - neighborhood: "Foster City", - petPolicy: - "Pets allowed except the following; pit bull, malamute, akita, rottweiler, doberman, staffordshire terrier, presa canario, chowchow, american bull dog, karelian bear dog, st bernard, german shepherd, husky, great dane, any hybrid or mixed breed of the aforementioned breeds. 50 pound weight limit. 2 pets per household limit. $500 pet deposit per pet. $60 pet rent per pet.", - servicesOffered: null, - smokingPolicy: "Non-Smoking", - unitAmenities: "Washer and dryer, AC and Heater, Gas Stove", - unitsAvailable: 4, - yearBuilt: 2021, -} -const tritonUnits: Array = [ - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "120.0", - annualIncomeMax: "177300.0", - annualIncomeMin: "84696.0", - floor: 1, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "7058.0", - monthlyRent: "3340.0", - monthlyRentAsPercentOfIncome: null, - numBathrooms: null, - numBedrooms: 2, - number: null, - priorityType: null, - sqFeet: "1100", - status: UnitStatus.occupied, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "80.0", - annualIncomeMax: "103350.0", - annualIncomeMin: "58152.0", - floor: 1, - maxOccupancy: 2, - minOccupancy: 1, - monthlyIncomeMin: "4858.0", - monthlyRent: "2624.0", - monthlyRentAsPercentOfIncome: null, - numBathrooms: null, - numBedrooms: 1, - number: null, - priorityType: null, - sqFeet: "750", - status: UnitStatus.occupied, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "80.0", - annualIncomeMax: "103350.0", - annualIncomeMin: "58152.0", - floor: 1, - maxOccupancy: 2, - minOccupancy: 1, - monthlyIncomeMin: "4858.0", - monthlyRent: "2624.0", - monthlyRentAsPercentOfIncome: null, - numBathrooms: null, - numBedrooms: 1, - number: null, - priorityType: null, - sqFeet: "750", - status: UnitStatus.occupied, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "80.0", - annualIncomeMax: "103350.0", - annualIncomeMin: "58152.0", - floor: 1, - maxOccupancy: 2, - minOccupancy: 1, - monthlyIncomeMin: "4858.0", - monthlyRent: "2624.0", - monthlyRentAsPercentOfIncome: null, - numBathrooms: null, - numBedrooms: 1, - number: null, - priorityType: null, - sqFeet: "750", - status: UnitStatus.occupied, - }, - { - amiChart: getDefaultAmiChart() as AmiChart, - amiPercentage: "50.0", - annualIncomeMax: "103350.0", - annualIncomeMin: "38952.0", - floor: 1, - maxOccupancy: 2, - minOccupancy: 1, - monthlyIncomeMin: "3246.0", - monthlyRent: "1575.0", - monthlyRentAsPercentOfIncome: null, - numBathrooms: null, - numBedrooms: 1, - number: null, - priorityType: null, - sqFeet: "750", - status: UnitStatus.occupied, - }, -] - -const tritonListing: ListingSeedType = { - applicationAddress: { - city: "Foster City", - state: "CA", - street: "55 Triton Park Lane", - zipCode: "94404", - latitude: 37.5658152, - longitude: -122.2704286, - }, - countyCode: CountyCode.alameda, - applicationDropOffAddress: null, - applicationDropOffAddressOfficeHours: null, - applicationMailingAddress: null, - applicationDueDate: getDate(5), - applicationFee: "38.0", - applicationOpenDate: getDate(-10), - applicationDueTime: null, - applicationOrganization: "Triton", - applicationPickUpAddress: { - city: "Foster City", - state: "CA", - street: "55 Triton Park Lane", - zipCode: "94404", - latitude: 37.5658152, - longitude: -122.2704286, - }, - image: { - label: "test_label", - fileId: "fileid", - }, - applicationPickUpAddressOfficeHours: null, - buildingSelectionCriteria: - "https://regional-dahlia-staging.s3-us-west-1.amazonaws.com/listings/triton/The_Triton_BMR_rental_information.pdf", - costsNotIncluded: - "Residents responsible for PG&E, Internet, Utilities - water, sewer, trash, admin fee. Pet Deposit is $500 with a $60 monthly pet rent. Residents required to maintain a renter's insurance policy as outlined in the lease agreement. Rent is due by the 3rd of each month. Late fee is $50.00. Resident to pay $25 for each returned check or rejected electronic payment. For additional returned checks, resident will pay a charge of $50.00.", - creditHistory: - "No collections, no bankruptcy, income is twice monthly rent A credit report will be completed on all applicants to verify credit ratings.\n\nIncome plus verified credit history will be entered into a credit scoring model to determine rental eligibility and security deposit levels. All decisions for residency are based on a system which considers credit history, rent history, income qualifications, and employment history. An approved decision based on the system does not automatically constittute an approval of residency. Applicant(s) and occupant(s) aged 18 years or older MUST also pass the criminal background check based on the criteria contained herein to be approved for residency. \n\nCredit recommendations other than an accept decision, will require a rental verification. Applications for residency will automatically be denied for the following reasons:\n\n- a. An outstanding debt to a previous landlord or an outstanding NSF check must be paid in full\n- b. An unsatisfied breach of a prior lease or a prior eviction of any applicant or occupant\n- c. More than four (4) late pays and two (2) NSF's in the last twenty-four (24) months", - criminalBackground: null, - CSVFormattingType: CSVFormattingType.basic, - depositMax: "800", - depositMin: "500", - disableUnitsAccordion: true, - displayWaitlistSize: false, - leasingAgentAddress: { - city: "Foster City", - state: "CA", - street: "55 Triton Park Lane", - zipCode: "94404", - latitude: 37.5658152, - longitude: -122.2704286, - }, - leasingAgentEmail: "thetriton@legacypartners.com", - leasingAgentName: "Francis Santos", - leasingAgentOfficeHours: "Monday - Friday, 9:00 am - 5:00 pm", - leasingAgentPhone: "650-437-2039", - leasingAgentTitle: "Business Manager", - name: "Test: Triton", - postmarkedApplicationsReceivedByDate: null, - programRules: null, - rentalAssistance: "", - rentalHistory: "No evictions", - requiredDocuments: - "Due at interview - Paystubs, 3 months’ bank statements, recent tax returns or non-tax affidavit, recent retirement statement, application to lease, application qualifying criteria, social security card, state or nation ID. For self-employed, copy of IRS Tax Return including schedule C and current or most recent clients. Unemployment if applicable. Child support/Alimony; current notice from DA office, a court order or a letter from the provider with copies of last two checks. Any other income etc", - reviewOrderType: "firstComeFirstServe" as ListingReviewOrder, - specialNotes: null, - status: ListingStatus.active, - waitlistCurrentSize: 400, - waitlistMaxSize: 600, - waitlistOpenSpots: 200, - isWaitlistOpen: true, - whatToExpect: null, -} - -export class ListingTritonSeed extends ListingDefaultSeed { - async seed() { - const unitTypeOneBdrm = await this.unitTypeRepository.findOneOrFail({ name: "oneBdrm" }) - const unitTypeTwoBdrm = await this.unitTypeRepository.findOneOrFail({ name: "twoBdrm" }) - - const alamedaJurisdiction = await this.jurisdictionRepository.findOneOrFail({ - name: CountyCode.alameda, - }) - const amiChart = await this.amiChartRepository.save({ - ...tritonAmiChart, - jurisdiction: alamedaJurisdiction, - }) - - const property = await this.propertyRepository.save({ - ...tritonProperty, - }) - - const unitsToBeCreated: Array> = tritonUnits.map( - (unit) => { - return { - ...unit, - property: { - id: property.id, - }, - amiChart, - } - } - ) - - unitsToBeCreated[0].unitType = unitTypeTwoBdrm - unitsToBeCreated[1].unitType = unitTypeOneBdrm - unitsToBeCreated[2].unitType = unitTypeOneBdrm - unitsToBeCreated[3].unitType = unitTypeOneBdrm - unitsToBeCreated[4].unitType = unitTypeOneBdrm - - await this.unitsRepository.save(unitsToBeCreated) - - const listingCreateDto: Omit< - DeepPartial, - keyof BaseEntity | "urlSlug" | "showWaitlist" - > = { - ...tritonListing, - property: property, - assets: getDefaultAssets(), - preferences: [getLiveWorkPreference()], - events: [], - } - - return await this.listingRepository.save(listingCreateDto) - } -} diff --git a/backend/core/src/seeds/listings/listings.ts b/backend/core/src/seeds/listings/listings.ts deleted file mode 100644 index a349558d1a..0000000000 --- a/backend/core/src/seeds/listings/listings.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { PropertyCreateDto } from "../../property/dto/property.dto" -import { ApplicationMethodCreateDto } from "../../application-methods/dto/application-method.dto" -import { PreferenceCreateDto } from "../../preferences/dto/preference.dto" -import { ListingEventCreateDto } from "../../listings/dto/listing-event.dto" -import { AssetCreateDto } from "../../assets/dto/asset.dto" -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { BaseEntity } from "typeorm" -import { UserCreateDto } from "../../auth/dto/user-create.dto" -import { ListingCreateDto } from "../../listings/dto/listing-create.dto" -import { UnitCreateDto } from "../../units/dto/unit-create.dto" - -export type PropertySeedType = Omit< - PropertyCreateDto, - | "propertyGroups" - | "listings" - | "units" - | "unitsSummarized" - | "householdSizeMin" - | "householdSizeMax" -> - -export type UnitSeedType = Omit - -export type ApplicationMethodSeedType = ApplicationMethodCreateDto - -export type ListingSeedType = Omit< - ListingCreateDto, - | keyof BaseEntity - | "property" - | "urlSlug" - | "applicationMethods" - | "events" - | "assets" - | "preferences" - | "leasingAgents" - | "showWaitlist" - | "units" - | "propertyGroups" - | "accessibility" - | "amenities" - | "buildingAddress" - | "buildingTotalUnits" - | "developer" - | "householdSizeMax" - | "householdSizeMin" - | "neighborhood" - | "petPolicy" - | "smokingPolicy" - | "unitsAvailable" - | "unitAmenities" - | "servicesOffered" - | "yearBuilt" - | "unitsSummary" - | "unitsSummarized" - | "amiChartOverrides" - | "jurisdiction" -> - -export type PreferenceSeedType = Omit - -export type AssetDtoSeedType = Omit - -// Properties that are ommited in DTOS derived types are relations and getters -export interface ListingSeed { - amiChart: AmiChartCreateDto - units: Array - applicationMethods: Array - property: PropertySeedType - preferences: Array - listingEvents: Array - assets: Array - listing: ListingSeedType - leasingAgents: UserCreateDto[] -} diff --git a/backend/core/src/seeds/listings/shared.ts b/backend/core/src/seeds/listings/shared.ts deleted file mode 100644 index abd0b226f9..0000000000 --- a/backend/core/src/seeds/listings/shared.ts +++ /dev/null @@ -1,692 +0,0 @@ -// AMI Charts -import { - AssetDtoSeedType, - ListingSeedType, - PreferenceSeedType, - PropertySeedType, - UnitSeedType, -} from "./listings" -import { CSVFormattingType } from "../../csv/types/csv-formatting-type-enum" -import { ListingStatus } from "../../listings/types/listing-status-enum" -import { InputType } from "../../shared/types/input-type" -import { AmiChart } from "../../ami-charts/entities/ami-chart.entity" -import { ListingEventType } from "../../listings/types/listing-event-type-enum" -import { AmiChartCreateDto } from "../../ami-charts/dto/ami-chart.dto" -import { ListingEventCreateDto } from "../../listings/dto/listing-event.dto" -import { UnitStatus } from "../../units/types/unit-status-enum" -import { ListingReviewOrder } from "../../listings/types/listing-review-order-enum" -import { CountyCode } from "../../shared/types/county-code" -import { UserCreateDto } from "../../auth/dto/user-create.dto" - -export const getDate = (days: number) => { - const someDate = new Date() - someDate.setDate(someDate.getDate() + days) - return someDate -} - -export function getDefaultAmiChart() { - return JSON.parse(JSON.stringify(defaultAmiChart)) -} - -export enum PriorityTypes { - mobility = "Mobility", - hearing = "Hearing", - visual = "Visual", - hearingVisual = "Hearing and Visual", - mobilityHearing = "Mobility and Hearing", - mobilityVisual = "Mobility and Visual", - mobilityHearingVisual = "Mobility, Hearing and Visual", -} - -export const defaultAmiChart: Omit = { - name: "AlamedaCountyTCAC2021", - items: [ - { - percentOfAmi: 80, - householdSize: 1, - income: 76720, - }, - { - percentOfAmi: 80, - householdSize: 2, - income: 87680, - }, - { - percentOfAmi: 80, - householdSize: 3, - income: 98640, - }, - { - percentOfAmi: 80, - householdSize: 4, - income: 109600, - }, - { - percentOfAmi: 80, - householdSize: 5, - income: 11840, - }, - { - percentOfAmi: 80, - householdSize: 6, - income: 127200, - }, - { - percentOfAmi: 80, - householdSize: 7, - income: 135920, - }, - { - percentOfAmi: 80, - householdSize: 8, - income: 144720, - }, - { - percentOfAmi: 60, - householdSize: 1, - income: 57540, - }, - { - percentOfAmi: 60, - householdSize: 2, - income: 65760, - }, - { - percentOfAmi: 60, - householdSize: 3, - income: 73980, - }, - { - percentOfAmi: 60, - householdSize: 4, - income: 82200, - }, - { - percentOfAmi: 60, - householdSize: 5, - income: 88800, - }, - { - percentOfAmi: 60, - householdSize: 6, - income: 95400, - }, - { - percentOfAmi: 60, - householdSize: 7, - income: 101940, - }, - { - percentOfAmi: 60, - householdSize: 8, - income: 108540, - }, - { - percentOfAmi: 50, - householdSize: 1, - income: 47950, - }, - { - percentOfAmi: 50, - householdSize: 2, - income: 54800, - }, - { - percentOfAmi: 50, - householdSize: 3, - income: 61650, - }, - { - percentOfAmi: 50, - householdSize: 4, - income: 68500, - }, - { - percentOfAmi: 50, - householdSize: 5, - income: 74000, - }, - { - percentOfAmi: 50, - householdSize: 6, - income: 79500, - }, - { - percentOfAmi: 50, - householdSize: 7, - income: 84950, - }, - { - percentOfAmi: 50, - householdSize: 8, - income: 90450, - }, - { - percentOfAmi: 45, - householdSize: 1, - income: 43155, - }, - { - percentOfAmi: 45, - householdSize: 2, - income: 49320, - }, - { - percentOfAmi: 45, - householdSize: 3, - income: 55485, - }, - { - percentOfAmi: 45, - householdSize: 4, - income: 61650, - }, - { - percentOfAmi: 45, - householdSize: 5, - income: 66600, - }, - { - percentOfAmi: 45, - householdSize: 6, - income: 71550, - }, - { - percentOfAmi: 45, - householdSize: 7, - income: 76455, - }, - { - percentOfAmi: 45, - householdSize: 8, - income: 81405, - }, - { - percentOfAmi: 40, - householdSize: 1, - income: 38360, - }, - { - percentOfAmi: 40, - householdSize: 2, - income: 43840, - }, - { - percentOfAmi: 40, - householdSize: 3, - income: 49320, - }, - { - percentOfAmi: 40, - householdSize: 4, - income: 54800, - }, - { - percentOfAmi: 40, - householdSize: 5, - income: 59200, - }, - { - percentOfAmi: 40, - householdSize: 6, - income: 63600, - }, - { - percentOfAmi: 40, - householdSize: 7, - income: 67960, - }, - { - percentOfAmi: 40, - householdSize: 8, - income: 72360, - }, - { - percentOfAmi: 30, - householdSize: 1, - income: 28770, - }, - { - percentOfAmi: 30, - householdSize: 2, - income: 32880, - }, - { - percentOfAmi: 30, - householdSize: 3, - income: 36990, - }, - { - percentOfAmi: 30, - householdSize: 4, - income: 41100, - }, - { - percentOfAmi: 30, - householdSize: 5, - income: 44400, - }, - { - percentOfAmi: 30, - householdSize: 6, - income: 47700, - }, - { - percentOfAmi: 30, - householdSize: 7, - income: 50970, - }, - { - percentOfAmi: 30, - householdSize: 8, - income: 54270, - }, - { - percentOfAmi: 20, - householdSize: 1, - income: 19180, - }, - { - percentOfAmi: 20, - householdSize: 2, - income: 21920, - }, - { - percentOfAmi: 20, - householdSize: 3, - income: 24660, - }, - { - percentOfAmi: 20, - householdSize: 4, - income: 27400, - }, - { - percentOfAmi: 20, - householdSize: 5, - income: 29600, - }, - { - percentOfAmi: 20, - householdSize: 6, - income: 31800, - }, - { - percentOfAmi: 20, - householdSize: 7, - income: 33980, - }, - { - percentOfAmi: 20, - householdSize: 8, - income: 36180, - }, - ], -} - -// Events -export function getDefaultListingEvents() { - return JSON.parse(JSON.stringify(defaultListingEvents)) -} - -export const defaultListingEvents: Array = [ - { - startTime: getDate(10), - endTime: getDate(10), - note: "Custom open house event note", - type: ListingEventType.openHouse, - url: "example.com", - label: "Custom Event URL Label", - }, - { - startTime: getDate(10), - endTime: getDate(10), - note: "Custom public lottery event note", - type: ListingEventType.publicLottery, - url: "example2.com", - label: "Custom Event URL Label", - }, -] - -// Assets -export function getDefaultAssets() { - return JSON.parse(JSON.stringify(defaultAssets)) -} - -export const defaultAssets: Array = [ - { - label: "building", - fileId: - "https://regional-dahlia-staging.s3-us-west-1.amazonaws.com/listings/triton/thetriton.png", - }, -] -// Properties -export function getDefaultProperty() { - return JSON.parse(JSON.stringify(defaultProperty)) -} - -export const defaultProperty: PropertySeedType = { - accessibility: "Custom accessibility text", - amenities: "Custom property amenities text", - buildingAddress: { - city: "San Francisco", - state: "CA", - street: "548 Market Street", - street2: "Suite #59930", - zipCode: "94104", - latitude: 37.789673, - longitude: -122.40151, - }, - buildingTotalUnits: 100, - developer: "Developer", - neighborhood: "Custom neighborhood text", - petPolicy: "Custom pet text", - servicesOffered: "Custom services offered text", - smokingPolicy: "Custom smoking text", - unitAmenities: "Custom unit amenities text", - unitsAvailable: 2, - yearBuilt: 2021, -} - -// Unit Sets -export function getDefaultUnits() { - return JSON.parse(JSON.stringify(defaultUnits)) -} - -export const defaultUnits: Array = [ - { - amiChart: defaultAmiChart as AmiChart, - amiPercentage: "30", - annualIncomeMax: "45600", - annualIncomeMin: "36168", - bmrProgramChart: false, - floor: 1, - maxOccupancy: 3, - minOccupancy: 1, - monthlyIncomeMin: "3014", - monthlyRent: "1219", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 1, - number: null, - sqFeet: "635", - status: UnitStatus.available, - }, - { - amiChart: defaultAmiChart as AmiChart, - amiPercentage: "30", - annualIncomeMax: "66600", - annualIncomeMin: "41616", - bmrProgramChart: false, - floor: 2, - maxOccupancy: 5, - minOccupancy: 2, - monthlyIncomeMin: "3468", - monthlyRent: "1387", - monthlyRentAsPercentOfIncome: null, - numBathrooms: 1, - numBedrooms: 2, - number: null, - sqFeet: "748", - status: UnitStatus.available, - }, -] - -export const defaultLeasingAgents: Omit[] = [ - { - firstName: "First", - lastName: "Last", - middleName: "Middle", - email: "leasing-agent-1@example.com", - emailConfirmation: "leasing-agent-1@example.com", - password: "abcdef", - passwordConfirmation: "Abcdef1", - dob: new Date(), - }, - { - firstName: "First", - lastName: "Last", - middleName: "Middle", - email: "leasing-agent-2@example.com", - emailConfirmation: "leasing-agent-2@example.com", - password: "abcdef", - passwordConfirmation: "Abcdef1", - dob: new Date(), - }, -] - -// Listings -export function getDefaultListing() { - return JSON.parse(JSON.stringify(defaultListing)) -} - -export const defaultListing: ListingSeedType = { - applicationAddress: { - city: "San Francisco", - state: "CA", - street: "548 Market Street", - street2: "Suite #59930", - zipCode: "94104", - latitude: 37.789673, - longitude: -122.40151, - }, - countyCode: CountyCode.alameda, - applicationDropOffAddress: null, - applicationDropOffAddressOfficeHours: null, - applicationMailingAddress: null, - applicationDueDate: getDate(10), - applicationDueTime: null, - applicationFee: "20", - applicationOpenDate: getDate(-10), - applicationOrganization: "Application Organization", - applicationPickUpAddress: { - city: "San Francisco", - state: "CA", - street: "548 Market Street", - street2: "Suite #59930", - zipCode: "94104", - latitude: 37.789673, - longitude: -122.40151, - }, - applicationPickUpAddressOfficeHours: "Custom pick up address office hours text", - buildingSelectionCriteria: "example.com", - costsNotIncluded: "Custom costs not included text", - creditHistory: "Custom credit history text", - criminalBackground: "Custom criminal background text", - CSVFormattingType: CSVFormattingType.basic, - depositMax: "500", - depositMin: "500", - disableUnitsAccordion: true, - displayWaitlistSize: false, - image: { - label: "test_label", - fileId: "fileid", - }, - leasingAgentAddress: { - city: "San Francisco", - state: "CA", - street: "548 Market Street", - street2: "Suite #59930", - zipCode: "94104", - latitude: 37.789673, - longitude: -122.40151, - }, - leasingAgentEmail: "hello@exygy.com", - leasingAgentName: "Leasing Agent Name", - leasingAgentOfficeHours: "Custom leasing agent office hours", - leasingAgentPhone: "(415) 992-7251", - leasingAgentTitle: "Leasing Agent Title", - name: "Default Listing Seed", - postmarkedApplicationsReceivedByDate: null, - programRules: "Custom program rules text", - rentalAssistance: "Custom rental assistance text", - rentalHistory: "Custom rental history text", - requiredDocuments: "Custom required documents text", - reviewOrderType: "lottery" as ListingReviewOrder, - specialNotes: "Custom special notes text", - status: ListingStatus.active, - waitlistCurrentSize: null, - waitlistOpenSpots: null, - isWaitlistOpen: false, - waitlistMaxSize: null, - whatToExpect: "Custom what to expect text", -} - -// Preferences -export function getLiveWorkPreference() { - return JSON.parse(JSON.stringify(liveWorkPreference)) -} - -export const liveWorkPreference: PreferenceSeedType = { - ordinal: 1, - page: 1, - title: "Live/Work in County", - subtitle: "Live/Work in County subtitle", - description: "At least one household member lives or works in County", - links: [ - { - title: "Link Title", - url: "example.com", - }, - ], - formMetadata: { - key: "liveWork", - options: [ - { - key: "live", - extraData: [], - }, - { - key: "work", - extraData: [], - }, - ], - }, -} -export function getDisplaceePreference() { - return JSON.parse(JSON.stringify(displaceePreference)) -} - -export const displaceePreference: PreferenceSeedType = { - ordinal: 1, - page: 1, - title: "Displacee Tenant Housing", - subtitle: "Displacee Tenant Housing subtitle", - description: - "At least one member of my household was displaced from a residential property due to redevelopment activity by Housing Authority or City.", - links: [], - formMetadata: { - key: "displacedTenant", - options: [ - { - key: "general", - extraData: [ - { - key: "name", - type: InputType.text, - }, - { - key: "address", - type: InputType.address, - }, - ], - }, - { - key: "missionCorridor", - extraData: [ - { - key: "name", - type: InputType.text, - }, - { - key: "address", - type: InputType.address, - }, - ], - }, - ], - }, -} - -export function getPbvPreference() { - return JSON.parse(JSON.stringify(pbvPreference)) -} - -export const pbvPreference: PreferenceSeedType = { - page: 1, - ordinal: 1, - title: "Housing Authority Project-Based Voucher", - subtitle: "", - description: - "You are currently applying to be in a general applicant waiting list. Of the total apartments available in this application process, several have Project-Based Vouchers for rental subsidy assistance from the Housing Authority. With that subsidy, tenant households pay 30% of their income as rent. These tenants are required to verify their income annually with the property manager as well as the Housing Authority.", - links: [], - formMetadata: { - key: "PBV", - customSelectText: "Please select any of the following that apply to you", - hideGenericDecline: true, - hideFromListing: true, - options: [ - { - key: "residency", - extraData: [], - }, - { - key: "family", - extraData: [], - }, - { - key: "veteran", - extraData: [], - }, - { - key: "homeless", - extraData: [], - }, - { - key: "noneApplyButConsider", - exclusive: true, - description: false, - extraData: [], - }, - { - key: "doNotConsider", - exclusive: true, - description: false, - extraData: [], - }, - ], - }, -} - -export function getHopwaPreference() { - return JSON.parse(JSON.stringify(hopwaPreference)) -} - -export const hopwaPreference: PreferenceSeedType = { - page: 1, - ordinal: 1, - title: "Housing Opportunities for Persons with AIDS", - subtitle: "", - description: - "There are apartments set-aside for households eligible for the HOPWA program (Housing Opportunities for Persons with AIDS), which are households where a person has been medically diagnosed with HIV/AIDS. These apartments also have Project-Based Section rental subsidies (tenant pays 30% of household income).", - links: [], - formMetadata: { - key: "HOPWA", - customSelectText: - "Please indicate if you are interested in applying for one of these HOPWA apartments", - hideGenericDecline: true, - hideFromListing: true, - options: [ - { - key: "hopwa", - extraData: [], - }, - { - key: "doNotConsider", - exclusive: true, - description: false, - extraData: [], - }, - ], - }, -} diff --git a/backend/core/src/shared/decorators/enforceLowerCase.decorator.ts b/backend/core/src/shared/decorators/enforceLowerCase.decorator.ts new file mode 100644 index 0000000000..432ae8152a --- /dev/null +++ b/backend/core/src/shared/decorators/enforceLowerCase.decorator.ts @@ -0,0 +1,5 @@ +import { Transform } from "class-transformer" + +export function EnforceLowerCase() { + return Transform((value: string) => (value ? value.toLowerCase() : value)) +} diff --git a/backend/core/src/shared/decorators/isLength.decorator.ts b/backend/core/src/shared/decorators/isLength.decorator.ts new file mode 100644 index 0000000000..f72f1a11c0 --- /dev/null +++ b/backend/core/src/shared/decorators/isLength.decorator.ts @@ -0,0 +1,25 @@ +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from "class-validator" + +export function IsLength(property: string, validationOptions?: ValidationOptions) { + return (object: unknown, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [property], + validator: LengthConstraint, + }) + } +} + +@ValidatorConstraint({ name: "IsLength" }) +export class LengthConstraint implements ValidatorConstraintInterface { + validate(value: string) { + return value.length >= 3 || value.length === 0 + } +} diff --git a/backend/core/src/shared/email/email.service.ts b/backend/core/src/shared/email/email.service.ts deleted file mode 100644 index db2d44bb86..0000000000 --- a/backend/core/src/shared/email/email.service.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { Injectable, Logger, Scope } from "@nestjs/common" -import { SendGridService } from "@anchan828/nest-sendgrid" -import { ResponseError } from "@sendgrid/helpers/classes" -import Handlebars from "handlebars" -import path from "path" -import { User } from "../../auth/entities/user.entity" -import { Listing } from "../../listings/entities/listing.entity" -import Polyglot from "node-polyglot" -import fs from "fs" -import { ConfigService } from "@nestjs/config" -import { Application } from "../../applications/entities/application.entity" -import { TranslationsService } from "../../translations/translations.service" -import { Language } from "../types/language-enum" -import { JurisdictionResolverService } from "../../jurisdictions/services/jurisdiction-resolver.service" -import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" - -@Injectable({ scope: Scope.REQUEST }) -export class EmailService { - polyglot: Polyglot - - constructor( - private readonly sendGrid: SendGridService, - private readonly configService: ConfigService, - private readonly translationService: TranslationsService, - private readonly jurisdictionResolverService: JurisdictionResolverService - ) { - this.polyglot = new Polyglot({ - phrases: {}, - }) - const polyglot = this.polyglot - Handlebars.registerHelper("t", function ( - phrase: string, - options?: number | Polyglot.InterpolationOptions - ) { - return polyglot.t(phrase, options) - }) - const parts = this.partials() - Handlebars.registerPartial(parts) - } - - public async welcome(user: User, appUrl: string, confirmationUrl: string) { - const language = user.language || Language.en - const jurisdiction = await this.jurisdictionResolverService.getJurisdiction() - void (await this.loadTranslations(jurisdiction, language)) - if (this.configService.get("NODE_ENV") === "production") { - Logger.log( - `Preparing to send a welcome email to ${user.email} from ${this.configService.get( - "EMAIL_FROM_ADDRESS" - )}...` - ) - } - await this.send( - user.email, - "Welcome to Bloom", - this.template("register-email")({ - user: user, - confirmationUrl: confirmationUrl, - appOptions: { appUrl: appUrl }, - }) - ) - } - - public async confirmation(listing: Listing, application: Application, appUrl: string) { - const jurisdiction = await this.jurisdictionResolverService.getJurisdiction() - void (await this.loadTranslations(jurisdiction, application.language || Language.en)) - let whatToExpectText - const listingUrl = `${appUrl}/listing/${listing.id}` - const compiledTemplate = this.template("confirmation") - - if (this.configService.get("NODE_ENV") == "production") { - Logger.log( - `Preparing to send a confirmation email to ${ - application.applicant.emailAddress - } from ${this.configService.get("EMAIL_FROM_ADDRESS")}...` - ) - } - - if (listing.applicationDueDate) { - if (!listing.waitlistMaxSize) { - whatToExpectText = this.polyglot.t("confirmation.whatToExpect.lottery", { - lotteryDate: listing.applicationDueDate, - }) - } else { - whatToExpectText = this.polyglot.t("confirmation.whatToExpect.noLottery", { - lotteryDate: listing.applicationDueDate, - }) - } - } else { - whatToExpectText = this.polyglot.t("confirmation.whatToExpect.FCFS") - } - const user = { - firstName: application.applicant.firstName, - middleName: application.applicant.middleName, - lastName: application.applicant.lastName, - } - await this.send( - application.applicant.emailAddress, - this.polyglot.t("confirmation.subject"), - compiledTemplate({ - listing: listing, - listingUrl: listingUrl, - application: application, - whatToExpectText: whatToExpectText, - user: user, - }) - ) - } - - public async forgotPassword(user: User, appUrl: string) { - const jurisdiction = await this.jurisdictionResolverService.getJurisdiction() - void (await this.loadTranslations(jurisdiction, user.language)) - const compiledTemplate = this.template("forgot-password") - const resetUrl = `${appUrl}/reset-password?token=${user.resetToken}` - - if (this.configService.get("NODE_ENV") == "production") { - Logger.log( - `Preparing to send a forget password email to ${user.email} from ${this.configService.get< - string - >("EMAIL_FROM_ADDRESS")}...` - ) - } - - await this.send( - user.email, - this.polyglot.t("forgotPassword.subject"), - compiledTemplate({ - resetUrl: resetUrl, - resetOptions: { appUrl: appUrl }, - user: user, - }) - ) - } - - private async loadTranslations(jurisdiction: Jurisdiction | null, language: Language) { - const translation = await this.translationService.getTranslationByLanguageAndJurisdictionOrDefaultEn( - language, - jurisdiction ? jurisdiction.id : null - ) - this.polyglot.replace(translation.translations) - } - - private template(view: string) { - return Handlebars.compile( - fs.readFileSync( - path.join(path.resolve(__dirname, "..", "..", "views"), `/${view}.hbs`), - "utf8" - ) - ) - } - - private partial(view: string) { - return fs.readFileSync( - path.join(path.resolve(__dirname, "..", "..", "views"), `/${view}`), - "utf8" - ) - } - - private partials() { - const partials = {} - const dirName = path.resolve(__dirname, "..", "..", "views/partials") - - fs.readdirSync(dirName).forEach((filename) => { - partials[filename.slice(0, -4)] = this.partial("partials/" + filename) - }) - - return partials - } - - private async send(to: string, subject: string, body: string, retry = 3) { - await this.sendGrid.send( - { - to: to, - from: this.configService.get("EMAIL_FROM_ADDRESS"), - subject: subject, - html: body, - }, - false, - (error) => { - if (error instanceof ResponseError) { - const { response } = error - const { body: errBody } = response - console.error(`Error sending email to: ${to}! Error body: ${errBody}`) - if (retry > 0) { - void this.send(to, subject, body, retry - 1) - } - } - } - ) - } - - async invite(user: User, appUrl: string, confirmationUrl: string) { - void (await this.loadTranslations(null, user.language || Language.en)) - await this.send( - user.email, - this.polyglot.t("invite.hello"), - this.template("invite")({ - user: user, - confirmationUrl: confirmationUrl, - appOptions: { appUrl }, - }) - ) - } -} diff --git a/backend/core/src/shared/entities/address.entity.ts b/backend/core/src/shared/entities/address.entity.ts index 286c2f442a..d7cc9cb8f2 100644 --- a/backend/core/src/shared/entities/address.entity.ts +++ b/backend/core/src/shared/entities/address.entity.ts @@ -59,7 +59,7 @@ export class Address { @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsString({ groups: [ValidationsGroupsEnum.default] }) - @MaxLength(64, { groups: [ValidationsGroupsEnum.default] }) + @MaxLength(128, { groups: [ValidationsGroupsEnum.default] }) street2?: string | null @Column({ type: "text", nullable: true }) diff --git a/backend/core/src/shared/filter/custom_filters.ts b/backend/core/src/shared/filter/custom_filters.ts deleted file mode 100644 index 7b6b1a5038..0000000000 --- a/backend/core/src/shared/filter/custom_filters.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { WhereExpression } from "typeorm" -import { - AvailabilityFilterEnum, - ListingFilterKeys, -} from "../../listings/types/listing-filter-keys-enum" -import { filterTypeToFieldMap } from "../../listings/dto/filter-type-to-field-map" - -export function addSeniorHousingQuery(qb: WhereExpression, filterValue: string) { - const whereParameterName = ListingFilterKeys.seniorHousing - const seniorHousingCommunityType = "senior62" - const reservedCommunityTypeColumnName = `LOWER(CAST(${ - filterTypeToFieldMap[ListingFilterKeys.seniorHousing] - } as text))` - if (filterValue == "true") { - qb.andWhere(`${reservedCommunityTypeColumnName} = LOWER(:${whereParameterName})`, { - [whereParameterName]: seniorHousingCommunityType, - }) - } else if (filterValue == "false") { - qb.andWhere( - `(${reservedCommunityTypeColumnName} IS NULL OR ${reservedCommunityTypeColumnName} <> LOWER(:${whereParameterName}))`, - { - [whereParameterName]: seniorHousingCommunityType, - } - ) - } -} - -export function addAvailabilityQuery( - qb: WhereExpression, - filterValue: AvailabilityFilterEnum, - includeNulls?: boolean -) { - const whereParameterName = "availability" - switch (filterValue) { - case AvailabilityFilterEnum.hasAvailability: - qb.andWhere( - `(unitsSummary.total_available >= :${whereParameterName}${ - includeNulls ? ` OR unitsSummary.total_available IS NULL` : "" - })`, - { - [whereParameterName]: 1, - } - ) - return - case AvailabilityFilterEnum.noAvailability: - qb.andWhere( - `(unitsSummary.total_available = :${whereParameterName}${ - includeNulls ? ` OR unitsSummary.total_available IS NULL` : "" - })`, - { - [whereParameterName]: 0, - } - ) - return - case AvailabilityFilterEnum.waitlist: - qb.andWhere( - `(listings.is_waitlist_open = :${whereParameterName}${ - includeNulls ? ` OR listings.is_waitlist_open is NULL` : "" - })`, - { - [whereParameterName]: true, - } - ) - return - default: - return - } -} - -export function addMinAmiPercentageFilter( - qb: WhereExpression, - filterValue: number, - includeNulls?: boolean -) { - const whereParameterName = "amiPercentage_unitsSummary" - const whereParameterName2 = "amiPercentage_listings" - - // Check the listing.ami_percentage field iff the field is not set on the Units Summary table. - qb.andWhere( - `((unitsSummary.ami_percentage IS NOT NULL AND unitsSummary.ami_percentage >= :${whereParameterName}) ` + - `OR (unitsSummary.ami_percentage IS NULL AND listings.ami_percentage_max >= :${whereParameterName2}) - ${ - includeNulls - ? `OR unitsSummary.ami_percentage is NULL AND listings.ami_percentage_max is NULL` - : "" - })`, - { - [whereParameterName]: filterValue, - [whereParameterName2]: filterValue, - } - ) - return -} diff --git a/backend/core/src/shared/filter/index.ts b/backend/core/src/shared/filter/index.ts deleted file mode 100644 index 8d360fb948..0000000000 --- a/backend/core/src/shared/filter/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { HttpException, HttpStatus } from "@nestjs/common" -import { WhereExpression } from "typeorm" -import { Compare } from "../dto/filter.dto" -import { - AvailabilityFilterEnum, - ListingFilterKeys, -} from "../../listings/types/listing-filter-keys-enum" -import { - addSeniorHousingQuery, - addAvailabilityQuery, - addMinAmiPercentageFilter, -} from "./custom_filters" - -/** - * - * @param filterParams - * @param filterTypeToFieldMap - * @param qb The query on which filters are applied. - */ -/** - * Add filters to provided QueryBuilder, using the provided map to find the field name. - * The order of the params matters: - * - A $comparison must be first. - * - Comparisons in $comparison will be applied to each filter in order. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function addFilters, FilterFieldMap>( - filters: FilterParams, - filterTypeToFieldMap: FilterFieldMap, - qb: WhereExpression -): void { - for (const [index, filter] of filters.entries()) { - const comparison = filter["$comparison"] - const includeNulls = filter["$include_nulls"] - for (const filterKey in filter) { - if ( - filter[filterKey] === undefined || - filter[filterKey] === null || - filterKey === "$comparison" || - filterKey === "$include_nulls" - ) { - continue - } - // Throw if this is not a supported filter type - if (!(filterKey in ListingFilterKeys)) { - throw new HttpException("Filter Not Implemented", HttpStatus.NOT_IMPLEMENTED) - } - - const filterValue = filter[filterKey] - // Handle custom filters here, before dropping into generic filter handler - switch (filterKey) { - case ListingFilterKeys.seniorHousing: - addSeniorHousingQuery(qb, filterValue) - continue - case ListingFilterKeys.availability: - addAvailabilityQuery(qb, filterValue as AvailabilityFilterEnum, includeNulls) - continue - case ListingFilterKeys.minAmiPercentage: - addMinAmiPercentageFilter(qb, parseInt(filterValue), includeNulls) - continue - } - - const whereParameterName = `${filterKey}_${index}` - const filterField = filterTypeToFieldMap[filterKey] - switch (comparison) { - case Compare.IN: - qb.andWhere( - `(LOWER(CAST(${filterField} as text)) IN (:...${whereParameterName})${ - includeNulls ? ` OR ${filterField} IS NULL` : "" - })`, - { - [whereParameterName]: filterValue - .split(",") - .map((s) => s.trim().toLowerCase()) - .filter((s) => s.length !== 0), - } - ) - break - case Compare["<>"]: - case Compare["="]: - qb.andWhere( - `(LOWER(CAST(${filterField} as text)) ${comparison} LOWER(:${whereParameterName})${ - includeNulls ? ` OR ${filterField} IS NULL` : "" - })`, - { - [whereParameterName]: filterValue, - } - ) - break - case Compare[">="]: - case Compare["<="]: - qb.andWhere( - `(${filterField} ${comparison} :${whereParameterName}${ - includeNulls ? ` OR ${filterField} IS NULL` : "" - })`, - { - [whereParameterName]: filterValue, - } - ) - break - case Compare.NA: - // If we're here, it's because we expected this filter to be handled by a custom filter handler - // that ignores the $comparison param, but it was not. - throw new HttpException( - `Filter "${filter}" expected to be handled by a custom filter handler, but one was not implemented.`, - HttpStatus.NOT_IMPLEMENTED - ) - default: - throw new HttpException("Comparison Not Implemented", HttpStatus.NOT_IMPLEMENTED) - } - } - } -} diff --git a/backend/core/src/shared/filters/catch-all-filter.ts b/backend/core/src/shared/filters/catch-all-filter.ts new file mode 100644 index 0000000000..dfd06276cc --- /dev/null +++ b/backend/core/src/shared/filters/catch-all-filter.ts @@ -0,0 +1,21 @@ +import { ArgumentsHost, Catch } from "@nestjs/common" +import { BaseExceptionFilter } from "@nestjs/core" + +@Catch() +export class CatchAllFilter extends BaseExceptionFilter { + catch(exception: any, host: ArgumentsHost) { + console.error({ message: exception?.response?.message, stack: exception.stack, exception }) + if (exception.name === "EntityNotFound") { + const response = host.switchToHttp().getResponse() + response.status(404).json({ message: exception.message }) + } else if (exception.message === "tokenExpired") { + const response = host.switchToHttp().getResponse() + response.status(404).json({ message: exception.message }) + } else if (exception.response === "emailInUse") { + const response = host.switchToHttp().getResponse() + response.status(409).json({ message: "That email is already in use" }) + } else { + super.catch(exception, host) + } + } +} diff --git a/backend/core/src/shared/http-methods-to-actions.ts b/backend/core/src/shared/http-methods-to-actions.ts new file mode 100644 index 0000000000..0cec7a3ebc --- /dev/null +++ b/backend/core/src/shared/http-methods-to-actions.ts @@ -0,0 +1,9 @@ +import { authzActions } from "../auth/enum/authz-actions.enum" + +export const httpMethodsToAction = { + PUT: authzActions.update, + PATCH: authzActions.update, + DELETE: authzActions.delete, + POST: authzActions.create, + GET: authzActions.read, +} diff --git a/backend/core/src/cache/listing-lang-cache.interceptor.ts b/backend/core/src/shared/interceptors/listing-lang-cache.interceptor.ts similarity index 100% rename from backend/core/src/cache/listing-lang-cache.interceptor.ts rename to backend/core/src/shared/interceptors/listing-lang-cache.interceptor.ts diff --git a/backend/core/src/middleware/logger.middleware.ts b/backend/core/src/shared/middlewares/logger.middleware.ts similarity index 100% rename from backend/core/src/middleware/logger.middleware.ts rename to backend/core/src/shared/middlewares/logger.middleware.ts diff --git a/backend/core/src/shared/query-filter/base-query-filter.ts b/backend/core/src/shared/query-filter/base-query-filter.ts new file mode 100644 index 0000000000..dbaa83cbe9 --- /dev/null +++ b/backend/core/src/shared/query-filter/base-query-filter.ts @@ -0,0 +1,91 @@ +import { HttpException, HttpStatus } from "@nestjs/common" +import { Compare } from "../dto/filter.dto" +import { WhereExpression } from "typeorm" +import { IBaseQueryFilter } from "./index" + +export class BaseQueryFilter implements IBaseQueryFilter { + protected static _shouldSkipKey(filter, filterKey) { + return ( + filter[filterKey] === undefined || filter[filterKey] === null || filterKey === "$comparison" + ) + } + protected static _isSupportedFilterTypeOrThrow( + filterType, + filterTypeToFieldMap: FilterFieldMap + ) { + if (!(filterType in filterTypeToFieldMap)) { + throw new HttpException("Filter Not Implemented", HttpStatus.NOT_IMPLEMENTED) + } + } + protected static _getComparisonOperator(filter) { + return filter["$comparison"] + } + + protected static _getFilterField( + filterKey, + filterTypeToFieldMap: FilterFieldMap + ) { + return filterTypeToFieldMap[filterKey] + } + + protected static _getFilterValue(filter, filterKey) { + return filter[filterKey] + } + + protected static _getWhereParameterName(filterKey, index) { + return `${filterKey}_${index}` + } + + protected static _compare(qb, filter, filterKey, filterTypeToFieldMap, index) { + const whereParameterName = BaseQueryFilter._getWhereParameterName(filterKey, index) + const filterField = BaseQueryFilter._getFilterField(filterKey, filterTypeToFieldMap) + const filterValue = BaseQueryFilter._getFilterValue(filter, filterKey) + const comparison = BaseQueryFilter._getComparisonOperator(filter) + switch (comparison) { + case Compare.IN: + qb.andWhere(`LOWER(CAST(${filterField} as text)) IN (:...${whereParameterName})`, { + [whereParameterName]: filterValue + .split(",") + .map((s) => s.trim().toLowerCase()) + .filter((s) => s.length !== 0), + }) + break + case Compare["<>"]: + case Compare["="]: + case Compare[">="]: + qb.andWhere( + `LOWER(CAST(${filterField} as text)) ${comparison} LOWER(:${whereParameterName})`, + { + [whereParameterName]: filterValue, + } + ) + break + case Compare.NA: + // If we're here, it's because we expected this filter to be handled by a custom filter handler + // that ignores the $comparison param, but it was not. + throw new HttpException( + `Filter "${filter}" expected to be handled by a custom filter handler, but one was not implemented.`, + HttpStatus.NOT_IMPLEMENTED + ) + default: + throw new HttpException("Comparison Not Implemented", HttpStatus.NOT_IMPLEMENTED) + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addFilters( + filters: FilterParams, + filterTypeToFieldMap: FilterFieldMap, + qb: WhereExpression + ) { + for (const [index, filter] of filters.entries()) { + for (const filterKey in filter) { + if (BaseQueryFilter._shouldSkipKey(filter, filterKey)) { + continue + } + BaseQueryFilter._isSupportedFilterTypeOrThrow(filterKey, filterTypeToFieldMap) + BaseQueryFilter._compare(qb, filter, filterKey, filterTypeToFieldMap, index) + } + } + } +} diff --git a/backend/core/src/shared/query-filter/custom-filters.ts b/backend/core/src/shared/query-filter/custom-filters.ts new file mode 100644 index 0000000000..46f9c05e4c --- /dev/null +++ b/backend/core/src/shared/query-filter/custom-filters.ts @@ -0,0 +1,104 @@ +import { getMetadataArgsStorage, WhereExpression } from "typeorm" +import { AvailabilityFilterEnum } from "../../listings/types/listing-filter-keys-enum" +import { UnitGroup } from "../../units-summary/entities/unit-group.entity" +import { UnitType } from "../../unit-types/entities/unit-type.entity" + +export function addAvailabilityQuery( + qb: WhereExpression, + filterValue: AvailabilityFilterEnum, + includeNulls?: boolean +) { + const whereParameterName = "availability" + switch (filterValue) { + case AvailabilityFilterEnum.hasAvailability: + qb.andWhere( + `(unitGroups.total_available >= :${whereParameterName}${ + includeNulls ? ` OR unitGroups.total_available IS NULL` : "" + })`, + { + [whereParameterName]: 1, + } + ) + return + case AvailabilityFilterEnum.noAvailability: + qb.andWhere( + `(unitGroups.total_available = :${whereParameterName}${ + includeNulls ? ` OR unitGroups.total_available IS NULL` : "" + })`, + { + [whereParameterName]: 0, + } + ) + return + case AvailabilityFilterEnum.waitlist: + qb.andWhere( + `(listings.is_waitlist_open = :${whereParameterName}${ + includeNulls ? ` OR listings.is_waitlist_open is NULL` : "" + })`, + { + [whereParameterName]: true, + } + ) + return + default: + return + } +} + +export function addBedroomsQuery(qb: WhereExpression, filterValue: number[]) { + const typeOrmMetadata = getMetadataArgsStorage() + const unitGroupEntityMetadata = typeOrmMetadata.tables.find((table) => table.target === UnitGroup) + const unitTypeEntityMetadata = typeOrmMetadata.tables.find((table) => table.target === UnitType) + const whereParameterName = "unitGroups_numBedrooms" + + const unitGroupUnitTypeJoinTableName = `${unitGroupEntityMetadata.name}_unit_type_${unitTypeEntityMetadata.name}` + qb.andWhere( + ` + ( + SELECT bool_or(num_bedrooms IN (:...${whereParameterName})) FROM ${unitGroupEntityMetadata.name} + LEFT JOIN ${unitGroupUnitTypeJoinTableName} ON ${unitGroupUnitTypeJoinTableName}.unit_group_id = ${unitGroupEntityMetadata.name}.id + LEFT JOIN ${unitTypeEntityMetadata.name} ON ${unitTypeEntityMetadata.name}.id = ${unitGroupUnitTypeJoinTableName}.unit_types_id + WHERE ${unitGroupEntityMetadata.name}.listing_id = listings.id + ) = true + `, + { + [whereParameterName]: filterValue, + } + ) + return +} + +export function addMinAmiPercentageFilter( + qb: WhereExpression, + filterValue: number, + includeNulls?: boolean +) { + const whereParameterName = "amiPercentage_unitGroups" + const whereParameterName2 = "amiPercentage_listings" + + // Check the listing.ami_percentage field iff the field is not set on the Unit Groups table. + qb.andWhere( + `(("unitGroupsAmiLevels"."ami_percentage" IS NOT NULL AND "unitGroupsAmiLevels"."ami_percentage" >= :${whereParameterName}) ` + + `OR ("unitGroupsAmiLevels"."ami_percentage" IS NULL AND listings.ami_percentage_max >= :${whereParameterName2}) + ${ + includeNulls + ? `OR "unitGroupsAmiLevels"."ami_percentage" is NULL AND listings.ami_percentage_max is NULL` + : "" + })`, + { + [whereParameterName]: filterValue, + [whereParameterName2]: filterValue, + } + ) + return +} + +export function addFavoritedFilter(qb: WhereExpression, filterValue: string) { + const val = filterValue.split(",").filter((elem) => !!elem) + if (val.length) { + qb.andWhere("listings.id IN (:...favoritedListings) ", { + favoritedListings: val, + }) + } + return +} diff --git a/backend/core/src/shared/query-filter/custom_filters.ts b/backend/core/src/shared/query-filter/custom_filters.ts new file mode 100644 index 0000000000..04ccf1cef1 --- /dev/null +++ b/backend/core/src/shared/query-filter/custom_filters.ts @@ -0,0 +1,122 @@ +import { WhereExpression } from "typeorm" +import { ListingMarketingTypeEnum } from "../../../types" + +export function addAvailabilityQuery(qb: WhereExpression, filterValue: string) { + const val = filterValue?.split(",") + const whereClause = [] + const inputArgs: Record = {} + val.forEach((option) => { + switch (option) { + case "vacantUnits": + whereClause.push("unitgroups.total_available >= :vacantUnits") + inputArgs.vacantUnits = 1 + return + case "openWaitlist": + whereClause.push("coalesce(unitgroups.open_waitlist, false) = :openWaitlist") + inputArgs.openWaitlist = true + return + case "closedWaitlist": + whereClause.push("coalesce(unitgroups.open_waitlist, false) = :closedWaitlist") + inputArgs.closedWaitlist = false + return + case "comingSoon": + whereClause.push("listings.marketing_type = :marketing_type") + inputArgs.marketing_type = ListingMarketingTypeEnum.comingSoon + return + default: + return + } + }) + qb.andWhere(`(${whereClause.join(" OR ")})`, { ...inputArgs }) +} + +export function addBedroomsQuery(qb: WhereExpression, filterValue: string) { + const val = filterValue.split(",").filter((elem) => !!elem) + if (val.length) { + qb.andWhere("unitTypes.name IN (:...unitTypes) ", { + unitTypes: val, + }) + } + return +} + +export function addMinAmiPercentageFilter( + qb: WhereExpression, + filterValue: number, + includeNulls?: boolean +) { + const whereParameterName = "amiPercentage_unitGroups" + const whereParameterName2 = "amiPercentage_listings" + + // Check the listing.ami_percentage field iff the field is not set on the Unit Groups table. + qb.andWhere( + `(("unitGroupsAmiLevels"."ami_percentage" IS NOT NULL AND "unitGroupsAmiLevels"."ami_percentage" >= :${whereParameterName}) ` + + `OR ("unitGroupsAmiLevels"."ami_percentage" IS NULL AND listings.ami_percentage_max >= :${whereParameterName2}) + ${ + includeNulls + ? `OR "unitGroupsAmiLevels"."ami_percentage" is NULL AND listings.ami_percentage_max is NULL` + : "" + })`, + { + [whereParameterName]: filterValue, + [whereParameterName2]: filterValue, + } + ) + return +} + +export function addFavoritedFilter(qb: WhereExpression, filterValue: string) { + const val = filterValue.split(",").filter((elem) => !!elem) + if (val.length) { + qb.andWhere("listings.id IN (:...favoritedListings) ", { + favoritedListings: val, + }) + } + return +} + +export function addProgramFilter(qb: WhereExpression, filterValue: string) { + const val = filterValue.split(",").filter((elem) => !!elem) + if (val.length) { + qb.andWhere("programs.id IN (:...communityPrograms) ", { + communityPrograms: val, + }) + } + return +} + +export function addRegionFilter(qb: WhereExpression, filterValue: string) { + const val = filterValue.split(",").filter((elem) => !!elem) + if (val.length) { + qb.andWhere("property.region IN (:...region) ", { + region: val, + }) + } + return +} + +export function addAccessibilityFilter(qb: WhereExpression, filterValue: string) { + const val = filterValue.split(",").filter((elem) => !!elem) + const whereClause = val + .map((key) => { + return `listing_features.${key} = true` + }) + .join(" OR ") + qb.andWhere(`(${whereClause})`) + return +} + +export function addRentFilter( + qb: WhereExpression, + filterValue: string, + comparison: string, + filterName: string +) { + if (filterValue) { + qb.andWhere( + `(amilevels.flat_rent_value ${comparison} :${filterName} OR amilevels.percentage_of_income_value IS NOT NULL)`, + { [filterName]: filterValue } + ) + } + return +} diff --git a/backend/core/src/shared/filter/index.spec.ts b/backend/core/src/shared/query-filter/index.spec.ts similarity index 76% rename from backend/core/src/shared/filter/index.spec.ts rename to backend/core/src/shared/query-filter/index.spec.ts index 7348611459..c580323cbf 100644 --- a/backend/core/src/shared/filter/index.spec.ts +++ b/backend/core/src/shared/query-filter/index.spec.ts @@ -69,31 +69,5 @@ describe("FilterAdder", () => { addFilters([filter], filterTypeToFieldMap, mockQueryBuilder) }).toThrow("Comparison Not Implemented") }) - - it("should add both custom and standard filters", () => { - const filters = [ - { - $comparison: "NA", - minAmiPercentage: "40", - }, - { - $comparison: "=", - name: "Coliseum", - }, - ] - - addFilters(filters, filterTypeToFieldMap, mockQueryBuilder) - - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( - expect.stringContaining("ami_percentage"), - { - amiPercentage_unitsSummary: 40, - amiPercentage_listings: 40, - } - ) - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(expect.stringContaining("="), { - name_1: expect.stringContaining("Coliseum"), - }) - }) }) }) diff --git a/backend/core/src/shared/query-filter/index.ts b/backend/core/src/shared/query-filter/index.ts new file mode 100644 index 0000000000..21e644aacf --- /dev/null +++ b/backend/core/src/shared/query-filter/index.ts @@ -0,0 +1,153 @@ +import { HttpException, HttpStatus } from "@nestjs/common" +import { WhereExpression } from "typeorm" +import { Compare } from "../dto/filter.dto" +import { + AvailabilityFilterEnum, + ListingFilterKeys, +} from "../../listings/types/listing-filter-keys-enum" +import { + addAvailabilityQuery, + addBedroomsQuery, + addMinAmiPercentageFilter, + addFavoritedFilter, + addProgramFilter, + addAccessibilityFilter, + addRegionFilter, + addRentFilter, +} from "./custom_filters" +import { UserFilterKeys } from "../../auth/types/user-filter-keys" +import { addIsPortalUserQuery } from "../../auth/filters/user-query-filter" + +export interface IBaseQueryFilter { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addFilters( + filters: FilterParams, + filterTypeToFieldMap: FilterFieldMap, + qb: WhereExpression + ) +} + +/** + * + * @param filterParams + * @param filterTypeToFieldMap + * @param qb The query on which filters are applied. + */ +/** + * Add filters to provided QueryBuilder, using the provided map to find the field name. + * The order of the params matters: + * - A $comparison must be first. + * - Comparisons in $comparison will be applied to each filter in order. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function addFilters, FilterFieldMap>( + filters: FilterParams, + filterTypeToFieldMap: FilterFieldMap, + qb: WhereExpression +): void { + for (const [index, filter] of filters.entries()) { + const comparison = filter["$comparison"] + const includeNulls = filter["$include_nulls"] + for (const filterKey in filter) { + if ( + filter[filterKey] === undefined || + filter[filterKey] === null || + filterKey === "$comparison" || + filterKey === "$include_nulls" + ) { + continue + } + // Throw if this is not a supported filter type + if (!(filterKey in ListingFilterKeys || filterKey in UserFilterKeys)) { + throw new HttpException("Filter Not Implemented", HttpStatus.NOT_IMPLEMENTED) + } + + const filterValue = filter[filterKey] + // Handle custom filters here, before dropping into generic filter handler + switch (filterKey) { + // custom listing filters + case ListingFilterKeys.availability: + addAvailabilityQuery(qb, filterValue as AvailabilityFilterEnum) + continue + case ListingFilterKeys.bedrooms: + case ListingFilterKeys.bedRoomSize: + addBedroomsQuery(qb, filterValue) + continue + case ListingFilterKeys.minAmiPercentage: + addMinAmiPercentageFilter(qb, parseInt(filterValue), includeNulls) + continue + case ListingFilterKeys.favorited: + addFavoritedFilter(qb, filterValue) + continue + case ListingFilterKeys.communityPrograms: + addProgramFilter(qb, filterValue) + continue + case ListingFilterKeys.accessibility: + addAccessibilityFilter(qb, filterValue) + continue + case ListingFilterKeys.region: + addRegionFilter(qb, filterValue) + continue + case ListingFilterKeys.minRent: + addRentFilter(qb, filterValue, ">=", "minRent") + continue + case ListingFilterKeys.maxRent: + addRentFilter(qb, filterValue, "<=", "maxRent") + continue + //custom user filters + case UserFilterKeys.isPortalUser: + addIsPortalUserQuery(qb, filterValue.toString()) + continue + } + + const whereParameterName = `${filterKey}_${index}` + const filterField = filterTypeToFieldMap[filterKey] + switch (comparison) { + case Compare.IN: + qb.andWhere( + `(LOWER(CAST(${filterField} as text)) IN (:...${whereParameterName})${ + includeNulls ? ` OR ${filterField} IS NULL` : "" + })`, + { + [whereParameterName]: String(filterValue) + .split(",") + .map((s) => s.trim().toLowerCase()) + .filter((s) => s.length !== 0), + } + ) + break + case Compare["<>"]: + case Compare["="]: + qb.andWhere( + `(LOWER(CAST(${filterField} as text)) ${comparison} LOWER(:${whereParameterName})${ + includeNulls ? ` OR ${filterField} IS NULL` : "" + })`, + { + [whereParameterName]: filterValue, + } + ) + break + case Compare[">="]: + case Compare["<="]: + qb.andWhere( + `(${filterField} ${comparison} :${whereParameterName}${ + includeNulls ? ` OR ${filterField} IS NULL` : "" + })`, + { + [whereParameterName]: filterValue, + } + ) + break + case Compare.NA: + // If we're here, it's because we expected this filter to be handled by a custom filter handler + // that ignores the $comparison param, but it was not. + throw new HttpException( + `Filter "${filter}" expected to be handled by a custom filter handler, but one was not implemented.`, + HttpStatus.NOT_IMPLEMENTED + ) + default: + throw new HttpException("Comparison Not Implemented", HttpStatus.NOT_IMPLEMENTED) + } + } + } +} diff --git a/backend/core/src/shared/services/abstract-service.ts b/backend/core/src/shared/services/abstract-service.ts index a50d118603..2b463f0ed1 100644 --- a/backend/core/src/shared/services/abstract-service.ts +++ b/backend/core/src/shared/services/abstract-service.ts @@ -1,29 +1,18 @@ -import { Repository } from "typeorm" +import { FindManyOptions, FindOneOptions, Repository } from "typeorm" import { Inject, NotFoundException } from "@nestjs/common" -import { FindConditions } from "typeorm/find-options/FindConditions" -import { ObjectLiteral } from "typeorm/common/ObjectLiteral" import { getRepositoryToken } from "@nestjs/typeorm" import { EntityClassOrSchema } from "@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type" -import { assignDefined } from "../assign-defined" import { ClassType } from "class-transformer/ClassTransformer" +import { assignDefined } from "../utils/assign-defined" export interface GenericUpdateDto { id?: string } -export interface QueryOneOptions { - where: FindConditions[] | FindConditions | ObjectLiteral | string -} - -export interface QueryManyOptions extends QueryOneOptions { - skip?: number - take?: number -} - export interface AbstractService { - list(queryManyOptions?: QueryManyOptions): Promise + list(findConditions?: FindManyOptions): Promise create(dto: TCreateDto): Promise - findOne(queryOneOptions: QueryOneOptions): Promise + findOne(findConditions: FindOneOptions): Promise delete(objId: string): Promise update(dto: TUpdateDto): Promise } @@ -34,16 +23,16 @@ export function AbstractServiceFactory implements AbstractService { @Inject(getRepositoryToken(entity)) repository: Repository - list(queryManyOptions?: QueryManyOptions): Promise { - return this.repository.find(queryManyOptions) + list(findConditions?: FindManyOptions): Promise { + return this.repository.find(findConditions) } async create(dto: TCreateDto): Promise { return await this.repository.save(dto) } - async findOne(queryOneOptions: QueryOneOptions): Promise { - const obj = await this.repository.findOne(queryOneOptions) + async findOne(findConditions: FindOneOptions): Promise { + const obj = await this.repository.findOne(findConditions) if (!obj) { throw new NotFoundException() } diff --git a/backend/core/src/shared/services/county-code-resolver.service.ts b/backend/core/src/shared/services/county-code-resolver.service.ts index 909e784241..03109dd02a 100644 --- a/backend/core/src/shared/services/county-code-resolver.service.ts +++ b/backend/core/src/shared/services/county-code-resolver.service.ts @@ -10,7 +10,7 @@ export class CountyCodeResolverService { getCountyCode(): CountyCode { const countyCode: CountyCode | undefined = CountyCode[this.req.get("county-code")] if (!countyCode) { - return CountyCode.alameda + return CountyCode.detroit } return countyCode } diff --git a/backend/core/src/shared/shared.module.ts b/backend/core/src/shared/shared.module.ts index 1860c803eb..f7c0722748 100644 --- a/backend/core/src/shared/shared.module.ts +++ b/backend/core/src/shared/shared.module.ts @@ -11,16 +11,20 @@ import Joi from "joi" .valid("development", "staging", "production", "test") .default("development"), EMAIL_API_KEY: Joi.string().required(), - EMAIL_FROM_ADDRESS: Joi.string().required(), DATABASE_URL: Joi.string().required(), - REDIS_TLS_URL: Joi.string().required(), - REDIS_USE_TLS: Joi.number().required(), THROTTLE_TTL: Joi.number().default(1), - THROTTLE_LIMIT: Joi.number().default(9999999999999999), + THROTTLE_LIMIT: Joi.number().default(100), APP_SECRET: Joi.string().required().min(16), CLOUDINARY_SECRET: Joi.string().required(), CLOUDINARY_KEY: Joi.string().required(), PARTNERS_PORTAL_URL: Joi.string().required(), + MFA_CODE_LENGTH: Joi.number().default(6), + MFA_CODE_VALID_MS: Joi.number().default(1000 * 60 * 5), + TWILIO_ACCOUNT_SID: Joi.string().default("AC_dummy_account_sid"), + TWILIO_AUTH_TOKEN: Joi.string().default("dummy_auth_token"), + TWILIO_PHONE_NUMBER: Joi.string().default("dummy_phone_number"), + AUTH_LOCK_LOGIN_AFTER_FAILED_ATTEMPTS: Joi.number().default(5), + AUTH_LOCK_LOGIN_COOLDOWN_MS: Joi.number().default(1000 * 60 * 30), }), }), ], diff --git a/backend/core/src/shared/types/has-keys.ts b/backend/core/src/shared/types/has-keys.ts new file mode 100644 index 0000000000..85c8db60e9 --- /dev/null +++ b/backend/core/src/shared/types/has-keys.ts @@ -0,0 +1,3 @@ +export type HasKeys = { + [P in keyof T]: unknown +} diff --git a/backend/core/src/shared/types/language-enum.ts b/backend/core/src/shared/types/language-enum.ts index d666315525..245b5c8110 100644 --- a/backend/core/src/shared/types/language-enum.ts +++ b/backend/core/src/shared/types/language-enum.ts @@ -3,4 +3,5 @@ export enum Language { es = "es", vi = "vi", zh = "zh", + tl = "tl", } diff --git a/backend/core/src/shared/types/orderdir-enum.ts b/backend/core/src/shared/types/orderdir-enum.ts new file mode 100644 index 0000000000..c5544abe52 --- /dev/null +++ b/backend/core/src/shared/types/orderdir-enum.ts @@ -0,0 +1,4 @@ +export enum OrderDirEnum { + ASC = "ASC", + DESC = "DESC", +} diff --git a/backend/core/src/shared/unit-transformation-helpers.spec.ts b/backend/core/src/shared/unit-transformation-helpers.spec.ts new file mode 100644 index 0000000000..76ca3cc036 --- /dev/null +++ b/backend/core/src/shared/unit-transformation-helpers.spec.ts @@ -0,0 +1,74 @@ +import { MinMax } from "../units/types/min-max" +import { setMinMax } from "./unit-transformation-helpers" + +describe("Unit Transformation Helpers", () => { + it("setsMinMax when range is undefined", () => { + let testRange: MinMax + + // eslint-disable-next-line prefer-const + testRange = setMinMax(testRange, 5) + + expect(testRange.min).toBe(5) + expect(testRange.max).toBe(5) + }) + + it("setsMinMax updates max when range is already set", () => { + let testRange: MinMax = { + min: 1, + max: 5, + } + + testRange = setMinMax(testRange, 7) + + expect(testRange.min).toBe(1) + expect(testRange.max).toBe(7) + }) + + it("SetsMinMax updates min when range is already set", () => { + let testRange: MinMax = { + min: 3, + max: 5, + } + + testRange = setMinMax(testRange, 1) + + expect(testRange.min).toBe(1) + expect(testRange.max).toBe(5) + }) + + it("SetsMinMax doesn't update if value already set as min", () => { + let testRange: MinMax = { + min: 1, + max: 5, + } + + testRange = setMinMax(testRange, 1) + + expect(testRange.min).toBe(1) + expect(testRange.max).toBe(5) + }) + + it("SetsMinMax doesn't update if value already set as max", () => { + let testRange: MinMax = { + min: 1, + max: 5, + } + + testRange = setMinMax(testRange, 5) + + expect(testRange.min).toBe(1) + expect(testRange.max).toBe(5) + }) + + it("SetsMinMax returns range if value is null", () => { + let testRange: MinMax = { + min: 1, + max: 5, + } + + testRange = setMinMax(testRange, null) + + expect(testRange.min).toBe(1) + expect(testRange.max).toBe(5) + }) +}) diff --git a/backend/core/src/shared/unit-transformation-helpers.ts b/backend/core/src/shared/unit-transformation-helpers.ts new file mode 100644 index 0000000000..b44f96aa54 --- /dev/null +++ b/backend/core/src/shared/unit-transformation-helpers.ts @@ -0,0 +1,16 @@ +import { MinMax } from "../units/types/min-max" + +export function setMinMax(range: MinMax, value: number): MinMax { + if (value === null) return range + if (range === undefined) { + return { + min: value, + max: value, + } + } else { + range.min = Math.min(range.min, value) + range.max = Math.max(range.max, value) + + return range + } +} diff --git a/backend/core/src/shared/units-transformations.spec.ts b/backend/core/src/shared/units-transformations.spec.ts index 5adbfde42b..92bb5a9a92 100644 --- a/backend/core/src/shared/units-transformations.spec.ts +++ b/backend/core/src/shared/units-transformations.spec.ts @@ -1,55 +1,13 @@ -import { AmiChart } from "../ami-charts/entities/ami-chart.entity" -import { UnitAmiChartOverride } from "../units/entities/unit-ami-chart-override.entity" -import { mergeAmiChartWithOverrides } from "./units-transformations" +import { getUnitGroupSummary, getHouseholdMaxIncomeSummary } from "./units-transformations" -describe("Unit Transformations", () => { - it("Ami chart items are correctly overwritten", () => { - let amiChart: AmiChart = { - id: "id", - createdAt: new Date(), - updatedAt: new Date(), - name: "name", - items: [ - { - percentOfAmi: 1, - householdSize: 1, - income: 1, - }, - { - percentOfAmi: 2, - householdSize: 2, - income: 2, - }, - { - percentOfAmi: 3, - householdSize: 3, - income: 3, - }, - ], - jurisdiction: { - id: "id", - createdAt: new Date(), - updatedAt: new Date(), - name: "name", - }, - } +describe("getUnitGroupSummary", () => { + it("plaeholder", () => { + // + }) +}) - const amiChartOverride: UnitAmiChartOverride = { - id: "id", - createdAt: new Date(), - updatedAt: new Date(), - items: [ - { - percentOfAmi: 2, - householdSize: 2, - income: 20, - }, - ], - } - amiChart = mergeAmiChartWithOverrides(amiChart, amiChartOverride) - expect(amiChart.items.length).toBe(3) - expect(amiChart.items[0].income).toBe(1) - expect(amiChart.items[1].income).toBe(20) - expect(amiChart.items[2].income).toBe(3) +describe("getHouseholdMaxIncomeSummary", () => { + it("placeholder", () => { + // }) }) diff --git a/backend/core/src/shared/units-transformations.ts b/backend/core/src/shared/units-transformations.ts index 29f2c52656..2b07092b31 100644 --- a/backend/core/src/shared/units-transformations.ts +++ b/backend/core/src/shared/units-transformations.ts @@ -1,388 +1,162 @@ -import { UnitStatus } from "../units/types/unit-status-enum" -import { Unit } from "../units/entities/unit.entity" -import { MinMax } from "../units/types/min-max" -import { MinMaxCurrency } from "../units/types/min-max-currency" -import { UnitSummary } from "../units/types/unit-summary" -import { UnitsSummarized } from "../units/types/units-summarized" -import { UnitTypeDto } from "../unit-types/dto/unit-type.dto" -import { UnitType } from "../unit-types/entities/unit-type.entity" -import { UnitAccessibilityPriorityType } from "../unit-accessbility-priority-types/entities/unit-accessibility-priority-type.entity" -import { AmiChart } from "../ami-charts/entities/ami-chart.entity" -import { AmiChartItem } from "../ami-charts/entities/ami-chart-item.entity" -import { UnitAmiChartOverride } from "../units/entities/unit-ami-chart-override.entity" - -export type AnyDict = { [key: string]: unknown } -type Units = Unit[] +import { UnitGroupSummary } from "../units/types/unit-group-summary" -const usd = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: 0, - maximumFractionDigits: 0, -}) - -const minMax = (baseValue: MinMax, newValue: number): MinMax => { - return { - min: Math.min(baseValue.min, newValue), - max: Math.max(baseValue.max, newValue), - } -} - -const minMaxCurrency = (baseValue: MinMaxCurrency, newValue: number): MinMaxCurrency => { - return { - min: usd.format(Math.min(parseFloat(baseValue.min.replace(/[^0-9.-]+/g, "")), newValue)), - max: usd.format(Math.max(parseFloat(baseValue.max.replace(/[^0-9.-]+/g, "")), newValue)), - } -} +import { HouseholdMaxIncomeSummary } from "../units/types/household-max-income-summary" +import { UnitSummaries } from "../units/types/unit-summaries" -const getAmiChartItemUniqueKey = (amiChartItem: AmiChartItem) => { - return amiChartItem.householdSize.toString() + "-" + amiChartItem.percentOfAmi.toString() -} +import { AmiChart } from "../ami-charts/entities/ami-chart.entity" -export const mergeAmiChartWithOverrides = (amiChart: AmiChart, override: UnitAmiChartOverride) => { - const householdAmiPercentageOverrideMap: Map = override.items.reduce( - (acc, amiChartItem) => { - acc.set(getAmiChartItemUniqueKey(amiChartItem), amiChartItem) - return acc - }, - new Map() +import { UnitGroup } from "../units-summary/entities/unit-group.entity" +import { MinMax } from "../units/types/min-max" +import { MonthlyRentDeterminationType } from "../units-summary/types/monthly-rent-determination.enum" +import { HUDMSHDA2021 } from "../seeder/seeds/ami-charts/HUD-MSHDA2021" +import { setMinMax } from "./unit-transformation-helpers" + +// One row for every unit group, with rent and ami ranges across all ami levels +// Used to display the main pricing table +export const getUnitGroupSummary = (unitGroups: UnitGroup[] = []): UnitGroupSummary[] => { + const summary: UnitGroupSummary[] = [] + + const sortedUnitGroups = unitGroups?.sort( + (a, b) => + a.unitType.sort((c, d) => c.numBedrooms - d.numBedrooms)[0].numBedrooms - + b.unitType.sort((e, f) => e.numBedrooms - f.numBedrooms)[0].numBedrooms ) - for (const amiChartItem of amiChart.items) { - const amiChartItemOverride = householdAmiPercentageOverrideMap.get( - getAmiChartItemUniqueKey(amiChartItem) - ) - if (amiChartItemOverride) { - amiChartItem.income = amiChartItemOverride.income - } - } - return amiChart -} - -// Creates data used to display a table of household size/unit size by maximum income per the AMI charts on the units -// Unit sets can have multiple AMI charts used, in which case the table displays ranges -const hmiData = (units: Units, maxHouseholdSize: number, amiCharts: AmiChart[]) => { - // Currently, BMR chart is just toggling whether or not the first column shows Household Size or Unit Type - if (!units || units.length === 0) { - return null - } - const showUnitType = units[0].bmrProgramChart - - type ChartAndPercentage = { - percentage: number - chart: AmiChart - } - - // All unique AMI percentages across all units - const allPercentages: number[] = [ - ...new Set( - units.filter((item) => item != null).map((unit) => parseInt(unit.amiPercentage, 10)) - ), - ].sort(function (a, b) { - return a - b - }) - - const amiChartMap: Record = amiCharts.reduce((acc, amiChart) => { - acc[amiChart.id] = amiChart - return acc - }, {}) - - // All unique combinations of an AMI percentage and an AMI chart across all units - const uniquePercentageChartSet: ChartAndPercentage[] = [ - ...new Set( - units - .map((unit) => { - let amiChart = amiChartMap[unit.amiChartId] - if (unit.amiChartOverride) { - amiChart = mergeAmiChartWithOverrides(amiChart, unit.amiChartOverride) - } - return { - percentage: parseInt(unit.amiPercentage, 10), - chart: amiChart, - } - }) - .map((item) => JSON.stringify(item)) - ), - ].map((uniqueSetString) => JSON.parse(uniqueSetString)) - - const hmiHeaders = { - sizeColumn: showUnitType ? "t.unitType" : "listings.householdSize", - } as AnyDict - const bmrHeaders = [ - "listings.unitTypes.studio", - "listings.unitTypes.oneBdrm", - "listings.unitTypes.twoBdrm", - "listings.unitTypes.threeBdrm", - "listings.unitTypes.fourBdrm", - ] - const hmiRows = [] as AnyDict[] - - // 1. If there are multiple AMI levels, show each AMI level (max income per - // year only) for each size (number of cols = the size col + # ami levels) - // 2. If there is only one AMI level, show max income per month and per - // year for each size (number of cols = the size col + 2 for each income style) - if (allPercentages.length > 1) { - allPercentages.forEach((percent) => { - // Pass translation with its respective argument with format `key*argumentName:argumentValue` - hmiHeaders[`ami${percent}`] = `listings.percentAMIUnit*percent:${percent}` - }) - } else { - hmiHeaders["maxIncomeMonth"] = "listings.maxIncomeMonth" - hmiHeaders["maxIncomeYear"] = "listings.maxIncomeYear" - } - - const findAmiValueInChart = ( - amiChart: AmiChartItem[], - householdSize: number, - percentOfAmi: number - ) => { - return amiChart.find((item) => { - return item.householdSize === householdSize && item.percentOfAmi === percentOfAmi - })?.income - } - - const yearlyCurrencyStringToMonthly = (currency: string) => { - return usd.format(parseFloat(currency.replace(/[^0-9.-]+/g, "")) / 12) - } - - // Build row data by household size - new Array(maxHouseholdSize).fill(maxHouseholdSize).forEach((_, index) => { - const currentHouseholdSize = index + 1 - const rowData = { - sizeColumn: showUnitType ? bmrHeaders[index] : currentHouseholdSize, - } - - let rowHasData = false // Row is valid if at least one column is filled, otherwise don't push the row - allPercentages.forEach((currentAmiPercent) => { - // Get all the charts that we're using with this percentage and size - const uniquePercentCharts = uniquePercentageChartSet.filter((uniqueChartAndPercentage) => { - return uniqueChartAndPercentage.percentage === currentAmiPercent - }) - // If we don't have data for this AMI percentage and household size, this cell is empty - if (uniquePercentCharts.length === 0) { - if (allPercentages.length === 1) { - rowData["maxIncomeMonth"] = "" - rowData["maxIncomeYear"] = "" - } else { - rowData[`ami${currentAmiPercent}`] = "" - } + sortedUnitGroups.forEach((group) => { + let rentAsPercentIncomeRange: MinMax, rentRange: MinMax, amiPercentageRange: MinMax + group.amiLevels.forEach((level) => { + if (level.monthlyRentDeterminationType === MonthlyRentDeterminationType.flatRent) { + rentRange = setMinMax(rentRange, level.flatRentValue) } else { - if (!uniquePercentCharts[0].chart) { - return - } - // If we have chart data, create a max income range string - const firstChartValue = findAmiValueInChart( - uniquePercentCharts[0].chart.items, - currentHouseholdSize, - currentAmiPercent - ) - if (!firstChartValue) { - return - } - const maxIncomeRange = uniquePercentCharts.reduce( - (incomeRange, uniqueSet) => { - return minMaxCurrency( - incomeRange, - findAmiValueInChart(uniqueSet.chart.items, currentHouseholdSize, currentAmiPercent) - ) - }, - { min: usd.format(firstChartValue), max: usd.format(firstChartValue) } as MinMaxCurrency + rentAsPercentIncomeRange = setMinMax( + rentAsPercentIncomeRange, + level.percentageOfIncomeValue ) - if (allPercentages.length === 1) { - rowData[ - "maxIncomeMonth" - ] = `listings.monthlyIncome*income:${yearlyCurrencyStringToMonthly(maxIncomeRange.max)}` - rowData["maxIncomeYear"] = `listings.annualIncome*income:${maxIncomeRange.max}` - } else { - rowData[`ami${currentAmiPercent}`] = `listings.annualIncome*income:${maxIncomeRange.max}` - } - rowHasData = true } + + amiPercentageRange = setMinMax(amiPercentageRange, level.amiPercentage) }) - if (rowHasData) { - hmiRows.push(rowData) + const groupSummary: UnitGroupSummary = { + unitTypes: group.unitType + .sort((a, b) => (a.numBedrooms < b.numBedrooms ? -1 : 1)) + .map((type) => type.name), + rentAsPercentIncomeRange, + rentRange: rentRange && { + min: rentRange.min ? `$${rentRange.min}` : "", + max: rentRange.max ? `$${rentRange.max}` : "", + }, + amiPercentageRange, + openWaitlist: group.openWaitlist, + unitVacancies: group.totalAvailable, + bathroomRange: { + min: group.bathroomMin, + max: group.bathroomMax, + }, + floorRange: { + min: group.floorMin, + max: group.floorMax, + }, + sqFeetRange: { + min: group.sqFeetMin, + max: group.sqFeetMax, + }, } + summary.push(groupSummary) }) - return { columns: hmiHeaders, rows: hmiRows } -} - -const getCurrencyString = (initialValue: string) => { - return usd.format(getRoundedNumber(initialValue)) -} - -const getRoundedNumber = (initialValue: string) => { - return parseFloat(parseFloat(initialValue).toFixed(2)) -} - -const getDefaultSummaryRanges = (unit: Unit) => { - return { - areaRange: { min: parseFloat(unit.sqFeet), max: parseFloat(unit.sqFeet) }, - minIncomeRange: { - min: getCurrencyString(unit.monthlyIncomeMin), - max: getCurrencyString(unit.monthlyIncomeMin), - }, - occupancyRange: { min: unit.minOccupancy, max: unit.maxOccupancy }, - rentRange: { - min: getCurrencyString(unit.monthlyRent), - max: getCurrencyString(unit.monthlyRent), - }, - rentAsPercentIncomeRange: { - min: parseFloat(unit.monthlyRentAsPercentOfIncome), - max: parseFloat(unit.monthlyRentAsPercentOfIncome), - }, - floorRange: { - min: unit.floor, - max: unit.floor, - }, - unitType: unit.unitType, - totalAvailable: 0, - } + return summary } -const getUnitsSummary = (unit: Unit, existingSummary?: UnitSummary) => { - if (!existingSummary) { - return getDefaultSummaryRanges(unit) +// One row for every household size, with max income ranged pulled from all ami charts +// Used to display the maximum income table +export const getHouseholdMaxIncomeSummary = ( + unitGroups: UnitGroup[] = [], + amiCharts: AmiChart[] +): HouseholdMaxIncomeSummary => { + const columns = { + householdSize: "householdSize", } - const summary = existingSummary + const rows = [] - // Income Range - summary.minIncomeRange = minMaxCurrency( - summary.minIncomeRange, - getRoundedNumber(unit.monthlyIncomeMin) - ) - - // Occupancy Range - summary.occupancyRange = minMax(summary.occupancyRange, unit.minOccupancy) - summary.occupancyRange = minMax(summary.occupancyRange, unit.maxOccupancy) - - // Rent Ranges - summary.rentAsPercentIncomeRange = minMax( - summary.rentAsPercentIncomeRange, - parseFloat(unit.monthlyRentAsPercentOfIncome) - ) - summary.rentRange = minMaxCurrency(summary.rentRange, getRoundedNumber(unit.monthlyRent)) - - // Floor Range - if (unit.floor) { - summary.floorRange = minMax(summary.floorRange, unit.floor) + if (!amiCharts || (amiCharts && amiCharts.length === 0)) { + return { + columns, + rows, + } } - // Area Range - summary.areaRange = minMax(summary.areaRange, parseFloat(unit.sqFeet)) - - return summary -} - -type UnitMap = { - [key: string]: Unit[] -} - -const UnitTypeSort = ["studio", "oneBdrm", "twoBdrm", "threeBdrm"] + // if there are two amiCharts (Detroit only has two), then we can use HUD-MSHDA2021, which is a merge of the two, with the max values of both (HUD had higher values for 30 and 80%) + const amiChartItems = amiCharts.length === 2 ? HUDMSHDA2021.items : amiCharts[0].items -// Allows for multiples rows under one unit type if the rent methods differ -export const summarizeUnitsByTypeAndRent = (units: Units): UnitSummary[] => { - const summaries: UnitSummary[] = [] - const unitMap: UnitMap = {} + let occupancyRange: MinMax + const amiPercentages = new Set() + // aggregate household sizes across all groups based off of the occupancy range + unitGroups.forEach((group) => { + if (occupancyRange === undefined) { + occupancyRange = { + min: group.minOccupancy, + max: group.maxOccupancy, + } + } else { + occupancyRange.min = Math.min(occupancyRange.min, group.minOccupancy) + occupancyRange.max = Math.max(occupancyRange.max, group.maxOccupancy) + } - units.forEach((unit) => { - const currentUnitType = unit.unitType - const currentUnitRent = unit.monthlyRentAsPercentOfIncome - const thisKey = currentUnitType?.name.concat(currentUnitRent) - if (!(thisKey in unitMap)) unitMap[thisKey] = [] - unitMap[thisKey].push(unit) + group.amiLevels.forEach((level) => { + amiPercentages.add(level.amiPercentage) + }) }) - for (const key in unitMap) { - const finalSummary = unitMap[key].reduce((summary, unit, index) => { - return getUnitsSummary(unit, index === 0 ? null : summary) - }, {} as UnitSummary) - finalSummary.totalAvailable = unitMap[key].filter( - (unit) => unit.status === UnitStatus.available - ).length - summaries.push(finalSummary) - } + Array.from(amiPercentages) + .filter((percentage) => percentage !== null) + .sort() + .forEach((percentage) => { + // preface with percentage to keep insertion order + columns[`percentage${percentage}`] = percentage + }) - return summaries.sort((a, b) => { - return ( - UnitTypeSort.indexOf(a.unitType.name) - UnitTypeSort.indexOf(b.unitType.name) || - Number(a.minIncomeRange.min) - Number(b.minIncomeRange.min) - ) - }) -} + const hmiMap = {} + + // for the occupancy range, get the max income per percentage of AMI across the AMI charts + amiChartItems.forEach((item) => { + if ( + item.householdSize >= occupancyRange.min && + item.householdSize <= occupancyRange.max && + amiPercentages.has(item.percentOfAmi) + ) { + if (hmiMap[item.householdSize] === undefined) { + hmiMap[item.householdSize] = {} + } -// One row per unit type -export const summarizeUnitsByType = (units: Units, unitTypes: UnitTypeDto[]): UnitSummary[] => { - const summaries = unitTypes.map( - (unitType: UnitTypeDto): UnitSummary => { - const summary = {} as UnitSummary - const unitsByType = units.filter((unit: Unit) => unit.unitType.name == unitType.name) - const finalSummary = Array.from(unitsByType).reduce((summary, unit, index) => { - return getUnitsSummary(unit, index === 0 ? null : summary) - }, summary) - return finalSummary + hmiMap[item.householdSize][item.percentOfAmi] = item.income } - ) - return summaries.sort((a, b) => { - return ( - UnitTypeSort.indexOf(a.unitType.name) - UnitTypeSort.indexOf(b.unitType.name) || - Number(a.minIncomeRange.min) - Number(b.minIncomeRange.min) - ) }) -} -const summarizeByAmi = (units: Units, amiPercentages: string[]) => { - return amiPercentages.map((percent: string) => { - const unitsByAmiPercentage = units.filter((unit: Unit) => unit.amiPercentage == percent) - return { - percent: percent, - byUnitType: summarizeUnitsByTypeAndRent(unitsByAmiPercentage), + // set rows from hmiMap + for (const householdSize in hmiMap) { + const obj = { + householdSize, } - }) -} - -export const getUnitTypes = (units: Unit[]): UnitType[] => { - const unitTypes = new Map() - for (const unitType of units.map((unit) => unit.unitType).filter((item) => item != null)) { - unitTypes.set(unitType.id, unitType) + for (const ami in hmiMap[householdSize]) { + obj[`percentage${ami}`] = hmiMap[householdSize][ami] + } + rows.push(obj) } - return Array.from(unitTypes.values()) + return { + columns, + rows, + } } -export const summarizeUnits = (units: Unit[], amiCharts: AmiChart[]): UnitsSummarized => { - const data = {} as UnitsSummarized +export const summarizeUnits = (units: UnitGroup[], amiCharts: AmiChart[]): UnitSummaries => { + const data = {} as UnitSummaries if (!units || (units && units.length === 0)) { return data } - const unitTypes = new Map() - for (const unitType of units.map((unit) => unit.unitType).filter((item) => item != null)) { - unitTypes.set(unitType.id, unitType) - } - data.unitTypes = getUnitTypes(units) - - const priorityTypes = new Map() - for (const priorityType of units - .map((unit) => unit.priorityType) - .filter((item) => item != null)) { - priorityTypes.set(priorityType.id, priorityType) - } - data.priorityTypes = Array.from(priorityTypes.values()) - - data.amiPercentages = Array.from( - new Set(units.map((unit) => unit.amiPercentage).filter((item) => item != null)) - ) - data.byUnitTypeAndRent = summarizeUnitsByTypeAndRent(units) - data.byUnitType = summarizeUnitsByType(units, data.unitTypes) - data.byAMI = summarizeByAmi(units, data.amiPercentages) - data.hmi = hmiData( - units, - data.byUnitType.reduce((maxHousehold, summary) => { - return Math.max(maxHousehold, summary.occupancyRange.max) - }, 0), - amiCharts - ) + data.unitGroupSummary = getUnitGroupSummary(units) + data.householdMaxIncomeSummary = getHouseholdMaxIncomeSummary(units, amiCharts) return data } diff --git a/backend/core/src/libs/arrayLib/index.ts b/backend/core/src/shared/utils/array-index.ts similarity index 100% rename from backend/core/src/libs/arrayLib/index.ts rename to backend/core/src/shared/utils/array-index.ts diff --git a/backend/core/src/shared/assign-defined.ts b/backend/core/src/shared/utils/assign-defined.ts similarity index 100% rename from backend/core/src/shared/assign-defined.ts rename to backend/core/src/shared/utils/assign-defined.ts diff --git a/backend/core/src/shared/utils/cap-and-split.ts b/backend/core/src/shared/utils/cap-and-split.ts new file mode 100644 index 0000000000..74927f7262 --- /dev/null +++ b/backend/core/src/shared/utils/cap-and-split.ts @@ -0,0 +1,7 @@ +import { capitalizeFirstLetter } from "./capitalize-first-letter" + +export function capAndSplit(str: string): string { + let newStr = capitalizeFirstLetter(str) + newStr = newStr.split(/(?=[A-Z])/).join(" ") + return newStr +} diff --git a/backend/core/src/shared/utils/capitalize-first-letter.ts b/backend/core/src/shared/utils/capitalize-first-letter.ts new file mode 100644 index 0000000000..13061c721e --- /dev/null +++ b/backend/core/src/shared/utils/capitalize-first-letter.ts @@ -0,0 +1,3 @@ +export function capitalizeFirstLetter(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1) +} diff --git a/backend/core/src/shared/utils/deep-find.ts b/backend/core/src/shared/utils/deep-find.ts new file mode 100644 index 0000000000..6022e37556 --- /dev/null +++ b/backend/core/src/shared/utils/deep-find.ts @@ -0,0 +1,13 @@ +export function deepFind(obj, path) { + const paths = path.split(".") + let current = obj + + for (let i = 0; i < paths.length; ++i) { + if (current[paths[i]] == undefined) { + return undefined + } else { + current = current[paths[i]] + } + } + return current +} diff --git a/backend/core/src/shared/utils/format-boolean.ts b/backend/core/src/shared/utils/format-boolean.ts new file mode 100644 index 0000000000..cf91c5b89e --- /dev/null +++ b/backend/core/src/shared/utils/format-boolean.ts @@ -0,0 +1,3 @@ +export function formatBoolean(val) { + return val ? "Yes" : "No" +} diff --git a/backend/core/src/shared/utils/format-local-date.ts b/backend/core/src/shared/utils/format-local-date.ts new file mode 100644 index 0000000000..453c3179e1 --- /dev/null +++ b/backend/core/src/shared/utils/format-local-date.ts @@ -0,0 +1,21 @@ +import dayjs from "dayjs" +import utc from "dayjs/plugin/utc" +import tz from "dayjs/plugin/timezone" +import advanced from "dayjs/plugin/advancedFormat" +import { isEmpty } from "./is-empty" +dayjs.extend(utc) +dayjs.extend(tz) +dayjs.extend(advanced) + +export const formatLocalDate = ( + rawDate: string | Date, + format: string, + timeZone?: string +): string => { + if (!isEmpty(rawDate)) { + const utcDate = dayjs.utc(rawDate) + if (!isEmpty(timeZone)) return utcDate.tz(timeZone.replace("-", "/")).format(format) + return utcDate.format(format) + } + return "" +} diff --git a/backend/core/src/shared/utils/get-birthday.ts b/backend/core/src/shared/utils/get-birthday.ts new file mode 100644 index 0000000000..dd8b2e4c29 --- /dev/null +++ b/backend/core/src/shared/utils/get-birthday.ts @@ -0,0 +1,7 @@ +export function getBirthday(day, month, year) { + let birthday = "" + if (day && month && year) { + birthday = `${month}/${day}/${year}` + } + return birthday +} diff --git a/backend/core/src/libs/miscLib/index.ts b/backend/core/src/shared/utils/is-empty.ts similarity index 100% rename from backend/core/src/libs/miscLib/index.ts rename to backend/core/src/shared/utils/is-empty.ts diff --git a/backend/core/src/shared/views/change-email.hbs b/backend/core/src/shared/views/change-email.hbs new file mode 100644 index 0000000000..d1d321efb9 --- /dev/null +++ b/backend/core/src/shared/views/change-email.hbs @@ -0,0 +1,11 @@ +

{{t "t.hello"}} {{> user-name }},

+

+ {{t "changeEmail.message" appOptions}} +

+

+ {{t "changeEmail.onChangeEmailMessage"}} +

+

+ {{t "changeEmail.changeMyEmail"}} +

+{{> footer }} diff --git a/backend/core/src/shared/views/confirmation.hbs b/backend/core/src/shared/views/confirmation.hbs new file mode 100644 index 0000000000..9cc04f3cea --- /dev/null +++ b/backend/core/src/shared/views/confirmation.hbs @@ -0,0 +1,7 @@ +

{{t "t.hello"}} {{> user-name }},

+

{{t "confirmation.thankYouForApplying"}} {{listing.name}}

+

{{t "confirmation.yourConfirmationNumber"}} {{application.confirmationCode}}

+

{{t "confirmation.whatToExpectNext"}}
{{whatToExpectText}}

+

{{t "confirmation.shouldBeChosen"}}

+{{> leasing-agent }} +{{> footer }} diff --git a/backend/core/src/views/forgot-password.hbs b/backend/core/src/shared/views/forgot-password.hbs similarity index 100% rename from backend/core/src/views/forgot-password.hbs rename to backend/core/src/shared/views/forgot-password.hbs diff --git a/backend/core/src/views/invite.hbs b/backend/core/src/shared/views/invite.hbs similarity index 100% rename from backend/core/src/views/invite.hbs rename to backend/core/src/shared/views/invite.hbs diff --git a/backend/core/src/shared/views/mfa-code.hbs b/backend/core/src/shared/views/mfa-code.hbs new file mode 100644 index 0000000000..38d0008144 --- /dev/null +++ b/backend/core/src/shared/views/mfa-code.hbs @@ -0,0 +1,8 @@ +

{{t "t.hello"}} {{> user-name }}

+

+ {{t "mfaCodeEmail.message" }} +

+

+ {{t "mfaCodeEmail.mfaCode" mfaCodeOptions}} +

+{{> footer }} diff --git a/backend/core/src/views/partials/feedback.hbs b/backend/core/src/shared/views/partials/feedback.hbs similarity index 100% rename from backend/core/src/views/partials/feedback.hbs rename to backend/core/src/shared/views/partials/feedback.hbs diff --git a/backend/core/src/shared/views/partials/footer.hbs b/backend/core/src/shared/views/partials/footer.hbs new file mode 100644 index 0000000000..fdb0898cb8 --- /dev/null +++ b/backend/core/src/shared/views/partials/footer.hbs @@ -0,0 +1,4 @@ +

+ {{t "footer.thankYou"}}
+ {{t "footer.footer"}} +

\ No newline at end of file diff --git a/backend/core/src/views/partials/leasing-agent.hbs b/backend/core/src/shared/views/partials/leasing-agent.hbs similarity index 100% rename from backend/core/src/views/partials/leasing-agent.hbs rename to backend/core/src/shared/views/partials/leasing-agent.hbs diff --git a/backend/core/src/views/partials/user-name.hbs b/backend/core/src/shared/views/partials/user-name.hbs similarity index 100% rename from backend/core/src/views/partials/user-name.hbs rename to backend/core/src/shared/views/partials/user-name.hbs diff --git a/backend/core/src/views/register-email.hbs b/backend/core/src/shared/views/register-email.hbs similarity index 82% rename from backend/core/src/views/register-email.hbs rename to backend/core/src/shared/views/register-email.hbs index bdf9511159..afe9514dea 100644 --- a/backend/core/src/views/register-email.hbs +++ b/backend/core/src/shared/views/register-email.hbs @@ -1,4 +1,4 @@ -

{{t "t.hello"}} {{> user-name }}

+

{{t "t.hello"}} {{> user-name }},

{{t "register.welcomeMessage" appOptions}}

diff --git a/backend/core/src/sms/controllers/sms.controller.spec.ts b/backend/core/src/sms/controllers/sms.controller.spec.ts new file mode 100644 index 0000000000..57f8e2f2b2 --- /dev/null +++ b/backend/core/src/sms/controllers/sms.controller.spec.ts @@ -0,0 +1,55 @@ +import { Test, TestingModule } from "@nestjs/testing" +import { SmsService } from "../services/sms.service" +import { SmsController } from "./sms.controller" +import { AuthzService } from "../../auth/services/authz.service" +import { HttpException } from "@nestjs/common" + +// Cypress brings in Chai types for the global expect, but we want to use jest +// expect here so we need to re-declare it. +// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 +declare const expect: jest.Expect + +const mockedSmsService = { send: jest.fn() } + +describe("SmsController", () => { + let controller: SmsController + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { provide: AuthzService, useValue: {} }, + { provide: SmsService, useValue: mockedSmsService }, + ], + controllers: [SmsController], + }).compile() + + controller = module.get(SmsController) + }) + + it("should be defined", () => { + expect(controller).toBeDefined() + }) + + describe("send", () => { + it("as non-admin throws exception", async () => { + await expect( + controller.send( + { user: { roles: { isAdmin: false } } }, + { body: "test body", phoneNumber: "+15555555555" } + ) + ).rejects.toThrow(HttpException) + }) + + it("as admin sends to service", async () => { + await controller.send( + { user: { roles: { isAdmin: true } } }, + { body: "test body", phoneNumber: "+15555555555" } + ) + + expect(mockedSmsService.send).toHaveBeenCalledWith({ + body: "test body", + phoneNumber: "+15555555555", + }) + }) + }) +}) diff --git a/backend/core/src/sms/controllers/sms.controller.ts b/backend/core/src/sms/controllers/sms.controller.ts new file mode 100644 index 0000000000..2a14a0b581 --- /dev/null +++ b/backend/core/src/sms/controllers/sms.controller.ts @@ -0,0 +1,41 @@ +import { + Body, + Controller, + HttpException, + HttpStatus, + Post, + Request, + UseGuards, + UsePipes, + ValidationPipe, +} from "@nestjs/common" +import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger" +import { ResourceType } from "../../auth/decorators/resource-type.decorator" +import { User } from "../../auth/entities/user.entity" +import { AuthzGuard } from "../../auth/guards/authz.guard" +import { OptionalAuthGuard } from "../../auth/guards/optional-auth.guard" +import { AuthContext } from "../../auth/types/auth-context" +import { defaultValidationPipeOptions } from "../../shared/default-validation-pipe-options" +import { StatusDto } from "../../shared/dto/status.dto" +import { SmsDto } from "../dto/sms.dto" +import { SmsService } from "../services/sms.service" + +@Controller("sms") +@ApiBearerAuth() +@ApiTags("sms") +@ResourceType("user") +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +export class SmsController { + constructor(private readonly smsService: SmsService) {} + + @Post() + @UseGuards(OptionalAuthGuard, AuthzGuard) + @ApiOperation({ summary: "Send an SMS", operationId: "send-sms" }) + async send(@Request() req, @Body() dto: SmsDto): Promise { + // Only admins are allowed to send SMS messages. + if (!new AuthContext(req.user as User).user.roles?.isAdmin) { + throw new HttpException("Only administrators can send SMS messages.", HttpStatus.FORBIDDEN) + } + return await this.smsService.send(dto) + } +} diff --git a/backend/core/src/sms/dto/sms.dto.ts b/backend/core/src/sms/dto/sms.dto.ts new file mode 100644 index 0000000000..111b5d2ac5 --- /dev/null +++ b/backend/core/src/sms/dto/sms.dto.ts @@ -0,0 +1,13 @@ +import { Expose } from "class-transformer" +import { IsPhoneNumber, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +export class SmsDto { + @Expose() + @IsString() + body: string + + @Expose() + @IsPhoneNumber("US", { groups: [ValidationsGroupsEnum.default] }) + phoneNumber: string +} diff --git a/backend/core/src/sms/services/sms.service.spec.ts b/backend/core/src/sms/services/sms.service.spec.ts new file mode 100644 index 0000000000..55cbd738fd --- /dev/null +++ b/backend/core/src/sms/services/sms.service.spec.ts @@ -0,0 +1,123 @@ +import { HttpException } from "@nestjs/common" +import { Test, TestingModule } from "@nestjs/testing" +import { Listing } from "../../listings/entities/listing.entity" +import { UserService } from "../../auth/services/user.service" +import { SmsService } from "./sms.service" +import { TwilioService } from "./twilio.service" +import { ListingMarketingTypeEnum } from "../../listings/types/listing-marketing-type-enum" + +// Cypress brings in Chai types for the global expect, but we want to use jest +// expect here so we need to re-declare it. +// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 +declare const expect: jest.Expect + +const mockedTwilioService = { send: jest.fn() } +const mockedUserService = { listAllUsers: jest.fn() } + +const mockListing: Listing = { + name: "Mock Listing", + id: "", + createdAt: null, + updatedAt: null, + referralApplication: null, + property: null, + applications: [], + showWaitlist: false, + unitSummaries: null, + unitGroups: null, + applicationMethods: [], + applicationDropOffAddress: null, + applicationMailingAddress: null, + events: [], + jurisdiction: null, + assets: [], + status: null, + displayWaitlistSize: false, + hasId: null, + marketingType: ListingMarketingTypeEnum.Marketing, + listingPreferences: [], + save: jest.fn(), + remove: jest.fn(), + softRemove: jest.fn(), + recover: jest.fn(), + reload: jest.fn(), +} + +describe("SmsService", () => { + let service: SmsService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SmsService, + { + provide: TwilioService, + useValue: mockedTwilioService, + }, + { + provide: UserService, + useValue: mockedUserService, + }, + ], + }).compile() + + mockedTwilioService.send = jest.fn().mockResolvedValue({ errorCode: 0 }) + service = await module.resolve(SmsService) + }) + + it("should be defined", () => { + expect(service).toBeDefined() + }) + + describe("listing notifications", () => { + it("sends a new-listing notification to opted-in users (who have phone numbers)", async () => { + mockedUserService.listAllUsers.mockResolvedValue([ + // User with a phone number, who is opted in for SMS notifications + { phoneNumber: "+12222222222", preferences: { sendSmsNotifications: true } }, + // User with a phone number, who is opted out of SMS notifications + { phoneNumber: "+13333333333", preferences: { sendSmsNotifications: false } }, + // User who is opted in for SMS notifications, but has no phone number + { preferences: { sendSmsNotifications: true } }, + ]) + + await service.sendNewListingNotification(mockListing) + + expect(mockedTwilioService.send).toHaveBeenCalledTimes(1) + expect(mockedTwilioService.send).toHaveBeenCalledWith( + "A new listing was recently added to Detroit Home Connect: Mock Listing.", + "+12222222222" + ) + }) + + it("throws an exception if the Twilio call returns an error", async () => { + mockedUserService.listAllUsers.mockResolvedValue([ + { phoneNumber: "+12222222222", preferences: { sendSmsNotifications: true } }, + ]) + mockedTwilioService.send.mockResolvedValue({ + errorCode: 1, + errorMessage: "test error message", + }) + + await expect(service.sendNewListingNotification(mockListing)).rejects.toThrow(HttpException) + }) + }) + + describe("send", () => { + it("sends to Twilio", async () => { + await service.send({ phoneNumber: "+15555555555", body: "test body" }) + + expect(mockedTwilioService.send).toHaveBeenCalledWith("test body", "+15555555555") + }) + + it("throws an exception when Twilio errors out", async () => { + mockedTwilioService.send.mockResolvedValue({ + errorCode: 1, + errorMessage: "test error message", + }) + + await expect( + service.send({ phoneNumber: "+15555555555", body: "test body" }) + ).rejects.toThrow(HttpException) + }) + }) +}) diff --git a/backend/core/src/sms/services/sms.service.ts b/backend/core/src/sms/services/sms.service.ts new file mode 100644 index 0000000000..28a5246263 --- /dev/null +++ b/backend/core/src/sms/services/sms.service.ts @@ -0,0 +1,41 @@ +import { HttpException, HttpStatus, Injectable, Scope } from "@nestjs/common" +import { User } from "../../auth/entities/user.entity" +import { UserService } from "../../auth/services/user.service" +import { Listing } from "../../listings/entities/listing.entity" +import { StatusDto } from "../../shared/dto/status.dto" +import { mapTo } from "../../shared/mapTo" +import { SmsDto } from "../dto/sms.dto" +import { TwilioService } from "./twilio.service" + +@Injectable({ scope: Scope.REQUEST }) +export class SmsService { + constructor(private readonly twilio: TwilioService, private readonly userService: UserService) {} + + async sendNewListingNotification(listing: Listing): Promise { + // TODO(https://github.com/CityOfDetroit/bloom/issues/705): when Detroit Home Connect has a + // URL, update this message so that it includes a link to the new listing. + // TODO(https://github.com/CityOfDetroit/bloom/issues/705): translate this string. + const notificationBody = `A new listing was recently added to Detroit Home Connect: ${listing.name}.` + + // TODO(https://github.com/CityOfDetroit/bloom/issues/705): handle filtering in the DB query + // instead of here. + const users: User[] = await this.userService.listAllUsers() + for (const user of users) { + if (user.preferences.sendSmsNotifications && user.phoneNumber) { + const smsDto: SmsDto = { body: notificationBody, phoneNumber: user.phoneNumber } + await this.send(smsDto) + } + } + + return { status: "ok" } + } + + async send(dto: SmsDto): Promise { + const messageInstance = await this.twilio.send(dto.body, dto.phoneNumber) + if (messageInstance.errorCode) { + console.error("Error sending SMS: " + messageInstance.errorMessage) + throw new HttpException(messageInstance.errorMessage, HttpStatus.INTERNAL_SERVER_ERROR) + } + return mapTo(StatusDto, { status: "ok" }) + } +} diff --git a/backend/core/src/sms/services/twilio.service.ts b/backend/core/src/sms/services/twilio.service.ts new file mode 100644 index 0000000000..51e6c073eb --- /dev/null +++ b/backend/core/src/sms/services/twilio.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from "@nestjs/common" +import { + MessageInstance, + MessageListInstanceCreateOptions, +} from "twilio/lib/rest/api/v2010/account/message" +import TwilioClient = require("twilio/lib/rest/Twilio") +import twilio = require("twilio") + +@Injectable() +export class TwilioService { + client: TwilioClient + + constructor() { + // this.client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN) + } + + async send(message: string, recipientPhoneNumber: string): Promise { + const messageOptions: MessageListInstanceCreateOptions = { + body: message, + from: process.env.TWILIO_FROM_NUMBER, + to: recipientPhoneNumber, + } + return this.client.messages.create(messageOptions) + } +} diff --git a/backend/core/src/sms/sms.module.ts b/backend/core/src/sms/sms.module.ts new file mode 100644 index 0000000000..41886ce071 --- /dev/null +++ b/backend/core/src/sms/sms.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common" +import { AuthModule } from "../auth/auth.module" +import { AuthzService } from "../auth/services/authz.service" +import { SmsService } from "./services/sms.service" +import { SmsController } from "./controllers/sms.controller" +import { TwilioService } from "./services/twilio.service" + +@Module({ + imports: [AuthModule], + providers: [AuthzService, SmsService, TwilioService], + exports: [SmsService], + controllers: [SmsController], +}) +export class SmsModule {} diff --git a/backend/core/src/translations/entities/generated-listing-translation.entity.ts b/backend/core/src/translations/entities/generated-listing-translation.entity.ts new file mode 100644 index 0000000000..df098ff98e --- /dev/null +++ b/backend/core/src/translations/entities/generated-listing-translation.entity.ts @@ -0,0 +1,27 @@ +import { Column, Entity } from "typeorm" +import { AbstractEntity } from "../../shared/entities/abstract.entity" +import { Expose, Type } from "class-transformer" +import { Language } from "../../shared/types/language-enum" + +@Entity({ name: "generated_listing_translations" }) +export class GeneratedListingTranslation extends AbstractEntity { + @Column() + @Expose() + listingId: string + + @Column() + @Expose() + jurisdictionId: string + + @Column({ enum: Language }) + @Expose() + language: Language + + @Column({ type: "jsonb" }) + @Expose() + translations: any + + @Column() + @Type(() => Date) + timestamp: Date +} diff --git a/backend/core/src/translations/entities/translation.entity.ts b/backend/core/src/translations/entities/translation.entity.ts index a40d302348..f853dfa497 100644 --- a/backend/core/src/translations/entities/translation.entity.ts +++ b/backend/core/src/translations/entities/translation.entity.ts @@ -1,7 +1,7 @@ -import { Column, Entity, Index, JoinColumn, OneToOne } from "typeorm" +import { Column, Entity, Index, ManyToOne } from "typeorm" import { AbstractEntity } from "../../shared/entities/abstract.entity" import { Expose } from "class-transformer" -import { IsEnum, IsJSON } from "class-validator" +import { IsDefined, IsEnum } from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { ApiProperty } from "@nestjs/swagger" import { Language } from "../../shared/types/language-enum" @@ -11,8 +11,11 @@ import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" @Entity({ name: "translations" }) @Index(["jurisdiction", "language"], { unique: true }) export class Translation extends AbstractEntity { - @OneToOne(() => Jurisdiction) - @JoinColumn() + @ManyToOne(() => Jurisdiction, { + onDelete: "NO ACTION", + onUpdate: "NO ACTION", + eager: true, + }) jurisdiction: Jurisdiction @Column({ enum: Language }) @@ -23,6 +26,6 @@ export class Translation extends AbstractEntity { @Column({ type: "jsonb" }) @Expose() - @IsJSON({ groups: [ValidationsGroupsEnum.default] }) + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) translations: TranslationsType } diff --git a/backend/core/src/translations/services/google-translate.service.ts b/backend/core/src/translations/services/google-translate.service.ts new file mode 100644 index 0000000000..f6694996a9 --- /dev/null +++ b/backend/core/src/translations/services/google-translate.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@nestjs/common" +import { Language } from "../../shared/types/language-enum" +import { Translate } from "@google-cloud/translate/build/src/v2" + +@Injectable() +export class GoogleTranslateService { + public isConfigured(): boolean { + const { GOOGLE_API_ID, GOOGLE_API_EMAIL, GOOGLE_API_KEY } = process.env + return !!GOOGLE_API_KEY && !!GOOGLE_API_EMAIL && !!GOOGLE_API_ID + } + public async fetch(values: string[], language: Language) { + return await GoogleTranslateService.makeTranslateService().translate(values, { + from: Language.en, + to: language, + }) + } + + private static makeTranslateService() { + return new Translate({ + credentials: { + private_key: process.env.GOOGLE_API_KEY.replace(/\\n/gm, "\n"), + client_email: process.env.GOOGLE_API_EMAIL, + }, + projectId: process.env.GOOGLE_API_ID, + }) + } +} diff --git a/backend/core/src/translations/services/translations.service.spec.ts b/backend/core/src/translations/services/translations.service.spec.ts new file mode 100644 index 0000000000..00126cc2a3 --- /dev/null +++ b/backend/core/src/translations/services/translations.service.spec.ts @@ -0,0 +1,212 @@ +import { Test, TestingModule } from "@nestjs/testing" +import { getRepositoryToken } from "@nestjs/typeorm" +import { nanoid } from "nanoid" +import { TranslationsService } from "./translations.service" +import { Translation } from "../entities/translation.entity" +import { Language } from "../../shared/types/language-enum" +import { GeneratedListingTranslation } from "../entities/generated-listing-translation.entity" +import { GoogleTranslateService } from "./google-translate.service" +import { Listing } from "../../listings/entities/listing.entity" +import { BaseEntity } from "typeorm" +import { ApplicationMethodDto } from "../../application-methods/dto/application-method.dto" +import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity" +import { ListingMarketingTypeEnum } from "../../listings/types/listing-marketing-type-enum" + +// Cypress brings in Chai types for the global expect, but we want to use jest +// expect here so we need to re-declare it. +// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 +declare const expect: jest.Expect + +const getMockListing = () => { + const mockListingTemplate: Omit = { + additionalApplicationSubmissionNotes: undefined, + applicationConfig: undefined, + applicationDropOffAddress: undefined, + applicationDropOffAddressOfficeHours: undefined, + applicationDropOffAddressType: undefined, + applicationDueDate: undefined, + applicationFee: undefined, + applicationMailingAddress: undefined, + applicationMethods: [], + applicationOpenDate: undefined, + applicationOrganization: undefined, + applicationPickUpAddress: undefined, + applicationPickUpAddressOfficeHours: undefined, + applicationPickUpAddressType: undefined, + applicationMailingAddressType: undefined, + applications: [], + assets: [], + buildingSelectionCriteria: undefined, + buildingSelectionCriteriaFile: undefined, + commonDigitalApplication: false, + costsNotIncluded: "costs not included", + createdAt: undefined, + creditHistory: undefined, + criminalBackground: undefined, + customMapPin: undefined, + depositHelperText: undefined, + depositMax: undefined, + depositMin: undefined, + digitalApplication: false, + disableUnitsAccordion: undefined, + displayWaitlistSize: false, + events: [], + id: "", + images: [], + isWaitlistOpen: undefined, + jurisdiction: { id: "abcd" } as Jurisdiction, + leasingAgentAddress: undefined, + leasingAgentEmail: undefined, + leasingAgentName: undefined, + leasingAgentOfficeHours: undefined, + leasingAgentPhone: undefined, + leasingAgentTitle: undefined, + leasingAgents: undefined, + listingPreferences: [], + listingPrograms: [], + name: "", + paperApplication: false, + postmarkedApplicationsReceivedByDate: undefined, + programRules: undefined, + property: undefined, + referralOpportunity: false, + rentalAssistance: undefined, + rentalHistory: undefined, + requiredDocuments: undefined, + reservedCommunityDescription: undefined, + reservedCommunityMinAge: undefined, + reservedCommunityType: undefined, + result: undefined, + resultLink: undefined, + reviewOrderType: undefined, + specialNotes: undefined, + status: undefined, + unitSummaries: undefined, + unitGroups: [], + updatedAt: new Date(), + waitlistCurrentSize: undefined, + waitlistMaxSize: undefined, + waitlistOpenSpots: undefined, + whatToExpect: undefined, + marketingType: ListingMarketingTypeEnum.Marketing, + get referralApplication(): ApplicationMethodDto | undefined { + return undefined + }, + get showWaitlist(): boolean { + return false + }, + } + return JSON.parse(JSON.stringify(mockListingTemplate)) +} + +describe("TranslationsService", () => { + let service: TranslationsService + const translationRepositoryFindOneMock = { + findOne: jest.fn(), + } + const generatedListingTranslationRepositoryMock = { + findOne: jest.fn(), + save: jest.fn(), + } + const googleTranslateServiceMock = { + isConfigured: () => true, + fetch: jest.fn(), + } + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TranslationsService, + { + provide: getRepositoryToken(Translation), + useValue: translationRepositoryFindOneMock, + }, + { + provide: getRepositoryToken(GeneratedListingTranslation), + useValue: generatedListingTranslationRepositoryMock, + }, + { + provide: GoogleTranslateService, + useValue: googleTranslateServiceMock, + }, + ], + }).compile() + service = module.get(TranslationsService) + }) + + describe("Translations queries", () => { + it("Should return translations for given county code and language", async () => { + translationRepositoryFindOneMock.findOne.mockReturnValueOnce({}) + const jurisdictionId = nanoid() + const result = await service.getTranslationByLanguageAndJurisdictionOrDefaultEn( + Language.en, + jurisdictionId + ) + expect(result).toStrictEqual({}) + expect(translationRepositoryFindOneMock.findOne.mock.calls[0][0].where.language).toEqual( + Language.en + ) + expect( + translationRepositoryFindOneMock.findOne.mock.calls[0][0].where.jurisdiction.id + ).toEqual(jurisdictionId) + }) + + it("should fetch translations if none are persisted", async () => { + const mockListing = getMockListing() + generatedListingTranslationRepositoryMock.findOne.mockResolvedValueOnce(undefined) + generatedListingTranslationRepositoryMock.save.mockResolvedValueOnce({ translations: [{}] }) + googleTranslateServiceMock.fetch.mockResolvedValueOnce([]) + + await service.translateListing(mockListing as Listing, Language.es) + expect(generatedListingTranslationRepositoryMock.findOne).toHaveBeenCalledTimes(1) + expect(googleTranslateServiceMock.fetch).toHaveBeenCalledTimes(1) + expect(generatedListingTranslationRepositoryMock.save).toHaveBeenCalledTimes(1) + }) + + it("should not fetch translations if any are persisted", async () => { + const mockListing = getMockListing() + const translations = [["costs not included ES translation"]] + const persistedTranslation = { + timestamp: mockListing.updatedAt, + translations, + } + generatedListingTranslationRepositoryMock.findOne.mockResolvedValueOnce(persistedTranslation) + + const result = await service.translateListing(mockListing as Listing, Language.es) + expect(generatedListingTranslationRepositoryMock.findOne).toHaveBeenCalledTimes(1) + expect(googleTranslateServiceMock.fetch).toHaveBeenCalledTimes(0) + expect(generatedListingTranslationRepositoryMock.save).toHaveBeenCalledTimes(0) + expect(result.costsNotIncluded).toBe(translations[0][0]) + }) + + it("should fetch translations if timestamp is older than listing updatedAt", async () => { + const mockListing = getMockListing() + mockListing.updatedAt = new Date() + + const translations = [["costs not included ES translation"]] + const newTranslations = [["costs not included ES translation 2"]] + const persistedTranslation = { + timestamp: new Date(mockListing.updatedAt.getTime() - 1000), + translations, + } + const newPersistedTranslation = { + timestamp: mockListing.updatedAt, + translations: newTranslations, + } + + generatedListingTranslationRepositoryMock.findOne.mockResolvedValueOnce(persistedTranslation) + generatedListingTranslationRepositoryMock.save.mockResolvedValueOnce(newPersistedTranslation) + googleTranslateServiceMock.fetch.mockResolvedValueOnce(newTranslations) + + const result = await service.translateListing(mockListing as Listing, Language.es) + expect(generatedListingTranslationRepositoryMock.findOne).toHaveBeenCalledTimes(1) + expect(googleTranslateServiceMock.fetch).toHaveBeenCalledTimes(1) + expect(generatedListingTranslationRepositoryMock.save).toHaveBeenCalledTimes(1) + expect(result.costsNotIncluded).toBe(newTranslations[0][0]) + }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) +}) diff --git a/backend/core/src/translations/services/translations.service.ts b/backend/core/src/translations/services/translations.service.ts new file mode 100644 index 0000000000..02b2aecade --- /dev/null +++ b/backend/core/src/translations/services/translations.service.ts @@ -0,0 +1,172 @@ +import { Injectable, NotFoundException } from "@nestjs/common" +import { AbstractServiceFactory } from "../../shared/services/abstract-service" +import { Translation } from "../entities/translation.entity" +import { TranslationCreateDto, TranslationUpdateDto } from "../dto/translation.dto" +import { InjectRepository } from "@nestjs/typeorm" +import { Repository } from "typeorm" +import { Language } from "../../shared/types/language-enum" +import { Listing } from "../../listings/entities/listing.entity" +import { GoogleTranslateService } from "./google-translate.service" +import { GeneratedListingTranslation } from "../entities/generated-listing-translation.entity" +import * as lodash from "lodash" + +@Injectable() +export class TranslationsService extends AbstractServiceFactory< + Translation, + TranslationCreateDto, + TranslationUpdateDto +>(Translation) { + constructor( + @InjectRepository(GeneratedListingTranslation) + private readonly generatedListingTranslationRepository: Repository, + private readonly googleTranslateService: GoogleTranslateService + ) { + super() + } + + public async getTranslationByLanguageAndJurisdictionOrDefaultEn( + language: Language, + jurisdictionId: string | null + ) { + try { + return await this.findOne({ + where: { + language, + ...(jurisdictionId && { + jurisdiction: { + id: jurisdictionId, + }, + }), + ...(!jurisdictionId && { + jurisdiction: null, + }), + }, + }) + } catch (e) { + if (e instanceof NotFoundException && language != Language.en) { + console.warn(`Fetching translations for ${language} failed, defaulting to english.`) + return this.findOne({ + where: { + language: Language.en, + ...(jurisdictionId && { + jurisdiction: { + id: jurisdictionId, + }, + }), + ...(!jurisdictionId && { + jurisdiction: null, + }), + }, + }) + } else { + throw e + } + } + } + + public async translateListing(listing: Listing, language: Language) { + if (!this.googleTranslateService.isConfigured()) { + console.warn("listing translation requested, but google translate service is not configured") + return + } + + const pathsToFilter = [ + "applicationPickUpAddressOfficeHours", + "costsNotIncluded", + "creditHistory", + "criminalBackground", + "programRules", + "rentalAssistance", + "rentalHistory", + "requiredDocuments", + "specialNotes", + "whatToExpect", + "whatToExpectAdditionalText", + "property.accessibility", + "property.amenities", + "property.neighborhood", + "property.petPolicy", + "property.servicesOffered", + "property.smokingPolicy", + "property.unitAmenities", + ] + + for (let i = 0; i < listing.events.length; i++) { + pathsToFilter.push(`events[${i}].note`) + pathsToFilter.push(`events[${i}].label`) + } + + for (let i = 0; i < listing.listingPreferences.length; i++) { + pathsToFilter.push(`listingPreferences[${i}].preference.title`) + pathsToFilter.push(`listingPreferences[${i}].preference.description`) + pathsToFilter.push(`listingPreferences[${i}].preference.subtitle`) + } + + for (let i = 0; i < listing.listingPrograms.length; i++) { + pathsToFilter.push(`listingPrograms[${i}].program.title`) + pathsToFilter.push(`listingPrograms[${i}].program.description`) + pathsToFilter.push(`listingPrograms[${i}].program.subtitle`) + } + const listingPathsAndValues: { [key: string]: any } = {} + for (const path of pathsToFilter) { + const value = lodash.get(listing, path) + if (value) { + listingPathsAndValues[path] = lodash.get(listing, path) + } + } + + // Caching + let persistedTranslatedValues + persistedTranslatedValues = await this.getPersistedTranslatedValues(listing, language) + + if ( + Object.keys(listingPathsAndValues).length > 0 && + (!persistedTranslatedValues || persistedTranslatedValues.timestamp < listing.updatedAt) + ) { + const newTranslations = await this.googleTranslateService.fetch( + Object.values(listingPathsAndValues), + language + ) + persistedTranslatedValues = await this.persistTranslatedValues( + persistedTranslatedValues?.id, + listing, + language, + newTranslations + ) + } + + for (const [index, path] of Object.keys(listingPathsAndValues).entries()) { + // accessing 0th index here because google translate service response returns multiple + // possible arrays with results, we are interested in first + lodash.set(listing, path, persistedTranslatedValues.translations[0][index]) + } + + return listing + } + + private async persistTranslatedValues( + id: string | undefined, + listing: Listing, + language: Language, + translatedValues: any + ) { + return await this.generatedListingTranslationRepository.save({ + id, + listingId: listing.id, + jurisdictionId: listing.jurisdiction.id, + language, + translations: translatedValues, + timestamp: listing.updatedAt, + }) + } + + private async getPersistedTranslatedValues(listing: Listing, language: Language) { + return this.generatedListingTranslationRepository.findOne({ + where: { + jurisdictionId: listing.jurisdiction.id, + language, + listingId: listing.id, + }, + }) + } +} diff --git a/backend/core/src/translations/translations.controller.ts b/backend/core/src/translations/translations.controller.ts index 56456f68ed..df7f38f1a3 100644 --- a/backend/core/src/translations/translations.controller.ts +++ b/backend/core/src/translations/translations.controller.ts @@ -15,9 +15,9 @@ import { AuthzGuard } from "../auth/guards/authz.guard" import { ResourceType } from "../auth/decorators/resource-type.decorator" import { mapTo } from "../shared/mapTo" import { defaultValidationPipeOptions } from "../shared/default-validation-pipe-options" -import { TranslationsService } from "./translations.service" import { TranslationCreateDto, TranslationDto, TranslationUpdateDto } from "./dto/translation.dto" import { OptionalAuthGuard } from "../auth/guards/optional-auth.guard" +import { TranslationsService } from "./services/translations.service" @Controller("/translations") @ApiTags("translations") diff --git a/backend/core/src/translations/translations.module.ts b/backend/core/src/translations/translations.module.ts index d35a701978..7d14caaa91 100644 --- a/backend/core/src/translations/translations.module.ts +++ b/backend/core/src/translations/translations.module.ts @@ -1,14 +1,19 @@ import { forwardRef, Module } from "@nestjs/common" import { TypeOrmModule } from "@nestjs/typeorm" import { Translation } from "./entities/translation.entity" -import { TranslationsService } from "./translations.service" import { TranslationsController } from "./translations.controller" import { AuthModule } from "../auth/auth.module" +import { TranslationsService } from "./services/translations.service" +import { GoogleTranslateService } from "./services/google-translate.service" +import { GeneratedListingTranslation } from "./entities/generated-listing-translation.entity" @Module({ - imports: [TypeOrmModule.forFeature([Translation]), forwardRef(() => AuthModule)], + imports: [ + TypeOrmModule.forFeature([Translation, GeneratedListingTranslation]), + forwardRef(() => AuthModule), + ], controllers: [TranslationsController], - providers: [TranslationsService], + providers: [TranslationsService, GoogleTranslateService], exports: [TranslationsService], }) export class TranslationsModule {} diff --git a/backend/core/src/translations/translations.service.spec.ts b/backend/core/src/translations/translations.service.spec.ts deleted file mode 100644 index b688d19fbb..0000000000 --- a/backend/core/src/translations/translations.service.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Test, TestingModule } from "@nestjs/testing" -import { getRepositoryToken } from "@nestjs/typeorm" -import { nanoid } from "nanoid" -import { Translation } from "./entities/translation.entity" -import { TranslationsService } from "./translations.service" -import { Language } from "../shared/types/language-enum" - -// Cypress brings in Chai types for the global expect, but we want to use jest -// expect here so we need to re-declare it. -// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 -declare const expect: jest.Expect - -describe("TranslationsService", () => { - let service: TranslationsService - const translationRepositoryFindOneMock = jest.fn() - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - TranslationsService, - { - provide: getRepositoryToken(Translation), - useValue: { findOne: translationRepositoryFindOneMock }, - }, - ], - }).compile() - - service = module.get(TranslationsService) - translationRepositoryFindOneMock.mockReturnValue({}) - }) - - it("should be defined", () => { - expect(service).toBeDefined() - }) - - describe("Translations queries", () => { - it("Should return translations for given county code and language", async () => { - const jurisdictionId = nanoid() - const result = await service.getTranslationByLanguageAndJurisdictionOrDefaultEn( - Language.en, - jurisdictionId - ) - expect(result).toStrictEqual({}) - expect(translationRepositoryFindOneMock.mock.calls[0][0].where.language).toEqual(Language.en) - expect(translationRepositoryFindOneMock.mock.calls[0][0].where.jurisdiction.id).toEqual( - jurisdictionId - ) - }) - }) - - afterEach(() => { - jest.clearAllMocks() - }) -}) diff --git a/backend/core/src/translations/translations.service.ts b/backend/core/src/translations/translations.service.ts deleted file mode 100644 index 5e35c9dad2..0000000000 --- a/backend/core/src/translations/translations.service.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { AbstractServiceFactory } from "../shared/services/abstract-service" -import { Injectable, NotFoundException } from "@nestjs/common" -import { Translation } from "./entities/translation.entity" -import { Translate } from "@google-cloud/translate/build/src/v2" -import { TranslationCreateDto, TranslationUpdateDto } from "./dto/translation.dto" -import { Language } from "../shared/types/language-enum" -import { Listing } from "../listings/entities/listing.entity" -import { isEmpty } from "../libs/miscLib" - -const TRANSLATION_KEYS = [ - "applicationPickUpAddressOfficeHours", - "costsNotIncluded", - "creditHistory", - "criminalBackground", - "programRules", - "rentalAssistance", - "rentalHistory", - "requiredDocuments", - "specialNotes", - { - events: ["note", "label"], - property: [ - "accessibility", - "amenities", - "neighborhood", - "petPolicy", - "servicesOffered", - "smokingPolicy", - "unitAmenities", - ], - whatToExpect: ["applicantsWillBeContacted", "allInfoWillBeVerified", "bePreparedIfChosen"], - preferences: ["title", "description", "subtitle"], - }, -] -@Injectable() -export class TranslationsService extends AbstractServiceFactory< - Translation, - TranslationCreateDto, - TranslationUpdateDto ->(Translation) { - public async getTranslationByLanguageAndJurisdictionOrDefaultEn( - language: Language, - jurisdictionId: string | null - ) { - try { - return await this.findOne({ - where: { - language, - ...(jurisdictionId && { - jurisdiction: { - id: jurisdictionId, - }, - }), - ...(!jurisdictionId && { - jurisdiction: null, - }), - }, - }) - } catch (e) { - if (e instanceof NotFoundException && language != Language.en) { - console.warn(`Fetching translations for ${language} failed, defaulting to english.`) - return this.findOne({ - where: { - language: Language.en, - ...(jurisdictionId && { - jurisdiction: { - id: jurisdictionId, - }, - }), - ...(!jurisdictionId && { - jurisdiction: null, - }), - }, - }) - } else { - throw e - } - } - } - - public async translateListing(listing: Listing, language: Language) { - /** - * check for necessary keys before continuing - */ - const { GOOGLE_API_ID, GOOGLE_API_EMAIL, GOOGLE_API_KEY } = process.env - if (!GOOGLE_API_ID || !GOOGLE_API_EMAIL || !GOOGLE_API_KEY) return - - // Get key-value pairs from listing to be translated - const translations = this.getTranslations(listing) - if (!translations?.values || (translations?.values && translations.values.length === 0)) return - const translatedValues = await this.translateService().translate(translations.values, { - from: Language.en, - to: language, - }) - // Attach translated values to the listing - translations.keys.forEach((key, index) => { - this.setValue(listing, key, translatedValues[0][index]) - }) - } - - private translateService = () => { - return new Translate({ - credentials: { - private_key: process.env.GOOGLE_API_KEY.replace(/\\n/gm, "\n"), - client_email: process.env.GOOGLE_API_EMAIL, - }, - projectId: process.env.GOOGLE_API_ID, - }) - } - // Sets value to the object by string path. eg. "property.accessibility" or "preferences.0.title" - private setValue = (object, path, value) => - path - .split(".") - .reduce( - (currentObject, currentPath, index) => - (currentObject[currentPath] = - path.split(".").length === ++index ? value : currentObject[currentPath] || {}), - object - ) - - // Returns not null key-values pairs also from nested properties - private findData = (keys, object, results, parent = null) => { - keys.forEach((key) => { - if (typeof key === "string") { - if (Array.isArray(object)) { - object.forEach((value, i) => { - if (isEmpty(value[key]) === false) { - results.keys.push(parent ? [parent, i, key].join(".") : key) - results.values.push(value[key]) - } - }) - } else { - console.log("object[key] = ", object[key]) - if (object[key] && isEmpty(object[key]) === false) { - results.keys.push(parent ? [parent, key].join(".") : key) - results.values.push(object[key]) - } - } - return - } else { - for (const k in key) { - if (object[k]) { - this.findData(key[k], object[k], results, k) - } - } - } - }) - } - private getTranslations = (listing: Listing) => { - const result = { keys: [], values: [] } - this.findData(TRANSLATION_KEYS, listing, result) - return result - } -} diff --git a/backend/core/src/units-summary/dto/unit-group-ami-level.dto.ts b/backend/core/src/units-summary/dto/unit-group-ami-level.dto.ts new file mode 100644 index 0000000000..fdf3fd0482 --- /dev/null +++ b/backend/core/src/units-summary/dto/unit-group-ami-level.dto.ts @@ -0,0 +1,49 @@ +import { OmitType } from "@nestjs/swagger" +import { Expose, plainToClass, Transform, Type } from "class-transformer" +import { IsOptional, IsUUID, ValidateNested } from "class-validator" +import { IdDto } from "../../shared/dto/id.dto" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { UnitGroupAmiLevel } from "../entities/unit-group-ami-level.entity" + +export class UnitGroupAmiLevelDto extends OmitType(UnitGroupAmiLevel, [ + "unitGroup", + "amiChart", +] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDto) + @Transform( + (value, obj) => { + return obj.amiChartId ? plainToClass(IdDto, { id: obj.amiChartId }) : undefined + }, + { toClassOnly: true } + ) + amiChart?: IdDto +} + +export class UnitGroupAmiLevelCreateDto extends OmitType(UnitGroupAmiLevelDto, [ + "id", + "amiChart", +] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDto) + amiChart?: IdDto +} + +export class UnitGroupAmiLevelUpdateDto extends OmitType(UnitGroupAmiLevelCreateDto, [ + "amiChart", +] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID() + id?: string + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => IdDto) + amiChart?: IdDto +} diff --git a/backend/core/src/units-summary/dto/unit-group.dto.ts b/backend/core/src/units-summary/dto/unit-group.dto.ts new file mode 100644 index 0000000000..e3940778d1 --- /dev/null +++ b/backend/core/src/units-summary/dto/unit-group.dto.ts @@ -0,0 +1,60 @@ +import { OmitType } from "@nestjs/swagger" +import { Expose, Type } from "class-transformer" +import { IsDefined, IsOptional, IsUUID, ValidateNested } from "class-validator" +import { UnitTypeDto } from "../../unit-types/dto/unit-type.dto" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { UnitGroup } from "../entities/unit-group.entity" +import { + UnitGroupAmiLevelCreateDto, + UnitGroupAmiLevelDto, + UnitGroupAmiLevelUpdateDto, +} from "./unit-group-ami-level.dto" +import { IdDto } from "../../shared/dto/id.dto" + +export class UnitGroupDto extends OmitType(UnitGroup, [ + "listing", + "unitType", + "amiLevels", +] as const) { + @Expose() + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitTypeDto) + unitType: UnitTypeDto[] + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitGroupAmiLevelDto) + amiLevels: UnitGroupAmiLevelDto[] +} + +export class UnitGroupCreateDto extends OmitType(UnitGroupDto, [ + "id", + "unitType", + "amiLevels", + "listingId", +] as const) { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => IdDto) + unitType: IdDto[] + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitGroupAmiLevelCreateDto) + amiLevels: UnitGroupAmiLevelCreateDto[] +} +export class UnitGroupUpdateDto extends OmitType(UnitGroupCreateDto, ["amiLevels"] as const) { + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID() + id?: string + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitGroupAmiLevelUpdateDto) + amiLevels: UnitGroupAmiLevelUpdateDto[] +} diff --git a/backend/core/src/units-summary/dto/units-summary.dto.ts b/backend/core/src/units-summary/dto/units-summary.dto.ts deleted file mode 100644 index a605697bf3..0000000000 --- a/backend/core/src/units-summary/dto/units-summary.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { OmitType } from "@nestjs/swagger" -import { Expose, Type } from "class-transformer" -import { IsString, IsUUID, ValidateNested } from "class-validator" -import { IdDto } from "../../shared/dto/id.dto" -import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" -import { UnitsSummary } from "../entities/units-summary.entity" - -export class UnitsSummaryDto extends OmitType(UnitsSummary, ["listing", "unitType"] as const) { - @Expose() - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => IdDto) - listing: IdDto - - @Expose() - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => IdDto) - unitType: IdDto -} - -export class UnitsSummaryCreateDto extends OmitType(UnitsSummaryDto, ["id"] as const) {} -export class UnitsSummaryUpdateDto extends OmitType(UnitsSummaryCreateDto, [] as const) { - @Expose() - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @IsUUID() - id: string -} diff --git a/backend/core/src/units-summary/entities/unit-group-ami-level.entity.ts b/backend/core/src/units-summary/entities/unit-group-ami-level.entity.ts new file mode 100644 index 0000000000..1096eded5e --- /dev/null +++ b/backend/core/src/units-summary/entities/unit-group-ami-level.entity.ts @@ -0,0 +1,55 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, RelationId } from "typeorm" +import { Expose } from "class-transformer" +import { IsEnum, IsNumber, IsOptional, IsString, IsUUID } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { MonthlyRentDeterminationType } from "../types/monthly-rent-determination.enum" +import { AmiChart } from "../../ami-charts/entities/ami-chart.entity" +import { UnitGroup } from "./unit-group.entity" +import { ApiProperty } from "@nestjs/swagger" + +@Entity({ name: "unit_group_ami_levels" }) +export class UnitGroupAmiLevel { + @PrimaryGeneratedColumn("uuid") + @Expose() + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + id: string + + @ManyToOne(() => AmiChart, { eager: false, nullable: true }) + amiChart?: AmiChart | null + + @RelationId( + (unitsSummaryAmiLevelEntity: UnitGroupAmiLevel) => unitsSummaryAmiLevelEntity.amiChart + ) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + amiChartId?: string | null + + @ManyToOne(() => UnitGroup, (unitGroup: UnitGroup) => unitGroup.amiLevels) + unitGroup: UnitGroup + + @Column({ type: "integer", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + amiPercentage: number | null + + @Column({ type: "enum", enum: MonthlyRentDeterminationType, nullable: false }) + @Expose() + @IsEnum(MonthlyRentDeterminationType, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ enum: MonthlyRentDeterminationType, enumName: "MonthlyRentDeterminationType" }) + monthlyRentDeterminationType: MonthlyRentDeterminationType + + @Column({ type: "numeric", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + flatRentValue?: number | null + + @Column({ type: "numeric", nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + percentageOfIncomeValue?: number | null +} diff --git a/backend/core/src/units-summary/entities/unit-group.entity.ts b/backend/core/src/units-summary/entities/unit-group.entity.ts new file mode 100644 index 0000000000..ef2568ab1e --- /dev/null +++ b/backend/core/src/units-summary/entities/unit-group.entity.ts @@ -0,0 +1,132 @@ +import { + Column, + Entity, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + RelationId, +} from "typeorm" +import { + IsBoolean, + IsDefined, + IsNumber, + IsOptional, + IsString, + IsUUID, + ValidateNested, +} from "class-validator" +import { Expose, Type } from "class-transformer" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { UnitType } from "../../unit-types/entities/unit-type.entity" +import { UnitAccessibilityPriorityType } from "../../unit-accessbility-priority-types/entities/unit-accessibility-priority-type.entity" +import { Listing } from "../../listings/entities/listing.entity" +import { UnitGroupAmiLevel } from "./unit-group-ami-level.entity" + +@Entity({ name: "unit_group" }) +export class UnitGroup { + @PrimaryGeneratedColumn("uuid") + @Expose() + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + id: string + + @ManyToMany(() => UnitType, { eager: true }) + @JoinTable() + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitType) + unitType: UnitType[] + + @ManyToOne(() => Listing, (listing) => listing.unitGroups, {}) + listing: Listing + + @RelationId((unitGroupEntity: UnitGroup) => unitGroupEntity.listing) + @Expose() + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + listingId: string + + @OneToMany(() => UnitGroupAmiLevel, (UnitGroupAmiLevel) => UnitGroupAmiLevel.unitGroup, { + eager: true, + cascade: true, + }) + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitGroupAmiLevel) + amiLevels: UnitGroupAmiLevel[] + + @Column({ nullable: true, type: "integer" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + maxOccupancy?: number | null + + @Column({ nullable: true, type: "integer" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + minOccupancy?: number | null + + @Column({ nullable: true, type: "integer" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + floorMin?: number | null + + @Column({ nullable: true, type: "integer" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + floorMax?: number | null + + @Column({ nullable: true, type: "numeric" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + sqFeetMin?: number | null + + @Column({ nullable: true, type: "numeric" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + sqFeetMax?: number | null + + @ManyToOne(() => UnitAccessibilityPriorityType, { eager: true, nullable: true }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => UnitAccessibilityPriorityType) + priorityType?: UnitAccessibilityPriorityType | null + + @Column({ nullable: true, type: "integer" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + totalCount?: number | null + + @Column({ nullable: true, type: "integer" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + totalAvailable?: number | null + + @Column({ nullable: true, type: "numeric" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + bathroomMin?: number | null + + @Column({ nullable: true, type: "numeric" }) + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + bathroomMax?: number | null + + @Column({ type: "boolean", nullable: false, default: true }) + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + openWaitlist: boolean +} diff --git a/backend/core/src/units-summary/entities/units-summary.entity.ts b/backend/core/src/units-summary/entities/units-summary.entity.ts deleted file mode 100644 index fda4bf7956..0000000000 --- a/backend/core/src/units-summary/entities/units-summary.entity.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm" -import { - IsNumber, - IsNumberString, - IsOptional, - IsString, - IsUUID, - ValidateNested, -} from "class-validator" -import { Expose, Type } from "class-transformer" -import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" -import { UnitType } from "../../unit-types/entities/unit-type.entity" -import { UnitAccessibilityPriorityType } from "../../unit-accessbility-priority-types/entities/unit-accessibility-priority-type.entity" -import { Listing } from "../../listings/entities/listing.entity" - -@Entity({ name: "units_summary" }) -class UnitsSummary { - @PrimaryGeneratedColumn("uuid") - @Expose() - @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - id: string - - @ManyToOne(() => UnitType, { eager: true }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => UnitType) - unitType: UnitType - - @ManyToOne(() => Listing, (listing) => listing.unitsSummary, {}) - listing: Listing - - @Column({ nullable: true, type: "integer" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - monthlyRentMin?: number | null - - @Column({ nullable: true, type: "integer" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - monthlyRentMax?: number | null - - @Column({ nullable: true, type: "numeric", precision: 8, scale: 2 }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - monthlyRentAsPercentOfIncome?: string | null - - @Column({ nullable: true, type: "integer" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - amiPercentage?: number | null - - @Column({ nullable: true, type: "text" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - minimumIncomeMin?: string | null - - @Column({ nullable: true, type: "text" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumberString({}, { groups: [ValidationsGroupsEnum.default] }) - minimumIncomeMax?: string | null - - @Column({ nullable: true, type: "integer" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - maxOccupancy?: number | null - - @Column({ nullable: true, type: "integer" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - minOccupancy?: number | null - - @Column({ nullable: true, type: "integer" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - floorMin?: number | null - - @Column({ nullable: true, type: "integer" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - floorMax?: number | null - - @Column({ nullable: true, type: "numeric", precision: 8, scale: 2 }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - sqFeetMin?: string | null - - @Column({ nullable: true, type: "numeric", precision: 8, scale: 2 }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - sqFeetMax?: string | null - - @ManyToOne(() => UnitAccessibilityPriorityType, { eager: true, nullable: true }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => UnitAccessibilityPriorityType) - priorityType?: UnitAccessibilityPriorityType | null - - @Column({ nullable: true, type: "integer" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - totalCount?: number | null - - @Column({ nullable: true, type: "integer" }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) - totalAvailable?: number | null -} - -export { UnitsSummary as default, UnitsSummary } diff --git a/backend/core/src/units-summary/types/monthly-rent-determination.enum.ts b/backend/core/src/units-summary/types/monthly-rent-determination.enum.ts new file mode 100644 index 0000000000..516ce5e1d8 --- /dev/null +++ b/backend/core/src/units-summary/types/monthly-rent-determination.enum.ts @@ -0,0 +1,4 @@ +export enum MonthlyRentDeterminationType { + flatRent = "flatRent", + percentageOfIncome = "percentageOfIncome", +} diff --git a/backend/core/src/units/dto/units-csv-query-params.ts b/backend/core/src/units/dto/units-csv-query-params.ts new file mode 100644 index 0000000000..ae9fa375f4 --- /dev/null +++ b/backend/core/src/units/dto/units-csv-query-params.ts @@ -0,0 +1,15 @@ +import { Expose } from "class-transformer" +import { ApiProperty } from "@nestjs/swagger" +import { IsOptional, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +export class UnitsCsvQueryParams { + @Expose() + @ApiProperty({ + type: String, + required: false, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + timeZone?: string +} diff --git a/backend/core/src/units/types/hmi.ts b/backend/core/src/units/types/hmi.ts deleted file mode 100644 index 6f73f259f9..0000000000 --- a/backend/core/src/units/types/hmi.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AnyDict } from "../../shared/units-transformations" -import { ApiProperty } from "@nestjs/swagger" - -export class HMI { - @ApiProperty() - columns: AnyDict - - @ApiProperty({ type: [Object] }) - rows: AnyDict[] -} diff --git a/backend/core/src/units/types/household-max-income-columns.ts b/backend/core/src/units/types/household-max-income-columns.ts new file mode 100644 index 0000000000..a30782ad92 --- /dev/null +++ b/backend/core/src/units/types/household-max-income-columns.ts @@ -0,0 +1,92 @@ +import { Expose } from "class-transformer" +import { IsDefined, IsOptional, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { ApiProperty } from "@nestjs/swagger" + +export class HMIColumns { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString() + @ApiProperty() + householdSize: string + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 20?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 25?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 30?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 35?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 40?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 45?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 50?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 55?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 60?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 70?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 80?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 100?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 120?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 125?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 140?: number + + @Expose() + @IsOptional() + @ApiProperty({ required: false }) + 150?: number +} diff --git a/backend/core/src/units/types/household-max-income-summary.ts b/backend/core/src/units/types/household-max-income-summary.ts new file mode 100644 index 0000000000..ef86c6ef34 --- /dev/null +++ b/backend/core/src/units/types/household-max-income-summary.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from "@nestjs/swagger" +import { HMIColumns } from "./household-max-income-columns" + +export class HouseholdMaxIncomeSummary { + @ApiProperty() + columns: HMIColumns + + @ApiProperty({ type: [HMIColumns] }) + rows: HMIColumns[] +} diff --git a/backend/core/src/units/types/min-max-string.ts b/backend/core/src/units/types/min-max-string.ts new file mode 100644 index 0000000000..dfd9636266 --- /dev/null +++ b/backend/core/src/units/types/min-max-string.ts @@ -0,0 +1,18 @@ +import { Expose } from "class-transformer" +import { IsDefined, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { ApiProperty } from "@nestjs/swagger" + +export class MinMaxString { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString() + @ApiProperty() + min: string + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString() + @ApiProperty() + max: string +} diff --git a/backend/core/src/units/types/unit-group-summary.ts b/backend/core/src/units/types/unit-group-summary.ts new file mode 100644 index 0000000000..9b022443b3 --- /dev/null +++ b/backend/core/src/units/types/unit-group-summary.ts @@ -0,0 +1,66 @@ +import { Expose, Type } from "class-transformer" +import { IsDefined, IsNumber, IsString, ValidateNested, IsBoolean } from "class-validator" +import { ApiProperty } from "@nestjs/swagger" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { MinMaxCurrency } from "./min-max-currency" +import { MinMax } from "./min-max" + +export class UnitGroupSummary { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + unitTypes?: string[] | null + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty({ required: false }) + rentAsPercentIncomeRange?: MinMax + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMaxCurrency) + @ApiProperty({ required: false }) + rentRange?: MinMaxCurrency + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + amiPercentageRange: MinMax + + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + openWaitlist: boolean + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + unitVacancies: number + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty({ required: false }) + floorRange?: MinMax + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty({ required: false }) + sqFeetRange?: MinMax + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty({ required: false }) + bathroomRange?: MinMax +} diff --git a/backend/core/src/units/types/unit-summaries.ts b/backend/core/src/units/types/unit-summaries.ts new file mode 100644 index 0000000000..bddb935fbe --- /dev/null +++ b/backend/core/src/units/types/unit-summaries.ts @@ -0,0 +1,19 @@ +import { Expose, Type } from "class-transformer" +import { IsDefined, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { UnitGroupSummary } from "./unit-group-summary" +import { HouseholdMaxIncomeSummary } from "./household-max-income-summary" +import { ApiProperty } from "@nestjs/swagger" + +export class UnitSummaries { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) + @Type(() => UnitGroupSummary) + @ApiProperty({ type: [UnitGroupSummary] }) + unitGroupSummary: UnitGroupSummary[] + + @Expose() + @ApiProperty({ type: HouseholdMaxIncomeSummary }) + householdMaxIncomeSummary: HouseholdMaxIncomeSummary +} diff --git a/backend/core/src/units/types/unit-summary-by-ami.ts b/backend/core/src/units/types/unit-summary-by-ami.ts deleted file mode 100644 index 96ec93e785..0000000000 --- a/backend/core/src/units/types/unit-summary-by-ami.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Expose, Type } from "class-transformer" -import { IsDefined, IsString, ValidateNested } from "class-validator" -import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" -import { UnitSummary } from "./unit-summary" -import { ApiProperty } from "@nestjs/swagger" - -export class UnitSummaryByAMI { - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() - percent: string - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => UnitSummary) - @ApiProperty({ type: [UnitSummary] }) - byUnitType: UnitSummary[] -} diff --git a/backend/core/src/units/types/unit-summary.ts b/backend/core/src/units/types/unit-summary.ts deleted file mode 100644 index b72f128c1e..0000000000 --- a/backend/core/src/units/types/unit-summary.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Expose, Type } from "class-transformer" -import { IsDefined, IsOptional, IsString, ValidateNested } from "class-validator" -import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" -import { MinMaxCurrency } from "./min-max-currency" -import { MinMax } from "./min-max" -import { ApiProperty } from "@nestjs/swagger" -import { UnitTypeDto } from "../../unit-types/dto/unit-type.dto" - -export class UnitSummary { - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() - unitType?: UnitTypeDto | null - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => MinMaxCurrency) - @ApiProperty() - minIncomeRange: MinMaxCurrency - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => MinMax) - @ApiProperty() - occupancyRange: MinMax - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => MinMax) - @ApiProperty() - rentAsPercentIncomeRange: MinMax - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => MinMaxCurrency) - @ApiProperty() - rentRange: MinMaxCurrency - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) - @ApiProperty() - totalAvailable: number - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => MinMax) - @ApiProperty() - areaRange: MinMax - - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) - @Type(() => MinMax) - @ApiProperty({ type: MinMax, required: false }) - floorRange?: MinMax -} diff --git a/backend/core/src/units/types/unit-type-summary.ts b/backend/core/src/units/types/unit-type-summary.ts new file mode 100644 index 0000000000..321d87495e --- /dev/null +++ b/backend/core/src/units/types/unit-type-summary.ts @@ -0,0 +1,42 @@ +import { Expose, Type } from "class-transformer" +import { IsDefined, IsOptional, IsString, ValidateNested } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" +import { MinMax } from "./min-max" +import { ApiProperty } from "@nestjs/swagger" +import { UnitTypeDto } from "../../unit-types/dto/unit-type.dto" + +export class UnitTypeSummary { + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + unitTypes?: UnitTypeDto[] | null + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty() + occupancyRange: MinMax + + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty() + areaRange?: MinMax + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty({ type: MinMax, required: false }) + floorRange?: MinMax + + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) + @Type(() => MinMax) + @ApiProperty({ type: MinMax, required: false }) + bathroomRange?: MinMax +} diff --git a/backend/core/src/units/types/units-summarized.ts b/backend/core/src/units/types/units-summarized.ts deleted file mode 100644 index 06a03a29c0..0000000000 --- a/backend/core/src/units/types/units-summarized.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Expose, Type } from "class-transformer" -import { IsDefined, IsString, ValidateNested } from "class-validator" -import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" -import { UnitSummary } from "./unit-summary" -import { UnitSummaryByAMI } from "./unit-summary-by-ami" -import { HMI } from "./hmi" -import { ApiProperty } from "@nestjs/swagger" -import { UnitTypeDto } from "../../unit-types/dto/unit-type.dto" -import { UnitAccessibilityPriorityType } from "../../unit-accessbility-priority-types/entities/unit-accessibility-priority-type.entity" - -export class UnitsSummarized { - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) - @ApiProperty({ type: [UnitTypeDto] }) - unitTypes: UnitTypeDto[] - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) - @ApiProperty({ type: [UnitAccessibilityPriorityType] }) - priorityTypes: UnitAccessibilityPriorityType[] - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default], each: true }) - @ApiProperty() - amiPercentages: string[] - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => UnitSummary) - @ApiProperty({ type: [UnitSummary] }) - byUnitTypeAndRent: UnitSummary[] - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => UnitSummary) - @ApiProperty({ type: [UnitSummary] }) - byUnitType: UnitSummary[] - - @Expose() - @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) - @Type(() => UnitSummaryByAMI) - @ApiProperty({ type: [UnitSummaryByAMI] }) - byAMI: UnitSummaryByAMI[] - - @Expose() - @ApiProperty({ type: HMI }) - hmi: HMI -} diff --git a/backend/core/src/views/base.view.ts b/backend/core/src/views/base.view.ts new file mode 100644 index 0000000000..94df565346 --- /dev/null +++ b/backend/core/src/views/base.view.ts @@ -0,0 +1,39 @@ +import { SelectQueryBuilder } from "typeorm" + +export interface View { + select?: string[] + leftJoins?: { + join: string + alias: string + }[] + leftJoinAndSelect?: [string, string][] +} + +export class BaseView { + qb: SelectQueryBuilder + view: View + constructor(qb: SelectQueryBuilder) { + this.qb = qb + this.view = undefined + } + + getViewQb(): SelectQueryBuilder { + this.qb.select(this.view.select) + + this.view.leftJoins.forEach((join) => { + this.qb.leftJoin(join.join, join.alias) + }) + + return this.qb + } +} + +export const getBaseAddressSelect = (schemas: string[]): string[] => { + const fields = ["city", "state", "street", "street2", "zipCode", "latitude", "longitude"] + + let select: string[] = [] + schemas.forEach((schema) => { + select = select.concat(fields.map((field) => `${schema}.${field}`)) + }) + return select +} diff --git a/backend/core/src/views/change-email.hbs b/backend/core/src/views/change-email.hbs new file mode 100644 index 0000000000..d1d321efb9 --- /dev/null +++ b/backend/core/src/views/change-email.hbs @@ -0,0 +1,11 @@ +

{{t "t.hello"}} {{> user-name }},

+

+ {{t "changeEmail.message" appOptions}} +

+

+ {{t "changeEmail.onChangeEmailMessage"}} +

+

+ {{t "changeEmail.changeMyEmail"}} +

+{{> footer }} diff --git a/backend/core/src/views/confirmation.hbs b/backend/core/src/views/confirmation.hbs deleted file mode 100644 index 54bc9dab7a..0000000000 --- a/backend/core/src/views/confirmation.hbs +++ /dev/null @@ -1,7 +0,0 @@ -

{{t "t.hello"}} {{> user-name }},

-

{{t "confirmation.thankYouForApplying"}} {{listing.name}}

-

{{t "confirmation.yourConfirmationNumber"}} {{application.id}}

-

{{t "confirmation.whatToExpectNext"}}
{{whatToExpectText}}

-

{{t "confirmation.shouldBeChosen"}}

-{{> leasing-agent }} -{{> footer }} \ No newline at end of file diff --git a/backend/core/src/views/new-listing.hbs b/backend/core/src/views/new-listing.hbs new file mode 100644 index 0000000000..f556f41616 --- /dev/null +++ b/backend/core/src/views/new-listing.hbs @@ -0,0 +1,438 @@ + + + + + + Rental Opportunity + + + + + + + + + + + + + + + +
+ +   + + + + diff --git a/backend/core/src/views/partials/footer.hbs b/backend/core/src/views/partials/footer.hbs deleted file mode 100644 index 7d7ab0ad1f..0000000000 --- a/backend/core/src/views/partials/footer.hbs +++ /dev/null @@ -1,4 +0,0 @@ -

- {{t "footer.thankYou"}},
- {{t "footer.footer"}} -

\ No newline at end of file diff --git a/backend/core/src/views/update-listing.hbs b/backend/core/src/views/update-listing.hbs new file mode 100644 index 0000000000..5c4a295046 --- /dev/null +++ b/backend/core/src/views/update-listing.hbs @@ -0,0 +1,446 @@ + + + + + + Rental Opportunity + + + + + + + + + + + + + + + +
+ +   + + + + diff --git a/backend/core/test/activity-logs/activity-log.e2e-spec.ts b/backend/core/test/activity-logs/activity-log.e2e-spec.ts new file mode 100644 index 0000000000..1c1eec86d9 --- /dev/null +++ b/backend/core/test/activity-logs/activity-log.e2e-spec.ts @@ -0,0 +1,143 @@ +import { Test } from "@nestjs/testing" +import { INestApplication } from "@nestjs/common" +import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm" +// Use require because of the CommonJS/AMD style export. +// See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require +import dbOptions from "../../ormconfig.test" +import supertest from "supertest" +import { applicationSetup } from "../../src/app.module" +import { AuthModule } from "../../src/auth/auth.module" +import { setAuthorization } from "../utils/set-authorization-helper" +import { ActivityLogModule } from "../../src/activity-log/activity-log.module" +import { ActivityLog } from "../../src/activity-log/entities/activity-log.entity" +import { Repository } from "typeorm" +import { authzActions } from "../../src/auth/enum/authz-actions.enum" +import { Application } from "../../src/applications/entities/application.entity" +import { ApplicationsModule } from "../../src/applications/applications.module" +import { ThrottlerModule } from "@nestjs/throttler" +import { User } from "../../src/auth/entities/user.entity" +import { getTestAppBody } from "../lib/get-test-app-body" +import { ListingsModule } from "../../src/listings/listings.module" +import { Listing } from "../../src/listings/entities/listing.entity" + +// Cypress brings in Chai types for the global expect, but we want to use jest +// expect here so we need to re-declare it. +// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 +declare const expect: jest.Expect +jest.setTimeout(30000) + +describe.skip("Activity", () => { + let app: INestApplication + let adminId: string + let adminAccessToken: string + let activityLogsRepository: Repository + let applicationsRepository: Repository + let listingsRepository: Repository+ + beforeAll(async () => { + /* eslint-enable @typescript-eslint/no-empty-function */ + const moduleRef = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRoot(dbOptions), + AuthModule, + ActivityLogModule, + ApplicationsModule, + ThrottlerModule.forRoot({ + ttl: 60, + limit: 2, + ignoreUserAgents: [/^node-superagent.*$/], + }), + ListingsModule, + ], + }).compile() + app = moduleRef.createNestApplication() + app = applicationSetup(app) + await app.init() + const res = await supertest(app.getHttpServer()) + .post("/auth/login") + .send({ email: "admin@example.com", password: "abcdef" }) + .expect(201) + adminAccessToken = res.body.accessToken + const userRepository = app.get>(getRepositoryToken(User)) + adminId = (await userRepository.findOne({ email: "admin@example.com" })).id + activityLogsRepository = app.get>(getRepositoryToken(ActivityLog)) + applicationsRepository = app.get>(getRepositoryToken(Application)) + listingsRepository = app.get>(getRepositoryToken(Listing)) + + const listingsRes = await supertest(app.getHttpServer()) + .get("/listings?limit=all&view=full") + .expect(200) + const appBody = getTestAppBody(listingsRes.body.items[0].id) + await supertest(app.getHttpServer()) + .post(`/applications/submit`) + .send(appBody) + .set("jurisdictionName", "Alameda") + .set(...setAuthorization(adminAccessToken)) + }) + + beforeEach(async () => { + await activityLogsRepository.createQueryBuilder().delete().execute() + }) + + it(`should capture application edit`, async () => { + const testApplication = ( + await applicationsRepository.find({ take: 1, relations: ["listing"] }) + )[0] + await supertest(app.getHttpServer()) + .put(`/applications/${testApplication.id}`) + .set(...setAuthorization(adminAccessToken)) + .send(testApplication) + .expect(200) + const activityLogs = await activityLogsRepository.find({ relations: ["user"] }) + expect(activityLogs.length).toBe(1) + expect(activityLogs[0].recordId).toBe(testApplication.id) + expect(activityLogs[0].user.id).toBe(adminId) + expect(activityLogs[0].action).toBe(authzActions.update) + expect(activityLogs[0].module).toBe("application") + expect(activityLogs[0].metadata).toBe(null) + }) + + it(`should not capture application edit that failed`, async () => { + const testApplication = ( + await applicationsRepository.find({ take: 1, relations: ["listing"] }) + )[0] + await supertest(app.getHttpServer()) + .put(`/applications/${testApplication.id}`) + .send({ + ...testApplication, + listing: null, + }) + .set(...setAuthorization(adminAccessToken)) + .expect(400) + const activityLogs = await activityLogsRepository.find({ relations: ["user"] }) + expect(activityLogs.length).toBe(0) + }) + + it(`should capture listing status as activity log metadata`, async () => { + const testListing = (await listingsRepository.find({ take: 1 }))[0] + + const listingGetRes = await supertest(app.getHttpServer()) + .get(`/listings/${testListing.id}`) + .expect(200) + + await supertest(app.getHttpServer()) + .put(`/listings/${testListing.id}`) + .send({ + ...listingGetRes.body, + }) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + + const activityLogs = await activityLogsRepository.find({ relations: ["user"] }) + expect(activityLogs.length).toBe(1) + expect(activityLogs[0].recordId).toBe(testListing.id) + expect(activityLogs[0].user.id).toBe(adminId) + expect(activityLogs[0].action).toBe(authzActions.update) + expect(activityLogs[0].module).toBe("listing") + expect(activityLogs[0].metadata).toStrictEqual({ status: listingGetRes.body.status }) + }) + + afterAll(async () => { + await app.close() + }) +}) diff --git a/backend/core/test/afs/afs.e2e-spec.ts b/backend/core/test/afs/afs.e2e-spec.ts index 10269fda0e..8b27bbf8af 100644 --- a/backend/core/test/afs/afs.e2e-spec.ts +++ b/backend/core/test/afs/afs.e2e-spec.ts @@ -6,7 +6,6 @@ import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" import { ApplicationsModule } from "../../src/applications/applications.module" import { ListingsModule } from "../../src/listings/listings.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" import { Repository } from "typeorm" @@ -22,7 +21,8 @@ import { Listing } from "../../src/listings/entities/listing.entity" import { ListingStatus } from "../../src/listings/types/listing-status-enum" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" +import { EmailService } from "../../src/email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. diff --git a/backend/core/test/ami-charts/ami-charts.e2e-spec.ts b/backend/core/test/ami-charts/ami-charts.e2e-spec.ts index 6e834bd414..c5bfb1f801 100644 --- a/backend/core/test/ami-charts/ami-charts.e2e-spec.ts +++ b/backend/core/test/ami-charts/ami-charts.e2e-spec.ts @@ -4,16 +4,16 @@ import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm" import supertest from "supertest" import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import { AmiChartsModule } from "../../src/ami-charts/ami-charts.module" import { AmiChartCreateDto } from "../../src/ami-charts/dto/ami-chart.dto" import { Jurisdiction } from "../../src/jurisdictions/entities/jurisdiction.entity" import { Repository } from "typeorm" +import { EmailService } from "../../src/email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. diff --git a/backend/core/test/application-methods/application-methods.e2e-spec.ts b/backend/core/test/application-methods/application-methods.e2e-spec.ts index b02b558b32..aed81b043b 100644 --- a/backend/core/test/application-methods/application-methods.e2e-spec.ts +++ b/backend/core/test/application-methods/application-methods.e2e-spec.ts @@ -4,14 +4,14 @@ import { TypeOrmModule } from "@nestjs/typeorm" import supertest from "supertest" import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import { ApplicationMethodsModule } from "../../src/application-methods/applications-methods.module" import { ApplicationMethodType } from "../../src/application-methods/types/application-method-type-enum" +import { EmailService } from "../../src/email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. diff --git a/backend/core/test/applications/applications.e2e-spec.ts b/backend/core/test/applications/applications.e2e-spec.ts index 1eda8dad15..35308184e1 100644 --- a/backend/core/test/applications/applications.e2e-spec.ts +++ b/backend/core/test/applications/applications.e2e-spec.ts @@ -6,12 +6,11 @@ import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" import { ApplicationsModule } from "../../src/applications/applications.module" import { ListingsModule } from "../../src/listings/listings.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import { InputType } from "../../src/shared/types/input-type" import { Repository } from "typeorm" import { Application } from "../../src/applications/entities/application.entity" @@ -19,10 +18,11 @@ import { ListingDto } from "../../src/listings/dto/listing.dto" import { HouseholdMember } from "../../src/applications/entities/household-member.entity" import { ThrottlerModule } from "@nestjs/throttler" import { getTestAppBody } from "../lib/get-test-app-body" -import { Listing } from "../../types" import { UserDto } from "../../src/auth/dto/user.dto" -import { UserService } from "../../src/auth/services/user.service" import { UserCreateDto } from "../../src/auth/dto/user-create.dto" +import { Listing } from "../../src/listings/entities/listing.entity" +import { EmailService } from "../../src/email/email.service" +import { UserRepository } from "../../src/auth/repositories/user-repository" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. @@ -54,7 +54,7 @@ describe("Applications", () => { AuthModule, ListingsModule, ApplicationsModule, - TypeOrmModule.forFeature([Application, HouseholdMember]), + TypeOrmModule.forFeature([Application, HouseholdMember, Listing, UserRepository]), ThrottlerModule.forRoot({ ttl: 60, limit: 2, @@ -342,7 +342,7 @@ describe("Applications", () => { await supertest(app.getHttpServer()).get(`/applications/${res.body.id}`).expect(403) }) - it(`should allow an admin to search for users application using search query param`, async () => { + it(`should allow an admin to search for users application using search query param with exact match`, async () => { const body = getTestAppBody(listing1Id) body.applicant.firstName = "MyName" const createRes = await supertest(app.getHttpServer()) @@ -362,8 +362,25 @@ describe("Applications", () => { expect(res.body.items[0].id === createRes.body.id) expect(res.body.items[0]).toMatchObject(createRes.body) }) + it(`should not allow an admin to search for users application using a search query param of less than 3 characters`, async () => { + const body = getTestAppBody(listing1Id) + body.applicant.firstName = "John" + const createRes = await supertest(app.getHttpServer()) + .post(`/applications/submit`) + .send(body) + .expect(201) + expect(createRes.body).toMatchObject(body) + expect(createRes.body).toHaveProperty("createdAt") + expect(createRes.body).toHaveProperty("updatedAt") + expect(createRes.body).toHaveProperty("id") + + await supertest(app.getHttpServer()) + .get(`/applications/?search=jo`) + .set(...setAuthorization(adminAccessToken)) + .expect(400) + }) - it(`should allow an admin to search for users application using search query param with partial textquery`, async () => { + it(`should allow an admin to search for users application using search query param with partial textquery of at least three 3 characters`, async () => { const body = getTestAppBody(listing1Id) body.applicant.firstName = "John" const createRes = await supertest(app.getHttpServer()) @@ -419,7 +436,7 @@ describe("Applications", () => { expect(createRes.body).toHaveProperty("updatedAt") expect(createRes.body).toHaveProperty("id") const res = await supertest(app.getHttpServer()) - .get(`/applications/csv/?includeHeaders=true&listingId=${listing1Id}`) + .get(`/applications/csv/?listingId=${listing1Id}`) .set(...setAuthorization(adminAccessToken)) .expect(200) expect(typeof res.text === "string") @@ -434,13 +451,14 @@ describe("Applications", () => { .set(...setAuthorization(user1AccessToken)) .expect(201) await supertest(app.getHttpServer()) - .delete(`/applications/${createRes.body.id}`) + .delete(`/applications`) + .send({ id: createRes.body.id }) .set(...setAuthorization(adminAccessToken)) .expect(200) await supertest(app.getHttpServer()) .get(`/applications/${createRes.body.id}`) .set(...setAuthorization(user1AccessToken)) - .expect(404) + .expect(500) }) it(`should disallow users to delete their own applications`, async () => { @@ -451,7 +469,8 @@ describe("Applications", () => { .set(...setAuthorization(user1AccessToken)) .expect(201) await supertest(app.getHttpServer()) - .delete(`/applications/${createRes.body.id}`) + .delete(`/applications`) + .send({ id: createRes.body.id }) .set(...setAuthorization(user1AccessToken)) .expect(403) }) @@ -465,7 +484,7 @@ describe("Applications", () => { .expect(201) await supertest(app.getHttpServer()) .put(`/applications/${createRes.body.id}`) - .send(body) + .send(createRes.body) .set(...setAuthorization(user1AccessToken)) .expect(403) }) @@ -631,12 +650,12 @@ describe("Applications", () => { const newUser = await supertest(app.getHttpServer()) .post(`/user/?noWelcomeEmail=true`) - .set("jurisdictionName", "Alameda") + .set("jurisdictionName", "Detroit") .send(userCreateDto) .expect(201) - const userService = await app.resolve(UserService) - const user = await userService.findByEmail(userCreateDto.email) + const userRepository = await app.resolve(getRepositoryToken(UserRepository)) + const user = await userRepository.findByEmail(userCreateDto.email) await supertest(app.getHttpServer()) .put(`/user/confirm/`) @@ -657,6 +676,25 @@ describe("Applications", () => { expect(listApplicationsRes.body.items[0].id).toBe(appSubmisionRes.body.id) }) + it(`should not assign a user relation when partner submits an application`, async () => { + const body = getTestAppBody(listing1Id) + let appSubmisionRes = await supertest(app.getHttpServer()) + .post(`/applications`) + .set(...setAuthorization(adminAccessToken)) + .send(body) + .expect(201) + + expect(appSubmisionRes.body.user).toBeFalsy() + + appSubmisionRes = await supertest(app.getHttpServer()) + .post(`/applications/submit`) + .set(...setAuthorization(user1AccessToken)) + .send(body) + .expect(201) + + expect(appSubmisionRes.body.user).toBeTruthy() + }) + afterEach(async () => { await householdMembersRepository.createQueryBuilder().delete().execute() await applicationsRepository.createQueryBuilder().delete().execute() diff --git a/backend/core/test/assets/assets.e2e-spec.ts b/backend/core/test/assets/assets.e2e-spec.ts index 64a809d9db..03f203a15e 100644 --- a/backend/core/test/assets/assets.e2e-spec.ts +++ b/backend/core/test/assets/assets.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test } from "@nestjs/testing" import { AssetsController } from "../../src/assets/assets.controller" import { TypeOrmModule } from "@nestjs/typeorm" -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import { Asset } from "../../src/assets/entities/asset.entity" import { UploadService } from "../../src/assets/services/upload.service" import { SharedModule } from "../../src/shared/shared.module" diff --git a/backend/core/test/authz/authz.e2e-spec.ts b/backend/core/test/authz/authz.e2e-spec.ts index f3ec6f1cfc..28607f2837 100644 --- a/backend/core/test/authz/authz.e2e-spec.ts +++ b/backend/core/test/authz/authz.e2e-spec.ts @@ -2,13 +2,21 @@ import { Test } from "@nestjs/testing" import { INestApplication } from "@nestjs/common" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import supertest from "supertest" import { applicationSetup, AppModule } from "../../src/app.module" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" import { UserDto } from "../../src/auth/dto/user.dto" +import { v4 as uuidv4 } from "uuid" +import { Repository } from "typeorm" +import { Application } from "../../src/applications/entities/application.entity" +import { getRepositoryToken } from "@nestjs/typeorm" +import { getTestAppBody } from "../lib/get-test-app-body" +import { Listing } from "../../src/listings/entities/listing.entity" +import { ApplicationDto } from "../../src/applications/dto/application.dto" + jest.setTimeout(30000) describe("Authz", () => { @@ -19,6 +27,9 @@ describe("Authz", () => { const applicationsEndpoint = "/applications" const listingsEndpoint = "/listings" const userEndpoint = "/user" + let applicationsRepository: Repository + let listingsRepository: Repository+ let app1: ApplicationDto beforeAll(async () => { const moduleRef = await Test.createTestingModule({ @@ -35,6 +46,17 @@ describe("Authz", () => { .get("/user") .set(...setAuthorization(userAccessToken)) ).body + applicationsRepository = app.get>(getRepositoryToken(Application)) + listingsRepository = app.get>(getRepositoryToken(Listing)) + const listings = await listingsRepository.find({ take: 1 }) + const listing1Application = getTestAppBody(listings[0].id) + app1 = ( + await supertest(app.getHttpServer()) + .post(`/applications/submit`) + .send(listing1Application) + .set("jurisdictionName", "Detroit") + .set(...setAuthorization(userAccessToken)) + ).body }) describe("admin endpoints", () => { @@ -53,11 +75,11 @@ describe("Authz", () => { for (const endpoint of adminOnlyEndpoints) { // anonymous await supertest(app.getHttpServer()) - .get(endpoint + "/fake_id") + .get(endpoint + `/${uuidv4()}`) .expect(403) // logged in normal user await supertest(app.getHttpServer()) - .get(endpoint + "/fake_id") + .get(endpoint + `/${uuidv4()}`) .set(...setAuthorization(userAccessToken)) .expect(403) } @@ -78,12 +100,12 @@ describe("Authz", () => { for (const endpoint of adminOnlyEndpoints) { // anonymous await supertest(app.getHttpServer()) - .put(endpoint + "/fake_id") + .put(endpoint + `/${uuidv4()}`) .send({}) .expect(403) // logged in normal user await supertest(app.getHttpServer()) - .put(endpoint + "/fake_id") + .put(endpoint + `/${uuidv4()}`) .send({}) .set(...setAuthorization(userAccessToken)) .expect(403) @@ -93,11 +115,11 @@ describe("Authz", () => { for (const endpoint of adminOnlyEndpoints) { // anonymous await supertest(app.getHttpServer()) - .delete(endpoint + "/fake_id") + .delete(endpoint + `/${uuidv4()}`) .expect(403) // logged in normal user await supertest(app.getHttpServer()) - .delete(endpoint + "/fake_id") + .delete(endpoint + `/${uuidv4()}`) .set(...setAuthorization(userAccessToken)) .expect(403) } @@ -137,33 +159,39 @@ describe("Authz", () => { .expect(403) }) it("should not allow anonymous user to GET applications by ID", async () => { + const applications = await applicationsRepository.find({ take: 1 }) await supertest(app.getHttpServer()) - .get(applicationsEndpoint + "/fake_id") + .get(applicationsEndpoint + `/${applications[0].id}`) .expect(403) }) it(`should not allow normal/anonymous user to DELETE applications`, async () => { // anonymous + const applications = await applicationsRepository.find({ take: 1 }) await supertest(app.getHttpServer()) - .delete(applicationsEndpoint + "/fake_id") + .delete(applicationsEndpoint) + .send({ id: applications[0].id }) .expect(403) // logged in normal user await supertest(app.getHttpServer()) - .delete(applicationsEndpoint + "/fake_id") + .delete(applicationsEndpoint) + .send({ id: applications[0].id }) .set(...setAuthorization(userAccessToken)) .expect(403) }) it(`should not allow normal/anonymous user to PUT applications`, async () => { // anonymous await supertest(app.getHttpServer()) - .put(applicationsEndpoint + "/fake_id") + .put(applicationsEndpoint + `/${app1.id}`) + .send(app1) .expect(403) // logged in normal user await supertest(app.getHttpServer()) - .put(applicationsEndpoint + "/fake_id") + .put(applicationsEndpoint + `/${app1.id}`) .set(...setAuthorization(userAccessToken)) + .send(app1) .expect(403) }) - it(`should allow normal/anonymous user to POST applications`, async () => { + it.skip(`should allow normal/anonymous user to POST applications`, async () => { // anonymous await supertest(app.getHttpServer()) .post(applicationsEndpoint + "/submit") @@ -188,22 +216,22 @@ describe("Authz", () => { it(`should not allow normal/anonymous user to DELETE listings`, async () => { // anonymous await supertest(app.getHttpServer()) - .delete(listingsEndpoint + "/fake_id") + .delete(listingsEndpoint + `/${uuidv4()}`) .expect(403) // logged in normal user await supertest(app.getHttpServer()) - .delete(listingsEndpoint + "/fake_id") + .delete(listingsEndpoint + `/${uuidv4()}`) .set(...setAuthorization(userAccessToken)) .expect(403) }) it(`should not allow normal/anonymous user to PUT listings`, async () => { // anonymous await supertest(app.getHttpServer()) - .put(listingsEndpoint + "/fake_id") + .put(listingsEndpoint + `/${uuidv4()}`) .expect(403) // logged in normal user await supertest(app.getHttpServer()) - .put(listingsEndpoint + "/fake_id") + .put(listingsEndpoint + `/${uuidv4()}`) .set(...setAuthorization(userAccessToken)) .expect(403) }) diff --git a/backend/core/test/factories/listing.ts b/backend/core/test/factories/listing.ts index e37ebf184d..c47a2a949f 100644 --- a/backend/core/test/factories/listing.ts +++ b/backend/core/test/factories/listing.ts @@ -16,15 +16,6 @@ export default Factory.define(({ sequence, factories }) => ({ applicationOpenDate: null, applicationFee: "30.0", applicationOrganization: `Triton-${sequence}`, - applicationAddress: { - city: `Foster City-${sequence}`, - street: "55 Triton Park Lane", - zipCode: "94404", - state: "CA", - county: "San Jose", - latitude: 37.55695, - longitude: -122.27521, - }, blankPaperApplicationCanBePickedUp: true, buildingAddress: { city: `Foster City-${sequence}`, diff --git a/backend/core/test/jurisdictions/jurisdictions.e2e-spec.ts b/backend/core/test/jurisdictions/jurisdictions.e2e-spec.ts index 35935d694e..7e5943610e 100644 --- a/backend/core/test/jurisdictions/jurisdictions.e2e-spec.ts +++ b/backend/core/test/jurisdictions/jurisdictions.e2e-spec.ts @@ -1,16 +1,20 @@ import { Test } from "@nestjs/testing" import { INestApplication } from "@nestjs/common" -import { TypeOrmModule } from "@nestjs/typeorm" +import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import supertest from "supertest" import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" import { JurisdictionsModule } from "../../src/jurisdictions/jurisdictions.module" +import { Repository } from "typeorm" +import { Program } from "../../src/program/entities/program.entity" +import { Language } from "../../src/shared/types/language-enum" +import { Preference } from "../../src/preferences/entities/preference.entity" +import { EmailService } from "../../src/email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. @@ -21,12 +25,22 @@ jest.setTimeout(30000) describe("Jurisdictions", () => { let app: INestApplication let adminAccesstoken: string + let preferencesRepository: Repository + let programsRepository: Repository + beforeAll(async () => { /* eslint-disable @typescript-eslint/no-empty-function */ - const testEmailService = { confirmation: async () => {} } + const testEmailService = { + confirmation: async () => {}, + } /* eslint-enable @typescript-eslint/no-empty-function */ const moduleRef = await Test.createTestingModule({ - imports: [TypeOrmModule.forRoot(dbOptions), AuthModule, JurisdictionsModule], + imports: [ + TypeOrmModule.forRoot(dbOptions), + AuthModule, + JurisdictionsModule, + TypeOrmModule.forFeature([Preference, Program]), + ], }) .overrideProvider(EmailService) .useValue(testEmailService) @@ -35,6 +49,8 @@ describe("Jurisdictions", () => { app = applicationSetup(app) await app.init() adminAccesstoken = await getUserAccessToken(app, "admin@example.com", "abcdef") + preferencesRepository = app.get>(getRepositoryToken(Preference)) + programsRepository = app.get>(getRepositoryToken(Program)) }) it(`should return jurisdictions`, async () => { @@ -45,37 +61,73 @@ describe("Jurisdictions", () => { expect(Array.isArray(res.body)).toBe(true) }) - it(`should create and return a new jurisdiction`, async () => { + it(`should create and return a new jurisdiction with a preference`, async () => { + const newPreference = await preferencesRepository.save({ + title: "TestTitle", + subtitle: "TestSubtitle", + description: "TestDescription", + links: [], + }) + const newProgram = await programsRepository.save({ + question: "TestQuestion", + subtitle: "TestSubtitle", + description: "TestDescription", + subdescription: "TestDescription", + }) const res = await supertest(app.getHttpServer()) .post(`/jurisdictions`) .set(...setAuthorization(adminAccesstoken)) - .send({ name: "test" }) + .send({ + name: "test", + languages: [Language.en], + preferences: [newPreference], + programs: [newProgram], + publicUrl: "", + emailFromAddress: "", + }) .expect(201) + expect(res.body).toHaveProperty("id") expect(res.body).toHaveProperty("createdAt") expect(res.body).toHaveProperty("updatedAt") expect(res.body).toHaveProperty("name") + expect(res.body).toHaveProperty("preferences") expect(res.body.name).toBe("test") + expect(Array.isArray(res.body.preferences)).toBe(true) + expect(res.body.preferences.length).toBe(1) + expect(res.body.preferences[0].id).toBe(newPreference.id) + expect(res.body).toHaveProperty("programs") + expect(Array.isArray(res.body.programs)).toBe(true) + expect(res.body.programs.length).toBe(1) + expect(res.body.programs[0].id).toBe(newProgram.id) const getById = await supertest(app.getHttpServer()) .get(`/jurisdictions/${res.body.id}`) .expect(200) expect(getById.body.name).toBe("test") - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - afterAll(async () => { - await app.close() + expect(getById.body.preferences[0].id).toBe(newPreference.id) + expect(getById.body.programs[0].id).toBe(newProgram.id) }) it(`should create and return a new jurisdiction by name`, async () => { const res = await supertest(app.getHttpServer()) .post(`/jurisdictions`) .set(...setAuthorization(adminAccesstoken)) - .send({ name: "test2" }) + .send({ + name: "test2", + languages: [Language.en], + preferences: [], + publicUrl: "", + emailFromAddress: "", + }) + .send({ + name: "test2", + programs: [], + languages: [Language.en], + preferences: [], + publicUrl: "", + emailFromAddress: "", + }) .expect(201) expect(res.body).toHaveProperty("id") expect(res.body).toHaveProperty("createdAt") @@ -87,6 +139,15 @@ describe("Jurisdictions", () => { .get(`/jurisdictions/byName/${res.body.name}`) .expect(200) expect(getByName.body.name).toBe("test2") + expect(getByName.body.languages[0]).toBe(Language.en) + }) + + it(`should not allow to create a jurisdiction with unsupported language`, async () => { + await supertest(app.getHttpServer()) + .post(`/jurisdictions`) + .set(...setAuthorization(adminAccesstoken)) + .send({ name: "test2", languages: ["non_existent_language"], emailFromAddress: "" }) + .expect(400) }) afterEach(() => { diff --git a/backend/core/test/lib/format-local-date.spec.ts b/backend/core/test/lib/format-local-date.spec.ts new file mode 100644 index 0000000000..9146baf6f6 --- /dev/null +++ b/backend/core/test/lib/format-local-date.spec.ts @@ -0,0 +1,19 @@ +import { formatLocalDate } from "../../src/shared/utils/format-local-date" + +declare const expect: jest.Expect + +describe("formatLocalDate", () => { + test("with an empty date string", () => { + expect(formatLocalDate("", "MM-DD-YYYY hh:mm:ssA z", "America/Detroit")).toEqual("") + }) + test("with a format and no timezone", () => { + expect(formatLocalDate("2023-04-01T17:00:00.000Z", "MM-DD-YYYY hh:mm:ssA")).toEqual( + "04-01-2023 05:00:00PM" + ) + }) + test("with a format and timezone", () => { + expect( + formatLocalDate("2023-04-01T17:00:00.000Z", "MM-DD-YYYY hh:mm:ssA z", "America/Detroit") + ).toEqual("04-01-2023 01:00:00PM EDT") + }) +}) diff --git a/backend/core/test/lib/get-test-app-body.ts b/backend/core/test/lib/get-test-app-body.ts index 0ed65433b4..f0598d8e6d 100644 --- a/backend/core/test/lib/get-test-app-body.ts +++ b/backend/core/test/lib/get-test-app-body.ts @@ -93,7 +93,7 @@ export const getTestAppBody: (listingId?: string) => ApplicationUpdate = (listin }, demographics: { ethnicity: "", - race: "", + race: [], gender: "", sexualOrientation: "", howDidYouHear: [], @@ -101,6 +101,8 @@ export const getTestAppBody: (listingId?: string) => ApplicationUpdate = (listin incomeVouchers: true, income: "100.00", incomePeriod: IncomePeriod.perMonth, + householdStudent: false, + householdExpectingChanges: false, householdMembers: [], preferredUnit: [], preferences: [], diff --git a/backend/core/test/listings/listings.e2e-spec.ts b/backend/core/test/listings/listings.e2e-spec.ts index be7bf316c9..9422c360dc 100644 --- a/backend/core/test/listings/listings.e2e-spec.ts +++ b/backend/core/test/listings/listings.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test } from "@nestjs/testing" -import { TypeOrmModule } from "@nestjs/typeorm" -import { ListingsModule } from "../../src/listings/listings.module" +import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm" import supertest from "supertest" +import { ListingsModule } from "../../src/listings/listings.module" import { applicationSetup } from "../../src/app.module" import { ListingDto } from "../../src/listings/dto/listing.dto" import { getUserAccessToken } from "../utils/get-user-access-token" @@ -18,9 +18,10 @@ import { ListingEventType } from "../../src/listings/types/listing-event-type-en import { Listing } from "../../src/listings/entities/listing.entity" import qs from "qs" import { ListingUpdateDto } from "../../src/listings/dto/listing-update.dto" - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const dbOptions = require("../../ormconfig.test") +import { Program } from "../../src/program/entities/program.entity" +import { Repository } from "typeorm" +import { INestApplication } from "@nestjs/common" +import dbOptions from "../../ormconfig.test" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. @@ -29,7 +30,10 @@ declare const expect: jest.Expect jest.setTimeout(30000) describe("Listings", () => { - let app + let app: INestApplication + let programsRepository: Repository + let adminAccessToken: string + beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [ @@ -38,11 +42,14 @@ describe("Listings", () => { AssetsModule, ApplicationMethodsModule, PaperApplicationsModule, + TypeOrmModule.forFeature([Program]), ], }).compile() app = moduleRef.createNestApplication() app = applicationSetup(app) await app.init() + programsRepository = app.get>(getRepositoryToken(Program)) + adminAccessToken = await getUserAccessToken(app, "admin@example.com", "abcdef") }) it("should return all listings", async () => { @@ -55,7 +62,7 @@ describe("Listings", () => { // but the last listing. const page = "1" // This is the number of listings in ../../src/seed.ts minus 1 - const limit = 9 + const limit = 13 const params = "/?page=" + page + "&limit=" + limit.toString() const res = await supertest(app.getHttpServer()) .get("/listings" + params) @@ -64,57 +71,29 @@ describe("Listings", () => { }) it("should return the last page of paginated listings", async () => { - // Make the limit 1 less than the full number of listings, so that the second page contains - // only one listing. - const page = "2" - const limit = "12" - const params = "/?page=" + page + "&limit=" + limit - const res = await supertest(app.getHttpServer()) - .get("/listings" + params) - .expect(200) - expect(res.body.items.length).toEqual(1) - }) - - // TODO: replace jsonpath with SQL-level filtering - it("should return only the specified listings", async () => { - const query = - "/?limit=all&jsonpath=%24%5B%3F%28%40.applicationAddress.city%3D%3D%22Foster%20City%22%29%5D" - const res = await supertest(app.getHttpServer()).get(`/listings${query}`).expect(200) - expect(res.body.items.length).toEqual(1) - expect(res.body.items[0].applicationAddress.city).toEqual("Foster City") - }) - - // TODO: replace jsonpath with SQL-level filtering - it("shouldn't return any listings for incorrect query", async () => { - const query = "/?jsonpath=%24%5B%3F(%40.applicationNONSENSE.argh%3D%3D%22San+Jose%22)%5D" - const res = await supertest(app.getHttpServer()).get(`/listings${query}`).expect(200) - expect(res.body.items.length).toEqual(0) - }) - - // TODO: replace jsonpath with SQL-level filtering - it("should return only active listings", async () => { - const query = "/?limit=all&jsonpath=%24%5B%3F%28%40.status%3D%3D%22active%22%29%5D" - const res = await supertest(app.getHttpServer()).get(`/listings${query}`).expect(200) - expect(res.body.items.map((listing) => listing.id).length).toBeGreaterThan(0) - }) - - it("should return listings with matching zipcodes", async () => { - const queryParams = { - limit: "all", - filter: [ - { - $comparison: "IN", - zipcode: "94621,94404", - }, - ], + let queryParams = { + limit: 1, + page: 1, + view: "base", + } + let query = qs.stringify(queryParams) + let res = await supertest(app.getHttpServer()).get(`/listings?${query}`).expect(200) + const totalItems = res.body.meta.totalItems + + queryParams = { + limit: totalItems - 1, + page: 2, + view: "base", } - const query = qs.stringify(queryParams) - const res = await supertest(app.getHttpServer()).get(`/listings?${query}`).expect(200) - expect(res.body.items.length).toBeGreaterThanOrEqual(2) + query = qs.stringify(queryParams) + res = await supertest(app.getHttpServer()).get(`/listings?${query}`).expect(200) + expect(res.body.items.length).toEqual(1) }) it("should modify property related fields of a listing and return a modified value", async () => { - const res = await supertest(app.getHttpServer()).get("/listings").expect(200) + const res = await supertest(app.getHttpServer()) + .get("/listings?orderBy=applicationDates") + .expect(200) const listing: ListingDto = { ...res.body.items[0] } @@ -125,19 +104,32 @@ describe("Listings", () => { const oldOccupancy = Number(listing.units[0].maxOccupancy) listing.units[0].maxOccupancy = oldOccupancy + 1 - const adminAccessToken = await getUserAccessToken(app, "admin@example.com", "abcdef") + listing.neighborhoodAmenities = { + groceryStores: "grocery location", + } const putResponse = await supertest(app.getHttpServer()) .put(`/listings/${listing.id}`) .send(listing) .set(...setAuthorization(adminAccessToken)) + .expect(200) const modifiedListing: ListingDto = putResponse.body expect(modifiedListing.amenities).toBe(amenitiesValue) expect(modifiedListing.units[0].maxOccupancy).toBe(oldOccupancy + 1) + expect(modifiedListing.neighborhoodAmenities).toEqual({ + groceryStores: "grocery location", + publicTransportation: null, + schools: null, + parksAndCommunityCenters: null, + pharmacies: null, + healthCareResources: null, + }) }) it("should add/overwrite image in existing listing", async () => { - const res = await supertest(app.getHttpServer()).get("/listings").expect(200) + const res = await supertest(app.getHttpServer()) + .get("/listings?orderBy=applicationDates") + .expect(200) const listing: ListingUpdateDto = { ...res.body.items[0] } @@ -147,9 +139,13 @@ describe("Listings", () => { fileId: fileId, label: label, } - listing.image = image - const adminAccessToken = await getUserAccessToken(app, "admin@example.com", "abcdef") + const assetCreateResponse = await supertest(app.getHttpServer()) + .post(`/assets`) + .send(image) + .set(...setAuthorization(adminAccessToken)) + .expect(201) + listing.images = [{ image: assetCreateResponse.body, ordinal: 1 }] const putResponse = await supertest(app.getHttpServer()) .put(`/listings/${listing.id}`) @@ -158,11 +154,7 @@ describe("Listings", () => { .expect(200) const modifiedListing: ListingDto = putResponse.body - expect(modifiedListing.image.fileId).toBe(fileId) - expect(modifiedListing.image.label).toBe(label) - expect(modifiedListing.image).toHaveProperty("id") - expect(modifiedListing.image).toHaveProperty("createdAt") - expect(modifiedListing.image).toHaveProperty("updatedAt") + expect(modifiedListing.images[0].image.id).toBe(assetCreateResponse.body.id) }) it("should add/overwrite application methods in existing listing", async () => { @@ -170,8 +162,6 @@ describe("Listings", () => { const listing: Listing = { ...res.body.items[0] } - const adminAccessToken = await getUserAccessToken(app, "admin@example.com", "abcdef") - const assetCreateDto: AssetCreateDto = { fileId: "testFileId2", label: "testLabel2", @@ -208,12 +198,12 @@ describe("Listings", () => { }) it("should add/overwrite listing events in existing listing", async () => { - const res = await supertest(app.getHttpServer()).get("/listings").expect(200) + const res = await supertest(app.getHttpServer()) + .get("/listings?orderBy=applicationDates") + .expect(200) const listing: ListingUpdateDto = { ...res.body.items[0] } - const adminAccessToken = await getUserAccessToken(app, "admin@example.com", "abcdef") - const listingEvent: ListingEventCreateDto = { type: ListingEventType.openHouse, startTime: new Date(), @@ -245,7 +235,6 @@ describe("Listings", () => { expect(modifiedListing.events[0].file.label).toBe(listingEvent.file.label) }) - // TODO: enable this test suite once AMI values are added to Bloom seeds describe.skip("AMI Filter", () => { it("should return listings with AMI >= the filter value", async () => { const paramsWithEqualAmi = { @@ -378,28 +367,88 @@ describe("Listings", () => { }) }) - it("defaults to sorting listings by applicationDueDate, then applicationOpenDate", async () => { + describe.skip("Unit size filtering", () => { + it("should return listings with >= 1 bedroom", async () => { + const params = { + view: "base", + limit: "all", + filter: [ + { + $comparison: ">=", + bedrooms: "1", + }, + ], + } + const res = await supertest(app.getHttpServer()) + .get("/listings?" + qs.stringify(params)) + .expect(200) + + const listings: Listing[] = res.body.items + expect(listings.length).toBeGreaterThan(0) + // expect that all listings have at least one unit with >= 1 bedroom + /* expect( + listings.map((listing) => { + listing.unitsSummary.find((unit) => { + unit.unitType.some((unitType) => unitType.numBedrooms >= 1) + }) !== undefined + }) + ).not.toContain(false) */ + }) + + it("should return listings with exactly 1 bedroom", async () => { + const params = { + view: "base", + limit: "all", + filter: [ + { + $comparison: "=", + bedrooms: "1", + }, + ], + } + const res = await supertest(app.getHttpServer()) + .get("/listings?" + qs.stringify(params)) + .expect(200) + + const listings: Listing[] = res.body.items + expect(listings.length).toBeGreaterThan(0) + // expect that all listings have at least one unit with exactly 1 bedroom + /* expect( + listings.map((listing) => { + listing.unitsSummary.find((unit) => { + unit.unitType.some((unitType) => unitType.numBedrooms >= 1) + }) !== undefined + }) + ).not.toContain(false) */ + }) + }) + + it("defaults to sorting listings by name", async () => { const res = await supertest(app.getHttpServer()).get(`/listings?limit=all`).expect(200) const listings = res.body.items // The Coliseum seed has the soonest applicationDueDate (1 day in the future) - expect(listings[0].name).toBe("Test: Coliseum") + expect(listings[0].name).toBe("Medical Center Village") // Triton and "Default, No Preferences" share the next-soonest applicationDueDate - // (5 days in the future). Between the two, Triton appears first because it has - // the earlier applicationOpenDate. + // (5 days in the future). Between the two, Triton 1 appears first because it has + // the closer applicationOpenDate. const secondListing = listings[1] - expect(secondListing.name).toBe("Test: Triton") + expect(secondListing.name).toBe("Melrose Square Homes") const thirdListing = listings[2] - expect(thirdListing.name).toBe("Test: Default, No Preferences") + expect(thirdListing.name).toBe("New Center Commons") + const fourthListing = listings[3] + expect(fourthListing.name).toBe("New Center Pavilion") const secondListingAppDueDate = new Date(secondListing.applicationDueDate) const thirdListingAppDueDate = new Date(thirdListing.applicationDueDate) - expect(secondListingAppDueDate.getDate()).toEqual(thirdListingAppDueDate.getDate()) + expect(secondListingAppDueDate.getDate()).toBeGreaterThanOrEqual( + thirdListingAppDueDate.getDate() + ) const secondListingAppOpenDate = new Date(secondListing.applicationOpenDate) const thirdListingAppOpenDate = new Date(thirdListing.applicationOpenDate) - expect(secondListingAppOpenDate.getTime()).toBeLessThanOrEqual( + expect(secondListingAppOpenDate.getTime()).toBeGreaterThanOrEqual( thirdListingAppOpenDate.getTime() ) @@ -427,9 +476,11 @@ describe("Listings", () => { it("sorts results within a page, and across sequential pages", async () => { // Get the first page of 5 results. - const firstPage = await supertest(app.getHttpServer()) - .get(`/listings?orderBy=mostRecentlyUpdated&limit=5&page=1`) - .expect(200) + const firstPage = await supertest(app.getHttpServer()).get( + `/listings?orderBy=mostRecentlyUpdated&limit=5&page=1` + ) + //.expect(200) + console.log("firstPage = ", firstPage) // Verify that listings on the first page are ordered from most to least recently updated. for (let i = 0; i < 4; ++i) { @@ -457,6 +508,45 @@ describe("Listings", () => { } }) + it("should add/overwrite and remove listing programs in existing listing", async () => { + const res = await supertest(app.getHttpServer()).get("/listings").expect(200) + const listing: ListingUpdateDto = { ...res.body.items[0] } + const newProgram = await programsRepository.save({ + title: "TestTitle", + subtitle: "TestSubtitle", + description: "TestDescription", + }) + listing.listingPrograms = [{ program: newProgram, ordinal: 1 }] + + const putResponse = await supertest(app.getHttpServer()) + .put(`/listings/${listing.id}`) + .send(listing) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + + const listingResponse = await supertest(app.getHttpServer()) + .get(`/listings/${putResponse.body.id}`) + .expect(200) + + expect(listingResponse.body.listingPrograms[0].program.id).toBe(newProgram.id) + expect(listingResponse.body.listingPrograms[0].program.title).toBe(newProgram.title) + expect(listingResponse.body.listingPrograms[0].ordinal).toBe(1) + + await supertest(app.getHttpServer()) + .put(`/listings/${listing.id}`) + .send({ + ...putResponse.body, + listingPrograms: [], + }) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + + const listingResponse2 = await supertest(app.getHttpServer()) + .get(`/listings/${putResponse.body.id}`) + .expect(200) + expect(listingResponse2.body.listingPrograms.length).toBe(0) + }) + afterEach(() => { jest.clearAllMocks() }) diff --git a/backend/core/test/paper-applications/paper-applications.e2e-spec.ts b/backend/core/test/paper-applications/paper-applications.e2e-spec.ts index 1fe8d10315..e77e550afc 100644 --- a/backend/core/test/paper-applications/paper-applications.e2e-spec.ts +++ b/backend/core/test/paper-applications/paper-applications.e2e-spec.ts @@ -4,15 +4,15 @@ import { TypeOrmModule } from "@nestjs/typeorm" import supertest from "supertest" import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import { Language } from "../../src/shared/types/language-enum" import { PaperApplicationsModule } from "../../src/paper-applications/paper-applications.module" import { AssetsModule } from "../../src/assets/assets.module" +import { EmailService } from "../../src/email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. diff --git a/backend/core/test/programs/programs.e2e-spec.ts b/backend/core/test/programs/programs.e2e-spec.ts new file mode 100644 index 0000000000..571107dd56 --- /dev/null +++ b/backend/core/test/programs/programs.e2e-spec.ts @@ -0,0 +1,79 @@ +import { Test } from "@nestjs/testing" +import { INestApplication } from "@nestjs/common" +import { TypeOrmModule } from "@nestjs/typeorm" +// Use require because of the CommonJS/AMD style export. +// See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require +import dbOptions from "../../ormconfig.test" +import supertest from "supertest" +import { applicationSetup } from "../../src/app.module" +import { AuthModule } from "../../src/auth/auth.module" +import { getUserAccessToken } from "../utils/get-user-access-token" +import { setAuthorization } from "../utils/set-authorization-helper" +import { ProgramsModule } from "../../src/program/programs.module" +import { ProgramCreateDto } from "../../src/program/dto/program-create.dto" +import { EmailService } from "../../src/email/email.service" + +// Cypress brings in Chai types for the global expect, but we want to use jest +// expect here so we need to re-declare it. +// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 +declare const expect: jest.Expect +jest.setTimeout(30000) + +describe("Programs", () => { + let app: INestApplication + let adminAccessToken: string + + beforeAll(async () => { + /* eslint-disable @typescript-eslint/no-empty-function */ + const testEmailService = { confirmation: async () => {} } + /* eslint-enable @typescript-eslint/no-empty-function */ + const moduleRef = await Test.createTestingModule({ + imports: [TypeOrmModule.forRoot(dbOptions), AuthModule, ProgramsModule], + }) + .overrideProvider(EmailService) + .useValue(testEmailService) + .compile() + app = moduleRef.createNestApplication() + app = applicationSetup(app) + await app.init() + adminAccessToken = await getUserAccessToken(app, "admin@example.com", "abcdef") + }) + + it(`should return programs`, async () => { + const res = await supertest(app.getHttpServer()) + .get(`/programs`) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + expect(Array.isArray(res.body)).toBe(true) + }) + + it(`should create and return a new program`, async () => { + const newProgram: ProgramCreateDto = { + title: "title", + description: "description", + subtitle: "subtitle", + } + const res = await supertest(app.getHttpServer()) + .post(`/programs`) + .set(...setAuthorization(adminAccessToken)) + .send(newProgram) + .expect(201) + expect(res.body).toHaveProperty("id") + expect(res.body).toHaveProperty("createdAt") + expect(res.body).toHaveProperty("updatedAt") + expect(res.body.id).toBe(res.body.id) + expect(res.body.title).toBe(newProgram.title) + + const getById = await supertest(app.getHttpServer()).get(`/programs/${res.body.id}`).expect(200) + expect(getById.body.id).toBe(res.body.id) + expect(getById.body.title).toBe(newProgram.title) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + afterAll(async () => { + await app.close() + }) +}) diff --git a/backend/core/test/properties/properties.e2e-spec.ts b/backend/core/test/properties/properties.e2e-spec.ts index 57b6e4dd05..16890621a2 100644 --- a/backend/core/test/properties/properties.e2e-spec.ts +++ b/backend/core/test/properties/properties.e2e-spec.ts @@ -3,14 +3,14 @@ import { INestApplication } from "@nestjs/common" import { TypeOrmModule } from "@nestjs/typeorm" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import supertest from "supertest" import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" import { PropertiesModule } from "../../src/property/properties.module" +import { EmailService } from "../../src/email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. diff --git a/backend/core/test/reserved-community-types/reserved-community-types.e2e-spec.ts b/backend/core/test/reserved-community-types/reserved-community-types.e2e-spec.ts index 5a6b966f98..a140b93a87 100644 --- a/backend/core/test/reserved-community-types/reserved-community-types.e2e-spec.ts +++ b/backend/core/test/reserved-community-types/reserved-community-types.e2e-spec.ts @@ -3,17 +3,17 @@ import { INestApplication } from "@nestjs/common" import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import supertest from "supertest" import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" import { ReservedCommunityTypesModule } from "../../src/reserved-community-type/reserved-community-types.module" import { JurisdictionsModule } from "../../src/jurisdictions/jurisdictions.module" import { Jurisdiction } from "../../src/jurisdictions/entities/jurisdiction.entity" import { Repository } from "typeorm" +import { EmailService } from "../../src/email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. diff --git a/backend/core/test/unit-accessibility-priority-types/unit-accessibility-priority-types.e2e-spec.ts b/backend/core/test/unit-accessibility-priority-types/unit-accessibility-priority-types.e2e-spec.ts index 5394873918..cc39e95859 100644 --- a/backend/core/test/unit-accessibility-priority-types/unit-accessibility-priority-types.e2e-spec.ts +++ b/backend/core/test/unit-accessibility-priority-types/unit-accessibility-priority-types.e2e-spec.ts @@ -4,13 +4,13 @@ import { TypeOrmModule } from "@nestjs/typeorm" import supertest from "supertest" import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import { UnitAccessibilityPriorityTypesModule } from "../../src/unit-accessbility-priority-types/unit-accessibility-priority-types.module" +import { EmailService } from "../../src/email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. diff --git a/backend/core/test/unit-rent-types/unit-rent-types.e2e-spec.ts b/backend/core/test/unit-rent-types/unit-rent-types.e2e-spec.ts index 5e4e1e8db0..05cc765861 100644 --- a/backend/core/test/unit-rent-types/unit-rent-types.e2e-spec.ts +++ b/backend/core/test/unit-rent-types/unit-rent-types.e2e-spec.ts @@ -4,13 +4,13 @@ import { TypeOrmModule } from "@nestjs/typeorm" import supertest from "supertest" import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import { UnitRentTypesModule } from "../../src/unit-rent-types/unit-rent-types.module" +import { EmailService } from "../../src/email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. diff --git a/backend/core/test/unit-types/unit-types.e2e-spec.ts b/backend/core/test/unit-types/unit-types.e2e-spec.ts index c9c46d70c1..8c2c4aade8 100644 --- a/backend/core/test/unit-types/unit-types.e2e-spec.ts +++ b/backend/core/test/unit-types/unit-types.e2e-spec.ts @@ -4,13 +4,13 @@ import { TypeOrmModule } from "@nestjs/typeorm" import supertest from "supertest" import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" import { setAuthorization } from "../utils/set-authorization-helper" import { UnitTypesModule } from "../../src/unit-types/unit-types.module" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" +import { EmailService } from "../../src/email/email.service" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. diff --git a/backend/core/test/user/user-preferences.e2e-spec.ts b/backend/core/test/user/user-preferences.e2e-spec.ts new file mode 100644 index 0000000000..d9bf26678d --- /dev/null +++ b/backend/core/test/user/user-preferences.e2e-spec.ts @@ -0,0 +1,156 @@ +import { Test } from "@nestjs/testing" +import { INestApplication } from "@nestjs/common" +import { TypeOrmModule } from "@nestjs/typeorm" +import { applicationSetup } from "../../src/app.module" +import { AuthModule } from "../../src/auth/auth.module" +import { getUserAccessToken } from "../utils/get-user-access-token" + +// Use require because of the CommonJS/AMD style export. +// See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require +import dbOptions from "../../ormconfig.test" +import supertest from "supertest" +import { setAuthorization } from "../utils/set-authorization-helper" +import { UserService } from "../../src/auth/services/user.service" +import { UserCreateDto } from "../../src/auth/dto/user-create.dto" +import { Listing } from "../../src/listings/entities/listing.entity" +import { Jurisdiction } from "../../src/jurisdictions/entities/jurisdiction.entity" +import { UserPreferencesDto } from "../../src/auth/dto/user-preferences.dto" +import { Language } from "../../src/shared/types/language-enum" +import { User } from "../../src/auth/entities/user.entity" +import { Application } from "../../src/applications/entities/application.entity" +import { EmailService } from "../../src/email/email.service" +import { UserPreferences } from "../../src/auth/entities/user-preferences.entity" + +// Cypress brings in Chai types for the global expect, but we want to use jest +// expect here so we need to re-declare it. +// see: https://github.com/cypress-io/cypress/issues/1319#issuecomment-593500345 +declare const expect: jest.Expect +jest.setTimeout(30000) + +describe("Users", () => { + let app: INestApplication + let userService: UserService + let adminAccessToken: string + + const testEmailService = { + /* eslint-disable @typescript-eslint/no-empty-function */ + confirmation: async () => {}, + welcome: async () => {}, + invite: async () => {}, + changeEmail: async () => {}, + forgotPassword: async () => {}, + /* eslint-enable @typescript-eslint/no-empty-function */ + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRoot(dbOptions), + TypeOrmModule.forFeature([Listing, Jurisdiction, User, Application, UserPreferences]), + AuthModule, + ], + }) + .overrideProvider(EmailService) + .useValue(testEmailService) + .compile() + app = moduleRef.createNestApplication() + app = applicationSetup(app) + await app.init() + userService = await moduleRef.resolve(UserService) + adminAccessToken = await getUserAccessToken(app, "admin@example.com", "abcdef") + }) + + it("should disallow preference changes across users", async () => { + const createAndConfirmUser = async (createDto: UserCreateDto) => { + const userCreateResponse = await supertest(app.getHttpServer()) + .post(`/user/`) + .set("jurisdictionName", "Detroit") + .send(createDto) + .expect(201) + + const userService = await app.resolve(UserService) + const user = await userService.findByEmail(createDto.email) + + await supertest(app.getHttpServer()) + .put(`/user/confirm/`) + .send({ token: user.confirmationToken }) + .expect(200) + + const accessToken = await getUserAccessToken(app, createDto.email, createDto.password) + return { accessToken, userId: userCreateResponse.body.id } + } + + const user1CreateDto: UserCreateDto = { + password: "Abcdef1!", + passwordConfirmation: "Abcdef1!", + email: "user-1@example.com", + emailConfirmation: "user-1@example.com", + firstName: "First", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + language: Language.en, + } + + const user2CreateDto: UserCreateDto = { + password: "Abcdef1!", + passwordConfirmation: "Abcdef1!", + email: "user-2@example.com", + emailConfirmation: "user-2@example.com", + firstName: "First", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + language: Language.en, + } + + const { userId: user1Id, accessToken: user1AccessToken } = await createAndConfirmUser( + user1CreateDto + ) + const { accessToken: user2AccessToken } = await createAndConfirmUser(user2CreateDto) + + const user1ProfileUpdateDto: UserPreferencesDto = { + sendEmailNotifications: false, + sendSmsNotifications: false, + favoriteIds: ["example listing id"], + } + + const user2ProfileUpdateDto: UserPreferencesDto = { + sendEmailNotifications: true, + sendSmsNotifications: true, + favoriteIds: ["example of second listing id"], + } + + const userService = await app.resolve(UserService) + + // let user 1 edit their preferences + await supertest(app.getHttpServer()) + .put(`/userPreferences/${user1Id}`) + .send(user1ProfileUpdateDto) + .set(...setAuthorization(user1AccessToken)) + .expect(200) + + // verify the listing was added as a favorite to user 1 + let user = await userService.findByEmail(user1CreateDto.email) + expect(user.preferences.sendEmailNotifications === false) + expect(user.preferences.sendSmsNotifications === false) + expect(user.preferences.favoriteIds).toEqual(["example listing id"]) + + // Restrict user 2 editing user 1's preferences + await supertest(app.getHttpServer()) + .put(`/userPreferences/${user1Id}`) + .send(user2ProfileUpdateDto) + .set(...setAuthorization(user2AccessToken)) + .expect(403) + + // verify the listing was not added as a favorite user 1 + user = await userService.findByEmail(user1CreateDto.email) + expect(user.preferences.sendEmailNotifications === false) + expect(user.preferences.sendSmsNotifications === false) + expect(user.preferences.favoriteIds).toEqual(["example listing id"]) + }) +}) diff --git a/backend/core/test/user/user.e2e-spec.ts b/backend/core/test/user/user.e2e-spec.ts index 46ec55b10a..d9e10532a9 100644 --- a/backend/core/test/user/user.e2e-spec.ts +++ b/backend/core/test/user/user.e2e-spec.ts @@ -3,12 +3,12 @@ import { INestApplication } from "@nestjs/common" import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm" import { applicationSetup } from "../../src/app.module" import { AuthModule } from "../../src/auth/auth.module" -import { EmailService } from "../../src/shared/email/email.service" import { getUserAccessToken } from "../utils/get-user-access-token" +import qs from "qs" // Use require because of the CommonJS/AMD style export. // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import dbOptions = require("../../ormconfig.test") +import dbOptions from "../../ormconfig.test" import supertest from "supertest" import { setAuthorization } from "../utils/set-authorization-helper" import { UserDto } from "../../src/auth/dto/user.dto" @@ -21,6 +21,14 @@ import { Repository } from "typeorm" import { Jurisdiction } from "../../src/jurisdictions/entities/jurisdiction.entity" import { UserProfileUpdateDto } from "../../src/auth/dto/user-profile.dto" import { Language } from "../../src/shared/types/language-enum" +import { User } from "../../src/auth/entities/user.entity" +import { EnumUserFilterParamsComparison } from "../../types" +import { getTestAppBody } from "../lib/get-test-app-body" +import { Application } from "../../src/applications/entities/application.entity" +import { UserRoles } from "../../src/auth/entities/user-roles.entity" +import { EmailService } from "../../src/email/email.service" +import { MfaType } from "../../src/auth/types/mfa-type" +import { UserRepository } from "../../src/auth/repositories/user-repository" // Cypress brings in Chai types for the global expect, but we want to use jest // expect here so we need to re-declare it. @@ -28,13 +36,16 @@ import { Language } from "../../src/shared/types/language-enum" declare const expect: jest.Expect jest.setTimeout(30000) -describe("Applications", () => { +describe("Users", () => { let app: INestApplication let user1AccessToken: string let user2AccessToken: string let user2Profile: UserDto let listingRepository: Repository+ let applicationsRepository: Repository + let userRepository: UserService let jurisdictionsRepository: Repository + let usersRepository: Repository let adminAccessToken: string let userAccessToken: string @@ -43,18 +54,21 @@ describe("Applications", () => { confirmation: async () => {}, welcome: async () => {}, invite: async () => {}, + changeEmail: async () => {}, + forgotPassword: async () => {}, + sendMfaCode: jest.fn(), /* eslint-enable @typescript-eslint/no-empty-function */ } beforeEach(() => { - jest.clearAllMocks() + jest.resetAllMocks() }) beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot(dbOptions), - TypeOrmModule.forFeature([Listing, Jurisdiction]), + TypeOrmModule.forFeature([Listing, Jurisdiction, User, Application]), AuthModule, ], }) @@ -74,9 +88,12 @@ describe("Applications", () => { .set(...setAuthorization(user2AccessToken)) ).body listingRepository = moduleRef.get>(getRepositoryToken(Listing)) + applicationsRepository = moduleRef.get>(getRepositoryToken(Application)) jurisdictionsRepository = moduleRef.get>( getRepositoryToken(Jurisdiction) ) + usersRepository = moduleRef.get>(getRepositoryToken(User)) + userRepository = await moduleRef.resolve(UserService) adminAccessToken = await getUserAccessToken(app, "admin@example.com", "abcdef") userAccessToken = await getUserAccessToken(app, "test@example.com", "abcdef") }) @@ -173,6 +190,9 @@ describe("Applications", () => { expect(mockWelcome.mock.calls.length).toBe(1) expect(res.body).toHaveProperty("id") expect(res.body).not.toHaveProperty("passwordHash") + expect(res.body).toHaveProperty("passwordUpdatedAt") + expect(res.body).toHaveProperty("passwordValidForDays") + expect(res.body.passwordValidForDays).toBe(180) }) it("should not allow user to sign in before confirming the account", async () => { @@ -196,8 +216,8 @@ describe("Applications", () => { .send({ email: userCreateDto.email, password: userCreateDto.password }) .expect(401) - const userService = await app.resolve(UserService) - const user = await userService.findByEmail(userCreateDto.email) + const userRepository = await app.resolve(UserRepository) + const user = await userRepository.findByEmail(userCreateDto.email) await supertest(app.getHttpServer()) .put(`/user/confirm/`) @@ -263,6 +283,7 @@ describe("Applications", () => { jurisdictions: user2Profile.jurisdictions.map((jurisdiction) => ({ id: jurisdiction.id, })), + agreedToTermsOfService: false, } await supertest(app.getHttpServer()) .put(`/user/${user2UpdateDto.id}`) @@ -313,8 +334,8 @@ describe("Applications", () => { .set("jurisdictionName", "Alameda") .send(userCreateDto) .expect(201) - const userService = await app.resolve(UserService) - const user = await userService.findByEmail(userCreateDto.email) + const userRepository = await app.resolve(UserRepository) + const user = await userRepository.findByEmail(userCreateDto.email) await supertest(app.getHttpServer()) .put(`/user/confirm/`) @@ -389,8 +410,10 @@ describe("Applications", () => { expect(newUser.leasingAgentInListings[0].id).toBe(listing.id) expect(mockInvite.mock.calls.length).toBe(1) - const userService = await app.resolve(UserService) - const user = await userService.findByEmail(newUser.email) + const userRepository = await app.resolve(UserRepository) + const user = await userRepository.findByEmail(newUser.email) + user.mfaEnabled = false + await usersRepository.save(user) const password = "Abcdef1!" await supertest(app.getHttpServer()) @@ -401,7 +424,7 @@ describe("Applications", () => { expect(token).toBeDefined() }) - it("should allow user to update user profile throguh PUT /userProfile/:id endpoint", async () => { + it("should allow user to update user profile through PUT /userProfile/:id endpoint", async () => { const userCreateDto: UserCreateDto = { password: "Abcdef1!", passwordConfirmation: "Abcdef1!", @@ -420,8 +443,8 @@ describe("Applications", () => { .send(userCreateDto) .expect(201) - const userService = await app.resolve(UserService) - const user = await userService.findByEmail(userCreateDto.email) + const userRepository = await app.resolve(UserRepository) + const user = await userRepository.findByEmail(userCreateDto.email) await supertest(app.getHttpServer()) .put(`/user/confirm/`) @@ -442,6 +465,8 @@ describe("Applications", () => { ...userCreateDto, currentPassword: userCreateDto.password, firstName: "NewFirstName", + phoneNumber: "+12025550194", + agreedToTermsOfService: false, } await supertest(app.getHttpServer()) @@ -465,8 +490,8 @@ describe("Applications", () => { .send(createDto) .expect(201) - const userService = await app.resolve(UserService) - const user = await userService.findByEmail(createDto.email) + const userRepository = await app.resolve(UserRepository) + const user = await userRepository.findByEmail(createDto.email) await supertest(app.getHttpServer()) .put(`/user/confirm/`) @@ -511,6 +536,7 @@ describe("Applications", () => { ...userACreateDto, password: undefined, jurisdictions: [], + agreedToTermsOfService: false, } // Restrict user B editing user A's profile @@ -527,4 +553,462 @@ describe("Applications", () => { .set(...setAuthorization(adminAccessToken)) .expect(200) }) + + it("should allow filtering by isPartner user role", async () => { + const user = await userRepository._createUser( + { + dob: new Date(), + email: "michalp@airnauts.com", + firstName: "Michal", + jurisdictions: [], + language: Language.en, + lastName: "", + middleName: "", + roles: { isPartner: true, isAdmin: false }, + updatedAt: undefined, + passwordHash: "abcd", + mfaEnabled: false, + }, + null + ) + + const filters = [ + { + isPartner: true, + $comparison: EnumUserFilterParamsComparison["="], + }, + ] + + const res = await supertest(app.getHttpServer()) + .get(`/user/list/?${qs.stringify({ filter: filters })}`) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + + expect(res.body.items.map((user) => user.id).includes(user.id)).toBe(true) + expect(res.body.items.map((user) => user.roles.isPartner).some((isPartner) => !isPartner)).toBe( + false + ) + expect(res.body.items.map((user) => user.roles.isPartner).every((isPartner) => isPartner)).toBe( + true + ) + }) + + it("should get and delete a user by ID", async () => { + const user = await userRepository._createUser( + { + dob: new Date(), + email: "test+1@test.com", + firstName: "test", + jurisdictions: [], + language: Language.en, + lastName: "", + middleName: "", + roles: { isPartner: true, isAdmin: false }, + updatedAt: undefined, + passwordHash: "abcd", + mfaEnabled: false, + }, + null + ) + + const res = await supertest(app.getHttpServer()) + .get(`/user/${user.id}`) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + expect(res.body.id).toBe(user.id) + expect(res.body.email).toBe(user.email) + + await supertest(app.getHttpServer()) + .delete(`/user`) + .send({ id: user.id }) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + + await supertest(app.getHttpServer()) + .get(`/user/${user.id}`) + .set(...setAuthorization(adminAccessToken)) + .expect(404) + }) + + it("should create and delete a user with existing application by ID", async () => { + const listing = (await listingRepository.find({ take: 1 }))[0] + const user = await userRepository._createUser( + { + dob: new Date(), + email: "test+1@test.com", + firstName: "test", + jurisdictions: [], + language: Language.en, + lastName: "", + middleName: "", + roles: { isPartner: true, isAdmin: false }, + updatedAt: undefined, + passwordHash: "abcd", + mfaEnabled: false, + }, + null + ) + const applicationUpdate = getTestAppBody(listing.id) + const newApp = await applicationsRepository.save({ + ...applicationUpdate, + user, + confirmationCode: "abcdefgh", + }) + + await supertest(app.getHttpServer()) + .delete(`/user`) + .send({ id: user.id }) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + + const application = await applicationsRepository.findOneOrFail({ + where: { id: (newApp as Application).id }, + relations: ["user"], + }) + + expect(application.user).toBe(null) + }) + + it("should lower case email of new user", async () => { + const userCreateDto: UserCreateDto = { + password: "Abcdef1!", + passwordConfirmation: "Abcdef1!", + email: "TestingLowerCasing@LowerCasing.com", + emailConfirmation: "TestingLowerCasing@LowerCasing.com", + firstName: "First", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + } + const res = await supertest(app.getHttpServer()) + .post(`/user`) + .set("jurisdictionName", "Alameda") + .send(userCreateDto) + expect(res.body).toHaveProperty("id") + expect(res.body).not.toHaveProperty("passwordHash") + expect(res.body).toHaveProperty("email") + expect(res.body.email).toBe("testinglowercasing@lowercasing.com") + + const confirmation = await supertest(app.getHttpServer()) + .put(`/user/${res.body.id}`) + .set(...setAuthorization(adminAccessToken)) + .send({ + ...res.body, + confirmedAt: new Date(), + }) + .expect(200) + + expect(confirmation.body.confirmedAt).toBeDefined() + + await supertest(app.getHttpServer()) + .post("/auth/login") + .send({ email: userCreateDto.email.toLowerCase(), password: userCreateDto.password }) + .expect(201) + }) + + it("should change an email with confirmation flow", async () => { + const userCreateDto: UserCreateDto = { + password: "Abcdef1!", + passwordConfirmation: "Abcdef1!", + email: "confirm@confirm.com", + emailConfirmation: "confirm@confirm.com", + firstName: "First", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + } + + const res = await supertest(app.getHttpServer()) + .post(`/user/`) + .set("jurisdictionName", "Alameda") + .send(userCreateDto) + .expect(201) + + const userRepository = await app.resolve(UserRepository) + let user = await userRepository.findByEmail(userCreateDto.email) + + await supertest(app.getHttpServer()) + .put(`/user/confirm/`) + .send({ token: user.confirmationToken }) + .expect(200) + const userAccessToken = await getUserAccessToken( + app, + userCreateDto.email, + userCreateDto.password + ) + + const newEmail = "test+confirm@example.com" + await supertest(app.getHttpServer()) + .put(`/userProfile/${user.id}`) + .send({ ...res.body, newEmail, appUrl: "http://localhost" }) + .set(...setAuthorization(userAccessToken)) + .expect(200) + + // User should still be able to log in with the old email + await getUserAccessToken(app, userCreateDto.email, userCreateDto.password) + + user = await userRepository.findByEmail(userCreateDto.email) + await supertest(app.getHttpServer()) + .put(`/user/confirm/`) + .send({ token: user.confirmationToken }) + .expect(200) + + await getUserAccessToken(app, newEmail, userCreateDto.password) + }) + + it("should allow filtering by isPortalUser", async () => { + const usersRepository = app.get(UserRepository) + + const totalUsersCount = await usersRepository.count() + + const allUsersListRes = await supertest(app.getHttpServer()) + .get(`/user/list`) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + expect(allUsersListRes.body.meta.totalItems).toBe(totalUsersCount) + + const portalUsersFilter = [ + { + isPortalUser: true, + $comparison: EnumUserFilterParamsComparison["NA"], + }, + ] + const portalUsersListRes = await supertest(app.getHttpServer()) + .get(`/user/list?${qs.stringify({ filter: portalUsersFilter })}&limit=200`) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + expect( + portalUsersListRes.body.items.every( + (user: UserDto) => user.roles.isAdmin || user.roles.isPartner + ) + ) + expect(portalUsersListRes.body.meta.totalItems).toBeLessThan(totalUsersCount) + + const nonPortalUsersFilter = [ + { + isPortalUser: false, + $comparison: EnumUserFilterParamsComparison["NA"], + }, + ] + const nonPortalUsersListRes = await supertest(app.getHttpServer()) + .get(`/user/list?${qs.stringify({ filter: nonPortalUsersFilter })}&limit=200`) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + expect( + nonPortalUsersListRes.body.items.every( + (user: UserDto) => !!user.roles?.isAdmin && !!user.roles?.isPartner + ) + ) + expect(nonPortalUsersListRes.body.meta.totalItems).toBeLessThan(totalUsersCount) + }) + + it("should require mfa code for users with mfa enabled", async () => { + const userCreateDto: UserCreateDto = { + password: "Abcdef1!", + passwordConfirmation: "Abcdef1!", + email: "mfa@b.com", + emailConfirmation: "mfa@b.com", + firstName: "First", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + } + + await supertest(app.getHttpServer()) + .post(`/user/`) + .set("jurisdictionName", "Alameda") + .send(userCreateDto) + .expect(201) + + let user = await usersRepository.findOne({ email: userCreateDto.email }) + user.mfaEnabled = true + user = await usersRepository.save(user) + + await supertest(app.getHttpServer()) + .put(`/user/confirm/`) + .send({ token: user.confirmationToken }) + .expect(200) + + testEmailService.sendMfaCode = jest.fn() + + let getMfaInfoResponse = await supertest(app.getHttpServer()) + .post(`/auth/mfa-info`) + .send({ + email: userCreateDto.email, + password: userCreateDto.password, + }) + .expect(201) + + expect(getMfaInfoResponse.body.maskedPhoneNumber).toBeUndefined() + expect(getMfaInfoResponse.body.email).toBe(userCreateDto.email) + expect(getMfaInfoResponse.body.isMfaEnabled).toBe(true) + expect(getMfaInfoResponse.body.mfaUsedInThePast).toBe(false) + + await supertest(app.getHttpServer()) + .post(`/auth/request-mfa-code`) + .send({ + email: userCreateDto.email, + password: userCreateDto.password, + mfaType: MfaType.email, + }) + .expect(201) + + user = await usersRepository.findOne({ email: userCreateDto.email }) + expect(typeof user.mfaCode).toBe("string") + expect(user.mfaCodeUpdatedAt).toBeDefined() + expect(testEmailService.sendMfaCode).toBeCalled() + expect(testEmailService.sendMfaCode.mock.calls[0][2]).toBe(user.mfaCode) + + await supertest(app.getHttpServer()) + .post(`/auth/login`) + .send({ + email: userCreateDto.email, + password: userCreateDto.password, + mfaCode: user.mfaCode, + }) + .expect(201) + + getMfaInfoResponse = await supertest(app.getHttpServer()) + .post(`/auth/mfa-info`) + .send({ + email: userCreateDto.email, + password: userCreateDto.password, + }) + .expect(201) + expect(getMfaInfoResponse.body.mfaUsedInThePast).toBe(true) + }) + + it("should prevent user access if password is outdated", async () => { + const userCreateDto: UserCreateDto = { + password: "Abcdef1!", + passwordConfirmation: "Abcdef1!", + email: "password-outdated@b.com", + emailConfirmation: "password-outdated@b.com", + firstName: "First", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + } + + await supertest(app.getHttpServer()) + .post(`/user/`) + .set("jurisdictionName", "Alameda") + .send(userCreateDto) + .expect(201) + await supertest(app.getHttpServer()) + .post("/auth/login") + .send({ email: userCreateDto.email, password: userCreateDto.password }) + .expect(401) + + const userRepository = await app.resolve(UserRepository) + let user = await userRepository.findByEmail(userCreateDto.email) + + await supertest(app.getHttpServer()) + .put(`/user/confirm/`) + .send({ token: user.confirmationToken }) + .expect(200) + + const userAccessToken = await getUserAccessToken( + app, + userCreateDto.email, + userCreateDto.password + ) + + // User should be able to fetch it's own profile now + await supertest(app.getHttpServer()) + .get(`/user/${user.id}`) + .set(...setAuthorization(userAccessToken)) + .expect(200) + + // Put password updated at date 190 days in the past + user = await userRepository.findByEmail(userCreateDto.email) + user.roles = { isAdmin: true, isPartner: false } as UserRoles + user.passwordUpdatedAt = new Date(user.passwordUpdatedAt.getTime() - 190 * 24 * 60 * 60 * 1000) + + await usersRepository.save(user) + + // Confirm that both login and using existing access tokens stopped authenticating + await supertest(app.getHttpServer()) + .get(`/user/${user.id}`) + .set(...setAuthorization(userAccessToken)) + .expect(401) + + await supertest(app.getHttpServer()) + .post("/auth/login") + .send({ email: userCreateDto.email, password: userCreateDto.password }) + .expect(401) + + // Start password reset flow + await supertest(app.getHttpServer()) + .put(`/user/forgot-password`) + .send({ email: user.email }) + .expect(200) + + user = await usersRepository.findOne({ email: user.email }) + + const newPassword = "Abcefghjijk90!" + await supertest(app.getHttpServer()) + .put(`/user/update-password`) + .send({ token: user.resetToken, password: newPassword, passwordConfirmation: newPassword }) + .expect(200) + + // Confirm that login works again (passwordUpdateAt timestamp has been refreshed) + await supertest(app.getHttpServer()) + .post("/auth/login") + .send({ email: userCreateDto.email, password: newPassword }) + .expect(201) + }) + + // TODO: add back when adding search to users + it.skip("should not crash with empty search query param", async () => { + const usersRepository = app.get(UserRepository) + + const totalUsersCount = await usersRepository.count() + + const allUsersListRes = await supertest(app.getHttpServer()) + .get(`/user/list?search=`) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + expect(allUsersListRes.body.meta.totalItems).toBe(totalUsersCount) + }) + + // TODO: add back when adding search to users + it.skip("should find user by email and assigned listing", async () => { + const searchableEmailAddress = "searchable-email@example.com" + const listing = (await listingRepository.find({ take: 1 }))[0] + const userCreateDto: UserCreateDto = { + password: "Abcdef1!", + passwordConfirmation: "Abcdef1!", + email: searchableEmailAddress, + emailConfirmation: searchableEmailAddress, + firstName: "First", + middleName: "Mid", + lastName: "Last", + dob: new Date(), + } + + await supertest(app.getHttpServer()) + .post(`/user/`) + .set("jurisdictionName", "Alameda") + .send(userCreateDto) + .expect(201) + + let res = await supertest(app.getHttpServer()) + .get(`/user/list?search=${searchableEmailAddress}`) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + expect(res.body.items[0].email).toBe(searchableEmailAddress) + + const userRepository = await app.resolve(UserRepository) + const user = await userRepository.findByEmail(searchableEmailAddress) + + user.leasingAgentInListings = [{ id: listing.id } as Listing] + await userRepository.save(user) + + res = await supertest(app.getHttpServer()) + .get(`/user/list?search=${listing.name}`) + .set(...setAuthorization(adminAccessToken)) + .expect(200) + expect(res.body.items.map((item) => item.email).includes(searchableEmailAddress)).toBe(true) + }) }) diff --git a/backend/core/types/src/archer-listing.ts b/backend/core/types/src/archer-listing.ts index 07bc24404d..32d52faf3c 100644 --- a/backend/core/types/src/archer-listing.ts +++ b/backend/core/types/src/archer-listing.ts @@ -1,4 +1,10 @@ -import { AmiChart, Listing, ListingStatus, UnitStatus } from "./backend-swagger" +import { + AmiChart, + EnumJurisdictionLanguages, + Listing, + ListingStatus, + UnitStatus, +} from "./backend-swagger" const amiChart: AmiChart = { id: "somerandomid", @@ -6,10 +12,7 @@ const amiChart: AmiChart = { updatedAt: new Date(), name: "San Jose TCAC 2019", jurisdiction: { - id: "jurisdictiion_id", - createdAt: new Date(), - updatedAt: new Date(), - name: "Alameda", + id: "jurisdiction_id", }, items: [ { @@ -586,21 +589,11 @@ export const ArcherListing: Listing = { jurisdiction: { id: "id", name: "Alameda", + publicUrl: "", }, events: [], urlSlug: "listing-slug-abcdef", status: ListingStatus.active, - applicationAddress: { - id: "id", - createdAt: new Date(), - updatedAt: new Date(), - city: "San Jose", - street: "98 Archer Street", - zipCode: "95112", - state: "CA", - latitude: 37.36537, - longitude: -121.91071, - }, applicationDueDate: new Date("2019-12-31T15:22:57.000-07:00"), applicationMethods: [], applicationOrganization: "98 Archer Street", @@ -661,17 +654,17 @@ export const ArcherListing: Listing = { leasingAgentOfficeHours: "Monday, Tuesday & Friday, 9:00AM - 5:00PM", leasingAgentPhone: "(408) 217-8562", leasingAgentTitle: "", + listingPrograms: [], rentalAssistance: "Custom rental assistance", rentalHistory: "Two years of rental history will be verified with all applicable landlords. Household family members and/or personal friends are not acceptable landlord references. Two professional character references may be used in lieu of rental history for applicants with no prior rental history. An unlawful detainer report will be processed thourhg the U.D. Registry, Inc. Applicants will be disqualified if they have any evictions filing within the last 7 years. Refer to Tenant Selection Criteria or Qualification Criteria for details related to the qualification process.", - preferences: [], householdSizeMin: 2, householdSizeMax: 3, smokingPolicy: "Non-smoking building", unitsAvailable: 0, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - unitsSummarized: {}, + unitSummaries: {}, unitAmenities: "Dishwasher", developer: "Charities Housing ", yearBuilt: 2012, diff --git a/backend/core/types/src/backend-swagger.ts b/backend/core/types/src/backend-swagger.ts index d2f73e43a1..544676313e 100644 --- a/backend/core/types/src/backend-swagger.ts +++ b/backend/core/types/src/backend-swagger.ts @@ -97,6 +97,8 @@ export class AmiChartsService { params: { /** */ jurisdictionName?: string + /** */ + jurisdictionId?: string } = {} as any, options: IRequestOptions = {} ): Promise { @@ -104,7 +106,10 @@ export class AmiChartsService { let url = basePath + "/amiCharts" const configs: IRequestConfig = getConfigs("get", "application/json", url, options) - configs.params = { jurisdictionName: params["jurisdictionName"] } + configs.params = { + jurisdictionName: params["jurisdictionName"], + jurisdictionId: params["jurisdictionId"], + } let data = null configs.data = data @@ -444,6 +449,27 @@ export class ApplicationsService { axios(configs, resolve, reject) }) } + /** + * Delete application by id + */ + delete( + params: { + /** requestBody */ + body?: Id + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/applications" + + const configs: IRequestConfig = getConfigs("delete", "application/json", url, options) + + let data = params.body + + configs.data = data + axios(configs, resolve, reject) + }) + } /** * List applications as csv */ @@ -454,8 +480,6 @@ export class ApplicationsService { /** */ limit?: number /** */ - listingId?: string - /** */ search?: string /** */ userId?: string @@ -466,7 +490,7 @@ export class ApplicationsService { /** */ markedAsDuplicate?: boolean /** */ - includeHeaders?: boolean + listingId: string /** */ includeDemographics?: boolean } = {} as any, @@ -479,13 +503,12 @@ export class ApplicationsService { configs.params = { page: params["page"], limit: params["limit"], - listingId: params["listingId"], search: params["search"], userId: params["userId"], orderBy: params["orderBy"], order: params["order"], markedAsDuplicate: params["markedAsDuplicate"], - includeHeaders: params["includeHeaders"], + listingId: params["listingId"], includeDemographics: params["includeDemographics"], } let data = null @@ -500,13 +523,13 @@ export class ApplicationsService { retrieve( params: { /** */ - applicationId: string + id: string } = {} as any, options: IRequestOptions = {} ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/applications/{applicationId}" - url = url.replace("{applicationId}", params["applicationId"] + "") + let url = basePath + "/applications/{id}" + url = url.replace("{id}", params["id"] + "") const configs: IRequestConfig = getConfigs("get", "application/json", url, options) @@ -522,15 +545,15 @@ export class ApplicationsService { update( params: { /** */ - applicationId: string + id: string /** requestBody */ body?: ApplicationUpdate } = {} as any, options: IRequestOptions = {} ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/applications/{applicationId}" - url = url.replace("{applicationId}", params["applicationId"] + "") + let url = basePath + "/applications/{id}" + url = url.replace("{id}", params["id"] + "") const configs: IRequestConfig = getConfigs("put", "application/json", url, options) @@ -540,28 +563,6 @@ export class ApplicationsService { axios(configs, resolve, reject) }) } - /** - * Delete application by id - */ - delete( - params: { - /** */ - applicationId: string - } = {} as any, - options: IRequestOptions = {} - ): Promise { - return new Promise((resolve, reject) => { - let url = basePath + "/applications/{applicationId}" - url = url.replace("{applicationId}", params["applicationId"] + "") - - const configs: IRequestConfig = getConfigs("delete", "application/json", url, options) - - let data = null - - configs.data = data - axios(configs, resolve, reject) - }) - } /** * Submit application */ @@ -700,13 +701,61 @@ export class AuthService { /** * Token */ - token(options: IRequestOptions = {}): Promise { + token( + params: { + /** requestBody */ + body?: Token + } = {} as any, + options: IRequestOptions = {} + ): Promise { return new Promise((resolve, reject) => { let url = basePath + "/auth/token" const configs: IRequestConfig = getConfigs("post", "application/json", url, options) - let data = null + let data = params.body + + configs.data = data + axios(configs, resolve, reject) + }) + } + /** + * Request mfa code + */ + requestMfaCode( + params: { + /** requestBody */ + body?: RequestMfaCode + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/auth/request-mfa-code" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + + let data = params.body + + configs.data = data + axios(configs, resolve, reject) + }) + } + /** + * Get mfa info + */ + getMfaInfo( + params: { + /** requestBody */ + body?: GetMfaInfo + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/auth/mfa-info" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + + let data = params.body configs.data = data axios(configs, resolve, reject) @@ -753,6 +802,69 @@ export class UserService { axios(configs, resolve, reject) }) } + /** + * Delete user by id + */ + delete( + params: { + /** requestBody */ + body?: Id + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user" + + const configs: IRequestConfig = getConfigs("delete", "application/json", url, options) + + let data = params.body + + configs.data = data + axios(configs, resolve, reject) + }) + } + /** + * Resend partner confirmation + */ + resendPartnerConfirmation( + params: { + /** requestBody */ + body?: Email + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/resend-partner-confirmation" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + + let data = params.body + + configs.data = data + axios(configs, resolve, reject) + }) + } + /** + * Verifies token is valid + */ + isUserConfirmationTokenValid( + params: { + /** requestBody */ + body?: Confirm + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/is-confirmation-token-valid" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + + let data = params.body + + configs.data = data + axios(configs, resolve, reject) + }) + } /** * Resend confirmation */ @@ -858,6 +970,28 @@ export class UserService { axios(configs, resolve, reject) }) } + /** + * Get user by id + */ + retrieve( + params: { + /** */ + id: string + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/{id}" + url = url.replace("{id}", params["id"] + "") + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + + let data = null + + configs.data = data + axios(configs, resolve, reject) + }) + } /** * List users */ @@ -869,6 +1003,8 @@ export class UserService { limit?: number | "all" /** */ filter?: UserFilterParams[] + /** */ + search?: string } = {} as any, options: IRequestOptions = {} ): Promise { @@ -876,7 +1012,33 @@ export class UserService { let url = basePath + "/user/list" const configs: IRequestConfig = getConfigs("get", "application/json", url, options) - configs.params = { page: params["page"], limit: params["limit"], filter: params["filter"] } + configs.params = { + page: params["page"], + limit: params["limit"], + filter: params["filter"], + search: params["search"], + } + let data = null + + configs.data = data + axios(configs, resolve, reject) + }) + } + /** + * List users in CSV + */ + listAsCsv( + params: { + /** */ + timeZone?: string + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/user/csv" + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + configs.params = { timeZone: params["timeZone"] } let data = null configs.data = data @@ -930,6 +1092,33 @@ export class UserProfileService { } } +export class UserPreferencesService { + /** + * Update user preferences + */ + update( + params: { + /** */ + id: string + /** requestBody */ + body?: UserPreferences + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/userPreferences/{id}" + url = url.replace("{id}", params["id"] + "") + + const configs: IRequestConfig = getConfigs("put", "application/json", url, options) + + let data = params.body + + configs.data = data + axios(configs, resolve, reject) + }) + } +} + export class JurisdictionsService { /** * List jurisdictions @@ -1057,6 +1246,21 @@ export class JurisdictionsService { } export class ListingsService { + /** + * Returns Listing Metadata + */ + metadata(options: IRequestOptions = {}): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/listings/meta" + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + + let data = null + + configs.data = data + axios(configs, resolve, reject) + }) + } /** * List listings */ @@ -1073,7 +1277,9 @@ export class ListingsService { /** */ orderBy?: OrderByFieldsEnum /** */ - jsonpath?: string + orderDir?: OrderDirEnum + /** */ + search?: string } = {} as any, options: IRequestOptions = {} ): Promise { @@ -1087,7 +1293,8 @@ export class ListingsService { filter: params["filter"], view: params["view"], orderBy: params["orderBy"], - jsonpath: params["jsonpath"], + orderDir: params["orderDir"], + search: params["search"], } let data = null @@ -1116,21 +1323,42 @@ export class ListingsService { axios(configs, resolve, reject) }) } + /** + * Retrieve listings and units in csv + */ + listAsCsv( + params: { + /** */ + timeZone?: string + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/listings/csv" + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + configs.params = { timeZone: params["timeZone"] } + let data = null + + configs.data = data + axios(configs, resolve, reject) + }) + } /** * Get listing by id */ retrieve( params: { /** */ - listingId: string + id: string /** */ view?: string } = {} as any, options: IRequestOptions = {} ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/listings/{listingId}" - url = url.replace("{listingId}", params["listingId"] + "") + let url = basePath + "/listings/{id}" + url = url.replace("{id}", params["id"] + "") const configs: IRequestConfig = getConfigs("get", "application/json", url, options) configs.params = { view: params["view"] } @@ -1146,15 +1374,15 @@ export class ListingsService { update( params: { /** */ - listingId: string + id: string /** requestBody */ body?: ListingUpdate } = {} as any, options: IRequestOptions = {} ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/listings/{listingId}" - url = url.replace("{listingId}", params["listingId"] + "") + let url = basePath + "/listings/{id}" + url = url.replace("{id}", params["id"] + "") const configs: IRequestConfig = getConfigs("put", "application/json", url, options) @@ -1170,13 +1398,13 @@ export class ListingsService { delete( params: { /** */ - listingId: string + id: string } = {} as any, options: IRequestOptions = {} ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/listings/{listingId}" - url = url.replace("{listingId}", params["listingId"] + "") + let url = basePath + "/listings/{id}" + url = url.replace("{id}", params["id"] + "") const configs: IRequestConfig = getConfigs("delete", "application/json", url, options) @@ -1296,12 +1524,18 @@ export class PreferencesService { /** * List preferences */ - list(options: IRequestOptions = {}): Promise { + list( + params: { + /** */ + filter?: PreferencesFilterParams[] + } = {} as any, + options: IRequestOptions = {} + ): Promise { return new Promise((resolve, reject) => { let url = basePath + "/preferences" const configs: IRequestConfig = getConfigs("get", "application/json", url, options) - + configs.params = { filter: params["filter"] } let data = null configs.data = data @@ -1396,16 +1630,126 @@ export class PreferencesService { } } -export class PropertiesService { +export class ProgramsService { /** - * List properties + * List programs */ - list(options: IRequestOptions = {}): Promise { + list( + params: { + /** */ + filter?: ProgramsFilterParams[] + } = {} as any, + options: IRequestOptions = {} + ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/properties" + let url = basePath + "/programs" const configs: IRequestConfig = getConfigs("get", "application/json", url, options) - + configs.params = { filter: params["filter"] } + let data = null + + configs.data = data + axios(configs, resolve, reject) + }) + } + /** + * Create program + */ + create( + params: { + /** requestBody */ + body?: ProgramCreate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/programs" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + + let data = params.body + + configs.data = data + axios(configs, resolve, reject) + }) + } + /** + * Update program + */ + update( + params: { + /** requestBody */ + body?: ProgramUpdate + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/programs/{programId}" + + const configs: IRequestConfig = getConfigs("put", "application/json", url, options) + + let data = params.body + + configs.data = data + axios(configs, resolve, reject) + }) + } + /** + * Get program by id + */ + retrieve( + params: { + /** */ + programId: string + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/programs/{programId}" + url = url.replace("{programId}", params["programId"] + "") + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + + let data = null + + configs.data = data + axios(configs, resolve, reject) + }) + } + /** + * Delete program by id + */ + delete( + params: { + /** */ + programId: string + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/programs/{programId}" + url = url.replace("{programId}", params["programId"] + "") + + const configs: IRequestConfig = getConfigs("delete", "application/json", url, options) + + let data = null + + configs.data = data + axios(configs, resolve, reject) + }) + } +} + +export class PropertiesService { + /** + * List properties + */ + list(options: IRequestOptions = {}): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/properties" + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + let data = null configs.data = data @@ -1714,6 +2058,30 @@ export class ReservedCommunityTypesService { } } +export class SmsService { + /** + * Send an SMS + */ + sendSms( + params: { + /** requestBody */ + body?: Sms + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/sms" + + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + + let data = params.body + + configs.data = data + axios(configs, resolve, reject) + }) + } +} + export class TranslationsService { /** * List translations @@ -2251,30 +2619,12 @@ export interface AmiChartItem { income: number } -export interface Jurisdiction { +export interface Id { /** */ id: string - - /** */ - createdAt: Date - - /** */ - updatedAt: Date - - /** */ - name: string - - /** */ - notificationsSignUpURL?: string } export interface AmiChart { - /** */ - items: AmiChartItem[] - - /** */ - jurisdiction: Jurisdiction - /** */ id: string @@ -2284,13 +2634,14 @@ export interface AmiChart { /** */ updatedAt: Date + /** */ + items: AmiChartItem[] + /** */ name: string -} -export interface Id { /** */ - id: string + jurisdiction: Id } export interface AmiChartCreate { @@ -2298,30 +2649,30 @@ export interface AmiChartCreate { items: AmiChartItem[] /** */ - jurisdiction: Id + name: string /** */ - name: string + jurisdiction: Id } export interface AmiChartUpdate { /** */ - id?: string + items: AmiChartItem[] /** */ - createdAt?: Date + name: string /** */ - updatedAt?: Date + jurisdiction: Id /** */ - items: AmiChartItem[] + id?: string /** */ - jurisdiction: Id + createdAt?: Date /** */ - name: string + updatedAt?: Date } export interface Address { @@ -2493,7 +2844,7 @@ export interface Demographics { howDidYouHear: string[] /** */ - race?: string + race?: string[] } export interface HouseholdMember { @@ -2597,6 +2948,28 @@ export interface ApplicationPreference { options: ApplicationPreferenceOption[] } +export interface ApplicationProgramOption { + /** */ + key: string + + /** */ + checked: boolean + + /** */ + extraData?: AllExtraDataTypes[] +} + +export interface ApplicationProgram { + /** */ + key: string + + /** */ + claimed: boolean + + /** */ + options: ApplicationProgramOption[] +} + export interface Application { /** */ incomePeriod?: IncomePeriod @@ -2676,6 +3049,12 @@ export interface Application { /** */ sendMailToMailingAddress?: boolean + /** */ + householdExpectingChanges?: boolean + + /** */ + householdStudent?: boolean + /** */ incomeVouchers?: boolean @@ -2685,6 +3064,9 @@ export interface Application { /** */ preferences: ApplicationPreference[] + /** */ + programs?: ApplicationProgram[] + /** */ acceptedTerms?: boolean @@ -3127,7 +3509,7 @@ export interface DemographicsCreate { howDidYouHear: string[] /** */ - race?: string + race?: string[] } export interface HouseholdMemberCreate { @@ -3247,6 +3629,12 @@ export interface ApplicationCreate { /** */ sendMailToMailingAddress?: boolean + /** */ + householdExpectingChanges?: boolean + + /** */ + householdStudent?: boolean + /** */ incomeVouchers?: boolean @@ -3256,6 +3644,9 @@ export interface ApplicationCreate { /** */ preferences: ApplicationPreference[] + /** */ + programs?: ApplicationProgram[] + /** */ acceptedTerms?: boolean @@ -3432,7 +3823,7 @@ export interface DemographicsUpdate { howDidYouHear: string[] /** */ - race?: string + race?: string[] } export interface HouseholdMemberUpdate { @@ -3573,6 +3964,12 @@ export interface ApplicationUpdate { /** */ sendMailToMailingAddress?: boolean + /** */ + householdExpectingChanges?: boolean + + /** */ + householdStudent?: boolean + /** */ incomeVouchers?: boolean @@ -3582,6 +3979,9 @@ export interface ApplicationUpdate { /** */ preferences: ApplicationPreference[] + /** */ + programs?: ApplicationProgram[] + /** */ acceptedTerms?: boolean @@ -3607,12 +4007,23 @@ export interface PaginatedAssets { meta: PaginationMeta } +export interface UserErrorExtraModel { + /** */ + userErrorMessages: EnumUserErrorExtraModelUserErrorMessages +} + export interface Login { /** */ email: string /** */ password: string + + /** */ + mfaCode?: string + + /** */ + mfaType?: EnumLoginMfaType } export interface LoginResponse { @@ -3620,102 +4031,239 @@ export interface LoginResponse { accessToken: string } -export interface IdName { - /** */ - id: string +export interface Token {} +export interface RequestMfaCode { /** */ - name: string -} + email: string -export interface UserRoles { /** */ - user: Id + password: string /** */ - isAdmin?: boolean + mfaType: EnumRequestMfaCodeMfaType /** */ - isPartner?: boolean + phoneNumber?: string } -export interface User { +export interface RequestMfaCodeResponse { /** */ - language?: Language + phoneNumber?: string /** */ - leasingAgentInListings?: IdName[] + email?: string /** */ - roles?: CombinedRolesTypes + phoneNumberVerified?: boolean +} +export interface GetMfaInfo { /** */ - jurisdictions: Jurisdiction[] + email: string /** */ - id: string + password: string +} +export interface GetMfaInfoResponse { /** */ - confirmedAt?: Date + phoneNumber?: string /** */ - email: string + email?: string /** */ - firstName: string + isMfaEnabled: boolean /** */ - middleName?: string + mfaUsedInThePast: boolean +} +export interface IdName { /** */ - lastName: string + id: string /** */ - dob?: Date + name: string +} +export interface UserRoles { /** */ - createdAt: Date + user: Id /** */ - updatedAt: Date -} + isAdmin?: boolean -export interface UserCreate { /** */ - language?: Language + isPartner?: boolean +} +export interface Jurisdiction { /** */ - password: string + programs: Id[] /** */ - passwordConfirmation: string + preferences: Id[] /** */ - emailConfirmation: string + id: string /** */ - appUrl?: string + createdAt: Date /** */ - jurisdictions?: Id[] + updatedAt: Date /** */ - confirmedAt?: Date + name: string /** */ - email: string + notificationsSignUpURL?: string /** */ - firstName: string + languages: EnumJurisdictionLanguages[] /** */ - middleName?: string + partnerTerms?: string /** */ - lastName: string + publicUrl: string + + /** */ + emailFromAddress: string +} + +export interface UserPreferences { + /** */ + sendEmailNotifications?: boolean + + /** */ + sendSmsNotifications?: boolean + + /** */ + favoriteIds?: string[] +} + +export interface User { + /** */ + language?: Language + + /** */ + leasingAgentInListings?: IdName[] + + /** */ + roles?: CombinedRolesTypes + + /** */ + jurisdictions: Jurisdiction[] + + /** */ + preferences?: CombinedPreferencesTypes + + /** */ + id: string + + /** */ + passwordUpdatedAt: Date + + /** */ + passwordValidForDays: number + + /** */ + confirmedAt?: Date + + /** */ + email: string + + /** */ + firstName: string + + /** */ + middleName?: string + + /** */ + lastName: string /** */ dob?: Date + + /** */ + phoneNumber?: string + + /** */ + createdAt: Date + + /** */ + updatedAt: Date + + /** */ + mfaEnabled?: boolean + + /** */ + lastLoginAt?: Date + + /** */ + failedLoginAttemptsCount?: number + + /** */ + phoneNumberVerified?: boolean + + /** */ + agreedToTermsOfService: boolean + + /** */ + hitConfirmationURL?: Date +} + +export interface UserCreate { + /** */ + language?: Language + + /** */ + password: string + + /** */ + passwordConfirmation: string + + /** */ + emailConfirmation: string + + /** */ + appUrl?: string + + /** */ + jurisdictions?: Id[] + + /** */ + email: string + + /** */ + confirmedAt?: Date + + /** */ + firstName: string + + /** */ + middleName?: string + + /** */ + lastName: string + + /** */ + dob?: Date + + /** */ + phoneNumber?: string + + /** */ + phoneNumberVerified?: boolean + + /** */ + hitConfirmationURL?: Date + + /** */ + preferences?: CombinedPreferencesTypes } export interface UserBasic { @@ -3731,9 +4279,18 @@ export interface UserBasic { /** */ leasingAgentInListings?: Id[] + /** */ + preferences?: CombinedPreferencesTypes + /** */ id: string + /** */ + passwordUpdatedAt: Date + + /** */ + passwordValidForDays: number + /** */ confirmedAt?: Date @@ -3752,11 +4309,32 @@ export interface UserBasic { /** */ dob?: Date + /** */ + phoneNumber?: string + /** */ createdAt: Date /** */ updatedAt: Date + + /** */ + mfaEnabled?: boolean + + /** */ + lastLoginAt?: Date + + /** */ + failedLoginAttemptsCount?: number + + /** */ + phoneNumberVerified?: boolean + + /** */ + agreedToTermsOfService: boolean + + /** */ + hitConfirmationURL?: Date } export interface Email { @@ -3804,6 +4382,14 @@ export interface UpdatePassword { token: string } +export interface UserRolesUpdate { + /** */ + isAdmin?: boolean + + /** */ + isPartner?: boolean +} + export interface UserUpdate { /** */ language?: Language @@ -3826,9 +4412,21 @@ export interface UserUpdate { /** */ currentPassword?: string + /** */ + roles?: CombinedRolesTypes + /** */ jurisdictions: Id[] + /** */ + leasingAgentInListings?: Id[] + + /** */ + newEmail?: string + + /** */ + appUrl?: string + /** */ confirmedAt?: Date @@ -3843,6 +4441,21 @@ export interface UserUpdate { /** */ dob?: Date + + /** */ + phoneNumber?: string + + /** */ + phoneNumberVerified?: boolean + + /** */ + agreedToTermsOfService: boolean + + /** */ + hitConfirmationURL?: Date + + /** */ + preferences?: CombinedPreferencesTypes } export interface UserFilterParams { @@ -3854,6 +4467,9 @@ export interface UserFilterParams { /** */ isPartner?: boolean + + /** */ + isPortalUser?: boolean } export interface PaginatedUserList { @@ -3902,6 +4518,18 @@ export interface UserInvite { /** */ dob?: Date + + /** */ + phoneNumber?: string + + /** */ + phoneNumberVerified?: boolean + + /** */ + hitConfirmationURL?: Date + + /** */ + preferences?: CombinedPreferencesTypes } export interface UserProfileUpdate { @@ -3917,6 +4545,15 @@ export interface UserProfileUpdate { /** */ jurisdictions: Id[] + /** */ + newEmail?: string + + /** */ + appUrl?: string + + /** */ + preferences?: UserPreferences + /** */ id: string @@ -3937,6 +4574,12 @@ export interface UserProfileUpdate { /** */ updatedAt: Date + + /** */ + phoneNumber?: string + + /** */ + agreedToTermsOfService: boolean } export interface JurisdictionCreate { @@ -3945,6 +4588,24 @@ export interface JurisdictionCreate { /** */ notificationsSignUpURL?: string + + /** */ + languages: EnumJurisdictionCreateLanguages[] + + /** */ + partnerTerms?: string + + /** */ + publicUrl: string + + /** */ + emailFromAddress: string + + /** */ + programs: Id[] + + /** */ + preferences: Id[] } export interface JurisdictionUpdate { @@ -3962,6 +4623,97 @@ export interface JurisdictionUpdate { /** */ notificationsSignUpURL?: string + + /** */ + languages: EnumJurisdictionUpdateLanguages[] + + /** */ + partnerTerms?: string + + /** */ + publicUrl: string + + /** */ + emailFromAddress: string + + /** */ + programs: Id[] + + /** */ + preferences: Id[] +} + +export interface FormMetadataExtraData { + /** */ + type: InputType + + /** */ + key: string +} + +export interface FormMetadataOptions { + /** */ + key: string + + /** */ + extraData?: FormMetadataExtraData[] + + /** */ + description: boolean + + /** */ + exclusive: boolean +} + +export interface FormMetadata { + /** */ + key: string + + /** */ + options: FormMetadataOptions[] + + /** */ + hideGenericDecline: boolean + + /** */ + customSelectText: string + + /** */ + hideFromListing: boolean + + /** */ + type: FormMetaDataType +} + +export interface Program { + /** */ + id: string + + /** */ + createdAt: Date + + /** */ + updatedAt: Date + + /** */ + title?: string + + /** */ + subtitle?: string + + /** */ + description?: string + + /** */ + formMetadata?: FormMetadata +} + +export interface ListingMetadata { + /** */ + programs?: Program[] + + /** */ + unitTypes?: UnitType[] } export interface ListingFilterParams { @@ -3972,16 +4724,16 @@ export interface ListingFilterParams { $include_nulls?: boolean /** */ - name?: string + id?: string /** */ - status?: EnumListingFilterParamsStatus + name?: string /** */ - neighborhood?: string + status?: EnumListingFilterParamsStatus /** */ - bedrooms?: number + bedRoomSize?: string /** */ zipcode?: string @@ -3990,10 +4742,13 @@ export interface ListingFilterParams { leasingAgents?: string /** */ - availability?: EnumListingFilterParamsAvailability + availability?: string /** */ - seniorHousing?: boolean + program?: string + + /** */ + isVerified?: boolean /** */ minRent?: number @@ -4003,20 +4758,74 @@ export interface ListingFilterParams { /** */ minAmiPercentage?: number -} -export interface UnitAccessibilityPriorityType { /** */ - name: string + elevator?: boolean /** */ - id: string + wheelchairRamp?: boolean /** */ - createdAt: Date + serviceAnimalsAllowed?: boolean /** */ - updatedAt: Date + accessibleParking?: boolean + + /** */ + parkingOnSite?: boolean + + /** */ + inUnitWasherDryer?: boolean + + /** */ + laundryInBuilding?: boolean + + /** */ + barrierFreeEntrance?: boolean + + /** */ + rollInShower?: boolean + + /** */ + grabBars?: boolean + + /** */ + heatingInUnit?: boolean + + /** */ + acInUnit?: boolean + + /** */ + jurisdiction?: string + + /** */ + marketingType?: EnumListingFilterParamsMarketingType + + /** */ + favorited?: string + + /** */ + communityPrograms?: string + + /** */ + accessibility?: string + + /** */ + region?: string + + /** */ + section8Acceptance?: boolean + + /** */ + homeType?: string +} + +export interface MinMax { + /** */ + min: number + + /** */ + max: number } export interface MinMaxCurrency { @@ -4027,124 +4836,175 @@ export interface MinMaxCurrency { max: string } -export interface MinMax { +export interface UnitGroupSummary { + /** */ + unitTypes?: string[] + + /** */ + rentAsPercentIncomeRange?: MinMax + + /** */ + rentRange?: MinMaxCurrency + + /** */ + amiPercentageRange: MinMax + + /** */ + openWaitlist: boolean + + /** */ + unitVacancies: number + + /** */ + floorRange?: MinMax + + /** */ + sqFeetRange?: MinMax + + /** */ + bathroomRange?: MinMax +} + +export interface HMIColumns { + /** */ + "20"?: number + + /** */ + "25"?: number + + /** */ + "30"?: number + + /** */ + "35"?: number + + /** */ + "40"?: number + + /** */ + "45"?: number + + /** */ + "50"?: number + /** */ - min: number + "55"?: number /** */ - max: number -} + "60"?: number -export interface UnitSummary { /** */ - unitType: UnitType + "70"?: number /** */ - minIncomeRange: MinMaxCurrency + "80"?: number /** */ - occupancyRange: MinMax + "100"?: number /** */ - rentAsPercentIncomeRange: MinMax + "120"?: number /** */ - rentRange: MinMaxCurrency + "125"?: number /** */ - totalAvailable: number + "140"?: number /** */ - areaRange: MinMax + "150"?: number /** */ - floorRange?: MinMax + householdSize: string } -export interface UnitSummaryByAMI { +export interface HouseholdMaxIncomeSummary { /** */ - percent: string + columns: HMIColumns /** */ - byUnitType: UnitSummary[] + rows: HMIColumns[] } -export interface HMI { +export interface UnitSummaries { /** */ - columns: object + unitGroupSummary: UnitGroupSummary[] /** */ - rows: object[] + householdMaxIncomeSummary: HouseholdMaxIncomeSummary } -export interface UnitsSummarized { +export interface Asset { /** */ - unitTypes: UnitType[] + fileId: string /** */ - priorityTypes: UnitAccessibilityPriorityType[] + label: string /** */ - amiPercentages: string[] + id: string /** */ - byUnitTypeAndRent: UnitSummary[] + createdAt: Date /** */ - byUnitType: UnitSummary[] + updatedAt: Date +} +export interface ListingEvent { /** */ - byAMI: UnitSummaryByAMI[] + type: ListingEventType /** */ - hmi: HMI -} + id: string -export interface PreferenceLink { /** */ - title: string + createdAt: Date /** */ - url: string -} + updatedAt: Date -export interface FormMetadataExtraData { /** */ - type: InputType + startTime?: Date /** */ - key: string -} + endTime?: Date -export interface FormMetadataOptions { /** */ - key: string + url?: string /** */ - extraData?: FormMetadataExtraData[] + note?: string /** */ - description: boolean + label?: string /** */ - exclusive: boolean + file?: Asset } -export interface FormMetadata { +export interface ListingImage { /** */ - key: string + image: AssetUpdate /** */ - options: FormMetadataOptions[] + ordinal?: number +} +export interface ListingProgram { /** */ - hideGenericDecline: boolean + program: Program /** */ - customSelectText: string + ordinal?: number +} +export interface PreferenceLink { /** */ - hideFromListing: boolean + title: string + + /** */ + url: string } export interface Preference { @@ -4160,9 +5020,6 @@ export interface Preference { /** */ updatedAt: Date - /** */ - ordinal?: number - /** */ title?: string @@ -4174,58 +5031,45 @@ export interface Preference { /** */ formMetadata?: FormMetadata - - /** */ - page?: number } -export interface Asset { - /** */ - fileId: string - - /** */ - label: string - +export interface ListingPreference { /** */ - id: string - - /** */ - createdAt: Date + preference: Preference /** */ - updatedAt: Date + ordinal?: number } -export interface ListingEvent { - /** */ - type: ListingEventType - +export interface ListingNeighborhoodAmenities { /** */ - id: string + groceryStores?: string /** */ - createdAt: Date + publicTransportation?: string /** */ - updatedAt: Date + schools?: string /** */ - startTime?: Date + parksAndCommunityCenters?: string /** */ - endTime?: Date + pharmacies?: string /** */ - url?: string + healthCareResources?: string +} +export interface JurisdictionSlim { /** */ - note?: string + id: string /** */ - label?: string + name: string /** */ - file?: Asset + publicUrl: string } export interface ReservedCommunityType { @@ -4361,33 +5205,55 @@ export interface Unit { bmrProgramChart?: boolean } -export interface UnitsSummary { +export interface UnitGroupAmiLevel { /** */ - listing: Id + monthlyRentDeterminationType: MonthlyRentDeterminationType /** */ - unitType: Id + amiChart?: Id /** */ id: string /** */ - monthlyRentMin?: number + amiChartId?: string /** */ - monthlyRentMax?: number + amiPercentage: number /** */ - monthlyRentAsPercentOfIncome?: string + flatRentValue?: number + + /** */ + percentageOfIncomeValue?: number +} + +export interface UnitAccessibilityPriorityType { + /** */ + name: string /** */ - amiPercentage?: number + id: string + + /** */ + createdAt: Date + + /** */ + updatedAt: Date +} +export interface UnitGroup { /** */ - minimumIncomeMin?: string + unitType: UnitType[] /** */ - minimumIncomeMax?: string + amiLevels: UnitGroupAmiLevel[] + + /** */ + id: string + + /** */ + listingId: string /** */ maxOccupancy?: number @@ -4402,10 +5268,10 @@ export interface UnitsSummary { floorMax?: number /** */ - sqFeetMin?: string + sqFeetMin?: number /** */ - sqFeetMax?: string + sqFeetMax?: number /** */ priorityType?: CombinedPriorityTypeTypes @@ -4415,6 +5281,103 @@ export interface UnitsSummary { /** */ totalAvailable?: number + + /** */ + bathroomMin?: number + + /** */ + bathroomMax?: number + + /** */ + openWaitlist: boolean +} + +export interface ListingFeatures { + /** */ + elevator?: boolean + + /** */ + wheelchairRamp?: boolean + + /** */ + serviceAnimalsAllowed?: boolean + + /** */ + accessibleParking?: boolean + + /** */ + parkingOnSite?: boolean + + /** */ + inUnitWasherDryer?: boolean + + /** */ + laundryInBuilding?: boolean + + /** */ + barrierFreeEntrance?: boolean + + /** */ + rollInShower?: boolean + + /** */ + grabBars?: boolean + + /** */ + heatingInUnit?: boolean + + /** */ + acInUnit?: boolean + + /** */ + hearing?: boolean + + /** */ + visual?: boolean + + /** */ + mobility?: boolean + + /** */ + barrierFreeUnitEntrance?: boolean + + /** */ + loweredLightSwitch?: boolean + + /** */ + barrierFreeBathroom?: boolean + + /** */ + wideDoorways?: boolean + + /** */ + loweredCabinets?: boolean +} + +export interface ListingUtilities { + /** */ + water?: boolean + + /** */ + gas?: boolean + + /** */ + trash?: boolean + + /** */ + sewer?: boolean + + /** */ + electricity?: boolean + + /** */ + cable?: boolean + + /** */ + phone?: boolean + + /** */ + internet?: boolean } export interface Listing { @@ -4427,6 +5390,9 @@ export interface Listing { /** */ applicationDropOffAddressType?: ListingApplicationAddressType + /** */ + applicationMailingAddressType?: ListingApplicationAddressType + /** */ status: ListingStatus @@ -4434,22 +5400,25 @@ export interface Listing { reviewOrderType?: ListingReviewOrder /** */ - CSVFormattingType: CSVFormattingType + showWaitlist: boolean /** */ - showWaitlist: boolean + unitSummaries: UnitSummaries /** */ - unitsSummarized: UnitsSummarized + marketingType: ListingMarketingTypeEnum /** */ - applicationMethods: ApplicationMethod[] + marketingSeason?: ListingSeasonEnum + + /** */ + homeType?: HomeTypeEnum /** */ - preferences: Preference[] + region?: Region /** */ - applicationAddress?: CombinedApplicationAddressTypes + applicationMethods: ApplicationMethod[] /** */ applicationPickUpAddress?: CombinedApplicationPickUpAddressTypes @@ -4467,7 +5436,7 @@ export interface Listing { events: ListingEvent[] /** */ - image?: CombinedImageTypes + images?: ListingImage[] /** */ leasingAgentAddress?: CombinedLeasingAgentAddressTypes @@ -4476,7 +5445,16 @@ export interface Listing { leasingAgents?: UserBasic[] /** */ - jurisdiction: IdName + listingPrograms?: ListingProgram[] + + /** */ + listingPreferences: ListingPreference[] + + /** */ + neighborhoodAmenities?: CombinedNeighborhoodAmenitiesTypes + + /** */ + jurisdiction: JurisdictionSlim /** */ reservedCommunityType?: ReservedCommunityType @@ -4533,11 +5511,17 @@ export interface Listing { urlSlug: string /** */ - unitsSummary?: UnitsSummary[] + unitGroups?: UnitGroup[] /** */ countyCode?: string + /** */ + features?: ListingFeatures + + /** */ + utilities?: ListingUtilities + /** */ id: string @@ -4547,6 +5531,9 @@ export interface Listing { /** */ updatedAt: Date + /** */ + hrdId?: string + /** */ additionalApplicationSubmissionNotes?: string @@ -4563,13 +5550,13 @@ export interface Listing { referralOpportunity?: boolean /** */ - assets: AssetCreate[] + section8Acceptance?: boolean /** */ - applicationDueDate?: Date + assets: AssetCreate[] /** */ - applicationDueTime?: Date + applicationDueDate?: Date /** */ applicationOpenDate?: Date @@ -4604,6 +5591,9 @@ export interface Listing { /** */ depositMax?: string + /** */ + depositHelperText?: string + /** */ disableUnitsAccordion?: boolean @@ -4652,6 +5642,9 @@ export interface Listing { /** */ whatToExpect?: string + /** */ + whatToExpectAdditionalText?: string + /** */ applicationConfig?: object @@ -4673,39 +5666,52 @@ export interface Listing { /** */ waitlistOpenSpots?: number + /** */ + ownerCompany?: string + + /** */ + managementCompany?: string + + /** */ + managementWebsite?: string + + /** */ + amiPercentageMin?: number + + /** */ + amiPercentageMax?: number + /** */ customMapPin?: boolean -} -export interface PaginatedListing { /** */ - items: Listing[] + phoneNumber?: string /** */ - meta: PaginationMeta -} + publishedAt?: Date -export interface PreferenceCreate { /** */ - links?: PreferenceLink[] + closedAt?: Date /** */ - ordinal?: number + isVerified?: boolean /** */ - title?: string + verifiedAt?: Date /** */ - subtitle?: string + temporaryListingId?: number /** */ - description?: string + marketingDate?: Date +} +export interface PaginatedListing { /** */ - formMetadata?: FormMetadata + items: Listing[] /** */ - page?: number + meta: PaginationMeta } export interface ListingEventCreate { @@ -4731,6 +5737,14 @@ export interface ListingEventCreate { label?: string } +export interface ListingImageUpdate { + /** */ + image: AssetUpdate + + /** */ + ordinal?: number +} + export interface UnitAmiChartOverrideCreate { /** */ items: AmiChartItem[] @@ -4798,24 +5812,32 @@ export interface UnitCreate { bmrProgramChart?: boolean } -export interface UnitsSummaryCreate { +export interface UnitGroupAmiLevelCreate { /** */ - monthlyRentMin?: number + monthlyRentDeterminationType: MonthlyRentDeterminationType /** */ - monthlyRentMax?: number + amiChart?: Id /** */ - monthlyRentAsPercentOfIncome?: string + amiChartId?: string + + /** */ + amiPercentage: number + + /** */ + flatRentValue?: number /** */ - amiPercentage?: number + percentageOfIncomeValue?: number +} +export interface UnitGroupCreate { /** */ - minimumIncomeMin?: string + unitType: Id[] /** */ - minimumIncomeMax?: string + amiLevels: UnitGroupAmiLevelCreate[] /** */ maxOccupancy?: number @@ -4827,28 +5849,47 @@ export interface UnitsSummaryCreate { floorMin?: number /** */ - floorMax?: number + floorMax?: number + + /** */ + sqFeetMin?: number + + /** */ + sqFeetMax?: number + + /** */ + priorityType?: CombinedPriorityTypeTypes + + /** */ + totalCount?: number + + /** */ + totalAvailable?: number /** */ - sqFeetMin?: string + bathroomMin?: number /** */ - sqFeetMax?: string + bathroomMax?: number /** */ - priorityType?: CombinedPriorityTypeTypes + openWaitlist: boolean +} +export interface ListingPreferenceUpdate { /** */ - totalCount?: number + preference: Id /** */ - totalAvailable?: number + ordinal?: number +} +export interface ListingProgramUpdate { /** */ - listing: Id + program: Id /** */ - unitType: Id + ordinal?: number } export interface ListingCreate { @@ -4858,6 +5899,9 @@ export interface ListingCreate { /** */ applicationDropOffAddressType?: ListingApplicationAddressType + /** */ + applicationMailingAddressType?: ListingApplicationAddressType + /** */ status: ListingStatus @@ -4865,16 +5909,16 @@ export interface ListingCreate { reviewOrderType?: ListingReviewOrder /** */ - CSVFormattingType: CSVFormattingType + marketingType: ListingMarketingTypeEnum /** */ - applicationMethods: ApplicationMethodCreate[] + marketingSeason?: ListingSeasonEnum /** */ - preferences: PreferenceCreate[] + homeType?: HomeTypeEnum /** */ - applicationAddress?: CombinedApplicationAddressTypes + applicationMethods: ApplicationMethodCreate[] /** */ applicationPickUpAddress?: CombinedApplicationPickUpAddressTypes @@ -4892,7 +5936,7 @@ export interface ListingCreate { events: ListingEventCreate[] /** */ - image?: CombinedImageTypes + images?: ListingImageUpdate[] /** */ leasingAgentAddress?: CombinedLeasingAgentAddressTypes @@ -4927,6 +5971,9 @@ export interface ListingCreate { /** */ neighborhood?: string + /** */ + region?: string + /** */ petPolicy?: string @@ -4955,7 +6002,16 @@ export interface ListingCreate { result?: CombinedResultTypes /** */ - unitsSummary?: UnitsSummaryCreate[] + unitGroups?: UnitGroupCreate[] + + /** */ + listingPreferences: ListingPreferenceUpdate[] + + /** */ + listingPrograms?: ListingProgramUpdate[] + + /** */ + hrdId?: string /** */ additionalApplicationSubmissionNotes?: string @@ -4973,13 +6029,13 @@ export interface ListingCreate { referralOpportunity?: boolean /** */ - assets: AssetCreate[] + section8Acceptance?: boolean /** */ - applicationDueDate?: Date + assets: AssetCreate[] /** */ - applicationDueTime?: Date + applicationDueDate?: Date /** */ applicationOpenDate?: Date @@ -5014,6 +6070,9 @@ export interface ListingCreate { /** */ depositMax?: string + /** */ + depositHelperText?: string + /** */ disableUnitsAccordion?: boolean @@ -5062,6 +6121,9 @@ export interface ListingCreate { /** */ whatToExpect?: string + /** */ + whatToExpectAdditionalText?: string + /** */ applicationConfig?: object @@ -5083,37 +6145,50 @@ export interface ListingCreate { /** */ waitlistOpenSpots?: number + /** */ + ownerCompany?: string + + /** */ + managementCompany?: string + + /** */ + managementWebsite?: string + + /** */ + amiPercentageMin?: number + + /** */ + amiPercentageMax?: number + /** */ customMapPin?: boolean /** */ - countyCode?: string -} + phoneNumber?: string -export interface PreferenceUpdate { /** */ - links?: PreferenceLink[] + isVerified?: boolean /** */ - ordinal?: number + verifiedAt?: Date /** */ - title?: string + temporaryListingId?: number /** */ - subtitle?: string + marketingDate?: Date /** */ - description?: string + neighborhoodAmenities?: CombinedNeighborhoodAmenitiesTypes /** */ - formMetadata?: FormMetadata + countyCode?: string /** */ - page?: number + features?: ListingFeatures /** */ - id: string + utilities?: ListingUtilities } export interface ListingEventUpdate { @@ -5233,27 +6308,35 @@ export interface UnitUpdate { bmrProgramChart?: boolean } -export interface UnitsSummaryUpdate { +export interface UnitGroupAmiLevelUpdate { /** */ - id: string + monthlyRentDeterminationType: MonthlyRentDeterminationType + + /** */ + id?: string /** */ - monthlyRentMin?: number + amiChart?: Id /** */ - monthlyRentMax?: number + amiChartId?: string /** */ - monthlyRentAsPercentOfIncome?: string + amiPercentage: number + + /** */ + flatRentValue?: number /** */ - amiPercentage?: number + percentageOfIncomeValue?: number +} +export interface UnitGroupUpdate { /** */ - minimumIncomeMin?: string + id?: string /** */ - minimumIncomeMax?: string + amiLevels: UnitGroupAmiLevelUpdate[] /** */ maxOccupancy?: number @@ -5268,10 +6351,10 @@ export interface UnitsSummaryUpdate { floorMax?: number /** */ - sqFeetMin?: string + sqFeetMin?: number /** */ - sqFeetMax?: string + sqFeetMax?: number /** */ priorityType?: CombinedPriorityTypeTypes @@ -5283,10 +6366,16 @@ export interface UnitsSummaryUpdate { totalAvailable?: number /** */ - listing: Id + bathroomMin?: number /** */ - unitType: Id + bathroomMax?: number + + /** */ + openWaitlist: boolean + + /** */ + unitType: Id[] } export interface ListingUpdate { @@ -5296,6 +6385,9 @@ export interface ListingUpdate { /** */ applicationDropOffAddressType?: ListingApplicationAddressType + /** */ + applicationMailingAddressType?: ListingApplicationAddressType + /** */ status: ListingStatus @@ -5303,25 +6395,25 @@ export interface ListingUpdate { reviewOrderType?: ListingReviewOrder /** */ - CSVFormattingType: CSVFormattingType + marketingType: ListingMarketingTypeEnum /** */ - id?: string + marketingSeason?: ListingSeasonEnum /** */ - createdAt?: Date + homeType?: HomeTypeEnum /** */ - updatedAt?: Date + id?: string /** */ - applicationMethods: ApplicationMethodUpdate[] + createdAt?: Date /** */ - preferences: PreferenceUpdate[] + updatedAt?: Date /** */ - applicationAddress?: CombinedApplicationAddressTypes + applicationMethods: ApplicationMethodUpdate[] /** */ applicationPickUpAddress?: CombinedApplicationPickUpAddressTypes @@ -5339,7 +6431,7 @@ export interface ListingUpdate { events: ListingEventUpdate[] /** */ - image?: AssetUpdate + images?: ListingImageUpdate[] /** */ leasingAgentAddress?: CombinedLeasingAgentAddressTypes @@ -5374,6 +6466,9 @@ export interface ListingUpdate { /** */ neighborhood?: string + /** */ + region?: string + /** */ petPolicy?: string @@ -5402,7 +6497,16 @@ export interface ListingUpdate { result?: AssetUpdate /** */ - unitsSummary?: UnitsSummaryUpdate[] + unitGroups?: UnitGroupUpdate[] + + /** */ + listingPreferences: ListingPreferenceUpdate[] + + /** */ + listingPrograms?: ListingProgramUpdate[] + + /** */ + hrdId?: string /** */ additionalApplicationSubmissionNotes?: string @@ -5420,13 +6524,13 @@ export interface ListingUpdate { referralOpportunity?: boolean /** */ - assets: AssetCreate[] + section8Acceptance?: boolean /** */ - applicationDueDate?: Date + assets: AssetCreate[] /** */ - applicationDueTime?: Date + applicationDueDate?: Date /** */ applicationOpenDate?: Date @@ -5461,6 +6565,9 @@ export interface ListingUpdate { /** */ depositMax?: string + /** */ + depositHelperText?: string + /** */ disableUnitsAccordion?: boolean @@ -5509,6 +6616,9 @@ export interface ListingUpdate { /** */ whatToExpect?: string + /** */ + whatToExpectAdditionalText?: string + /** */ applicationConfig?: object @@ -5530,14 +6640,146 @@ export interface ListingUpdate { /** */ waitlistOpenSpots?: number + /** */ + ownerCompany?: string + + /** */ + managementCompany?: string + + /** */ + managementWebsite?: string + + /** */ + amiPercentageMin?: number + + /** */ + amiPercentageMax?: number + /** */ customMapPin?: boolean + /** */ + phoneNumber?: string + + /** */ + isVerified?: boolean + + /** */ + verifiedAt?: Date + + /** */ + temporaryListingId?: number + + /** */ + marketingDate?: Date + + /** */ + neighborhoodAmenities?: CombinedNeighborhoodAmenitiesTypes + /** */ countyCode?: string + + /** */ + features?: ListingFeatures + + /** */ + utilities?: ListingUtilities +} + +export interface PreferencesFilterParams { + /** */ + $comparison: EnumPreferencesFilterParamsComparison + + /** */ + $include_nulls?: boolean + + /** */ + jurisdiction?: string +} + +export interface PreferenceCreate { + /** */ + links?: PreferenceLink[] + + /** */ + title?: string + + /** */ + subtitle?: string + + /** */ + description?: string + + /** */ + formMetadata?: FormMetadata +} + +export interface PreferenceUpdate { + /** */ + links?: PreferenceLink[] + + /** */ + title?: string + + /** */ + subtitle?: string + + /** */ + description?: string + + /** */ + formMetadata?: FormMetadata + + /** */ + id: string +} + +export interface ProgramsFilterParams { + /** */ + $comparison: EnumProgramsFilterParamsComparison + + /** */ + $include_nulls?: boolean + + /** */ + jurisdiction?: string +} + +export interface ProgramCreate { + /** */ + title?: string + + /** */ + subtitle?: string + + /** */ + description?: string + + /** */ + formMetadata?: FormMetadata +} + +export interface ProgramUpdate { + /** */ + title?: string + + /** */ + subtitle?: string + + /** */ + description?: string + + /** */ + formMetadata?: FormMetadata + + /** */ + id: string } export interface Property { + /** */ + region?: Region + /** */ units: Unit[] @@ -5594,6 +6836,9 @@ export interface Property { } export interface PropertyCreate { + /** */ + region?: Region + /** */ buildingAddress: AddressUpdate @@ -5641,6 +6886,9 @@ export interface PropertyCreate { } export interface PropertyUpdate { + /** */ + region?: Region + /** */ id?: string @@ -5757,6 +7005,14 @@ export interface ReservedCommunityTypeUpdate { id: string } +export interface Sms { + /** */ + body: string + + /** */ + phoneNumber: string +} + export interface Translation { /** */ language: Language @@ -5869,6 +7125,7 @@ export enum Language { "es" = "es", "vi" = "vi", "zh" = "zh", + "tl" = "tl", } export enum ApplicationSubmissionType { @@ -5906,7 +7163,33 @@ export enum EnumApplicationsApiExtraModelOrder { "ASC" = "ASC", "DESC" = "DESC", } +export enum EnumUserErrorExtraModelUserErrorMessages { + "accountConfirmed" = "accountConfirmed", + "accountNotConfirmed" = "accountNotConfirmed", + "errorSaving" = "errorSaving", + "emailNotFound" = "emailNotFound", + "tokenExpired" = "tokenExpired", + "tokenMissing" = "tokenMissing", + "emailInUse" = "emailInUse", + "passwordOutdated" = "passwordOutdated", +} +export enum EnumLoginMfaType { + "sms" = "sms", + "email" = "email", +} +export enum EnumRequestMfaCodeMfaType { + "sms" = "sms", + "email" = "email", +} +export enum EnumJurisdictionLanguages { + "en" = "en", + "es" = "es", + "vi" = "vi", + "zh" = "zh", + "tl" = "tl", +} export type CombinedRolesTypes = UserRolesCreate +export type CombinedPreferencesTypes = UserPreferences export enum EnumUserFilterParamsComparison { "=" = "=", "<>" = "<>", @@ -5915,6 +7198,24 @@ export enum EnumUserFilterParamsComparison { "<=" = "<=", "NA" = "NA", } +export enum EnumJurisdictionCreateLanguages { + "en" = "en", + "es" = "es", + "vi" = "vi", + "zh" = "zh", + "tl" = "tl", +} +export enum EnumJurisdictionUpdateLanguages { + "en" = "en", + "es" = "es", + "vi" = "vi", + "zh" = "zh", + "tl" = "tl", +} +export enum FormMetaDataType { + "radio" = "radio", + "checkbox" = "checkbox", +} export enum EnumListingFilterParamsComparison { "=" = "=", "<>" = "<>", @@ -5928,19 +7229,28 @@ export enum EnumListingFilterParamsStatus { "pending" = "pending", "closed" = "closed", } -export enum EnumListingFilterParamsAvailability { - "hasAvailability" = "hasAvailability", - "noAvailability" = "noAvailability", - "waitlist" = "waitlist", +export enum EnumListingFilterParamsMarketingType { + "Marketing" = "Marketing", + "ComingSoon" = "ComingSoon", } export enum OrderByFieldsEnum { "mostRecentlyUpdated" = "mostRecentlyUpdated", "applicationDates" = "applicationDates", + "mostRecentlyClosed" = "mostRecentlyClosed", + "comingSoon" = "comingSoon", + "name" = "name", + "status" = "status", + "verified" = "verified", + "updatedAt" = "updatedAt", +} + +export enum OrderDirEnum { + "ASC" = "ASC", + "DESC" = "DESC", } export enum ListingApplicationAddressType { "leasingAgent" = "leasingAgent", - "mailingAddress" = "mailingAddress", } export enum ListingStatus { @@ -5954,11 +7264,30 @@ export enum ListingReviewOrder { "firstComeFirstServe" = "firstComeFirstServe", } -export enum CSVFormattingType { - "basic" = "basic", - "withDisplaceeNameAndAddress" = "withDisplaceeNameAndAddress", - "ohaFormat" = "ohaFormat", - "bhaFormat" = "bhaFormat", +export enum ListingMarketingTypeEnum { + "marketing" = "marketing", + "comingSoon" = "comingSoon", +} + +export enum ListingSeasonEnum { + "spring" = "spring", + "summer" = "summer", + "fall" = "fall", + "winter" = "winter", +} + +export enum HomeTypeEnum { + "apartment" = "apartment", + "duplex" = "duplex", + "house" = "house", + "townhome" = "townhome", +} + +export enum Region { + "Greater Downtown" = "Greater Downtown", + "Eastside" = "Eastside", + "Southwest" = "Southwest", + "Westside" = "Westside", } export enum ListingEventType { @@ -5973,13 +7302,33 @@ export enum UnitStatus { "occupied" = "occupied", "unavailable" = "unavailable", } + +export enum MonthlyRentDeterminationType { + "flatRent" = "flatRent", + "percentageOfIncome" = "percentageOfIncome", +} export type CombinedPriorityTypeTypes = UnitAccessibilityPriorityType -export type CombinedApplicationAddressTypes = AddressUpdate export type CombinedApplicationPickUpAddressTypes = AddressUpdate export type CombinedApplicationDropOffAddressTypes = AddressUpdate export type CombinedApplicationMailingAddressTypes = AddressUpdate export type CombinedBuildingSelectionCriteriaFileTypes = AssetUpdate -export type CombinedImageTypes = AssetCreate export type CombinedLeasingAgentAddressTypes = AddressUpdate +export type CombinedNeighborhoodAmenitiesTypes = ListingNeighborhoodAmenities export type CombinedResultTypes = AssetCreate export type CombinedBuildingAddressTypes = AddressUpdate +export enum EnumPreferencesFilterParamsComparison { + "=" = "=", + "<>" = "<>", + "IN" = "IN", + ">=" = ">=", + "<=" = "<=", + "NA" = "NA", +} +export enum EnumProgramsFilterParamsComparison { + "=" = "=", + "<>" = "<>", + "IN" = "IN", + ">=" = ">=", + "<=" = "<=", + "NA" = "NA", +} diff --git a/backend/core/yarn.lock b/backend/core/yarn.lock new file mode 100644 index 0000000000..33c9e34255 --- /dev/null +++ b/backend/core/yarn.lock @@ -0,0 +1,7570 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@anchan828/nest-sendgrid@^0.3.25": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@anchan828/nest-sendgrid/-/nest-sendgrid-0.3.36.tgz#412825288772991997d08ac149ca418c1d85307e" + integrity sha512-jyWgpRy4tHxHzGeGAGsWNpIBxfluO4Clpn/hFZCZF/dNvhGh1vHgkiRxyngW3KxFdO+8E/t5LvWp7o9hsd9B4w== + dependencies: + "@sendgrid/mail" "7.4.0" + deepmerge "4.2.2" + reflect-metadata "0.1.13" + +"@angular-devkit/core@13.3.5": + version "13.3.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.5.tgz#c5f32f4f99b5cad8df9cf3cf4da9c4b1335c1155" + integrity sha512-w7vzK4VoYP9rLgxJ2SwEfrkpKybdD+QgQZlsDBzT0C6Ebp7b4gkNcNVFo8EiZvfDl6Yplw2IAP7g7fs3STn0hQ== + dependencies: + ajv "8.9.0" + ajv-formats "2.1.1" + fast-json-stable-stringify "2.1.0" + magic-string "0.25.7" + rxjs "6.6.7" + source-map "0.7.3" + +"@angular-devkit/core@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.6.tgz#656284327d6f84a866a8d3cc8625895fe740602d" + integrity sha512-ZmD586B+RnM2CG5+jbXh2NVfIydTc/yKSjppYDDOv4I530YBm6vpfZMwClpiNk6XLbMv7KqX4Tlr4wfxlPYYbA== + dependencies: + ajv "8.9.0" + ajv-formats "2.1.1" + fast-json-stable-stringify "2.1.0" + magic-string "0.25.7" + rxjs "6.6.7" + source-map "0.7.3" + +"@angular-devkit/schematics-cli@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-13.3.6.tgz#5246c112b6b837a9d0a348cb6b79a8c4948e90c8" + integrity sha512-5tTuu9gbXM0bMk0sin4phmWA3U1Qz53zT/rpEfzQ/+c/s8CoqZ5N1qOnYtemRct3Jxsz1kn4TBpHeriR4r5hHg== + dependencies: + "@angular-devkit/core" "13.3.6" + "@angular-devkit/schematics" "13.3.6" + ansi-colors "4.1.1" + inquirer "8.2.0" + minimist "1.2.6" + symbol-observable "4.0.0" + +"@angular-devkit/schematics@13.3.5": + version "13.3.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.3.5.tgz#9cb03ac99ee14173a6fa00fd7ca94fa42600c163" + integrity sha512-0N/kL/Vfx0yVAEwa3HYxNx9wYb+G9r1JrLjJQQzDp+z9LtcojNf7j3oey6NXrDUs1WjVZOa/AIdRl3/DuaoG5w== + dependencies: + "@angular-devkit/core" "13.3.5" + jsonc-parser "3.0.0" + magic-string "0.25.7" + ora "5.4.1" + rxjs "6.6.7" + +"@angular-devkit/schematics@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.3.6.tgz#b02e1eff714c2cf44a54de92410d07cc8cefbb0e" + integrity sha512-yLh5xc92C/FiaAp27coPiKWpSUmwoXF7vMxbJYJTyOXlt0mUITAEAwtrZQNr4yAxW/yvgTdyg7PhXaveQNTUuQ== + dependencies: + "@angular-devkit/core" "13.3.6" + jsonc-parser "3.0.0" + magic-string "0.25.7" + ora "5.4.1" + rxjs "6.6.7" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.27.2": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.7.tgz#7fd698e531050cce432b073ab64857b99e0f3804" + integrity sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ== + +"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.21.3", "@babel/core@^7.7.5": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.27.7.tgz#0ddeab1e7b17317dad8c3c3a887716f66b5c4428" + integrity sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.5" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.27.3" + "@babel/helpers" "^7.27.6" + "@babel/parser" "^7.27.7" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.27.7" + "@babel/types" "^7.27.7" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.27.5": + version "7.27.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.5.tgz#3eb01866b345ba261b04911020cbe22dd4be8c8c" + integrity sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw== + dependencies: + "@babel/parser" "^7.27.5" + "@babel/types" "^7.27.3" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.27.1": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== + dependencies: + "@babel/types" "^7.27.3" + +"@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz#5bee4262a6ea5ddc852d0806199eb17ca3de9281" + integrity sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.27.1" + semver "^6.3.1" + +"@babel/helper-member-expression-to-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44" + integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-transforms@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz#db0bbcfba5802f9ef7870705a7ef8788508ede02" + integrity sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.3" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + +"@babel/helper-replace-supers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" + integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helpers@^7.27.6": + version "7.27.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.6.tgz#6456fed15b2cb669d2d1fabe84b66b34991d812c" + integrity sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.6" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.27.2", "@babel/parser@^7.27.5", "@babel/parser@^7.27.7": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.7.tgz#1687f5294b45039c159730e3b9c1f1b242e425e9" + integrity sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q== + dependencies: + "@babel/types" "^7.27.7" + +"@babel/plugin-proposal-decorators@^7.21.0": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.27.1.tgz#3686f424b2f8b2fee7579aa4df133a4f5244a596" + integrity sha512-DTxe4LBPrtFdsWzgpmbBKevg3e9PBy+dXRt19kSbucbZvL2uqtdqwwpluL1jfxYE0wIDTFp1nTy/q6gNLsxXrg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-decorators" "^7.27.1" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-decorators@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz#ee7dd9590aeebc05f9d4c8c0560007b05979a63d" + integrity sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-import-attributes@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/template@^7.27.2", "@babel/template@^7.3.3": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3", "@babel/traverse@^7.27.7": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.7.tgz#8355c39be6818362eace058cf7f3e25ac2ec3b55" + integrity sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.5" + "@babel/parser" "^7.27.7" + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.7" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.27.6", "@babel/types@^7.27.7", "@babel/types@^7.3.3": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.7.tgz#40eabd562049b2ee1a205fa589e629f945dce20f" + integrity sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@cnakazawa/watch@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" + integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@google-cloud/common@^3.0.0": + version "3.10.0" + resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-3.10.0.tgz#454d1155bb512109cd83c6183aabbd39f9aabda7" + integrity sha512-XMbJYMh/ZSaZnbnrrOFfR/oQrb0SxG4qh6hDisWCoEbFcBHV0qHQo4uXfeMCzolx2Mfkh6VDaOGg+hyJsmxrlw== + dependencies: + "@google-cloud/projectify" "^2.0.0" + "@google-cloud/promisify" "^2.0.0" + arrify "^2.0.1" + duplexify "^4.1.1" + ent "^2.2.0" + extend "^3.0.2" + google-auth-library "^7.14.0" + retry-request "^4.2.2" + teeny-request "^7.0.0" + +"@google-cloud/projectify@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-2.1.1.tgz#ae6af4fee02d78d044ae434699a630f8df0084ef" + integrity sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ== + +"@google-cloud/promisify@^2.0.0": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-2.0.4.tgz#9d8705ecb2baa41b6b2673f3a8e9b7b7e1abc52a" + integrity sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA== + +"@google-cloud/translate@^6.2.6": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@google-cloud/translate/-/translate-6.3.1.tgz#5777e6550f7d50c26dbf5bf15788912531054232" + integrity sha512-x6/NxMzhUA2ottO0RmRT5u/nhd9Yssond5b3RpgAe1Klb4TCuYep2lh9LUzpnWuCYhBCjh2/9lNkjTWj9kXLQg== + dependencies: + "@google-cloud/common" "^3.0.0" + "@google-cloud/promisify" "^2.0.0" + arrify "^2.0.0" + extend "^3.0.2" + google-gax "^2.24.1" + is-html "^2.0.0" + protobufjs "^6.8.8" + +"@grpc/grpc-js@^1.2.11": + version "1.13.4" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.13.4.tgz#922fbc496e229c5fa66802d2369bf181c1df1c5a" + integrity sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg== + dependencies: + "@grpc/proto-loader" "^0.7.13" + "@js-sdsl/ordered-map" "^4.4.2" + +"@grpc/grpc-js@~1.6.0": + version "1.6.12" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.6.12.tgz#20f710d8a8c5c396b2ae9530ba6c06b984614fdf" + integrity sha512-JmvQ03OTSpVd9JTlj/K3IWHSz4Gk/JMLUTtW7Zb0KvO1LcOYGATh5cNuRYzCAeDR3O8wq+q8FZe97eO9MBrkUw== + dependencies: + "@grpc/proto-loader" "^0.7.0" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.5.6": + version "0.5.6" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.6.tgz#1dea4b8a6412b05e2d58514d507137b63a52a98d" + integrity sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ== + dependencies: + lodash.camelcase "^4.3.0" + protobufjs "^6.8.6" + +"@grpc/proto-loader@^0.6.12": + version "0.6.13" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.13.tgz#008f989b72a40c60c96cd4088522f09b05ac66bc" + integrity sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g== + dependencies: + "@types/long" "^4.0.1" + lodash.camelcase "^4.3.0" + long "^4.0.0" + protobufjs "^6.11.3" + yargs "^16.2.0" + +"@grpc/proto-loader@^0.7.0", "@grpc/proto-loader@^0.7.13": + version "0.7.15" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz#4cdfbf35a35461fc843abe8b9e2c0770b5095e60" + integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + +"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" + integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^26.6.2" + jest-util "^26.6.2" + slash "^3.0.0" + +"@jest/core@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" + integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/reporters" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-changed-files "^26.6.2" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-resolve-dependencies "^26.6.3" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + jest-watcher "^26.6.2" + micromatch "^4.0.2" + p-each-series "^2.1.0" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" + integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== + dependencies: + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + +"@jest/fake-timers@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" + integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== + dependencies: + "@jest/types" "^26.6.2" + "@sinonjs/fake-timers" "^6.0.1" + "@types/node" "*" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" + +"@jest/globals@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" + integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/types" "^26.6.2" + expect "^26.6.2" + +"@jest/reporters@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" + integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.2" + graceful-fs "^4.2.4" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^4.0.3" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.0.2" + jest-haste-map "^26.6.2" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" + slash "^3.0.0" + source-map "^0.6.0" + string-length "^4.0.1" + terminal-link "^2.0.0" + v8-to-istanbul "^7.0.0" + optionalDependencies: + node-notifier "^8.0.0" + +"@jest/source-map@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" + integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.4" + source-map "^0.6.0" + +"@jest/test-result@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" + integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== + dependencies: + "@jest/console" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" + integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== + dependencies: + "@jest/test-result" "^26.6.2" + graceful-fs "^4.2.4" + jest-haste-map "^26.6.2" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + +"@jest/transform@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" + integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^26.6.2" + babel-plugin-istanbul "^6.0.0" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.4" + jest-haste-map "^26.6.2" + jest-regex-util "^26.0.0" + jest-util "^26.6.2" + micromatch "^4.0.2" + pirates "^4.0.1" + slash "^3.0.0" + source-map "^0.6.1" + write-file-atomic "^3.0.0" + +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.10" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.10.tgz#1cad974c8478e644c5cbce2a4b738137bb64bd4f" + integrity sha512-HM2F4B9N4cA0RH2KQiIZOHAZqtP4xGS4IZ+SFe1SIbO4dyjf9MTY2Bo3vHYnm0hglWfXqBrzUBSa+cJfl3Xvrg== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.8.tgz#0af4be466bcbcbc7206f69623e32b3cefe3b39cd" + integrity sha512-3EDAPd0B8X1gsQQgGHU8vyxSp2MB414z3roN67fY7nI0GV3GDthHfaWcbCfrC95tpAzA5xUvAuoO9Dxx/ywwRQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.2.tgz#4f25c8f17f28ccf70ed16e03f8fbf6d3998cb8fd" + integrity sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.27" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.27.tgz#3139cfeafce3aa9918454cce8b219eee39fd7df2" + integrity sha512-VO95AxtSFMelbg3ouljAYnfvTEwSWVt/2YLf+U5Ejd8iT5mXE2Sa/1LGyvySMne2CGsepGLI7KpF3EzE3Aq9Mg== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== + +"@nestjs/cli@^8.2.1": + version "8.2.8" + resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-8.2.8.tgz#63e5b477f90e6d0238365dcc6236b95bf4f0c807" + integrity sha512-y5Imcw1EY0OxD3POAM7SLUB1rFdn5FjbfSsyJrokjKmXY+i6KcBdbRrv3Ox7aeJ4W7wXuckIXZEUlK6lC52dnA== + dependencies: + "@angular-devkit/core" "13.3.6" + "@angular-devkit/schematics" "13.3.6" + "@angular-devkit/schematics-cli" "13.3.6" + "@nestjs/schematics" "^8.0.3" + chalk "3.0.0" + chokidar "3.5.3" + cli-table3 "0.6.2" + commander "4.1.1" + fork-ts-checker-webpack-plugin "7.2.11" + inquirer "7.3.3" + node-emoji "1.11.0" + ora "5.4.1" + os-name "4.0.1" + rimraf "3.0.2" + shelljs "0.8.5" + source-map-support "0.5.21" + tree-kill "1.2.2" + tsconfig-paths "3.14.1" + tsconfig-paths-webpack-plugin "3.5.2" + typescript "4.7.4" + webpack "5.73.0" + webpack-node-externals "3.0.0" + +"@nestjs/common@^8.3.1": + version "8.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-8.4.7.tgz#fc4a575b797e230bb5a0bcab6da8b796aa88d605" + integrity sha512-m/YsbcBal+gA5CFrDpqXqsSfylo+DIQrkFY3qhVIltsYRfu8ct8J9pqsTO6OPf3mvqdOpFGrV5sBjoyAzOBvsw== + dependencies: + axios "0.27.2" + iterare "1.2.1" + tslib "2.4.0" + uuid "8.3.2" + +"@nestjs/config@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-1.2.1.tgz#8344111361ef4bc1b41160f853f387ec050a8526" + integrity sha512-EgaGTXvG4unD5lGWmdSrUFrkGpX32lQGE/8qS60EnL82sIZV7HT1ZL7ib5S86P1nB+DnFDbDhDqTaZ3mivTyOg== + dependencies: + dotenv "16.0.0" + dotenv-expand "5.1.0" + lodash "4.17.21" + uuid "8.3.2" + +"@nestjs/core@^8.3.1": + version "8.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-8.4.7.tgz#fbec7fa744ac8749a4b966f759a6656c1cf43883" + integrity sha512-XB9uexHqzr2xkPo6QSiQWJJttyYYLmvQ5My64cFvWFi7Wk2NIus0/xUNInwX3kmFWB6pF1ab5Y2ZBvWdPwGBhw== + dependencies: + "@nuxtjs/opencollective" "0.3.2" + fast-safe-stringify "2.1.1" + iterare "1.2.1" + object-hash "3.0.0" + path-to-regexp "3.2.0" + tslib "2.4.0" + uuid "8.3.2" + +"@nestjs/jwt@^8.0.0": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-8.0.1.tgz#e00f6810705a75d5680903241f4e07272e59e6d5" + integrity sha512-9WGfgngX8aclC/MC+CH35Ooo4iPVKc+7xLXaBV6o4ty8g2uZdPomry7cSdK/e6Lv623O/84WapThnPoAtW/jvA== + dependencies: + "@types/jsonwebtoken" "8.5.8" + jsonwebtoken "8.5.1" + +"@nestjs/mapped-types@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-1.0.1.tgz#78b62041c7a407db4a90eb140567321602bed18e" + integrity sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg== + +"@nestjs/passport@^8.2.1": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@nestjs/passport/-/passport-8.2.2.tgz#32b3932b83740895f037eabaf812c44f5ec18b3a" + integrity sha512-Ytbn8j7WZ4INmEntOpdJY1isTgdQqZkx5ADz8zsZ5wAp0t8tc5GF/A+GlXlmn9/yRPwZHSbmHpv7Qt2EIiNnrw== + +"@nestjs/platform-express@^8.3.1": + version "8.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-8.4.7.tgz#402a3d3c47327a164bb3867615f423c29d1a6cd9" + integrity sha512-lPE5Ltg2NbQGRQIwXWY+4cNrXhJdycbxFDQ8mNxSIuv+LbrJBIdEB/NONk+LLn9N/8d2+I2LsIETGQrPvsejBg== + dependencies: + body-parser "1.20.0" + cors "2.8.5" + express "4.18.1" + multer "1.4.4-lts.1" + tslib "2.4.0" + +"@nestjs/schedule@^1.0.2": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@nestjs/schedule/-/schedule-1.1.0.tgz#7c8e937399bf5da3d6895e7179ae4bdc4377906e" + integrity sha512-0QpbwClUildXqlyoaygG+aIQZNNMv31XDyQxX+Ob1zw/3I8+AVrDlBwZHQ+tlhIcJFR8aG+VTH8xwIjXwtS1UA== + dependencies: + cron "1.8.2" + uuid "8.3.2" + +"@nestjs/schematics@^8.0.3", "@nestjs/schematics@^8.0.7": + version "8.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-8.0.11.tgz#5d0c56184826660a2c01b1c326dbdbb12880e864" + integrity sha512-W/WzaxgH5aE01AiIErE9QrQJ73VR/M/8p8pq0LZmjmNcjZqU5kQyOWUxZg13WYfSpJdOa62t6TZRtFDmgZPoIg== + dependencies: + "@angular-devkit/core" "13.3.5" + "@angular-devkit/schematics" "13.3.5" + fs-extra "10.1.0" + jsonc-parser "3.0.0" + pluralize "8.0.0" + +"@nestjs/swagger@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-5.2.0.tgz#7fbb66426fab9790f75e906e7d384aacef386a28" + integrity sha512-wcW2KCe7irwBw0WKsb03uFk4gnjqNcAcJxXGZxi6ljFiDgyLrQtx5MpE95o703tnn4V/hZR8MqMNFOXuup7Pqw== + dependencies: + "@nestjs/mapped-types" "1.0.1" + lodash "4.17.21" + path-to-regexp "3.2.0" + +"@nestjs/testing@^8.3.1": + version "8.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-8.4.7.tgz#fe4f356c0e081e25fe8c899a65e91dd88947fd13" + integrity sha512-aedpeJFicTBeiTCvJWUG45WMMS53f5eu8t2fXsfjsU1t+WdDJqYcZyrlCzA4dL1B7MfbqaTURdvuVVHTmJO8ag== + dependencies: + tslib "2.4.0" + +"@nestjs/throttler@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/throttler/-/throttler-2.0.1.tgz#97611c4bdffdb1df00fcaeb7ac55fcfee1864bf6" + integrity sha512-ginW73rmOjBN27USuGidetEoa8VSGzxW3kEuCquEd5mETEtBfgIm4901b9tuLDnsczttE01imCHZ53J7+AuLJg== + dependencies: + md5 "^2.2.1" + +"@nestjs/typeorm@~8.0.3": + version "8.0.5" + resolved "https://registry.yarnpkg.com/@nestjs/typeorm/-/typeorm-8.0.5.tgz#600d337f4ea68569161af9f511822f78173ccf6b" + integrity sha512-2kyhuHwFAK5rcOniZdgc46/iIVKAZRalXPRr3tQeZa9Xlly+NA5oXEDedoriapnQjzR2IiM7jfgUISISCOIZlw== + dependencies: + uuid "8.3.2" + +"@newrelic/aws-sdk@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@newrelic/aws-sdk/-/aws-sdk-3.1.0.tgz#0f8dd504a37ad92be608c340e5f836d848abdefa" + integrity sha512-SBFqCz1Hhn2HQvlFCEm2VwfHCGpemeokJ+NH7XphlfQ211OVVANQf49DOQCCS0uhnLHGbKMLmir8Layx57y48A== + +"@newrelic/koa@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@newrelic/koa/-/koa-5.0.0.tgz#ab9668c56a7a68ee37a6791382bd83b3824a7acf" + integrity sha512-4jzRXnUe38gQkZI8K4tWQ6CNdCNTi5uKILf1dTkyT6LpGxzDSLPwVyJ6xtMSMzr8SjIPG7lyNoWe42q8wFA7jg== + dependencies: + methods "^1.1.2" + +"@newrelic/native-metrics@^6.0.0": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@newrelic/native-metrics/-/native-metrics-6.0.2.tgz#3d97876f8e760b3f002cd278436fbecb5e322efe" + integrity sha512-MYJnPyR2lJAH0B5yKkHaBL+ui3ZLWViKNCAIXAeMmrNzEC/wlM6Yyl0Ryo6THX3E3Hq3ArXE4uQTXVn7x8ijKQ== + dependencies: + nan "^2.14.2" + semver "^5.5.1" + +"@newrelic/superagent@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@newrelic/superagent/-/superagent-4.0.0.tgz#e3c53825b64aa73a3c5f6d98ed0ccc97f86dbc42" + integrity sha512-n4iNrsV0908yHNZPNof7rm/mffclHaIxprCCWk15b4IRJik2VrtuIrK3mboUgNdv5pX4P7EZytY/D6kJgFkDGw== + dependencies: + methods "^1.1.2" + +"@nuxtjs/opencollective@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz#620ce1044f7ac77185e825e1936115bb38e2681c" + integrity sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA== + dependencies: + chalk "^4.1.0" + consola "^2.15.0" + node-fetch "^2.6.1" + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + +"@scarf/scarf@=1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.4.0.tgz#3bbb984085dbd6d982494538b523be1ce6562972" + integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ== + +"@sendgrid/client@^7.4.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.7.0.tgz#f8f67abd604205a0d0b1af091b61517ef465fdbf" + integrity sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA== + dependencies: + "@sendgrid/helpers" "^7.7.0" + axios "^0.26.0" + +"@sendgrid/helpers@^7.4.0", "@sendgrid/helpers@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@sendgrid/helpers/-/helpers-7.7.0.tgz#93fb4b6e2f0dc65080440d6a784cc93e8e148757" + integrity sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw== + dependencies: + deepmerge "^4.2.2" + +"@sendgrid/mail@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@sendgrid/mail/-/mail-7.4.0.tgz#126de0f0fc7c62d5d609140261fd598b6291fee3" + integrity sha512-SAARsfbl50OEJ99LYGKfgrYiV5O6+23aeGJuEBTHHSwRZ6KhD3n1BjPeIejbqgbqYLZJfNLxyU3o5xRdJPp3zg== + dependencies: + "@sendgrid/client" "^7.4.0" + "@sendgrid/helpers" "^7.4.0" + +"@sideway/address@^4.1.5": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" + integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + +"@sinonjs/commons@^1.7.0": + version "1.8.6" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" + integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sqltools/formatter@^1.2.2": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12" + integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw== + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/axios@^0.14.0": + version "0.14.4" + resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.4.tgz#174e3a05fe7677f13bc719f0d2a427f5defacedf" + integrity sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g== + dependencies: + axios "*" + +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.7.tgz#968cdc2366ec3da159f61166428ee40f370e56c2" + integrity sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng== + dependencies: + "@babel/types" "^7.20.7" + +"@types/body-parser@*": + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/cache-manager@^3.4.0": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@types/cache-manager/-/cache-manager-3.4.3.tgz#eba99bf795b997ad0c309658101398c34d7faecb" + integrity sha512-71aBXoFYXZW4TnDHHH8gExw2lS28BZaWeKefgsiJI7QYZeJfUEbMKw6CQtzGjlYQcGIWwB76hcCrkVA3YHSvsw== + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + +"@types/cron@^1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@types/cron/-/cron-1.7.3.tgz#993db7d54646f61128c851607b64ba4495deae93" + integrity sha512-iPmUXyIJG1Js+ldPYhOQcYU3kCAQ2FWrSkm1FJPoii2eYSn6wEW6onPukNTT0bfiflexNSRPl6KWmAIqS+36YA== + dependencies: + "@types/node" "*" + moment ">=2.14.0" + +"@types/eslint-scope@^3.7.3": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express-serve-static-core@^5.0.0": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz#41fec4ea20e9c7b22f024ab88a95c6bb288f51b8" + integrity sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" + integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "*" + +"@types/express@^4.17.8": + version "4.17.23" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.23.tgz#35af3193c640bfd4d7fe77191cd0ed411a433bef" + integrity sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/geojson@*": + version "7946.0.16" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== + +"@types/graceful-fs@^4.1.2": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@26.x": + version "26.0.24" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a" + integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w== + dependencies: + jest-diff "^26.0.0" + pretty-format "^26.0.0" + +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/jsonwebtoken@*": + version "9.0.10" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz#a7932a47177dcd4283b6146f3bd5c26d82647f09" + integrity sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA== + dependencies: + "@types/ms" "*" + "@types/node" "*" + +"@types/jsonwebtoken@8.5.8": + version "8.5.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz#01b39711eb844777b7af1d1f2b4cf22fda1c0c44" + integrity sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A== + dependencies: + "@types/node" "*" + +"@types/leaflet@^0": + version "0.7.40" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-0.7.40.tgz#ed2436ef2772827c8d33c35640df74bb6f0a9b6d" + integrity sha512-R2UwXOKwnKZi9zNm37WbPTAVuqHmysE6NVihkc5DUrovTirUxFSbZzvXrlwv0n5sibe0w8VF1bWu0ta4kZlAaA== + dependencies: + "@types/geojson" "*" + +"@types/long@^4.0.0", "@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + +"@types/mapbox@^1.6.42": + version "1.6.45" + resolved "https://registry.yarnpkg.com/@types/mapbox/-/mapbox-1.6.45.tgz#f34d68fe661316b5ee2cf2de215dfe9ff4e1eff8" + integrity sha512-u2CsTwjmRDMuX5pxU4bX2McJVv34qiluIw3HTpoTwgzdGxJa9BAYTF2ZOVWYJalwk0UegxQxMZL9T5rew6j9tg== + dependencies: + "@types/leaflet" "^0" + +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/minimatch@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + +"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "24.0.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.0.7.tgz#ee580f7850c7eabaeef61ef96b8d8c04fdf94f53" + integrity sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw== + dependencies: + undici-types "~7.8.0" + +"@types/node@^12.12.67": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + +"@types/normalize-package-data@^2.4.0": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== + +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + +"@types/passport-jwt@^3.0.3": + version "3.0.13" + resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-3.0.13.tgz#119267d2fc1af7d274a512731146183de5f2b53f" + integrity sha512-fjHaC6Bv8EpMMqzTnHP32SXlZGaNfBPC/Po5dmRGYi2Ky7ljXPbGnOy+SxZqa6iZvFgVhoJ1915Re3m93zmcfA== + dependencies: + "@types/express" "*" + "@types/jsonwebtoken" "*" + "@types/passport-strategy" "*" + +"@types/passport-local@^1.0.33": + version "1.0.38" + resolved "https://registry.yarnpkg.com/@types/passport-local/-/passport-local-1.0.38.tgz#8073758188645dde3515808999b1c218a6fe7141" + integrity sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport-strategy" "*" + +"@types/passport-strategy@*": + version "0.2.38" + resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.38.tgz#482abba0b165cd4553ec8b748f30b022bd6c04d3" + integrity sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport@*": + version "1.0.17" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.17.tgz#718a8d1f7000ebcf6bbc0853da1bc8c4bc7ea5e6" + integrity sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg== + dependencies: + "@types/express" "*" + +"@types/prettier@^2.0.0": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" + integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== + +"@types/qs@*": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.5" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74" + integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" + integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + +"@types/superagent@*": + version "8.1.9" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.9.tgz#28bfe4658e469838ed0bf66d898354bcab21f49f" + integrity sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + form-data "^4.0.0" + +"@types/supertest@^2.0.10": + version "2.0.16" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.16.tgz#7a1294edebecb960d957bbe9b26002a2b7f21cd7" + integrity sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg== + dependencies: + "@types/superagent" "*" + +"@types/validator@13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.0.0.tgz#365f1bf936aeaddd0856fc41aa1d6f82d88ee5b3" + integrity sha512-WAy5txG7aFX8Vw3sloEKp5p/t/Xt8jD3GRD9DacnFv6Vo8ubudAsRTXgxpQwU0mpzY/H8U4db3roDuCMjShBmw== + +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^15.0.0": + version "15.0.19" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.19.tgz#328fb89e46109ecbdb70c295d96ff2f46dfd01b9" + integrity sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA== + dependencies: + "@types/yargs-parser" "*" + +"@types/zen-observable@0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3" + integrity sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw== + +"@tyriar/fibonacci-heap@^2.0.7": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@tyriar/fibonacci-heap/-/fibonacci-heap-2.0.9.tgz#df3dcbdb1b9182168601f6318366157ee16666e9" + integrity sha512-bYuSNomfn4hu2tPiDN+JZtnzCpSpbJ/PNeulmocDy3xN2X5OkJL65zo6rPZp65cPPhLF9vfT/dgE+RtFRCSxOA== + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.3, abab@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-globals@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" + integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== + dependencies: + acorn "^7.1.1" + acorn-walk "^7.1.1" + +acorn-import-assertions@^1.7.6: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + +acorn-walk@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + +acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.11.0, acorn@^8.14.0, acorn@^8.2.4, acorn@^8.4.1: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +adler-32@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25" + integrity sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ== + dependencies: + exit-on-epipe "~1.0.1" + printj "~1.1.0" + +adler-32@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2" + integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== + +agent-base@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" + integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ajv-formats@2.1.1, ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.9.0.tgz#738019146638824dea25edcf299dcba1b0e7eb18" + integrity sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.0, ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +app-root-path@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86" + integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA== + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA== + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== + +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== + +arrify@^2.0.0, arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== + +async-retry@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + +async@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== + +async@^3.2.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +await-lock@^2.0.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef" + integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== + +axios@*: + version "1.10.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54" + integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +axios@0.21.2: + version "0.21.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017" + integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg== + dependencies: + follow-redirects "^1.14.0" + +axios@0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + +axios@^0.21.1: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + +axios@^0.26.0, axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + +babel-jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" + integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== + dependencies: + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/babel__core" "^7.1.7" + babel-plugin-istanbul "^6.0.0" + babel-preset-jest "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + slash "^3.0.0" + +babel-plugin-istanbul@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" + integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30" + integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + +babel-preset-jest@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" + integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== + dependencies: + babel-plugin-jest-hoist "^26.6.2" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.0, base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +bignumber.js@^9.0.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.0.tgz#bdba7e2a4c1a2eba08290e8dcad4f36393c92acd" + integrity sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.10.3" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== + +browserslist@^4.14.5, browserslist@^4.24.0: + version "4.25.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.1.tgz#ba9e8e6f298a1d86f829c9b975e07948967bb111" + integrity sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw== + dependencies: + caniuse-lite "^1.0.30001726" + electron-to-chromium "^1.5.173" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer-from@1.x, buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +cache-manager@^3.4.0: + version "3.6.3" + resolved "https://registry.yarnpkg.com/cache-manager/-/cache-manager-3.6.3.tgz#48052f3cf9ee4bac1cbb6adeedd69faf9da4ec04" + integrity sha512-dS4DnV6c6cQcVH5OxzIU1XZaACXwvVIiUPkFytnRmLOACuBGv3GQgRQ1RJGRRw4/9DF14ZK2RFlZu1TUgDniMg== + dependencies: + async "3.2.3" + lodash.clonedeep "^4.5.0" + lru-cache "6.0.0" + +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001726: + version "1.0.30001726" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz#a15bd87d5a4bf01f6b6f70ae7c97fdfd28b5ae47" + integrity sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw== + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + +casbin@5.13.0: + version "5.13.0" + resolved "https://registry.yarnpkg.com/casbin/-/casbin-5.13.0.tgz#286835c79246eb43319d3507371457efe6d2e434" + integrity sha512-pN8R37/ALma3OMBx7DP9QPUWs0qI+0t+VHeUHxwM4eIY+GR0bJT74blH/ob0f/hG7WpVuA8YO3he3WSV/MyV9w== + dependencies: + await-lock "^2.0.1" + csv-parse "^4.15.3" + expression-eval "^4.0.0" + picomatch "^2.2.3" + +cfb@^1.1.4: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44" + integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== + dependencies: + adler-32 "~1.3.0" + crc-32 "~1.2.0" + +chalk@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cjs-module-lexer@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" + integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== + +class-transformer@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.3.1.tgz#ee681a5439ff2230fc57f5056412d3befa70d597" + integrity sha512-cKFwohpJbuMovS8xVLmn8N2AUbAuc8pVo4zEfsUVo8qgECOogns1WVk/FkOZoxhOPTyTYFckuoH+13FO+MQ8GA== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +class-validator@^0.12.2: + version "0.12.2" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.12.2.tgz#2ceb72f88873e9c714cf5f9c278cbc71f6f6c8ef" + integrity sha512-TDzPzp8BmpsbPhQpccB3jMUE/3pK0TyqamrK0kcx+ZeFytMA+O6q87JZZGObHHnoo9GM8vl/JppIyKWeEA/EVw== + dependencies: + "@types/validator" "13.0.0" + google-libphonenumber "^3.2.8" + tslib ">=1.9.0" + validator "13.0.0" + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-highlight@^2.1.11: + version "2.1.11" + resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.11.tgz#49736fa452f0aaf4fae580e30acb26828d2dc1bf" + integrity sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg== + dependencies: + chalk "^4.0.0" + highlight.js "^10.7.1" + mz "^2.4.0" + parse5 "^5.1.1" + parse5-htmlparser2-tree-adapter "^6.0.0" + yargs "^16.0.0" + +cli-spinners@^2.5.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +cli-table3@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a" + integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +cloudinary-core@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/cloudinary-core/-/cloudinary-core-2.13.1.tgz#7abb62bf1db41773acfe1eebb7fd40571c34495b" + integrity sha512-z53GPNWnvU0Zi+ns8CIVbZBfj7ps/++zDvwIyiFuq5p1MoK+KUCg0k5mBceDDHTnx1gHmHUd9aohS+gDxPNt6w== + +cloudinary@^1.25.2: + version "1.41.3" + resolved "https://registry.yarnpkg.com/cloudinary/-/cloudinary-1.41.3.tgz#dc96725122099349adba6ab4eccda84fa13e8c2e" + integrity sha512-4o84y+E7dbif3lMns+p3UW6w6hLHEifbX/7zBJvaih1E9QNMZITENQ14GPYJC4JmhygYXsuuBb9bRA3xWEoOfg== + dependencies: + cloudinary-core "^2.13.0" + core-js "^3.30.1" + lodash "^4.17.21" + q "^1.5.1" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +codepage@~1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab" + integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA== + +collect-v8-coverage@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw== + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +component-emitter@^1.2.0, component-emitter@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + +consola@^2.15.0: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^1.4.0, convert-source-map@^1.6.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +cookiejar@^2.1.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== + +core-js@^3.30.1: + version "3.43.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.43.0.tgz#f7258b156523208167df35dea0cfd6b6ecd4ee88" + integrity sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cosmiconfig@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +crc-32@~1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cron@1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/cron/-/cron-1.8.2.tgz#4ac5e3c55ba8c163d84f3407bde94632da8370ce" + integrity sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg== + dependencies: + moment-timezone "^0.5.x" + +cross-spawn@^6.0.0: + version "6.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57" + integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.0: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== + +cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +csv-parse@^4.15.3: + version "4.16.3" + resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-4.16.3.tgz#7ca624d517212ebc520a36873c3478fa66efbaf7" + integrity sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg== + +csv-parser@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/csv-parser/-/csv-parser-3.2.0.tgz#7e5515e3763e963dc8660dc9dcfc3f0eaf72b0a9" + integrity sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA== + +csv-reader@^1.0.8: + version "1.0.12" + resolved "https://registry.yarnpkg.com/csv-reader/-/csv-reader-1.0.12.tgz#3b1d3d87c69ef08579ba052bb6e2395d52b0acf6" + integrity sha512-0AAgazKJUywtjvZbclNuovIiQY/WyvojWw15Y2k3kPixE+pDiOFnfg5FcH3CfDqqnrB2f3p5oPAc446EXD01Tw== + +data-urls@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" + integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + +dayjs@^1.10.7, dayjs@^1.8.29: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decimal.js@^10.2.1: + version "10.5.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.5.0.tgz#0f371c7cf6c4898ce0afb09836db73cd82010f22" + integrity sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw== + +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +deepmerge@4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA== + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA== + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +domexception@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" + integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== + dependencies: + webidl-conversions "^5.0.0" + +dotenv-expand@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== + +dotenv@16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411" + integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q== + +dotenv@^8.2.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" + integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +duplexify@^4.0.0, duplexify@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.2" + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.173: + version "1.5.177" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.177.tgz#db730d8254959184e65320a3a0b7edcd29c54f60" + integrity sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g== + +emittery@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" + integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + +enhanced-resolve@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" + integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + +enhanced-resolve@^5.7.0, enhanced-resolve@^5.9.3: + version "5.18.2" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz#7903c5b32ffd4b2143eeb4b92472bd68effd5464" + integrity sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.2.tgz#22a5ed2fd7ce0cbcff1d1474cf4909a44bdb6e85" + integrity sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + punycode "^1.4.1" + safe-regex-test "^1.1.0" + +errno@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" + integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== + dependencies: + prr "~1.0.1" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +es6-promise@^4.0.5: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escodegen@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +exec-sh@^0.3.2: + version "0.3.6" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc" + integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w== + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^4.0.0, execa@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +exit-on-epipe@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" + integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA== + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expect@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" + integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== + dependencies: + "@jest/types" "^26.6.2" + ansi-styles "^4.0.0" + jest-get-type "^26.3.0" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + +express@4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" + integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.0" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.10.3" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +express@^4.17.1: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +expression-eval@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/expression-eval/-/expression-eval-4.0.0.tgz#d6a07c93e8b33e635710419d4a595d9208b9cc5e" + integrity sha512-YHSnLTyIb9IKaho2IdQbvlei/pElxnGm48UgaXJ1Fe5au95Ck0R9ftm6rHJQuKw3FguZZ4eXVllJFFFc7LX0WQ== + dependencies: + jsep "^0.3.0" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q== + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.0, extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-safe-stringify@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + +fast-xml-parser@^4.0.0-beta.2: + version "4.5.3" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz#c54d6b35aa0f23dc1ea60b6c884340c006dc6efb" + integrity sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig== + dependencies: + strnum "^1.1.1" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ== + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +fishery@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/fishery/-/fishery-0.3.0.tgz#8f1d24f69600e38e0707943e62300cad42ae1f16" + integrity sha512-hY2LTDq213KquLftAVgvQOcZmtx5Kr0JadWvNJgnT0NkjH0MB+Xvg6XIxbMFwkDed0CyYpcAUThgj0qWVigmbw== + dependencies: + lodash.merge "^4.6.2" + +follow-redirects@^1.14.0, follow-redirects@^1.14.8, follow-redirects@^1.14.9, follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== + +fork-ts-checker-webpack-plugin@7.2.11: + version "7.2.11" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.11.tgz#aff3febbc11544ba3ad0ae4d5aa4055bd15cd26d" + integrity sha512-2e5+NyTUTE1Xq4fWo7KFEQblCaIvvINQwUX3jRmEGlgCTc1Ecqw/975EfQrQ0GEraxJTnp8KB9d/c8hlCHUMJA== + dependencies: + "@babel/code-frame" "^7.16.7" + chalk "^4.1.2" + chokidar "^3.5.3" + cosmiconfig "^7.0.1" + deepmerge "^4.2.2" + fs-extra "^10.0.0" + memfs "^3.4.1" + minimatch "^3.0.4" + schema-utils "^3.1.1" + semver "^7.3.5" + tapable "^2.2.1" + +form-data@^2.3.1: + version "2.5.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.3.tgz#f9bcf87418ce748513c0c3494bb48ec270c97acc" + integrity sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + mime-types "^2.1.35" + safe-buffer "^5.2.1" + +form-data@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.3.tgz#349c8f2c9d8f8f0c879ee0eb7cc0d300018d6b09" + integrity sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + mime-types "^2.1.35" + +form-data@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.3.tgz#608b1b3f3e28be0fccf5901fc85fb3641e5cf0ae" + integrity sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + +formidable@^1.2.0: + version "1.2.6" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" + integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA== + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@10.1.0, fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-monkey@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.1.2, fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gaxios@^4.0.0: + version "4.3.3" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.3.3.tgz#d44bdefe52d34b6435cc41214fdb160b64abfc22" + integrity sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.6.7" + +gcp-metadata@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.3.1.tgz#fb205fe6a90fef2fd9c85e6ba06e5559ee1eefa9" + integrity sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A== + dependencies: + gaxios "^4.0.0" + json-bigint "^1.0.0" + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +google-auth-library@^7.14.0: + version "7.14.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.14.1.tgz#e3483034162f24cc71b95c8a55a210008826213c" + integrity sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-gax@^2.24.1: + version "2.30.5" + resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-2.30.5.tgz#e836f984f3228900a8336f608c83d75f9cb73eff" + integrity sha512-Jey13YrAN2hfpozHzbtrwEfEHdStJh1GwaQ2+Akh1k0Tv/EuNVSuBtHZoKSBm5wBMvNsxTsEIZ/152NrYyZgxQ== + dependencies: + "@grpc/grpc-js" "~1.6.0" + "@grpc/proto-loader" "^0.6.12" + "@types/long" "^4.0.0" + abort-controller "^3.0.0" + duplexify "^4.0.0" + fast-text-encoding "^1.0.3" + google-auth-library "^7.14.0" + is-stream-ended "^0.1.4" + node-fetch "^2.6.1" + object-hash "^3.0.0" + proto3-json-serializer "^0.1.8" + protobufjs "6.11.3" + retry-request "^4.0.0" + +google-libphonenumber@^3.2.8: + version "3.2.42" + resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.42.tgz#deac6d626bb57540449a844154d84e91d8da7df1" + integrity sha512-60jm6Lu72WmlUJXUBJmmuZlHG2vDJ2gQ9pL5gcFsSe1Q4eigsm0Z1ayNHjMgqGUl0zey8JqKtO4QCHPV+5LCNQ== + +google-p12-pem@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.1.4.tgz#123f7b40da204de4ed1fbf2fd5be12c047fc8b3b" + integrity sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg== + dependencies: + node-forge "^1.3.1" + +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw== + +gtoken@^5.0.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.2.tgz#deb7dc876abe002178e0515e383382ea9446d58f" + integrity sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ== + dependencies: + gaxios "^4.0.0" + google-p12-pem "^3.1.3" + jws "^4.0.0" + +handlebars@^4.7.6: + version "4.7.8" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.2" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q== + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw== + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ== + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ== + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +hasown@^2.0.0, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +highlight.js@^10.7.1: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +html-encoding-sniffer@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" + integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== + dependencies: + whatwg-encoding "^1.0.5" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +html-tags@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" + integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +https-proxy-agent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" + integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== + dependencies: + agent-base "5" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13, ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inquirer@7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +inquirer@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" + integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.2.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-accessor-descriptor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz#3223b10628354644b86260db29b3e693f5ceedd4" + integrity sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA== + dependencies: + hasown "^2.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-buffer@^1.1.5, is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-data-descriptor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz#2109164426166d32ea38c405c1e0945d9e6a4eeb" + integrity sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw== + dependencies: + hasown "^2.0.0" + +is-descriptor@^0.1.0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.7.tgz#2727eb61fd789dcd5bdf0ed4569f551d2fe3be33" + integrity sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg== + dependencies: + is-accessor-descriptor "^1.0.1" + is-data-descriptor "^1.0.1" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.3.tgz#92d27cb3cd311c4977a4db47df457234a13cb306" + integrity sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw== + dependencies: + is-accessor-descriptor "^1.0.1" + is-data-descriptor "^1.0.1" + +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-html@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-html/-/is-html-2.0.0.tgz#b3ab2e27ccb7a12235448f51f115a6690f435fc8" + integrity sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg== + dependencies: + html-tags "^3.0.0" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg== + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-stream-ended@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" + integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA== + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.0.2: + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +iterare@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" + integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== + +jest-changed-files@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" + integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== + dependencies: + "@jest/types" "^26.6.2" + execa "^4.0.0" + throat "^5.0.0" + +jest-cli@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" + integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== + dependencies: + "@jest/core" "^26.6.3" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + import-local "^3.0.2" + is-ci "^2.0.0" + jest-config "^26.6.3" + jest-util "^26.6.2" + jest-validate "^26.6.2" + prompts "^2.0.1" + yargs "^15.4.1" + +jest-config@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" + integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^26.6.3" + "@jest/types" "^26.6.2" + babel-jest "^26.6.3" + chalk "^4.0.0" + deepmerge "^4.2.2" + glob "^7.1.1" + graceful-fs "^4.2.4" + jest-environment-jsdom "^26.6.2" + jest-environment-node "^26.6.2" + jest-get-type "^26.3.0" + jest-jasmine2 "^26.6.3" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + micromatch "^4.0.2" + pretty-format "^26.6.2" + +jest-diff@^26.0.0, jest-diff@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== + dependencies: + chalk "^4.0.0" + diff-sequences "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + +jest-docblock@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" + integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== + dependencies: + detect-newline "^3.0.0" + +jest-each@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" + integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== + dependencies: + "@jest/types" "^26.6.2" + chalk "^4.0.0" + jest-get-type "^26.3.0" + jest-util "^26.6.2" + pretty-format "^26.6.2" + +jest-environment-jsdom@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" + integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + jsdom "^16.4.0" + +jest-environment-node@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" + integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + +jest-get-type@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" + integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== + +jest-haste-map@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" + integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== + dependencies: + "@jest/types" "^26.6.2" + "@types/graceful-fs" "^4.1.2" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.4" + jest-regex-util "^26.0.0" + jest-serializer "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" + micromatch "^4.0.2" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.1.2" + +jest-jasmine2@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" + integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + expect "^26.6.2" + is-generator-fn "^2.0.0" + jest-each "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" + throat "^5.0.0" + +jest-leak-detector@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" + integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== + dependencies: + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + +jest-matcher-utils@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" + integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== + dependencies: + chalk "^4.0.0" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + +jest-message-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" + integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" + micromatch "^4.0.2" + pretty-format "^26.6.2" + slash "^3.0.0" + stack-utils "^2.0.2" + +jest-mock@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" + integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" + integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== + +jest-resolve-dependencies@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" + integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== + dependencies: + "@jest/types" "^26.6.2" + jest-regex-util "^26.0.0" + jest-snapshot "^26.6.2" + +jest-resolve@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" + integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== + dependencies: + "@jest/types" "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + jest-pnp-resolver "^1.2.2" + jest-util "^26.6.2" + read-pkg-up "^7.0.1" + resolve "^1.18.1" + slash "^3.0.0" + +jest-runner@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" + integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.7.1" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-config "^26.6.3" + jest-docblock "^26.0.0" + jest-haste-map "^26.6.2" + jest-leak-detector "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + jest-runtime "^26.6.3" + jest-util "^26.6.2" + jest-worker "^26.6.2" + source-map-support "^0.5.6" + throat "^5.0.0" + +jest-runtime@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" + integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/globals" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + cjs-module-lexer "^0.6.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.4" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + slash "^3.0.0" + strip-bom "^4.0.0" + yargs "^15.4.1" + +jest-serializer@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" + integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.4" + +jest-snapshot@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" + integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.0.0" + chalk "^4.0.0" + expect "^26.6.2" + graceful-fs "^4.2.4" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + jest-haste-map "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + natural-compare "^1.4.0" + pretty-format "^26.6.2" + semver "^7.3.2" + +jest-util@^26.1.0, jest-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + +jest-validate@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" + integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== + dependencies: + "@jest/types" "^26.6.2" + camelcase "^6.0.0" + chalk "^4.0.0" + jest-get-type "^26.3.0" + leven "^3.1.0" + pretty-format "^26.6.2" + +jest-watcher@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" + integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== + dependencies: + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^26.6.2" + string-length "^4.0.1" + +jest-worker@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^26.5.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" + integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== + dependencies: + "@jest/core" "^26.6.3" + import-local "^3.0.2" + jest-cli "^26.6.3" + +joi@^17.3.0: + version "17.13.3" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec" + integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== + dependencies: + "@hapi/hoek" "^9.3.0" + "@hapi/topo" "^5.1.0" + "@sideway/address" "^4.1.5" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsdom@^16.4.0: + version "16.7.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" + integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== + dependencies: + abab "^2.0.5" + acorn "^8.2.4" + acorn-globals "^6.0.0" + cssom "^0.4.4" + cssstyle "^2.3.0" + data-urls "^2.0.0" + decimal.js "^10.2.1" + domexception "^2.0.1" + escodegen "^2.0.0" + form-data "^3.0.0" + html-encoding-sniffer "^2.0.1" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^2.0.0" + webidl-conversions "^6.1.0" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.5.0" + ws "^7.4.6" + xml-name-validator "^3.0.0" + +jsep@^0.3.0: + version "0.3.5" + resolved "https://registry.yarnpkg.com/jsep/-/jsep-0.3.5.tgz#3fd79ebd92f6f434e4857d5272aaeef7d948264d" + integrity sha512-AoRLBDc6JNnKjNcmonituEABS5bcfqDhQAWWXNTFrqu6nVXBpBAGfcoTGZMFlIrh9FjmE1CQyX9CTNwZrXMMDA== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stringify-safe@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@2.x, json5@^2.1.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +json5@^1.0.1, json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +jsonc-parser@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonwebtoken@8.5.1, jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + +jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + +jwa@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" + integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jwa@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + +jwt-simple@^0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/jwt-simple/-/jwt-simple-0.5.6.tgz#3357adec55b26547114157be66748995b75b333a" + integrity sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw== + dependencies: + is-buffer "^1.1.5" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@4.17.21, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +long@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@6.0.0, lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +macos-release@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.1.tgz#bccac4a8f7b93163a8d163b8ebf385b3c5f55bf9" + integrity sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A== + +magic-string@0.25.7: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@1.x, make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w== + dependencies: + object-visit "^1.0.0" + +mapbox@^1.0.0-beta10: + version "1.0.0-beta10" + resolved "https://registry.yarnpkg.com/mapbox/-/mapbox-1.0.0-beta10.tgz#037561bcb95cbdc066d1dff7e2ce1d6c8539cb8e" + integrity sha512-2goOXWE4r7dIYlol0xrReJD1s4l8rTBVTQ2/pcZv5tLnxPWIh8RT3cEDN3cDzDQMpsbQhgNgd7icRZSBL9WkxQ== + dependencies: + es6-promise "^4.0.5" + rest "^2.0.0" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +md5@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.0, micromatch@^4.0.2: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0, mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@1.x, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +moment-timezone@^0.5.x: + version "0.5.48" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.48.tgz#111727bb274734a518ae154b5ca589283f058967" + integrity sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw== + dependencies: + moment "^2.29.4" + +moment@>=2.14.0, moment@^2.29.4: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multer@1.4.4-lts.1: + version "1.4.4-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4-lts.1.tgz#24100f701a4611211cfae94ae16ea39bb314e04d" + integrity sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + +multimatch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" + integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +mz@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nan@^2.14.2: + version "2.22.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb" + integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ== + +nanoid@^3.1.12: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nestjs-twilio@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/nestjs-twilio/-/nestjs-twilio-2.2.1.tgz#ae4329eac751baea4ea7ee5dad9217eb75eb0e0b" + integrity sha512-Om7Wz1IY81sVwqhfkBwD/L+B37QyteI3PP4RmY6fK7EopQEfsKHzVvLpavItybNs5qshMfVkJlRAbKL5hBDwgw== + +nestjs-typeorm-paginate@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/nestjs-typeorm-paginate/-/nestjs-typeorm-paginate-3.2.1.tgz#448aa22bb690d086043db3d7ca2b0441d0c81f42" + integrity sha512-AwmGQ4nncmVzK/T0Woaa/GOeUIAqUx63I+oz5moUe1ax8JWJfkF4iaZ32reiE72KOFhIMKqe8rL7MjJiz+Ed9A== + +newrelic@7.5.1: + version "7.5.1" + resolved "https://registry.yarnpkg.com/newrelic/-/newrelic-7.5.1.tgz#3ce7298a2ba3fb4ddd06296184134e4a38b986b2" + integrity sha512-l4vUEVvhyVf2c9KE3YFcSvtwe4ByGsEbA/Qzy4iotUcAp6DabsfLzHgQSNYa8CvCnB7MchMYJYvZg2HeMnzRrQ== + dependencies: + "@grpc/grpc-js" "^1.2.11" + "@grpc/proto-loader" "^0.5.6" + "@newrelic/aws-sdk" "^3.1.0" + "@newrelic/koa" "^5.0.0" + "@newrelic/superagent" "^4.0.0" + "@tyriar/fibonacci-heap" "^2.0.7" + async "^3.2.0" + concat-stream "^2.0.0" + https-proxy-agent "^4.0.0" + json-stringify-safe "^5.0.0" + readable-stream "^3.6.0" + semver "^5.3.0" + optionalDependencies: + "@newrelic/native-metrics" "^6.0.0" + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-emoji@1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== + dependencies: + lodash "^4.17.21" + +node-fetch@^2.6.1, node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-notifier@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.2.tgz#f3167a38ef0d2c8a866a83e318c1ba0efeb702c5" + integrity sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg== + dependencies: + growly "^1.3.0" + is-wsl "^2.2.0" + semver "^7.3.2" + shellwords "^0.1.1" + uuid "^8.3.0" + which "^2.0.2" + +node-polyglot@^2.4.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-polyglot/-/node-polyglot-2.6.0.tgz#3d5889664253d90babc0fcd3c12ae0ac7b98289f" + integrity sha512-ZZFkaYzIfGfBvSM6QhA9dM8EEaUJOVewzGSRcXWbJELXDj0lajAtKaENCYxvF5yE+TgHg6NQb0CmgYMsMdcNJQ== + dependencies: + hasown "^2.0.2" + object.entries "^1.1.8" + warning "^4.0.3" + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w== + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== + dependencies: + path-key "^2.0.0" + +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nwsapi@^2.2.0: + version "2.2.20" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.20.tgz#22e53253c61e7b0e7e93cef42c891154bcca11ef" + integrity sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA== + +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ== + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-hash@3.0.0, object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA== + dependencies: + isobject "^3.0.0" + +object.entries@^1.1.8: + version "1.1.9" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" + integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-object-atoms "^1.1.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ== + dependencies: + isobject "^3.0.1" + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +ora@5.4.1, ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +os-name@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/os-name/-/os-name-4.0.1.tgz#32cee7823de85a8897647ba4d76db46bf845e555" + integrity sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw== + dependencies: + macos-release "^2.5.0" + windows-release "^4.0.0" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +p-each-series@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" + integrity sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA== + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse5-htmlparser2-tree-adapter@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" + +parse5@6.0.1, parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + +parse5@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" + integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== + +passport-custom@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/passport-custom/-/passport-custom-1.1.1.tgz#71db3d7ec1d7d0085e8768507f61b26d88051c0a" + integrity sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg== + dependencies: + passport-strategy "1.x.x" + +passport-jwt@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-local@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" + integrity sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow== + dependencies: + passport-strategy "1.x.x" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270" + integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-to-regexp@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" + integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + +pg-cloudflare@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz#a1f3d226bab2c45ae75ea54d65ec05ac6cfafbef" + integrity sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg== + +pg-connection-string@^2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.9.1.tgz#bb1fd0011e2eb76ac17360dc8fa183b2d3465238" + integrity sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.10.1.tgz#481047c720be2d624792100cac1816f8850d31b2" + integrity sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg== + +pg-protocol@^1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.10.3.tgz#ac9e4778ad3f84d0c5670583bab976ea0a34f69f" + integrity sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ== + +pg-types@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^8.4.1: + version "8.16.3" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.16.3.tgz#160741d0b44fdf64680e45374b06d632e86c99fd" + integrity sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw== + dependencies: + pg-connection-string "^2.9.1" + pg-pool "^3.10.1" + pg-protocol "^1.10.3" + pg-types "2.2.0" + pgpass "1.0.5" + optionalDependencies: + pg-cloudflare "^1.2.7" + +pgpass@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" + integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pluralize@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +pop-iterate@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pop-iterate/-/pop-iterate-1.0.1.tgz#ceacfdab4abf353d7a0f2aaa2c1fc7b3f9413ba3" + integrity sha512-HRCx4+KJE30JhX84wBN4+vja9bNfysxg1y28l0DuJmkoaICiv2ZSilKddbS48pq50P8d2erAhqDLbp47yv3MbQ== + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +prettier@^1.15.2: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== + +pretty-format@^26.0.0, pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + +printj@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" + integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +proto3-json-serializer@^0.1.8: + version "0.1.9" + resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-0.1.9.tgz#705ddb41b009dd3e6fcd8123edd72926abf65a34" + integrity sha512-A60IisqvnuI45qNRygJjrnNjX2TMdQGMY+57tR3nul3ZgO2zXkR9OGR8AXxJhkqx84g0FTnrfi3D5fWMSdANdQ== + dependencies: + protobufjs "^6.11.2" + +protobufjs@6.11.3: + version "6.11.3" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74" + integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + +protobufjs@^6.11.2, protobufjs@^6.11.3, protobufjs@^6.8.6, protobufjs@^6.8.8: + version "6.11.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa" + integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + +protobufjs@^7.2.5: + version "7.5.3" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.3.tgz#13f95a9e3c84669995ec3652db2ac2fb00b89363" + integrity sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== + +psl@^1.1.33: + version "1.15.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6" + integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== + dependencies: + punycode "^2.3.1" + +pump@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" + integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +q@2.0.x: + version "2.0.3" + resolved "https://registry.yarnpkg.com/q/-/q-2.0.3.tgz#75b8db0255a1a5af82f58c3f3aaa1efec7d0d134" + integrity sha512-gv6vLGcmAOg96/fgo3d9tvA4dJNZL3fMyBqVRrGxQ+Q/o4k9QzbJ3NQF9cOO/71wRodoXhaPgphvMFU68qVAJQ== + dependencies: + asap "^2.0.0" + pop-iterate "^1.0.1" + weak-map "^1.0.5" + +q@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== + +qs@6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +qs@^6.5.1, qs@^6.9.4: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + dependencies: + side-channel "^1.1.0" + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +readable-stream@^2.0.1, readable-stream@^2.2.2, readable-stream@^2.3.5, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + +reflect-metadata@0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + +reflect-metadata@^0.1.13: + version "0.1.14" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" + integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== + +repeat-element@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== + +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.18.1: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rest@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/rest/-/rest-2.0.0.tgz#6dfadf66a405c49cfbd5b4bd25b59fd29cd861bc" + integrity sha512-MmTKuTuLVARHNnpL1jJjohscJcY3vsaKNz+b2J5v32BcjIVyimKrNv8r8uygeFp9gf8Cm1erhxypdzRFFEPZOA== + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +retry-request@^4.0.0, retry-request@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.2.2.tgz#b7d82210b6d2651ed249ba3497f07ea602f1a903" + integrity sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg== + dependencies: + debug "^4.1.1" + extend "^3.0.2" + +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rootpath@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/rootpath/-/rootpath-0.1.2.tgz#5b379a87dca906e9b91d690a599439bef267ea6b" + integrity sha512-R3wLbuAYejpxQjL/SjXo1Cjv4wcJECnMRT/FlcCfTwCBhaji9rWaRCoVEQ1SPiTJ4kKK+yh+bZLAV7SCafoDDw== + +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +rxjs@6.6.7, rxjs@^6.6.0: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +rxjs@^7.2.0, rxjs@^7.5.4: + version "7.8.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" + integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== + dependencies: + tslib "^2.1.0" + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg== + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + +sax@>=0.6.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== + +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + +schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +scmp@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/scmp/-/scmp-2.1.0.tgz#37b8e197c425bdeb570ab91cc356b311a11f9c9a" + integrity sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q== + +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + +semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sha.js@^2.4.11: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shelljs@0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.4, side-channel@^1.0.6, side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@0.5.21, source-map-support@^0.5.6, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + +source-map@0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.3: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +spdx-correct@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" + integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.21" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz#6d6e980c9df2b6fc905343a3b2d702a6239536c3" + integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg== + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +ssf@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" + integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== + dependencies: + frac "~1.1.2" + +stack-utils@^2.0.2: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g== + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + +stream-shift@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strnum@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.1.2.tgz#57bca4fbaa6f271081715dbc9ed7cee5493e28e4" + integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA== + +structured-log@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/structured-log/-/structured-log-0.2.0.tgz#b9be1794c39d6399f265666b84635a3307611c5b" + integrity sha512-W3Tps8PN5Mon37955/wuZZSXwBXiB52AUnd/oPVdmo+O+mLkr2fNajV6821gJ8irrgVQx3gYOqSWPMgj7Dy3Yg== + +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + +superagent@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" + integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.2.0" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.3.5" + +supertest@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-4.0.2.tgz#c2234dbdd6dc79b6f15b99c8d6577b90e4ce3f36" + integrity sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ== + dependencies: + methods "^1.1.2" + superagent "^3.8.3" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" + integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +swagger-axios-codegen@0.11.16: + version "0.11.16" + resolved "https://registry.yarnpkg.com/swagger-axios-codegen/-/swagger-axios-codegen-0.11.16.tgz#eae7dd2ad254eaac485db2dde558958f70437f46" + integrity sha512-OB5cvRSdfPsGaws/Y1YuNYG7/cGSp5PLc36294hUsFiP57cDdC5sxdXUnHcy00+6hpCe/1Y9vcGaouvevt4jeQ== + dependencies: + axios "^0.21.1" + camelcase "^5.0.0" + multimatch "^4.0.0" + pascalcase "^0.1.1" + prettier "^1.15.2" + structured-log "^0.2.0" + +swagger-ui-dist@>=4.11.0: + version "5.25.3" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.25.3.tgz#5eb728d9d1e481dfad2980839ab06fcbaaae6dc2" + integrity sha512-mqWJAhfl8mhVKJezwszUqRJAlrvKG/22am5xRUWzr7ya0MFaFBAAd7Nm+tD4BdKnVx7KRWkWYJMYRkFm5a8iTg== + dependencies: + "@scarf/scarf" "=1.4.0" + +swagger-ui-express@^4.1.4: + version "4.6.3" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-4.6.3.tgz#870d0892654fe80e6970a2d680e22521acd2dc19" + integrity sha512-CDje4PndhTD2HkgyKH3pab+LKspDeB/NhPN2OF1j+piYIamQqBYwAXWESOT1Yju2xFg51bRW9sUng2WxDjzArw== + dependencies: + swagger-ui-dist ">=4.11.0" + +symbol-observable@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tapable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.2.tgz#ab4984340d30cb9989a490032f086dbb8b56d872" + integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg== + +teeny-request@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-7.2.0.tgz#41347ece068f08d741e7b86df38a4498208b2633" + integrity sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.1" + stream-events "^1.0.5" + uuid "^8.0.0" + +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +terser-webpack-plugin@^5.1.3: + version "5.3.14" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" + +terser@^5.31.1: + version "5.43.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.43.1.tgz#88387f4f9794ff1a29e7ad61fb2932e25b4fdb6d" + integrity sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.14.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +throat@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" + integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg== + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg== + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tough-cookie@^4.0.0: + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tree-kill@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +ts-jest@26.4.1: + version "26.4.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.1.tgz#08ec0d3fc2c3a39e4a46eae5610b69fafa6babd0" + integrity sha512-F4aFq01aS6mnAAa0DljNmKr/Kk9y4HVZ1m6/rtJ0ED56cuxINGq3Q9eVAh+z5vcYKe5qnTMvv90vE8vUMFxomg== + dependencies: + "@types/jest" "26.x" + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + jest-util "^26.1.0" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + mkdirp "1.x" + semver "7.x" + yargs-parser "20.x" + +ts-loader@^8.0.4: + version "8.4.0" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.4.0.tgz#e845ea0f38d140bdc3d7d60293ca18d12ff2720f" + integrity sha512-6nFY3IZ2//mrPc+ImY3hNWx1vCHyEhl6V+wLmL4CZcm6g1CqX7UKrkc6y0i4FwcfOhxyMPCfaEvh20f4r9GNpw== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^4.0.0" + loader-utils "^2.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + +ts-node@10.8.0: + version "10.8.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.0.tgz#3ceb5ac3e67ae8025c1950626aafbdecb55d82ce" + integrity sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tsconfig-paths-webpack-plugin@3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.2.tgz#01aafff59130c04a8c4ebc96a3045c43c376449a" + integrity sha512-EhnfjHbzm5IYI9YPNVIxx1moxMI4bpHD2e0zTXeDNQcwjjRaGepP7IhTHJkyDBG0CAOoxRfe7jCG630Ou+C6Pw== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^3.9.0" + +tsconfig-paths@3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsconfig-paths@^3.9.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +tslib@>=1.9.0, tslib@^2.1.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +twilio@^3.71.3: + version "3.84.1" + resolved "https://registry.yarnpkg.com/twilio/-/twilio-3.84.1.tgz#b7a64db440240d3a06cc591fd339b81ca9b70db0" + integrity sha512-Q/xaPoayTj+bgJdnUgpE+EiB/VoNOG+byDFdlDej0FgxiHLgXKliZfVv6boqHPWvC1k7Dt0AK96OBFZ0P55QQg== + dependencies: + axios "^0.26.1" + dayjs "^1.8.29" + https-proxy-agent "^5.0.0" + jsonwebtoken "^8.5.1" + lodash "^4.17.21" + q "2.0.x" + qs "^6.9.4" + rootpath "^0.1.2" + scmp "^2.1.0" + url-parse "^1.5.9" + xmlbuilder "^13.0.2" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +typeorm-naming-strategies@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/typeorm-naming-strategies/-/typeorm-naming-strategies-1.3.0.tgz#82dd4e063bf6cc22fc3da1ce0d29854899c89c76" + integrity sha512-dxauvlpG7vhngNlgf9aTXlMeAA6IMxNkoaaffpP1EYF5rCy69tZGo0G9Y5P7ZaWqzlY2zx/ep3ilODdIFuqjPA== + +typeorm@0.2.41: + version "0.2.41" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.41.tgz#88758101ac158dc0a0a903d70eaacea2974281cc" + integrity sha512-/d8CLJJxKPgsnrZWiMyPI0rz2MFZnBQrnQ5XP3Vu3mswv2WPexb58QM6BEtmRmlTMYN5KFWUz8SKluze+wS9xw== + dependencies: + "@sqltools/formatter" "^1.2.2" + app-root-path "^3.0.0" + buffer "^6.0.3" + chalk "^4.1.0" + cli-highlight "^2.1.11" + debug "^4.3.1" + dotenv "^8.2.0" + glob "^7.1.6" + js-yaml "^4.0.0" + mkdirp "^1.0.4" + reflect-metadata "^0.1.13" + sha.js "^2.4.11" + tslib "^2.1.0" + xml2js "^0.4.23" + yargs "^17.0.1" + zen-observable-ts "^1.0.0" + +typescript@4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + +uglify-js@^3.1.4: + version "3.19.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== + +undici-types@~7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294" + integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ== + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg== + +url-parse@^1.5.3, url-parse@^1.5.9: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@8.3.2, uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-to-istanbul@^7.0.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1" + integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + source-map "^0.7.3" + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +validator@13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.0.0.tgz#0fb6c6bb5218ea23d368a8347e6d0f5a70e3bcab" + integrity sha512-anYx5fURbgF04lQV18nEQWZ/3wHGnxiKdG4aL8J+jEDsm98n/sU/bey+tYk6tnGJzm7ioh5FoqrAiQ6m03IgaA== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +w3c-hr-time@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== + dependencies: + browser-process-hrtime "^1.0.0" + +w3c-xmlserializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" + integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== + dependencies: + xml-name-validator "^3.0.0" + +walker@^1.0.7, walker@~1.0.5: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + +watchpack@^2.3.1: + version "2.4.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947" + integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +weak-map@^1.0.5: + version "1.0.8" + resolved "https://registry.yarnpkg.com/weak-map/-/weak-map-1.0.8.tgz#394c18a9e8262e790544ed8b55c6a4ddad1cb1a3" + integrity sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +webpack-node-externals@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#1a3407c158d547a9feb4229a9e3385b7b60c9917" + integrity sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ== + +webpack-sources@^3.2.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" + integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== + +webpack@5.73.0: + version "5.73.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38" + integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.4.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.9.3" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.3.1" + webpack-sources "^3.2.3" + +whatwg-encoding@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-mimetype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +whatwg-url@^8.0.0, whatwg-url@^8.5.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== + dependencies: + lodash "^4.7.0" + tr46 "^2.1.0" + webidl-conversions "^6.1.0" + +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +windows-release@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-4.0.0.tgz#4725ec70217d1bf6e02c7772413b29cdde9ec377" + integrity sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg== + dependencies: + execa "^4.0.2" + +wmf@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" + integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== + +word@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" + integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +ws@^7.4.6: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +xlsx@^0.17.4: + version "0.17.5" + resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.17.5.tgz#78b788fcfc0773d126cdcd7ea069cb7527c1ce81" + integrity sha512-lXNU0TuYsvElzvtI6O7WIVb9Zar1XYw7Xb3VAx2wn8N/n0whBYrCnHMxtFyIiUU1Wjf09WzmLALDfBO5PqTb1g== + dependencies: + adler-32 "~1.2.0" + cfb "^1.1.4" + codepage "~1.15.0" + crc-32 "~1.2.0" + ssf "~0.11.2" + wmf "~1.0.1" + word "~0.3.0" + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xml2js@^0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@^13.0.2: + version "13.0.2" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-13.0.2.tgz#02ae33614b6a047d1c32b5389c1fdacb2bce47a7" + integrity sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ== + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@20.x, yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + +yargs@^16.0.0, yargs@^16.0.3, yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^17.0.1, yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +zen-observable-ts@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz#2d1aa9d79b87058e9b75698b92791c1838551f83" + integrity sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA== + dependencies: + "@types/zen-observable" "0.8.3" + zen-observable "0.8.15" + +zen-observable@0.8.15: + version "0.8.15" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" + integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== diff --git a/backend/proxy/Dockerfile b/backend/proxy/Dockerfile index 2efe0e2724..8004436039 100644 --- a/backend/proxy/Dockerfile +++ b/backend/proxy/Dockerfile @@ -1,4 +1,47 @@ -FROM nginx -COPY ./default.conf /etc/nginx/conf.d/default.conf -CMD /bin/bash -c "envsubst '\$PORT \$BACKEND_V1_HOSTNAME \$BACKEND_V2_HOSTNAME' < /etc/nginx/conf.d/default.conf > /etc/nginx/conf.d/default.conf" && nginx -g 'daemon off;' +FROM nginx:1.11 +MAINTAINER David Galoyan + +ENV NGX_CACHE_PURGE_VERSION=2.4.1 + +# Install basic packages and build tools +RUN apt-get update && \ + apt-get install --no-install-recommends --no-install-suggests -y \ + wget \ + build-essential \ + libssl-dev \ + libpcre3 \ + zlib1g \ + zlib1g-dev \ + libpcre3-dev && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# download and extract sources +RUN NGINX_VERSION=`nginx -V 2>&1 | grep "nginx version" | awk -F/ '{ print $2}'` && \ + cd /tmp && \ + wget http://nginx.org/download/nginx-$NGINX_VERSION.tar.gz && \ + wget https://github.com/nginx-modules/ngx_cache_purge/archive/$NGX_CACHE_PURGE_VERSION.tar.gz \ + -O ngx_cache_purge-$NGX_CACHE_PURGE_VERSION.tar.gz && \ + tar -xf nginx-$NGINX_VERSION.tar.gz && \ + mv nginx-$NGINX_VERSION nginx && \ + rm nginx-$NGINX_VERSION.tar.gz && \ + tar -xf ngx_cache_purge-$NGX_CACHE_PURGE_VERSION.tar.gz && \ + mv ngx_cache_purge-$NGX_CACHE_PURGE_VERSION ngx_cache_purge && \ + rm ngx_cache_purge-$NGX_CACHE_PURGE_VERSION.tar.gz + +## move copy to here so the above can build from cache +COPY ./default.conf /etc/nginx/conf.d/default.conf.template +COPY ./shared-location.conf /etc/nginx/conf.d/shared-location.conf.template +COPY ./proxy.conf /etc/nginx/conf.d/proxy.conf + +# configure and build +RUN cd /tmp/nginx && \ + BASE_CONFIGURE_ARGS=`nginx -V 2>&1 | grep "configure arguments" | cut -d " " -f 3-` && \ + /bin/sh -c "./configure ${BASE_CONFIGURE_ARGS} --add-module=/tmp/ngx_cache_purge" && \ + make && make install && \ + rm -rf /tmp/nginx* \ + +ENV PROTOCOL=https + +CMD /bin/bash -c "envsubst '\$PORT,\$BACKEND_HOSTNAME,\$PROTOCOL' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && envsubst '\$PORT,\$BACKEND_HOSTNAME,\$PROTOCOL' < /etc/nginx/conf.d/shared-location.conf.template > /etc/nginx/shared-location.conf" && nginx -g 'daemon off;' diff --git a/backend/proxy/default.conf b/backend/proxy/default.conf index 996387cda7..aca8a7eecd 100644 --- a/backend/proxy/default.conf +++ b/backend/proxy/default.conf @@ -1,14 +1,44 @@ +proxy_cache_path /tmp/cache_nginx/ levels=1:2 keys_zone=webapp_cache:10m max_size=10g inactive=1440 use_temp_path=off; + +log_format upstreamlog '[$time_local] $remote_addr - $remote_user - $server_name $host to: $upstream_addr: $request $status upstream_response_time $upstream_response_time msec $msec request_time $request_time'; + server { listen $PORT; - location /v2 { - rewrite ^/v2(/.*) $1 break; - proxy_pass https://$BACKEND_V2_HOSTNAME; - proxy_ssl_server_name on; + proxy_set_header Host $BACKEND_HOSTNAME; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + access_log /var/log/nginx/access.log upstreamlog; + error_log /var/log/nginx/error.log info; + proxy_ssl_server_name on; + proxy_cache_purge PURGE from 0.0.0.0/0; + proxy_cache_valid 200 24h; + proxy_cache_valid 404 15s; + proxy_cache_valid 500 0s; + + location ~* "\/listings\/[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}" { + include shared-location.conf; + } + + location /listings { + include shared-location.conf; + } + + location /jurisdictions { + include shared-location.conf; + } + + location ~* "\/jurisdictions\/[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}" { + include shared-location.conf; + } + + location ~* "\/jurisdictions\/byName/.*" { + include shared-location.conf; } location / { - proxy_pass https://$BACKEND_V1_HOSTNAME; - proxy_ssl_server_name on; + proxy_pass $PROTOCOL://$BACKEND_HOSTNAME; } } diff --git a/backend/proxy/proxy.conf b/backend/proxy/proxy.conf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/proxy/shared-location.conf b/backend/proxy/shared-location.conf new file mode 100644 index 0000000000..c2b2b819fb --- /dev/null +++ b/backend/proxy/shared-location.conf @@ -0,0 +1,16 @@ + proxy_cache_min_uses 1; + proxy_cache_revalidate on; + proxy_cache_background_update on; + proxy_cache_lock on; + proxy_ssl_server_name on; + proxy_cache webapp_cache; + proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; + proxy_cache_key $uri$is_args$args$http_language; + if ($request_method = 'PURGE') { + # TODO: make vairable that's passed in for allow origin purge + add_header Access-Control-Allow-Origin *; + } + add_header X-Cache-Status $upstream_cache_status; + add_header Access-Control-Allow-Headers 'Content-Type, X-Language, X-JurisdictionName, Authorization'; + add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE, PURGE'; + proxy_pass $PROTOCOL://$BACKEND_HOSTNAME; diff --git a/bin/.env.template b/bin/.env.template new file mode 100644 index 0000000000..5d793b2b4f --- /dev/null +++ b/bin/.env.template @@ -0,0 +1,4 @@ +INSTANCE_CONNECTION_NAME=gcp:database:instance +PGUSER=postgres-user +PGPASSWORD=postgres-user-password +DATABASE_URL=postgres://$PGUSER:$PGPASSWORD@localhost:5432/bloom diff --git a/bin/run_prod_database_migration.sh b/bin/run_prod_database_migration.sh new file mode 100755 index 0000000000..317a76e1a0 --- /dev/null +++ b/bin/run_prod_database_migration.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +if [ -f ./.env ]; then + . ./.env +else + exit 1 ".env does not exist" +fi + +wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy +chmod +x cloud_sql_proxy +./cloud_sql_proxy -instances=$INSTANCE_CONNECTION_NAME=tcp:5432 & + +sleep 4s + +cd ../backend/core + +# Override the DATABASE_URL variable in backend/core/.env. +echo "DATABASE_URL=$DATABASE_URL" >> .env +yarn db:migration:run +# Now remove the last line. +head -n -1 .env | tee .env > /dev/null + +cd - +kill %1 +rm ./cloud_sql_proxy diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..2d2bec7c35 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,104 @@ +version: "3.7" + +services: + sites-public: + container_name: sites-public + build: + context: . + dockerfile: Dockerfile.sites-public + target: development + volumes: + - ./sites/public:/usr/src/app/sites/public + - /usr/src/app/node_modules + - /usr/src/app/sites/public/node_modules + ports: + # TODO: configure via .env separate from ./sites/public/.env + - "3000:3000" + env_file: + - ./sites/public/.env + environment: + # The URLs are different here because requests made using BACKEND_API_BASE are done from + # the NextJS server, which must address other containers by container name. Requests made + # using INCOMING_HOOK_BODY are done from the client side browser and must use localhost. + BACKEND_API_BASE: "http://backend-core:3100" + INCOMING_HOOK_BODY: "http://localhost:3100" + NEXTJS_PORT: "3000" + command: yarn dev + depends_on: + - backend-core + networks: + - frontend + sites-partners: + container_name: sites-partners + build: + context: . + dockerfile: Dockerfile.sites-partners + target: development + volumes: + - ./sites/partners:/usr/src/app/sites/partners + - /usr/src/app/node_modules + - /usr/src/app/sites/partners/node_modules + ports: + # TODO: configure via .env separate from ./sites/partners/.env + - "3001:3001" + env_file: + - ./sites/partners/.env + environment: + # Using this as the BASE works here because all requests are sent from the client's browser + # (not the NextJS server). + BACKEND_API_BASE: "http://localhost:3100" + NEXTJS_PORT: "3001" + # yarn dev uses a separate node debugger port + command: yarn next -p 3001 + depends_on: + - backend-core + networks: + - frontend + backend-core: + container_name: backend-core + build: + context: ./backend/core + target: development + volumes: + - ./backend/core:/usr/src/app + - /usr/src/app/node_modules + ports: + # TODO: configure 3100 via .env separate from ./backend/core/.env + - "3100:3100" + # This is the debug port. + - "9229:9229" + networks: + - frontend + - backend + command: /bin/sh -c "yarn db:migration:run && yarn nest start --debug" + env_file: + - ./backend/core/.env + # Override database connections to point to the container instead of localhost. + environment: + POSTGRES_USER: "postgres" + DATABASE_URL: "postgres://postgres:5432/bloom" + PGUSER: "postgres" + PGPASSWORD: "postgres" + PGDATABASE: "bloom" + depends_on: + - postgres + postgres: + container_name: postgres + image: postgres:13 + environment: + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "postgres" + POSTGRES_DB: "bloom" + PG_DATA: /var/lib/postgresql/data + ports: + - "5432:5432" + networks: + - backend + volumes: + - pgdata:/var/lib/postgresql/data + - /var/run/postgresql:/var/run/postgresql +volumes: + pgdata: +networks: + frontend: + backend: diff --git a/docs/Authentication.md b/docs/Authentication.md index 8d2827b3fa..9ae8e7ecd3 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -27,14 +27,14 @@ export class MyController { ``` Using the `DefaultAuthGuard` in this way requires the client to provide a valid JWT token as an -`Authentication` header using the standard `Bearer ` format. Tokens are checked for a valid signature and +`Authorization` header using the standard `Bearer ` format. Tokens are checked for a valid signature and valid expiry time (currently 10 minutes). Tokens may also be revoked by adding an entry to `revoked_tokens` table, or using the auth route `revoke_token`. ## Obtain a token To obtain a token, a user must first login. Currently, an email/password strategy is the only way to do this. A -client can `POST /auth/login` with body `{ username, password }`. This request will either return 401 or 200 with an +client can `POST /auth/login` with body `{ email, password }`. This request will either return 401 or 200 with an object containing `accessToken`. To renew a token, `POST /auth/token` with an existing valid token. diff --git a/docs/DeployServicesHeroku.md b/docs/DeployServicesHeroku.md index 0cf5b31f83..a4d59a0e03 100644 --- a/docs/DeployServicesHeroku.md +++ b/docs/DeployServicesHeroku.md @@ -2,10 +2,38 @@ Bloom is designed to use a set of independently run services that provide the data and business logic processing needed by the front-end apps. While the Bloom architecture accomodates services built and operated in a variety of environments, the reference implementation includes services that can be easily run within the [Heroku PaaS environment](https://www.heroku.com/). -## Monorepo Buildpack +## Resources + +- [Heroku Postgres](https://www.heroku.com/postgres) + +## Heroku Buildpacks + +### Monorepo Buildpack Since the Bloom repository uses a monorepo layout, all Heroku services must use the [monorepo buildpack](https://elements.heroku.com/buildpacks/lstoll/heroku-buildpack-monorepo). +### Node.js Buildpack + +Bloom's backend runs on Node.js and Heroku must be setup with [Heroku Buildpack for Node.js](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-nodejs). + ## Procfile +release: yarn herokusetup + +web: yarn start + ## Environment Variables + +APP_BASE=backend/core + +APP_SECRET='YOUR-LONG-SECRET-KEY' + +CLOUDINARY_SECRET= + +CLOUDINARY_KEY= + +DATABASE_URL= + +EMAIL_API_KEY='SENDGRID-API-KEY' + +PARTNERS_BASE_URL='PARTNER-PORTAL-URL' diff --git a/docs/Styling.md b/docs/Styling.md deleted file mode 100644 index d3aa7c0d7f..0000000000 --- a/docs/Styling.md +++ /dev/null @@ -1,15 +0,0 @@ -# Understand and Customizing the Bloom Look and Feel - -This document provides an overview of how to customize a specific Bloom implementation, as well as how to customize the core components. - -## CSS Frameworks - -Bloom relies on the [Tailwind CSS framework](https://tailwindcss.com/) for most component styling and utility classes. - -## SASS Structure - -## Customization Points - -## CSS Naming Conventions - -## Build Process diff --git a/docs/pull_request_template.md b/docs/pull_request_template.md index 28b90994c1..6a70ee0d76 100644 --- a/docs/pull_request_template.md +++ b/docs/pull_request_template.md @@ -1,8 +1,8 @@ # Pull Request Template -## Issue +## Issue Overview -Addresses #issue +This PR addresses #issue - [ ] This change addresses the issue in full - [ ] This change addresses only certain aspects of the issue @@ -13,16 +13,6 @@ Addresses #issue Please include a summary of the change and which issue(s) is addressed. Please also include relevant motivation and context. List any dependencies that are required for this change. -## Type of change - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Prototype/POC (not to merge) -- [ ] This change is a refactor/addresses technical debt -- [ ] This change requires a documentation update -- [ ] This change requires a SQL Script - ## How Can This Be Tested/Reviewed? Provide instructions so we can review. @@ -32,6 +22,7 @@ Describe the tests that you ran to verify your changes. Please also list any rel ## Checklist: - [ ] My code follows the style guidelines of this project +- [ ] I have added QA notes to the issue with applicable URLs - [ ] I have performed a self-review of my own code - [ ] I have reviewed the changes in a desktop view - [ ] I have reviewed the changes in a mobile view @@ -42,6 +33,20 @@ Describe the tests that you ran to verify your changes. Please also list any rel - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules - [ ] I have assigned reviewers -- [ ] I have updated the changelog to include a description of my changes -- [ ] I have run `yarn generate:client` if I made backend changes -- [ ] I have exported any new pieces in ui-components +- [ ] I have run `yarn generate:client` and/or created a migration if I made backend changes that require them +- [ ] My commit message(s) is/are polished, and any breaking changes are indicated in the message and are well-described +- [ ] Commits made across packages purposefully have the same commit message/version change, else are separated into different commits + +## Reviewer Notes: + +Steps to review a PR: + +- Read and understand the issue, and ensure the author has added QA notes +- Review the code itself from a style point of view +- Pull the changes down locally and test that the acceptance criteria is met +- Also review the acceptance criteria on the Netlify deploy preview (noting that these do not yet include any backend changes made in the PR) +- Either explicitly ask a clarifying question, request changes, or approve the PR if there are small remaining changes but the PR is otherwise good to go + +## On Merge: + +If you have one commit and message, squash. If you need each message to be applied, rebase and merge. diff --git a/lerna.json b/lerna.json index 4b1c280a2d..2e0524f09b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,16 @@ { - "packages": ["sites/public", "sites/partners", "backend/core", "shared-helpers", "ui-components"], - "version": "1.0.5", + "packages": ["sites/public", "sites/partners", "backend/core", "shared-helpers"], + "version": "independent", "npmClient": "yarn", - "useWorkspaces": true + "useWorkspaces": true, + "command": { + "publish": { + "conventionalCommits": true, + "message": "chore(release): publish" + }, + "version": { + "conventionalCommits": true, + "message": "chore(release): version" + } + } } diff --git a/package.json b/package.json index 75a22e1ab2..d75a913a15 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,36 @@ { "name": "bloom-housing", - "version": "1.0.5", + "version": "4.4.0", + "author": "Sean Albert ", "description": "Bloom is a system to manage processes for affordable housing", "workspaces": { "packages": [ "sites/public", "sites/partners", "backend/core", - "shared-helpers", - "ui-components" + "shared-helpers" ], "nohoist": [ "**/@anchan828/nest-sendgrid" ] }, - "repository": "https://github.com/Exygy/bloom.git", + "repository": "https://github.com/CityOfDetroit/bloom.git", "license": "Apache-2.0", "private": true, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "scripts": { "dev:app:public": "wait-on \"http-get://localhost:${PORT:-3100}/listings\" && cd sites/public && yarn dev", "test:app:public": "wait-on \"http-get://localhost:${PORT:-3100}/listings\" && cd sites/public && yarn test", "test:app:public:headless": "wait-on \"http-get://localhost:${PORT:-3100}/listings\" && cd sites/public && yarn test:headless", + "test:app:public:unit": "cd sites/public && yarn test:unit", "build:app:public": "cd sites/public && yarn build", "dev:app:partners": "wait-on \"http-get://localhost:${PORT:-3100}/listings\" && cd sites/partners && yarn dev", "test:app:partners": "wait-on \"http-get://localhost:${PORT:-3100}/listings\" && cd sites/partners && yarn test", "test:app:partners:headless": "wait-on \"http-get://localhost:${PORT:-3100}/listings\" && cd sites/partners && yarn test:headless", + "test:app:partners:unit": "cd sites/partners && yarn test:unit", "build:app:partners": "cd sites/partners && yarn build", "dev:backend": "cd backend/core && yarn dev", "dev:all": "concurrently --names \" BACKEND_CORE,APP_PUBLIC,APP_PARTNERS\" --prefix \"{name}\" \"yarn dev:backend\" \"yarn dev:app:public\" \"yarn dev:app:partners\"", @@ -32,21 +38,29 @@ "dev:partners": "concurrently \"yarn dev:backend\" \"yarn dev:app:partners\"", "dev:public": "concurrently \"yarn dev:backend\" \"yarn dev:app:public\"", "test:shared:helpers": "cd shared-helpers && yarn && yarn test", - "test:shared:ui": "cd ui-components && yarn && yarn test", - "test:shared:ui:a11y": "cd ui-components && yarn && yarn test:a11y", "test:backend:core:dbsetup": "cd backend/core && yarn db:migration:run && yarn db:seed", + "test:backend:core:testdbsetup": "cd backend/core && yarn test:db:setup", "test:backend:core": "cd backend/core && yarn test", - "test:e2e:backend:core": "cd backend/core && yarn test:e2e", + "test:e2e:backend:core": "cd backend/core && yarn test:e2e:local", "test:apps": "concurrently \"yarn dev:backend\" \"yarn test:app:public\"", "test:apps:headless": "concurrently \"yarn dev:backend\" \"yarn test:app:public:headless\"", - "lint": "eslint '**/*.ts' '**/*.tsx' '**/*.js'" + "test:public:unit-tests": "cd sites/public && yarn test:unit-tests", + "lint": "eslint '**/*.ts' '**/*.tsx' '**/*.js'", + "db:reseed": "cd backend/core && yarn db:reseed", + "install:all": "yarn install && cd backend/core && yarn install", + "setup": "yarn install:all && yarn db:reseed", + "clean": "rm -rf backend/core/dist/ backend/core/node_modules/ node_modules/ sites/partners/.next/ sites/partners/node_modules/ sites/public/.next/ sites/public/node_modules/" + }, + "dependencies": { + "react-remove-scroll": "2.5.4", + "tailwindcss": "npm:@tailwindcss/postcss7-compat@2.2.10" }, "devDependencies": { "@commitlint/cli": "^13.1.0", "@commitlint/config-conventional": "^13.1.0", "@types/jest": "^26.0.14", - "@typescript-eslint/eslint-plugin": "^4.5.0", - "@typescript-eslint/parser": "^4.5.0", + "@typescript-eslint/eslint-plugin": "^5.12.1", + "@typescript-eslint/parser": "^5.12.1", "commitizen": "^4.2.4", "concurrently": "^5.3.0", "cz-conventional-changelog": "^3.3.0", @@ -58,14 +72,14 @@ "eslint-plugin-react-hooks": "^4.1.2", "husky": "^4.3.0", "jest": "^26.5.3", - "lerna": "^3.22.1", + "lerna": "^4.0.0", "lint-staged": "^10.4.0", "prettier": "^2.1.0", - "react": "^17.0.2", - "react-test-renderer": "^17.0.2", + "react": "18.2.0", + "react-test-renderer": "18.2.0", "rimraf": "^3.0.2", "ts-jest": "^26.4.1", - "typescript": "^3.9.7", + "typescript": "4.6.4", "wait-on": "^5.2.0" }, "prettier": { @@ -75,9 +89,8 @@ }, "husky": { "hooks": { - "pre-commit": "echo 'Linting...' && lint-staged", - "commit-msg": "echo 'Verifying commit message...' && commitlint -E HUSKY_GIT_PARAMS", - "prepare-commit-msg": "echo 'Building commit message...' && exec < /dev/tty && git cz --hook || true" + "pre-commit": "echo '\n(1) Linting...\n----------' && lint-staged", + "commit-msg": "echo '\n(2) Verifying conventional commit format... \n (If this fails, install commitizen and commit with 'git cz' to automate the formatting!)\n----------' && commitlint -E HUSKY_GIT_PARAMS" } }, "lint-staged": { diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/shared-helpers/.jest/setup-tests.js b/shared-helpers/.jest/setup-tests.js index aaf2af2ea1..2e32959c7f 100644 --- a/shared-helpers/.jest/setup-tests.js +++ b/shared-helpers/.jest/setup-tests.js @@ -1 +1,21 @@ // Future home of additional Jest config +import "@testing-library/jest-dom/extend-expect" + +import { addTranslation } from "@bloom-housing/ui-components" +import generalTranslations from "@bloom-housing/ui-components/src/locales/general.json" + +// see: https://jestjs.io/docs/en/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom +window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + } +}) + +addTranslation(generalTranslations) diff --git a/shared-helpers/CHANGELOG.md b/shared-helpers/CHANGELOG.md index e4d87c4d45..c897b9ac15 100644 --- a/shared-helpers/CHANGELOG.md +++ b/shared-helpers/CHANGELOG.md @@ -2,3 +2,2693 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.4.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.2.3...@bloom-housing/shared-helpers@4.4.0) (2022-05-24) + + +* 2022-05-24 release (#2753) ([3beb6b7](https://github.com/seanmalbert/bloom/commit/3beb6b77f74e51ec37457d4676a1fd01d1304a65)), closes [#2753](https://github.com/seanmalbert/bloom/issues/2753) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.3.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.3.1-alpha.1...@bloom-housing/shared-helpers@4.3.1-alpha.2) (2022-05-24) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.3.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.3.1-alpha.0...@bloom-housing/shared-helpers@4.3.1-alpha.1) (2022-05-24) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.3.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.30...@bloom-housing/shared-helpers@4.3.1-alpha.0) (2022-05-16) + + +### Bug Fixes + +* remove alameda reference in demographics ([cc6761b](https://github.com/bloom-housing/bloom/commit/cc6761b22616f28ff2a0393766a6273c918376fd)) +* versioning issues ([#2311](https://github.com/bloom-housing/bloom/issues/2311)) ([c274a29](https://github.com/bloom-housing/bloom/commit/c274a2985061b389c2cae6386137a4caacd7f7c0)) + + +* 2022-04-08 release (#2646) ([aa9de52](https://github.com/bloom-housing/bloom/commit/aa9de524d5e849ffded475070abf529de77c9a92)), closes [#2646](https://github.com/bloom-housing/bloom/issues/2646) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-04-05 release (#2627) ([485fb48](https://github.com/bloom-housing/bloom/commit/485fb48cfbad48bcabfef5e2e704025f608aee89)), closes [#2627](https://github.com/bloom-housing/bloom/issues/2627) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-04-04 release (#2614) ([fecab85](https://github.com/bloom-housing/bloom/commit/fecab85c748a55ab4aff5d591c8e0ac702254559)), closes [#2614](https://github.com/bloom-housing/bloom/issues/2614) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-03-01 release (#2550) ([2f2264c](https://github.com/bloom-housing/bloom/commit/2f2264cffe41d0cc1ebb79ef5c894458694d9340)), closes [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-01-27 release (#2439) ([860f6af](https://github.com/bloom-housing/bloom/commit/860f6af6204903e4dcddf671d7ba54f3ec04f121)), closes [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) +* Release 11 11 21 (#2162) ([4847469](https://github.com/bloom-housing/bloom/commit/484746982e440c1c1c87c85089d86cd5968f1cae)), closes [#2162](https://github.com/bloom-housing/bloom/issues/2162) + + +### Features + +* add SRO unit type ([a4c1403](https://github.com/bloom-housing/bloom/commit/a4c140350a84a5bacfa65fb6714aa594e406945d)) +* new demographics sub-race questions ([910df6a](https://github.com/bloom-housing/bloom/commit/910df6ad3985980becdc2798076ed5dfeeb310b5)) + + +### Reverts + +* Revert "chore(release): version" ([47a2c67](https://github.com/bloom-housing/bloom/commit/47a2c67af5c7c41f360fafc6c5386476866ea403)) +* Revert "chore: removes application program partners" ([91e22d8](https://github.com/bloom-housing/bloom/commit/91e22d891104e8d4fc024d709a6a14cec1400733)) +* Revert "chore: removes application program display" ([740cf00](https://github.com/bloom-housing/bloom/commit/740cf00dc3a729eed037d56a8dfc5988decd2651)) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + + + + + +## [4.2.2-alpha.30](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.29...@bloom-housing/shared-helpers@4.2.2-alpha.30) (2022-05-13) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.3](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.2.2...@bloom-housing/shared-helpers@4.2.3) (2022-04-28) +## [4.2.2-alpha.29](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.28...@bloom-housing/shared-helpers@4.2.2-alpha.29) (2022-05-11) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.2.1...@bloom-housing/shared-helpers@4.2.2) (2022-04-19) +## [4.2.2-alpha.28](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.27...@bloom-housing/shared-helpers@4.2.2-alpha.28) (2022-05-11) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.27](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.26...@bloom-housing/shared-helpers@4.2.2-alpha.27) (2022-05-10) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.26](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.25...@bloom-housing/shared-helpers@4.2.2-alpha.26) (2022-05-05) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.25](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.24...@bloom-housing/shared-helpers@4.2.2-alpha.25) (2022-05-04) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.24](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.23...@bloom-housing/shared-helpers@4.2.2-alpha.24) (2022-05-04) + + +### Code Refactoring + +* remove backend dependencies from sidebar application components ([#2675](https://github.com/bloom-housing/bloom/issues/2675)) ([d2ebf87](https://github.com/bloom-housing/bloom/commit/d2ebf87c34af3f5b6168fa4e08663fea0a4a872c)) + + +### BREAKING CHANGES + +* the LeasingAgent component has been renamed to Contact with a new generalized prop set, the SidebarAddress component has been renamed to ContactAddress with a new generalized prop set + + + + + +## [4.2.2-alpha.23](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.22...@bloom-housing/shared-helpers@4.2.2-alpha.23) (2022-05-04) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.22](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.21...@bloom-housing/shared-helpers@4.2.2-alpha.22) (2022-05-03) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.21](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.20...@bloom-housing/shared-helpers@4.2.2-alpha.21) (2022-04-29) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.20](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.19...@bloom-housing/shared-helpers@4.2.2-alpha.20) (2022-04-29) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.19](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.18...@bloom-housing/shared-helpers@4.2.2-alpha.19) (2022-04-28) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.18](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.17...@bloom-housing/shared-helpers@4.2.2-alpha.18) (2022-04-28) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.17](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.16...@bloom-housing/shared-helpers@4.2.2-alpha.17) (2022-04-28) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.16](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.15...@bloom-housing/shared-helpers@4.2.2-alpha.16) (2022-04-27) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.15](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.14...@bloom-housing/shared-helpers@4.2.2-alpha.15) (2022-04-26) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.14](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.13...@bloom-housing/shared-helpers@4.2.2-alpha.14) (2022-04-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.13](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.12...@bloom-housing/shared-helpers@4.2.2-alpha.13) (2022-04-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.12](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.11...@bloom-housing/shared-helpers@4.2.2-alpha.12) (2022-04-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.11](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.10...@bloom-housing/shared-helpers@4.2.2-alpha.11) (2022-04-21) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.10](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.9...@bloom-housing/shared-helpers@4.2.2-alpha.10) (2022-04-21) + + +### Features + +* new category table component ([#2648](https://github.com/bloom-housing/bloom/issues/2648)) ([3b3fe46](https://github.com/bloom-housing/bloom/commit/3b3fe46dda3d0e553664c10cea46849551ce064c)) + + +### BREAKING CHANGES + +* There is a new prop interface for the StandardTable component and all components that use it, which includes passing cell content within a new object, allowing us to support new cell options - all tables will need to pass data with the new format. + + + + + +## [4.2.2-alpha.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.8...@bloom-housing/shared-helpers@4.2.2-alpha.9) (2022-04-20) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.7...@bloom-housing/shared-helpers@4.2.2-alpha.8) (2022-04-20) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.6...@bloom-housing/shared-helpers@4.2.2-alpha.7) (2022-04-20) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.5...@bloom-housing/shared-helpers@4.2.2-alpha.6) (2022-04-20) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.4...@bloom-housing/shared-helpers@4.2.2-alpha.5) (2022-04-19) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.3...@bloom-housing/shared-helpers@4.2.2-alpha.4) (2022-04-18) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.2...@bloom-housing/shared-helpers@4.2.2-alpha.3) (2022-04-18) + + +### Features + +* refactor ada form fields ([#2612](https://github.com/bloom-housing/bloom/issues/2612)) ([f516f21](https://github.com/bloom-housing/bloom/commit/f516f2164249cea5b622b6bb5cd6efb5455003ca)) + + + + + +## [4.2.2-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.1...@bloom-housing/shared-helpers@4.2.2-alpha.2) (2022-04-14) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.2-alpha.0...@bloom-housing/shared-helpers@4.2.2-alpha.1) (2022-04-13) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.2-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.1-alpha.4...@bloom-housing/shared-helpers@4.2.2-alpha.0) (2022-04-13) + + +* 2022-04-11 sync master (#2649) ([9d30acf](https://github.com/bloom-housing/bloom/commit/9d30acf7b53fca50a87fc8bd2658c11d3ed37427)), closes [#2649](https://github.com/bloom-housing/bloom/issues/2649) [#2037](https://github.com/bloom-housing/bloom/issues/2037) [#2095](https://github.com/bloom-housing/bloom/issues/2095) [#2162](https://github.com/bloom-housing/bloom/issues/2162) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2519](https://github.com/bloom-housing/bloom/issues/2519) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2534](https://github.com/bloom-housing/bloom/issues/2534) [#2544](https://github.com/bloom-housing/bloom/issues/2544) [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) + + +### BREAKING CHANGES + +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + +* fix: adds jurisdictionId to useSWR path + +* fix: recalculate units available on listing update + +picked form dev f1a3dbce6478b16542ed61ab20de5dfb9b797262 + +* feat: feat(backend): make use of new application confirmation codes + +picked from dev 3c45c2904818200eed4568931d4cc352fd2f449e + +* revert: revert "chore(deps): bump axios from 0.21.1 to 0.21.2 + +picked from dev 2b83bc0393afc42eed542e326d5ef75502ce119c + +* fix: app submission w/ no due date + +picked from dev 4af1f5a8448f16d347b4a65ecb85fda4d6ed71fc + +* feat: adds new preferences, reserved community type + +* feat: adds bottom border to preferences + +* feat: updates preference string + +* fix: preference cleanup for avance + +* refactor: remove applicationAddress + +picked from dev bf10632a62bf2f14922948c046ea3352ed010f4f + +* feat: refactor and add public site application flow cypress tests + +picked from dev 9ec0e8d05f9570773110754e7fdaf49254d1eab8 + +* feat: better seed data for ami-charts + +picked from dev d8b1d4d185731a589c563a32bd592d01537785f3 + +* feat: adds listing management cypress tests to partner portal + +* fix: listings management keep empty strings, remove empty objects + +picked from dev c4b1e833ec128f457015ac7ffa421ee6047083d9 + +* feat: one month rent + +picked from dev 883b0d53030e1c4d54f2f75bd5e188bb1d255f64 + +* test: view.spec.ts test + +picked from dev 324446c90138d8fac50aba445f515009b5a58bfb + +* refactor: removes jsonpath + +picked from dev deb39acc005607ce3076942b1f49590d08afc10c + +* feat: adds jurisdictions to pref seeds + +picked from dev 9e47cec3b1acfe769207ccbb33c07019cd742e33 + +* feat: new demographics sub-race questions + +picked from dev 9ab892694c1ad2fa8890b411b3b32af68ade1fc3 + +* feat: updates email confirmation for lottery + +picked from dev 1a5e824c96d8e23674c32ea92688b9f7255528d3 + +* fix: add ariaHidden to Icon component + +picked from dev c7bb86aec6fd5ad386c7ca50087d0113b14503be + +* fix: add ariaLabel prop to Button component + +picked from dev 509ddc898ba44c05e26f8ed8c777f1ba456eeee5 + +* fix: change the yes/no radio text to be more descriptive + +picked from dev 0c46054574535523d6f217bb0677bbe732b8945f + +* fix: remove alameda reference in demographics + +picked from dev 7d5991cbf6dbe0b61f2b14d265e87ce3687f743d + +* chore: release version + +picked from dev fe82f25dc349877d974ae62d228fea0354978fb7 + +* feat: ami chart jurisdictionalized + +picked from dev 0a5cbc88a9d9e3c2ff716fe0f44ca6c48f5dcc50 + +* refactor: make backend a peer dependency in ui-components + +picked from dev 952aaa14a77e0960312ff0eeee51399d1d6af9f3 + +* feat: add a phone number column to the user_accounts table + +picked from dev 2647df9ab9888a525cc8a164d091dda6482c502a + +* chore: removes application program partners + +* chore: removes application program display + +* Revert "chore: removes application program display" + +This reverts commit 14825b4a6c9cd1a7235e32074e32af18a71b5c26. + +* Revert "chore: removes application program partners" + +This reverts commit d7aa38c777972a2e21d9f816441caa27f98d3f86. + +* chore: yarn.lock and backend-swagger + +* fix: removes Duplicate identifier fieldGroupObjectToArray + +* feat: skip preferences if not on listing + +* chore(release): version + +* fix: cannot save custom mailing, dropoff, or pickup address + +* chore(release): version + +* chore: converge on one axios version, remove peer dependency + +* chore(release): version + +* feat: simplify Waitlist component and use more flexible schema + +* chore(release): version + +* fix: lottery results uploads now save + +* chore(release): version + +* feat: add SRO unit type + +* chore(release): version + +* fix: paper application submission + +* chore(release): version + +* fix: choose-language context + +* chore(release): version + +* fix: applications/view hide prefs + +* chore(release): version + +* feat: overrides fallback to english, tagalog support + +* chore(release): version + +* fix: account translations + +* chore(release): version + +* fix: units with invalid ami chart + +* chore(release): version + +* fix: remove description for the partners programs + +* fix: fix modal styles on mobile + +* fix: visual improvement to programs form display + +* fix: submission tests not running +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.2.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.2.0...@bloom-housing/shared-helpers@4.2.1) (2022-04-11) + + +* 2022-04-08 release (#2646) ([aa9de52](https://github.com/seanmalbert/bloom/commit/aa9de524d5e849ffded475070abf529de77c9a92)), closes [#2646](https://github.com/seanmalbert/bloom/issues/2646) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.2.1-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.1-alpha.3...@bloom-housing/shared-helpers@4.2.1-alpha.4) (2022-04-13) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.1-alpha.2...@bloom-housing/shared-helpers@4.2.1-alpha.3) (2022-04-08) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.1-alpha.1...@bloom-housing/shared-helpers@4.2.1-alpha.2) (2022-04-07) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.2.1-alpha.0...@bloom-housing/shared-helpers@4.2.1-alpha.1) (2022-04-07) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.2.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.3-alpha.5...@bloom-housing/shared-helpers@4.2.1-alpha.0) (2022-04-06) + + +* 2022-04-06 sync master (#2628) ([bc31833](https://github.com/bloom-housing/bloom/commit/bc31833f7ea5720a242d93a01bb1b539181fbad4)), closes [#2628](https://github.com/bloom-housing/bloom/issues/2628) [#2037](https://github.com/bloom-housing/bloom/issues/2037) [#2095](https://github.com/bloom-housing/bloom/issues/2095) [#2162](https://github.com/bloom-housing/bloom/issues/2162) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2519](https://github.com/bloom-housing/bloom/issues/2519) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2534](https://github.com/bloom-housing/bloom/issues/2534) [#2544](https://github.com/bloom-housing/bloom/issues/2544) [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) + + +### BREAKING CHANGES + +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + +* fix: adds jurisdictionId to useSWR path + +* fix: recalculate units available on listing update + +picked form dev f1a3dbce6478b16542ed61ab20de5dfb9b797262 + +* feat: feat(backend): make use of new application confirmation codes + +picked from dev 3c45c2904818200eed4568931d4cc352fd2f449e + +* revert: revert "chore(deps): bump axios from 0.21.1 to 0.21.2 + +picked from dev 2b83bc0393afc42eed542e326d5ef75502ce119c + +* fix: app submission w/ no due date + +picked from dev 4af1f5a8448f16d347b4a65ecb85fda4d6ed71fc + +* feat: adds new preferences, reserved community type + +* feat: adds bottom border to preferences + +* feat: updates preference string + +* fix: preference cleanup for avance + +* refactor: remove applicationAddress + +picked from dev bf10632a62bf2f14922948c046ea3352ed010f4f + +* feat: refactor and add public site application flow cypress tests + +picked from dev 9ec0e8d05f9570773110754e7fdaf49254d1eab8 + +* feat: better seed data for ami-charts + +picked from dev d8b1d4d185731a589c563a32bd592d01537785f3 + +* feat: adds listing management cypress tests to partner portal + +* fix: listings management keep empty strings, remove empty objects + +picked from dev c4b1e833ec128f457015ac7ffa421ee6047083d9 + +* feat: one month rent + +picked from dev 883b0d53030e1c4d54f2f75bd5e188bb1d255f64 + +* test: view.spec.ts test + +picked from dev 324446c90138d8fac50aba445f515009b5a58bfb + +* refactor: removes jsonpath + +picked from dev deb39acc005607ce3076942b1f49590d08afc10c + +* feat: adds jurisdictions to pref seeds + +picked from dev 9e47cec3b1acfe769207ccbb33c07019cd742e33 + +* feat: new demographics sub-race questions + +picked from dev 9ab892694c1ad2fa8890b411b3b32af68ade1fc3 + +* feat: updates email confirmation for lottery + +picked from dev 1a5e824c96d8e23674c32ea92688b9f7255528d3 + +* fix: add ariaHidden to Icon component + +picked from dev c7bb86aec6fd5ad386c7ca50087d0113b14503be + +* fix: add ariaLabel prop to Button component + +picked from dev 509ddc898ba44c05e26f8ed8c777f1ba456eeee5 + +* fix: change the yes/no radio text to be more descriptive + +picked from dev 0c46054574535523d6f217bb0677bbe732b8945f + +* fix: remove alameda reference in demographics + +picked from dev 7d5991cbf6dbe0b61f2b14d265e87ce3687f743d + +* chore: release version + +picked from dev fe82f25dc349877d974ae62d228fea0354978fb7 + +* feat: ami chart jurisdictionalized + +picked from dev 0a5cbc88a9d9e3c2ff716fe0f44ca6c48f5dcc50 + +* refactor: make backend a peer dependency in ui-components + +picked from dev 952aaa14a77e0960312ff0eeee51399d1d6af9f3 + +* feat: add a phone number column to the user_accounts table + +picked from dev 2647df9ab9888a525cc8a164d091dda6482c502a + +* chore: removes application program partners + +* chore: removes application program display + +* Revert "chore: removes application program display" + +This reverts commit 14825b4a6c9cd1a7235e32074e32af18a71b5c26. + +* Revert "chore: removes application program partners" + +This reverts commit d7aa38c777972a2e21d9f816441caa27f98d3f86. + +* chore: yarn.lock and backend-swagger + +* fix: removes Duplicate identifier fieldGroupObjectToArray + +* feat: skip preferences if not on listing + +* chore(release): version + +* fix: cannot save custom mailing, dropoff, or pickup address + +* chore(release): version + +* chore: converge on one axios version, remove peer dependency + +* chore(release): version + +* feat: simplify Waitlist component and use more flexible schema + +* chore(release): version + +* fix: lottery results uploads now save + +* chore(release): version + +* feat: add SRO unit type + +* chore(release): version + +* fix: paper application submission + +* chore(release): version + +* fix: choose-language context + +* chore(release): version + +* fix: applications/view hide prefs + +* chore(release): version + +* feat: overrides fallback to english, tagalog support + +* chore(release): version + +* fix: account translations + +* chore(release): version + +* fix: units with invalid ami chart + +* chore(release): version + +* fix: remove description for the partners programs + +* fix: fix modal styles on mobile + +* fix: visual improvement to programs form display + +* fix: submission tests not running +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +# [4.2.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.1.2...@bloom-housing/shared-helpers@4.2.0) (2022-04-06) + + +* 2022-04-05 release (#2627) ([485fb48](https://github.com/seanmalbert/bloom/commit/485fb48cfbad48bcabfef5e2e704025f608aee89)), closes [#2627](https://github.com/seanmalbert/bloom/issues/2627) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) +* 2022-04-04 release (#2614) ([fecab85](https://github.com/seanmalbert/bloom/commit/fecab85c748a55ab4aff5d591c8e0ac702254559)), closes [#2614](https://github.com/seanmalbert/bloom/issues/2614) [#2349](https://github.com/seanmalbert/bloom/issues/2349) [#2350](https://github.com/seanmalbert/bloom/issues/2350) [#2351](https://github.com/seanmalbert/bloom/issues/2351) [#2348](https://github.com/seanmalbert/bloom/issues/2348) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.1.3-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.3-alpha.4...@bloom-housing/shared-helpers@4.1.3-alpha.5) (2022-04-05) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.3-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.3-alpha.3...@bloom-housing/shared-helpers@4.1.3-alpha.4) (2022-04-05) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.3-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.3-alpha.2...@bloom-housing/shared-helpers@4.1.3-alpha.3) (2022-04-04) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.3-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.3-alpha.1...@bloom-housing/shared-helpers@4.1.3-alpha.2) (2022-04-04) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.3-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.3-alpha.0...@bloom-housing/shared-helpers@4.1.3-alpha.1) (2022-04-04) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.3-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.2-alpha.3...@bloom-housing/shared-helpers@4.1.3-alpha.0) (2022-03-30) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.2](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.1.1...@bloom-housing/shared-helpers@4.1.2) (2022-03-29) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.2-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.2-alpha.2...@bloom-housing/shared-helpers@4.1.2-alpha.3) (2022-03-29) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.2-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.2-alpha.1...@bloom-housing/shared-helpers@4.1.2-alpha.2) (2022-03-29) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.2-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.2-alpha.0...@bloom-housing/shared-helpers@4.1.2-alpha.1) (2022-03-28) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.2-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.1-alpha.5...@bloom-housing/shared-helpers@4.1.2-alpha.0) (2022-03-28) + + +* 2022 03 28 sync master (#2593) ([580283d](https://github.com/bloom-housing/bloom/commit/580283da22246b7d39978e7dfa08016b2c0c3757)), closes [#2593](https://github.com/bloom-housing/bloom/issues/2593) [#2037](https://github.com/bloom-housing/bloom/issues/2037) [#2095](https://github.com/bloom-housing/bloom/issues/2095) [#2162](https://github.com/bloom-housing/bloom/issues/2162) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2519](https://github.com/bloom-housing/bloom/issues/2519) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2534](https://github.com/bloom-housing/bloom/issues/2534) [#2544](https://github.com/bloom-housing/bloom/issues/2544) [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) + + +### BREAKING CHANGES + +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + +* fix: adds jurisdictionId to useSWR path + +* fix: recalculate units available on listing update + +picked form dev f1a3dbce6478b16542ed61ab20de5dfb9b797262 + +* feat: feat(backend): make use of new application confirmation codes + +picked from dev 3c45c2904818200eed4568931d4cc352fd2f449e + +* revert: revert "chore(deps): bump axios from 0.21.1 to 0.21.2 + +picked from dev 2b83bc0393afc42eed542e326d5ef75502ce119c + +* fix: app submission w/ no due date + +picked from dev 4af1f5a8448f16d347b4a65ecb85fda4d6ed71fc + +* feat: adds new preferences, reserved community type + +* feat: adds bottom border to preferences + +* feat: updates preference string + +* fix: preference cleanup for avance + +* refactor: remove applicationAddress + +picked from dev bf10632a62bf2f14922948c046ea3352ed010f4f + +* feat: refactor and add public site application flow cypress tests + +picked from dev 9ec0e8d05f9570773110754e7fdaf49254d1eab8 + +* feat: better seed data for ami-charts + +picked from dev d8b1d4d185731a589c563a32bd592d01537785f3 + +* feat: adds listing management cypress tests to partner portal + +* fix: listings management keep empty strings, remove empty objects + +picked from dev c4b1e833ec128f457015ac7ffa421ee6047083d9 + +* feat: one month rent + +picked from dev 883b0d53030e1c4d54f2f75bd5e188bb1d255f64 + +* test: view.spec.ts test + +picked from dev 324446c90138d8fac50aba445f515009b5a58bfb + +* refactor: removes jsonpath + +picked from dev deb39acc005607ce3076942b1f49590d08afc10c + +* feat: adds jurisdictions to pref seeds + +picked from dev 9e47cec3b1acfe769207ccbb33c07019cd742e33 + +* feat: new demographics sub-race questions + +picked from dev 9ab892694c1ad2fa8890b411b3b32af68ade1fc3 + +* feat: updates email confirmation for lottery + +picked from dev 1a5e824c96d8e23674c32ea92688b9f7255528d3 + +* fix: add ariaHidden to Icon component + +picked from dev c7bb86aec6fd5ad386c7ca50087d0113b14503be + +* fix: add ariaLabel prop to Button component + +picked from dev 509ddc898ba44c05e26f8ed8c777f1ba456eeee5 + +* fix: change the yes/no radio text to be more descriptive + +picked from dev 0c46054574535523d6f217bb0677bbe732b8945f + +* fix: remove alameda reference in demographics + +picked from dev 7d5991cbf6dbe0b61f2b14d265e87ce3687f743d + +* chore: release version + +picked from dev fe82f25dc349877d974ae62d228fea0354978fb7 + +* feat: ami chart jurisdictionalized + +picked from dev 0a5cbc88a9d9e3c2ff716fe0f44ca6c48f5dcc50 + +* refactor: make backend a peer dependency in ui-components + +picked from dev 952aaa14a77e0960312ff0eeee51399d1d6af9f3 + +* feat: add a phone number column to the user_accounts table + +picked from dev 2647df9ab9888a525cc8a164d091dda6482c502a + +* chore: removes application program partners + +* chore: removes application program display + +* Revert "chore: removes application program display" + +This reverts commit 14825b4a6c9cd1a7235e32074e32af18a71b5c26. + +* Revert "chore: removes application program partners" + +This reverts commit d7aa38c777972a2e21d9f816441caa27f98d3f86. + +* chore: yarn.lock and backend-swagger + +* fix: removes Duplicate identifier fieldGroupObjectToArray + +* feat: skip preferences if not on listing + +* chore(release): version + +* fix: cannot save custom mailing, dropoff, or pickup address + +* chore(release): version + +* chore: converge on one axios version, remove peer dependency + +* chore(release): version + +* feat: simplify Waitlist component and use more flexible schema + +* chore(release): version + +* fix: lottery results uploads now save + +* chore(release): version + +* feat: add SRO unit type + +* chore(release): version + +* fix: paper application submission + +* chore(release): version + +* fix: choose-language context + +* chore(release): version + +* fix: applications/view hide prefs + +* chore(release): version + +* feat: overrides fallback to english, tagalog support + +* chore(release): version + +* fix: account translations + +* chore(release): version + +* fix: units with invalid ami chart + +* chore(release): version + +* fix: remove description for the partners programs + +* fix: fix modal styles on mobile + +* fix: visual improvement to programs form display + +* fix: submission tests not running +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.1.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.1.0...@bloom-housing/shared-helpers@4.1.1) (2022-03-28) +## [4.1.1-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.1-alpha.4...@bloom-housing/shared-helpers@4.1.1-alpha.5) (2022-03-28) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.1-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.1-alpha.3...@bloom-housing/shared-helpers@4.1.1-alpha.4) (2022-03-25) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.1-alpha.2...@bloom-housing/shared-helpers@4.1.1-alpha.3) (2022-03-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.1-alpha.1...@bloom-housing/shared-helpers@4.1.1-alpha.2) (2022-03-16) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.1.1-alpha.0...@bloom-housing/shared-helpers@4.1.1-alpha.1) (2022-03-10) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.1.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.80...@bloom-housing/shared-helpers@4.1.1-alpha.0) (2022-03-02) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.80...@bloom-housing/shared-helpers@4.0.1) (2022-03-02) + +**Note:** Version bump only for package @bloom-housing/shared-helpers +# [4.1.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.0.3...@bloom-housing/shared-helpers@4.1.0) (2022-03-02) + + +* 2022-03-01 release (#2550) ([2f2264c](https://github.com/seanmalbert/bloom/commit/2f2264cffe41d0cc1ebb79ef5c894458694d9340)), closes [#2550](https://github.com/seanmalbert/bloom/issues/2550) [#2288](https://github.com/seanmalbert/bloom/issues/2288) [#2317](https://github.com/seanmalbert/bloom/issues/2317) [#2319](https://github.com/seanmalbert/bloom/issues/2319) [#2108](https://github.com/seanmalbert/bloom/issues/2108) [#2326](https://github.com/seanmalbert/bloom/issues/2326) [#2349](https://github.com/seanmalbert/bloom/issues/2349) [#2350](https://github.com/seanmalbert/bloom/issues/2350) [#2351](https://github.com/seanmalbert/bloom/issues/2351) [#2348](https://github.com/seanmalbert/bloom/issues/2348) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.0.1-alpha.80](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.79...@bloom-housing/shared-helpers@4.0.1-alpha.80) (2022-02-28) + + +### Features + +* updates to mfa styling ([#2532](https://github.com/bloom-housing/bloom/issues/2532)) ([7654efc](https://github.com/bloom-housing/bloom/commit/7654efc8a7c5cba0f7436fda62b886f646fe8a03)) + + + + + +## [4.0.1-alpha.79](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.78...@bloom-housing/shared-helpers@4.0.1-alpha.79) (2022-02-28) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.78](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.77...@bloom-housing/shared-helpers@4.0.1-alpha.78) (2022-02-26) + + +### Features + +* adds gtm tracking to rest of pages ([#2545](https://github.com/bloom-housing/bloom/issues/2545)) ([1c96f71](https://github.com/bloom-housing/bloom/commit/1c96f7101017aefd8bca70731265f6efb1ab5cf0)) + + + + + +## [4.0.3](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.0.2...@bloom-housing/shared-helpers@4.0.3) (2022-02-25) + + + + + +## [4.0.1-alpha.77](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.76...@bloom-housing/shared-helpers@4.0.1-alpha.77) (2022-02-25) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.76](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.75...@bloom-housing/shared-helpers@4.0.1-alpha.76) (2022-02-25) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.75](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.74...@bloom-housing/shared-helpers@4.0.1-alpha.75) (2022-02-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.74](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.73...@bloom-housing/shared-helpers@4.0.1-alpha.74) (2022-02-18) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.73](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.72...@bloom-housing/shared-helpers@4.0.1-alpha.73) (2022-02-17) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.72](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.71...@bloom-housing/shared-helpers@4.0.1-alpha.72) (2022-02-17) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.71](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.70...@bloom-housing/shared-helpers@4.0.1-alpha.71) (2022-02-17) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.70](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.69...@bloom-housing/shared-helpers@4.0.1-alpha.70) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.69](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.68...@bloom-housing/shared-helpers@4.0.1-alpha.69) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.68](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.67...@bloom-housing/shared-helpers@4.0.1-alpha.68) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.67](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.66...@bloom-housing/shared-helpers@4.0.1-alpha.67) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.66](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.65...@bloom-housing/shared-helpers@4.0.1-alpha.66) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.65](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.64...@bloom-housing/shared-helpers@4.0.1-alpha.65) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.64](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.63...@bloom-housing/shared-helpers@4.0.1-alpha.64) (2022-02-15) + + +### Features + +* **backend:** make listing image an array ([#2477](https://github.com/bloom-housing/bloom/issues/2477)) ([cab9800](https://github.com/bloom-housing/bloom/commit/cab98003e640c880be2218fa42321eadeec35e9c)) + + + + + +## [4.0.1-alpha.63](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.62...@bloom-housing/shared-helpers@4.0.1-alpha.63) (2022-02-15) + + +### Code Refactoring + +* remove backend dependencies from events components, consolidate ([#2495](https://github.com/bloom-housing/bloom/issues/2495)) ([d884689](https://github.com/bloom-housing/bloom/commit/d88468965bc67c74b8b3eaced20c77472e90331f)) + + +### BREAKING CHANGES + +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + + + + + +## [4.0.1-alpha.62](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.61...@bloom-housing/shared-helpers@4.0.1-alpha.62) (2022-02-15) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.61](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.60...@bloom-housing/shared-helpers@4.0.1-alpha.61) (2022-02-15) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.60](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.59...@bloom-housing/shared-helpers@4.0.1-alpha.60) (2022-02-15) + + +### Features + +* **backend:** add partners portal users multi factor authentication ([#2291](https://github.com/bloom-housing/bloom/issues/2291)) ([5b10098](https://github.com/bloom-housing/bloom/commit/5b10098d8668f9f42c60e90236db16d6cc517793)), closes [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) + + + + + +## [4.0.1-alpha.59](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.58...@bloom-housing/shared-helpers@4.0.1-alpha.59) (2022-02-14) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.58](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.57...@bloom-housing/shared-helpers@4.0.1-alpha.58) (2022-02-14) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.57](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.56...@bloom-housing/shared-helpers@4.0.1-alpha.57) (2022-02-12) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.56](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.55...@bloom-housing/shared-helpers@4.0.1-alpha.56) (2022-02-10) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.55](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.54...@bloom-housing/shared-helpers@4.0.1-alpha.55) (2022-02-10) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.54](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.53...@bloom-housing/shared-helpers@4.0.1-alpha.54) (2022-02-10) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.2](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.0.1...@bloom-housing/shared-helpers@4.0.2) (2022-02-09) + + + + + +## [4.0.1-alpha.53](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.52...@bloom-housing/shared-helpers@4.0.1-alpha.53) (2022-02-10) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.52](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.51...@bloom-housing/shared-helpers@4.0.1-alpha.52) (2022-02-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.51](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.50...@bloom-housing/shared-helpers@4.0.1-alpha.51) (2022-02-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.50](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.49...@bloom-housing/shared-helpers@4.0.1-alpha.50) (2022-02-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.49](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.48...@bloom-housing/shared-helpers@4.0.1-alpha.49) (2022-02-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.48](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.47...@bloom-housing/shared-helpers@4.0.1-alpha.48) (2022-02-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.47](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.46...@bloom-housing/shared-helpers@4.0.1-alpha.47) (2022-02-08) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.46](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.45...@bloom-housing/shared-helpers@4.0.1-alpha.46) (2022-02-07) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@3.0.1...@bloom-housing/shared-helpers@4.0.1) (2022-02-03) + + +* 2022-01-27 release (#2439) ([860f6af](https://github.com/seanmalbert/bloom/commit/860f6af6204903e4dcddf671d7ba54f3ec04f121)), closes [#2439](https://github.com/seanmalbert/bloom/issues/2439) [#2196](https://github.com/seanmalbert/bloom/issues/2196) [#2238](https://github.com/seanmalbert/bloom/issues/2238) [#2226](https://github.com/seanmalbert/bloom/issues/2226) [#2230](https://github.com/seanmalbert/bloom/issues/2230) [#2243](https://github.com/seanmalbert/bloom/issues/2243) [#2195](https://github.com/seanmalbert/bloom/issues/2195) [#2215](https://github.com/seanmalbert/bloom/issues/2215) [#2266](https://github.com/seanmalbert/bloom/issues/2266) [#2188](https://github.com/seanmalbert/bloom/issues/2188) [#2270](https://github.com/seanmalbert/bloom/issues/2270) [#2188](https://github.com/seanmalbert/bloom/issues/2188) [#2213](https://github.com/seanmalbert/bloom/issues/2213) [#2234](https://github.com/seanmalbert/bloom/issues/2234) [#1901](https://github.com/seanmalbert/bloom/issues/1901) [#2260](https://github.com/seanmalbert/bloom/issues/2260) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#2280](https://github.com/seanmalbert/bloom/issues/2280) [#2253](https://github.com/seanmalbert/bloom/issues/2253) [#2276](https://github.com/seanmalbert/bloom/issues/2276) [#2282](https://github.com/seanmalbert/bloom/issues/2282) [#2262](https://github.com/seanmalbert/bloom/issues/2262) [#2278](https://github.com/seanmalbert/bloom/issues/2278) [#2293](https://github.com/seanmalbert/bloom/issues/2293) [#2295](https://github.com/seanmalbert/bloom/issues/2295) [#2296](https://github.com/seanmalbert/bloom/issues/2296) [#2294](https://github.com/seanmalbert/bloom/issues/2294) [#2277](https://github.com/seanmalbert/bloom/issues/2277) [#2290](https://github.com/seanmalbert/bloom/issues/2290) [#2299](https://github.com/seanmalbert/bloom/issues/2299) [#2292](https://github.com/seanmalbert/bloom/issues/2292) [#2303](https://github.com/seanmalbert/bloom/issues/2303) [#2305](https://github.com/seanmalbert/bloom/issues/2305) [#2306](https://github.com/seanmalbert/bloom/issues/2306) [#2308](https://github.com/seanmalbert/bloom/issues/2308) [#2190](https://github.com/seanmalbert/bloom/issues/2190) [#2239](https://github.com/seanmalbert/bloom/issues/2239) [#2311](https://github.com/seanmalbert/bloom/issues/2311) [#2302](https://github.com/seanmalbert/bloom/issues/2302) [#2301](https://github.com/seanmalbert/bloom/issues/2301) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#2313](https://github.com/seanmalbert/bloom/issues/2313) [#2289](https://github.com/seanmalbert/bloom/issues/2289) [#2279](https://github.com/seanmalbert/bloom/issues/2279) [#2288](https://github.com/seanmalbert/bloom/issues/2288) [#2317](https://github.com/seanmalbert/bloom/issues/2317) [#2319](https://github.com/seanmalbert/bloom/issues/2319) [#2108](https://github.com/seanmalbert/bloom/issues/2108) [#2326](https://github.com/seanmalbert/bloom/issues/2326) [#2349](https://github.com/seanmalbert/bloom/issues/2349) [#2350](https://github.com/seanmalbert/bloom/issues/2350) [#2351](https://github.com/seanmalbert/bloom/issues/2351) [#2348](https://github.com/seanmalbert/bloom/issues/2348) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2434](https://github.com/seanmalbert/bloom/issues/2434) + +### BREAKING CHANGES + +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 + + + + + +## [4.0.1-alpha.45](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.44...@bloom-housing/shared-helpers@4.0.1-alpha.45) (2022-02-02) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.44](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.43...@bloom-housing/shared-helpers@4.0.1-alpha.44) (2022-02-02) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.43](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.42...@bloom-housing/shared-helpers@4.0.1-alpha.43) (2022-02-02) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.42](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.41...@bloom-housing/shared-helpers@4.0.1-alpha.42) (2022-02-02) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.41](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.40...@bloom-housing/shared-helpers@4.0.1-alpha.41) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.40](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.39...@bloom-housing/shared-helpers@4.0.1-alpha.40) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.39](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.38...@bloom-housing/shared-helpers@4.0.1-alpha.39) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.38](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.37...@bloom-housing/shared-helpers@4.0.1-alpha.38) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.37](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.36...@bloom-housing/shared-helpers@4.0.1-alpha.37) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.36](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.35...@bloom-housing/shared-helpers@4.0.1-alpha.36) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.35](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.34...@bloom-housing/shared-helpers@4.0.1-alpha.35) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.34](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.33...@bloom-housing/shared-helpers@4.0.1-alpha.34) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.33](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.32...@bloom-housing/shared-helpers@4.0.1-alpha.33) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.32](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.31...@bloom-housing/shared-helpers@4.0.1-alpha.32) (2022-01-31) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.31](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.30...@bloom-housing/shared-helpers@4.0.1-alpha.31) (2022-01-31) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.30](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.29...@bloom-housing/shared-helpers@4.0.1-alpha.30) (2022-01-27) + + +### Features + +* outdated password messaging updates ([b14e19d](https://github.com/bloom-housing/bloom/commit/b14e19d43099af2ba721d8aaaeeb2be886d05111)) + + + + + +## [4.0.1-alpha.29](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.28...@bloom-housing/shared-helpers@4.0.1-alpha.29) (2022-01-26) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.28](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.27...@bloom-housing/shared-helpers@4.0.1-alpha.28) (2022-01-26) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.27](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.26...@bloom-housing/shared-helpers@4.0.1-alpha.27) (2022-01-24) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.26](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.25...@bloom-housing/shared-helpers@4.0.1-alpha.26) (2022-01-24) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.25](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.24...@bloom-housing/shared-helpers@4.0.1-alpha.25) (2022-01-21) + + +### Bug Fixes + +* user status enum to camel case; gtm types ([fbb8004](https://github.com/bloom-housing/bloom/commit/fbb800496fa1c5f37d3d7738acf28755dd66f1dd)) + + +### Features + +* adds event logging to most of the pages ([dc88c0a](https://github.com/bloom-housing/bloom/commit/dc88c0a8b6be317cd624921b868bb17e77e31f11)) +* updates for gtm ([0251cd3](https://github.com/bloom-housing/bloom/commit/0251cd3d73be19d60c148aae01d12ab35f29bc85)) +* updates for gtm ([13578bb](https://github.com/bloom-housing/bloom/commit/13578bb864ea1b1918d5908982cb3095756811c7)) + + + + + +## [4.0.1-alpha.24](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.23...@bloom-housing/shared-helpers@4.0.1-alpha.24) (2022-01-20) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.23](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.22...@bloom-housing/shared-helpers@4.0.1-alpha.23) (2022-01-14) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.20...@bloom-housing/shared-helpers@3.0.1) (2022-01-13) + + + +### Bug Fixes + +* versioning issues ([#2311](https://github.com/seanmalbert/bloom/issues/2311)) ([c274a29](https://github.com/seanmalbert/bloom/commit/c274a2985061b389c2cae6386137a4caacd7f7c0)) + + + +### Features + +* add SRO unit type ([a4c1403](https://github.com/seanmalbert/bloom/commit/a4c140350a84a5bacfa65fb6714aa594e406945d)) + + +### Reverts + +* Revert "chore(release): version" ([47a2c67](https://github.com/seanmalbert/bloom/commit/47a2c67af5c7c41f360fafc6c5386476866ea403)) +* Revert "chore: removes application program partners" ([91e22d8](https://github.com/seanmalbert/bloom/commit/91e22d891104e8d4fc024d709a6a14cec1400733)) +* Revert "chore: removes application program display" ([740cf00](https://github.com/seanmalbert/bloom/commit/740cf00dc3a729eed037d56a8dfc5988decd2651)) + + + + + +## [4.0.1-alpha.22](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.21...@bloom-housing/shared-helpers@4.0.1-alpha.22) (2022-01-13) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.21](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.20...@bloom-housing/shared-helpers@4.0.1-alpha.21) (2022-01-13) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.20](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.19...@bloom-housing/shared-helpers@4.0.1-alpha.20) (2022-01-13) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.19](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.18...@bloom-housing/shared-helpers@4.0.1-alpha.19) (2022-01-11) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.18](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.17...@bloom-housing/shared-helpers@4.0.1-alpha.18) (2022-01-08) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.17](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.16...@bloom-housing/shared-helpers@4.0.1-alpha.17) (2022-01-07) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.16](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.15...@bloom-housing/shared-helpers@4.0.1-alpha.16) (2022-01-07) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.15](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.14...@bloom-housing/shared-helpers@4.0.1-alpha.15) (2022-01-07) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.14](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.13...@bloom-housing/shared-helpers@4.0.1-alpha.14) (2022-01-04) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.13](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.12...@bloom-housing/shared-helpers@4.0.1-alpha.13) (2022-01-04) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.12](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.11...@bloom-housing/shared-helpers@4.0.1-alpha.12) (2022-01-04) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.11](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.10...@bloom-housing/shared-helpers@4.0.1-alpha.11) (2022-01-03) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.10](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.9...@bloom-housing/shared-helpers@4.0.1-alpha.10) (2022-01-03) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.8...@bloom-housing/shared-helpers@4.0.1-alpha.9) (2022-01-03) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.1-alpha.7...@bloom-housing/shared-helpers@4.0.1-alpha.8) (2022-01-03) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [4.0.1-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.0...@bloom-housing/shared-helpers@4.0.1-alpha.7) (2022-01-03) + + +### Bug Fixes + +* bump version ([#2349](https://github.com/bloom-housing/bloom/issues/2349)) ([b9e3ba1](https://github.com/bloom-housing/bloom/commit/b9e3ba10aebd6534090f8be231a9ea77b3c929b6)) +* bump version ([#2350](https://github.com/bloom-housing/bloom/issues/2350)) ([05863f5](https://github.com/bloom-housing/bloom/commit/05863f55f3939bea4387bd7cf4eb1f34df106124)) + + +* 2227/lock login attempts frontend (#2260) ([281ea43](https://github.com/bloom-housing/bloom/commit/281ea435e618a73a73f233a7a494f961fbac8fa2)), closes [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) + + +### BREAKING CHANGES + +* sign-in pages have been updated + + + + + +## [4.0.1-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@4.0.0...@bloom-housing/shared-helpers@4.0.1-alpha.6) (2022-01-03) + + +### Bug Fixes + +* bump version ([#2349](https://github.com/bloom-housing/bloom/issues/2349)) ([b9e3ba1](https://github.com/bloom-housing/bloom/commit/b9e3ba10aebd6534090f8be231a9ea77b3c929b6)) +* bump version ([#2350](https://github.com/bloom-housing/bloom/issues/2350)) ([05863f5](https://github.com/bloom-housing/bloom/commit/05863f55f3939bea4387bd7cf4eb1f34df106124)) + + +* 2227/lock login attempts frontend (#2260) ([281ea43](https://github.com/bloom-housing/bloom/commit/281ea435e618a73a73f233a7a494f961fbac8fa2)), closes [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) + + +### BREAKING CHANGES + +* sign-in pages have been updated + + + + + +## [4.0.1-alpha.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.0.0...@bloom-housing/shared-helpers@4.0.1-alpha.1) (2021-12-23) + + +* 2227/lock login attempts frontend (#2260) ([281ea43](https://github.com/seanmalbert/bloom/commit/281ea435e618a73a73f233a7a494f961fbac8fa2)), closes [#2260](https://github.com/seanmalbert/bloom/issues/2260) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) + + +### BREAKING CHANGES + +* sign-in pages have been updated + + + + + +## [4.0.1-alpha.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@4.0.0...@bloom-housing/shared-helpers@4.0.1-alpha.0) (2021-12-23) + + +* 2227/lock login attempts frontend (#2260) ([281ea43](https://github.com/seanmalbert/bloom/commit/281ea435e618a73a73f233a7a494f961fbac8fa2)), closes [#2260](https://github.com/seanmalbert/bloom/issues/2260) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) + + +### BREAKING CHANGES + +* sign-in pages have been updated + + + + + +# [4.0.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.47...@bloom-housing/shared-helpers@4.0.0) (2021-12-22) + + +### Code Refactoring + +* removing helpers from ui-components that are backend dependent ([#2108](https://github.com/seanmalbert/bloom/issues/2108)) ([1d0c1f3](https://github.com/seanmalbert/bloom/commit/1d0c1f340781a3ba76c89462d8bee954dd40b889)) + + +### BREAKING CHANGES + +* moved some helpers from ui-components to shared-helpers + + + + + +## [3.0.1-alpha.47](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.46...@bloom-housing/shared-helpers@3.0.1-alpha.47) (2021-12-15) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.46](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.45...@bloom-housing/shared-helpers@3.0.1-alpha.46) (2021-12-15) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.45](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.44...@bloom-housing/shared-helpers@3.0.1-alpha.45) (2021-12-15) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.44](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.43...@bloom-housing/shared-helpers@3.0.1-alpha.44) (2021-12-14) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.43](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.42...@bloom-housing/shared-helpers@3.0.1-alpha.43) (2021-12-14) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.42](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.41...@bloom-housing/shared-helpers@3.0.1-alpha.42) (2021-12-13) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.41](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.39...@bloom-housing/shared-helpers@3.0.1-alpha.41) (2021-12-13) + + +### Bug Fixes + +* versioning issues ([#2311](https://github.com/bloom-housing/bloom/issues/2311)) ([0b1d143](https://github.com/bloom-housing/bloom/commit/0b1d143ab8b17add9d52533560f28d7a1f6dfd3d)) + + + + + +## [3.0.1-alpha.39](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.38...@bloom-housing/shared-helpers@3.0.1-alpha.39) (2021-12-10) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.38](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.37...@bloom-housing/shared-helpers@3.0.1-alpha.38) (2021-12-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.37](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.36...@bloom-housing/shared-helpers@3.0.1-alpha.37) (2021-12-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.36](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.35...@bloom-housing/shared-helpers@3.0.1-alpha.36) (2021-12-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.35](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.34...@bloom-housing/shared-helpers@3.0.1-alpha.35) (2021-12-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.34](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.33...@bloom-housing/shared-helpers@3.0.1-alpha.34) (2021-12-08) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.33](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.32...@bloom-housing/shared-helpers@3.0.1-alpha.33) (2021-12-07) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.32](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.31...@bloom-housing/shared-helpers@3.0.1-alpha.32) (2021-12-07) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.31](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.30...@bloom-housing/shared-helpers@3.0.1-alpha.31) (2021-12-07) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.30](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.29...@bloom-housing/shared-helpers@3.0.1-alpha.30) (2021-12-06) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.29](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.28...@bloom-housing/shared-helpers@3.0.1-alpha.29) (2021-12-03) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.28](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.27...@bloom-housing/shared-helpers@3.0.1-alpha.28) (2021-12-03) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.27](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.26...@bloom-housing/shared-helpers@3.0.1-alpha.27) (2021-12-03) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.26](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.25...@bloom-housing/shared-helpers@3.0.1-alpha.26) (2021-12-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.25](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.24...@bloom-housing/shared-helpers@3.0.1-alpha.25) (2021-12-01) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.24](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.23...@bloom-housing/shared-helpers@3.0.1-alpha.24) (2021-11-30) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.23](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.22...@bloom-housing/shared-helpers@3.0.1-alpha.23) (2021-11-29) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.22](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.21...@bloom-housing/shared-helpers@3.0.1-alpha.22) (2021-11-29) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.21](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.20...@bloom-housing/shared-helpers@3.0.1-alpha.21) (2021-11-29) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.20](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.19...@bloom-housing/shared-helpers@3.0.1-alpha.20) (2021-11-23) + + +### Bug Fixes + +* remove alameda reference in demographics ([#2209](https://github.com/bloom-housing/bloom/issues/2209)) ([7d5991c](https://github.com/bloom-housing/bloom/commit/7d5991cbf6dbe0b61f2b14d265e87ce3687f743d)) + + + + + +## [3.0.1-alpha.19](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.18...@bloom-housing/shared-helpers@3.0.1-alpha.19) (2021-11-23) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.18](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.17...@bloom-housing/shared-helpers@3.0.1-alpha.18) (2021-11-23) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.17](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.16...@bloom-housing/shared-helpers@3.0.1-alpha.17) (2021-11-23) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.16](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.15...@bloom-housing/shared-helpers@3.0.1-alpha.16) (2021-11-23) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.15](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.14...@bloom-housing/shared-helpers@3.0.1-alpha.15) (2021-11-23) + + +### Features + +* new demographics sub-race questions ([#2109](https://github.com/bloom-housing/bloom/issues/2109)) ([9ab8926](https://github.com/bloom-housing/bloom/commit/9ab892694c1ad2fa8890b411b3b32af68ade1fc3)) + + + + + +## [3.0.1-alpha.14](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.13...@bloom-housing/shared-helpers@3.0.1-alpha.14) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.13](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.12...@bloom-housing/shared-helpers@3.0.1-alpha.13) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.12](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.11...@bloom-housing/shared-helpers@3.0.1-alpha.12) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.11](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.10...@bloom-housing/shared-helpers@3.0.1-alpha.11) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.10](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.9...@bloom-housing/shared-helpers@3.0.1-alpha.10) (2021-11-17) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.8...@bloom-housing/shared-helpers@3.0.1-alpha.9) (2021-11-16) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.7...@bloom-housing/shared-helpers@3.0.1-alpha.8) (2021-11-15) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.6...@bloom-housing/shared-helpers@3.0.1-alpha.7) (2021-11-15) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.5...@bloom-housing/shared-helpers@3.0.1-alpha.6) (2021-11-15) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.4...@bloom-housing/shared-helpers@3.0.1-alpha.5) (2021-11-12) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.3...@bloom-housing/shared-helpers@3.0.1-alpha.4) (2021-11-12) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.2...@bloom-housing/shared-helpers@3.0.1-alpha.3) (2021-11-11) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.1...@bloom-housing/shared-helpers@3.0.1-alpha.2) (2021-11-10) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.1-alpha.0...@bloom-housing/shared-helpers@3.0.1-alpha.1) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [3.0.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@3.0.0...@bloom-housing/shared-helpers@3.0.1-alpha.0) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +# [3.0.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@2.0.0...@bloom-housing/shared-helpers@3.0.0) (2021-11-05) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +# [2.0.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/shared-helpers@1.0.6-alpha.5...@bloom-housing/shared-helpers@2.0.0) (2021-11-02) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [1.0.6-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@1.0.6-alpha.4...@bloom-housing/shared-helpers@1.0.6-alpha.5) (2021-10-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [1.0.6-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@1.0.6-alpha.3...@bloom-housing/shared-helpers@1.0.6-alpha.4) (2021-10-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [1.0.6-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@1.0.6-alpha.2...@bloom-housing/shared-helpers@1.0.6-alpha.3) (2021-10-22) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [1.0.6-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@1.0.6-alpha.1...@bloom-housing/shared-helpers@1.0.6-alpha.2) (2021-10-21) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + + + + + +## [1.0.6-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/shared-helpers@1.0.6-alpha.0...@bloom-housing/shared-helpers@1.0.6-alpha.1) (2021-10-19) + +**Note:** Version bump only for package @bloom-housing/shared-helpers + +## 1.0.6-alpha.0 (2021-10-19) + +### chore + +- Add new `shared-helpers` package ([#1911](https://github.com/bloom-housing/bloom/issues/1911)) ([6e5d91b](https://github.com/bloom-housing/bloom/commit/6e5d91be5ccafd3d4b5bc1a578f2246a5e7f905b)) + +### BREAKING CHANGES + +- Move form keys out of ui-components diff --git a/ui-components/__tests__/authentication/RequireLogin.test.tsx b/shared-helpers/__tests__/RequireLogin.test.tsx similarity index 94% rename from ui-components/__tests__/authentication/RequireLogin.test.tsx rename to shared-helpers/__tests__/RequireLogin.test.tsx index a113796c87..c2f6a79ffb 100644 --- a/ui-components/__tests__/authentication/RequireLogin.test.tsx +++ b/shared-helpers/__tests__/RequireLogin.test.tsx @@ -1,15 +1,16 @@ import React from "react" import { render } from "@testing-library/react" -import { RequireLogin } from "../../src/authentication/RequireLogin" -import { AuthContext } from "../../src/authentication/AuthContext" +import { RequireLogin } from "../src/RequireLogin" +import { AuthContext } from "../src/AuthContext" import { User } from "@bloom-housing/backend-core/types" -import { GenericRouter, NavigationContext } from "../../src/config/NavigationContext" +import { GenericRouter, NavigationContext } from "../src/NavigationContext" // Helpers const mockRouter: GenericRouter = { pathname: "", asPath: "", + back: () => {}, push(url: string) { this.pathname = url this.asPath = url @@ -25,6 +26,10 @@ const mockUser: User = { createdAt: new Date("2020-01-01"), updatedAt: new Date("2020-01-01"), jurisdictions: [], + mfaEnabled: false, + passwordUpdatedAt: new Date("2020-01-01"), + passwordValidForDays: 180, + agreedToTermsOfService: true, } let initialStateLoaded = false diff --git a/ui-components/__tests__/authentication/Timeout.test.tsx b/shared-helpers/__tests__/Timeout.test.tsx similarity index 85% rename from ui-components/__tests__/authentication/Timeout.test.tsx rename to shared-helpers/__tests__/Timeout.test.tsx index 9c7e09c146..5118de9ecf 100644 --- a/ui-components/__tests__/authentication/Timeout.test.tsx +++ b/shared-helpers/__tests__/Timeout.test.tsx @@ -1,7 +1,7 @@ import React from "react" import { render, cleanup } from "@testing-library/react" -import { LoggedInUserIdleTimeout } from "../../src/authentication/timeout" -import { AuthContext } from "../../src/authentication/AuthContext" +import { LoggedInUserIdleTimeout } from "../src/timeout" +import { AuthContext } from "../src/AuthContext" afterEach(cleanup) @@ -22,6 +22,10 @@ describe("", () => { createdAt: new Date(), updatedAt: new Date(), jurisdictions: [], + mfaEnabled: false, + passwordUpdatedAt: new Date(), + passwordValidForDays: 180, + agreedToTermsOfService: true, }, signOut: () => {}, }} diff --git a/shared-helpers/__tests__/filters.test.ts b/shared-helpers/__tests__/filters.test.ts new file mode 100644 index 0000000000..04bc82b771 --- /dev/null +++ b/shared-helpers/__tests__/filters.test.ts @@ -0,0 +1,114 @@ +import { cleanup } from "@testing-library/react" +import { + encodeToBackendFilterArray, + encodeToFrontendFilterString, + decodeFiltersFromFrontendUrl, + ListingFilterState, +} from "../src/filters" +import { parse } from "querystring" +import { EnumListingFilterParamsComparison } from "@bloom-housing/backend-core/types" + +afterEach(cleanup) + +describe("encode backend filter array", () => { + it("should handle single filter", () => { + const filter: ListingFilterState = { + zipcode: "48226", + } + expect(encodeToBackendFilterArray(filter)).toEqual([ + { + $comparison: EnumListingFilterParamsComparison["IN"], + zipcode: "48226", + }, + ]) + }) + + it("should handle multiple filters", () => { + const filter: ListingFilterState = { + bedRoomSize: "3", + zipcode: "48226", + } + expect(encodeToBackendFilterArray(filter)).toContainEqual({ + $comparison: EnumListingFilterParamsComparison["IN"], + bedRoomSize: "3", + }) + expect(encodeToBackendFilterArray(filter)).toContainEqual({ + $comparison: EnumListingFilterParamsComparison["IN"], + zipcode: "48226", + }) + }) + + it("should handle multiple bedroom filters", () => { + const filter: ListingFilterState = { + bedRoomSize: "2,3", + } + expect(encodeToBackendFilterArray(filter)).toEqual([ + { + $comparison: EnumListingFilterParamsComparison["IN"], + bedRoomSize: "2,3", + }, + ]) + }) +}) + +describe("encode filter state as frontend querystring", () => { + it("should handle single filter", () => { + const filter: ListingFilterState = { + zipcode: "48226", + } + expect(encodeToFrontendFilterString(filter)).toBe("&zipcode=48226") + }) + + it("should handle multiple filters", () => { + const filter: ListingFilterState = { + threeBdrm: true, + zipcode: "48226", + } + expect(encodeToFrontendFilterString(filter)).toBe("&threeBdrm=true&zipcode=48226") + }) + + it("should exclude empty filters", () => { + const filter: ListingFilterState = { + threeBdrm: true, + zipcode: "", + } + expect(encodeToFrontendFilterString(filter)).toBe("&threeBdrm=true") + }) +}) + +describe("get filter state from parsed url", () => { + it("should handle single filter", () => { + const filterString = parse("localhost:3000/listings?page=1&zipcode=48226") + const expected: ListingFilterState = { + zipcode: "48226", + } + expect(decodeFiltersFromFrontendUrl(filterString)).toStrictEqual(expected) + }) + + it("should handle multiple filters", () => { + const filterString = parse("localhost:3000/listings?page=1&threeBdrm=true&zipcode=48226") + const expected: ListingFilterState = { + threeBdrm: "true", + zipcode: "48226", + } + expect(decodeFiltersFromFrontendUrl(filterString)).toEqual(expected) + }) + + it("should handle no filters", () => { + const filterString = parse("localhost:3000/listings?page=1") + expect(decodeFiltersFromFrontendUrl(filterString)).toBe(undefined) + }) + + it("should handle no known filter keys", () => { + const filterString = parse("localhost:3000/listings?page=1&unknown=blah") + expect(decodeFiltersFromFrontendUrl(filterString)).toBe(undefined) + }) + + it("should handle some known filters", () => { + const filterString = parse("localhost:3000/listings?page=1&unknown=blah&zipcode=48226") + const expected: ListingFilterState = { + zipcode: "48226", + } + expect(decodeFiltersFromFrontendUrl(filterString)).toStrictEqual(expected) + }) +}) diff --git a/shared-helpers/__tests__/formKeys.test.ts b/shared-helpers/__tests__/formKeys.test.ts index 2b57162a78..85a80e6a59 100644 --- a/shared-helpers/__tests__/formKeys.test.ts +++ b/shared-helpers/__tests__/formKeys.test.ts @@ -1,6 +1,55 @@ -import * as formKeys from "../src/formKeys" +import { cleanup } from "@testing-library/react" +import { fieldGroupObjectToArray, prependRoot } from "../src/formKeys" -test("U.S. states exist", () => { - // it's 52 because DC and "" are included - expect(formKeys.stateKeys.length).toBe(52) +afterEach(cleanup) + +describe("formKeys helpers", () => { + it("prependRoot should prepend a string", () => { + const testArray = ["a", "b", "c"] + expect(prependRoot("rootKey", testArray)).toStrictEqual(["rootKey-a", "rootKey-b", "rootKey-c"]) + }) + + it("fieldGroupObjectToArray with only matching keys", () => { + const testObj = { + ["root-A"]: "A", + ["root-B"]: "B", + ["root-C"]: "C", + } + const expectedArray = ["A", "B", "C"] + expect(fieldGroupObjectToArray(testObj, "root")).toStrictEqual(expectedArray) + }) + + it("fieldGroupObjectToArray with only some matching keys", () => { + const testObj = { + testObj: {}, + ["root-A"]: "A", + ["other-B"]: "B", + ["root-C"]: "C", + ["other"]: "D", + } + const expectedArray = ["A", "C"] + expect(fieldGroupObjectToArray(testObj, "root")).toStrictEqual(expectedArray) + }) + + it("fieldGroupObjectToArray with subkeys", () => { + const testObj = { + ["root-A"]: "A", + ["root-B"]: "B", + ["root-C"]: "C", + ["root-D"]: "subKey", + } + const expectedArray = ["A", "B", "C", "D: subKey"] + expect(fieldGroupObjectToArray(testObj, "root")).toStrictEqual(expectedArray) + }) + + it("fieldGroupObjectToArray with subArrays", () => { + const testObj = { + ["root-A"]: "A", + ["root-B"]: "B", + ["root-C"]: "C", + ["root-D"]: ["1", "2"], + } + const expectedArray = ["A", "B", "C", "D: 1,2"] + expect(fieldGroupObjectToArray(testObj, "root")).toStrictEqual(expectedArray) + }) }) diff --git a/shared-helpers/__tests__/occupancyFormatting.test.tsx b/shared-helpers/__tests__/occupancyFormatting.test.tsx new file mode 100644 index 0000000000..db00b9c528 --- /dev/null +++ b/shared-helpers/__tests__/occupancyFormatting.test.tsx @@ -0,0 +1,116 @@ +import React from "react" +import { cleanup } from "@testing-library/react" +import { occupancyTable } from "../src/occupancyFormatting" +import { Listing, UnitType, UnitGroup } from "@bloom-housing/backend-core/types" + +const unitTypeStudio = { name: "studio", numBedrooms: 0 } as UnitType +const unitTypeOneBdrm = { name: "oneBdrm", numBedrooms: 1 } as UnitType +const unitTypeTwoBdrm = { name: "twoBdrm", numBedrooms: 2 } as UnitType +const unitTypeThreeBdrm = { name: "threeBdrm", numBedrooms: 3 } as UnitType +const unitTypeFourBdrm = { name: "fourBdrm", numBedrooms: 4 } as UnitType + +const unitGroups: Omit< + UnitGroup, + "id" | "listing" | "openWaitlist" | "amiLevels" | "listingId" +>[] = [ + { + unitType: [unitTypeStudio, unitTypeOneBdrm], + minOccupancy: 1, + maxOccupancy: 2, + }, + { + unitType: [unitTypeOneBdrm], + minOccupancy: 1, + maxOccupancy: 3, + }, + { + unitType: [unitTypeTwoBdrm], + minOccupancy: 2, + maxOccupancy: 6, + }, + { + unitType: [unitTypeTwoBdrm], + minOccupancy: 2, + maxOccupancy: undefined, + }, + { + unitType: [unitTypeTwoBdrm], + minOccupancy: undefined, + maxOccupancy: 2, + }, + { + unitType: [unitTypeFourBdrm], + minOccupancy: 1, + maxOccupancy: undefined, + }, + { + unitType: [unitTypeTwoBdrm], + minOccupancy: 1, + maxOccupancy: 1, + }, + { + unitType: [unitTypeThreeBdrm], + minOccupancy: 3, + maxOccupancy: 3, + }, + { + unitType: [unitTypeFourBdrm], + minOccupancy: undefined, + maxOccupancy: undefined, + }, + { + unitType: [unitTypeTwoBdrm, unitTypeOneBdrm], + minOccupancy: 1, + maxOccupancy: 7, + }, +] + +const testListing: Listing = {} as Listing +testListing.unitGroups = unitGroups as UnitGroup[] +afterEach(cleanup) + +// Differs from core due to unit groups +describe("occupancy formatting helpers", () => { + describe("occupancyTable", () => { + it("properly creates occupancy table", () => { + expect(occupancyTable(testListing)).toStrictEqual([ + { + occupancy: "1-2 people", + unitType: Studio, 1 BR, + }, + { + occupancy: "1-3 people", + unitType: 1 BR, + }, + { + occupancy: "1-7 people", + unitType: 1 BR, 2 BR, + }, + { + occupancy: "2-6 people", + unitType: 2 BR, + }, + { + occupancy: "at least 2 people", + unitType: 2 BR, + }, + { + occupancy: "at most 2 people", + unitType: 2 BR, + }, + { + occupancy: "1 person", + unitType: 2 BR, + }, + { + occupancy: "3 people", + unitType: 3 BR, + }, + { + occupancy: "at least 1 person", + unitType: 4 BR, + }, + ]) + }) + }) +}) diff --git a/shared-helpers/__tests__/pdfs.test.ts b/shared-helpers/__tests__/pdfs.test.ts new file mode 100644 index 0000000000..e9967b546b --- /dev/null +++ b/shared-helpers/__tests__/pdfs.test.ts @@ -0,0 +1,39 @@ +import { ListingEventType, ListingEvent } from "@bloom-housing/backend-core/types" +import { cleanup } from "@testing-library/react" +import { cloudinaryPdfFromId, pdfUrlFromListingEvents } from "../src/pdfs" + +afterEach(cleanup) + +describe("pdfs helpers", () => { + it("should format cloudinary url", () => { + expect(cloudinaryPdfFromId("1234", "exygy")).toBe( + `https://res.cloudinary.com/exygy/image/upload/1234.pdf` + ) + }) + it("should return correct pdf url for event if event type exists and file is cloudinary type", () => { + const listingEvents = [ + { type: ListingEventType.lotteryResults, file: { fileId: "1234", label: "cloudinaryPDF" } }, + { type: ListingEventType.openHouse, file: { fileId: "5678", label: "cloudinaryPDF" } }, + ] as ListingEvent[] + expect(pdfUrlFromListingEvents(listingEvents, ListingEventType.lotteryResults, "exygy")).toBe( + `https://res.cloudinary.com/exygy/image/upload/1234.pdf` + ) + }) + it("should return null if event type exists but is not cloudinary type", () => { + const listingEvents = [ + { type: ListingEventType.lotteryResults, file: { fileId: "1234" } }, + ] as ListingEvent[] + expect(pdfUrlFromListingEvents(listingEvents, ListingEventType.lotteryResults, "exygy")).toBe( + null + ) + }) + it("should return null if no event of type exists", () => { + const listingEvents = [ + { type: ListingEventType.lotteryResults }, + { type: ListingEventType.openHouse }, + ] as ListingEvent[] + expect(pdfUrlFromListingEvents(listingEvents, ListingEventType.publicLottery, "exygy")).toBe( + null + ) + }) +}) diff --git a/shared-helpers/__tests__/photos.test.ts b/shared-helpers/__tests__/photos.test.ts new file mode 100644 index 0000000000..094005bdc6 --- /dev/null +++ b/shared-helpers/__tests__/photos.test.ts @@ -0,0 +1,60 @@ +import { Listing } from "@bloom-housing/backend-core/types" +import { cleanup } from "@testing-library/react" +import { cloudinaryUrlFromId, CLOUDINARY_BUILDING_LABEL, imageUrlFromListing } from "../src/photos" + +afterEach(cleanup) + +describe("photos helper", () => { + const OLD_ENV = process.env + + beforeEach(() => { + jest.resetModules() + process.env = { ...OLD_ENV } + }) + + afterAll(() => { + process.env = OLD_ENV + }) + + it("should return correct cloudinary url", () => { + process.env.CLOUDINARY_CLOUD_NAME = "exygy" + expect(cloudinaryUrlFromId("1234")).toBe( + `https://res.cloudinary.com/exygy/image/upload/w_400,c_limit,q_65/1234.jpg` + ) + }) + + it("should return correct cloudinary url from a listing with new image field", () => { + process.env.CLOUDINARY_CLOUD_NAME = "exygy" + + const testListing = { + images: [ + { + ordinal: 0, + image: { + fileId: "1234", + label: CLOUDINARY_BUILDING_LABEL, + }, + }, + ], + } as Listing + + expect(imageUrlFromListing(testListing)).toBe( + `https://res.cloudinary.com/exygy/image/upload/w_400,c_limit,q_65/1234.jpg` + ) + }) + + it("should return correct id when falling back to old field", () => { + process.env.CLOUDINARY_CLOUD_NAME = "exygy" + + const testListing = { + assets: [ + { + fileId: "5678", + label: "building", + }, + ], + } as Listing + + expect(imageUrlFromListing(testListing)).toBe("5678") + }) +}) diff --git a/shared-helpers/__tests__/postmarkString.test.ts b/shared-helpers/__tests__/postmarkString.test.ts new file mode 100644 index 0000000000..baeafea776 --- /dev/null +++ b/shared-helpers/__tests__/postmarkString.test.ts @@ -0,0 +1,41 @@ +import { cleanup } from "@testing-library/react" +import { getPostmarkString } from "../src/postmarkString" +import { t } from "@bloom-housing/ui-components" + +afterEach(cleanup) + +describe("postmark string helper", () => { + it("no data", () => { + expect(getPostmarkString(null, null, null)).toBe("") + }) + it("excludes postmarks, excludes due date", () => { + expect(getPostmarkString(null, null, "Developer")).toBe( + t("listings.apply.submitPaperNoDueDateNoPostMark", { developer: "Developer" }) + ) + }) + it("includes postmarks, includes due date", () => { + expect(getPostmarkString("November 29th, 2021", "November 30th, 2021", "Developer")).toBe( + t("listings.apply.submitPaperDueDatePostMark", { + applicationDueDate: "November 29th, 2021", + postmarkReceivedByDate: "November 30th, 2021", + developer: "Developer", + }) + ) + }) + it("includes postmarks, excludes due date", () => { + expect(getPostmarkString(null, "November 30th, 2021", "Developer")).toBe( + t("listings.apply.submitPaperNoDueDatePostMark", { + postmarkReceivedByDate: "November 30th, 2021", + developer: "Developer", + }) + ) + }) + it("excludes postmarks, includes due date", () => { + expect(getPostmarkString("November 29th, 2021", null, "Developer")).toBe( + t("listings.apply.submitPaperDueDateNoPostMark", { + applicationDueDate: "November 29th, 2021", + developer: "Developer", + }) + ) + }) +}) diff --git a/shared-helpers/__tests__/stringFormatting.test.ts b/shared-helpers/__tests__/stringFormatting.test.ts new file mode 100644 index 0000000000..b6c3c63e82 --- /dev/null +++ b/shared-helpers/__tests__/stringFormatting.test.ts @@ -0,0 +1,36 @@ +import { cleanup } from "@testing-library/react" +import { getTimeRangeString, getCurrencyRange } from "../src/stringFormatting" + +afterEach(cleanup) + +describe("stringFormatting helpers", () => { + describe("getTimeRangeString", () => { + it("formats different parameters as a time range", () => { + expect(getTimeRangeString(new Date(2018, 8, 10, 10), new Date(2018, 8, 18, 11))).toBe( + "10:00am - 11:00am" + ) + }) + it("formats different parameters as one time", () => { + expect(getTimeRangeString(new Date(2018, 8, 10, 10), new Date(2018, 8, 18, 10))).toBe( + "10:00am" + ) + }) + }) + describe("getCurrencyRange", () => { + it("with just min", () => { + expect(getCurrencyRange(10, null)).toBe("$10") + }) + it("with just max", () => { + expect(getCurrencyRange(null, 10)).toBe("$10") + }) + it("with the same values", () => { + expect(getCurrencyRange(100, 100)).toBe("$100") + }) + it("with a range", () => { + expect(getCurrencyRange(100, 200)).toBe("$100 – $200") + }) + it("with neither", () => { + expect(getCurrencyRange(null, null)).toBe("") + }) + }) +}) diff --git a/shared-helpers/__tests__/unitTypes.test.ts b/shared-helpers/__tests__/unitTypes.test.ts new file mode 100644 index 0000000000..f72eb5d11d --- /dev/null +++ b/shared-helpers/__tests__/unitTypes.test.ts @@ -0,0 +1,103 @@ +import { cleanup } from "@testing-library/react" +import { getUniqueUnitTypes, sortUnitTypes } from "../src/unitTypes" +import { UnitStatus } from "@bloom-housing/backend-core/types" + +afterEach(cleanup) + +describe("unit type: sortUnitTypes helper", () => { + it("should return empty array if empty array is passed in", () => { + expect(sortUnitTypes([])).toStrictEqual([]) + }) + it("should sort basic arrays", () => { + expect( + sortUnitTypes([ + { id: "studio", name: "studio" }, + { id: "oneBdrm", name: "oneBdrm" }, + { id: "twoBdrm", name: "twoBdrm" }, + { id: "threeBdrm", name: "threeBdrm" }, + { id: "fourBdrm", name: "fourBdrm" }, + ]) + ).toStrictEqual([ + { id: "studio", name: "studio" }, + { id: "oneBdrm", name: "oneBdrm" }, + { id: "twoBdrm", name: "twoBdrm" }, + { id: "threeBdrm", name: "threeBdrm" }, + { id: "fourBdrm", name: "fourBdrm" }, + ]) + expect( + sortUnitTypes([ + { id: "fourBdrm", name: "fourBdrm" }, + { id: "studio", name: "studio" }, + { id: "oneBdrm", name: "oneBdrm" }, + { id: "twoBdrm", name: "twoBdrm" }, + { id: "threeBdrm", name: "threeBdrm" }, + ]) + ).toStrictEqual([ + { id: "studio", name: "studio" }, + { id: "oneBdrm", name: "oneBdrm" }, + { id: "twoBdrm", name: "twoBdrm" }, + { id: "threeBdrm", name: "threeBdrm" }, + { id: "fourBdrm", name: "fourBdrm" }, + ]) + }) + it("should sort complex arrays", () => { + expect( + sortUnitTypes([ + { id: "oneBdrm", name: "oneBdrm" }, + { id: "studio", name: "studio" }, + { id: "threeBdrm", name: "threeBdrm" }, + { id: "oneBdrm", name: "oneBdrm" }, + { id: "twoBdrm", name: "twoBdrm" }, + { id: "fourBdrm", name: "fourBdrm" }, + { id: "threeBdrm", name: "threeBdrm" }, + { id: "fourBdrm", name: "fourBdrm" }, + ]) + ).toStrictEqual([ + { id: "studio", name: "studio" }, + { id: "oneBdrm", name: "oneBdrm" }, + { id: "oneBdrm", name: "oneBdrm" }, + { id: "twoBdrm", name: "twoBdrm" }, + { id: "threeBdrm", name: "threeBdrm" }, + { id: "threeBdrm", name: "threeBdrm" }, + { id: "fourBdrm", name: "fourBdrm" }, + { id: "fourBdrm", name: "fourBdrm" }, + ]) + }) +}) + +describe("unit type: getUniqueUnitTypes helper", () => { + it("should return empty array if empty array is passed in", () => { + expect(getUniqueUnitTypes([])).toStrictEqual([]) + }) + it("should return empty array if all elements are invalid", () => { + expect( + getUniqueUnitTypes([ + { status: UnitStatus["available"], id: "", createdAt: new Date(), updatedAt: new Date() }, + ]) + ).toStrictEqual([]) + }) + it("should return 1 element if 1 valid element is passed in", () => { + expect( + getUniqueUnitTypes([ + { + status: UnitStatus["available"], + id: "example id", + createdAt: new Date(), + updatedAt: new Date(), + unitType: { + id: "Test", + createdAt: new Date(), + updatedAt: new Date(), + numBedrooms: 2, + name: "Example Name", + }, + }, + ]) + ).toStrictEqual([ + { + id: "Test", + name: "Example Name", + }, + ]) + }) +}) diff --git a/shared-helpers/index.ts b/shared-helpers/index.ts index a71ae234ee..a8434199d2 100644 --- a/shared-helpers/index.ts +++ b/shared-helpers/index.ts @@ -1 +1,27 @@ +export * from "./src/AuthContext" +export * from "./src/blankApplication" +export * from "./src/catchNetworkError" +export * from "./src/ConfigContext" +export * from "./src/filters" export * from "./src/formKeys" +export * from "./src/formatRange" +export * from "./src/formatRentRange" +export * from "./src/gtm" +export * from "./src/Icons" +export * from "./src/minMaxFinder" +export * from "./src/nextjs" +export * from "./src/occupancyFormatting" +export * from "./src/pdfs" +export * from "./src/photos" +export * from "./src/postmarkString" +export * from "./src/preferences" +export * from "./src/programHelpers" +export * from "./src/regions" +export * from "./src/RequireLogin" +export * from "./src/stringFormatting" +export * from "./src/timeout" +export * from "./src/token" +export * from "./src/unitTypes" +export * from "./src/useKeyPress" +export * from "./src/useRequireLoggedInUser" +export * from "./src/TransitionBanner" diff --git a/shared-helpers/jest.config.js b/shared-helpers/jest.config.js index 420e3bd1e5..a7a6e3ca90 100644 --- a/shared-helpers/jest.config.js +++ b/shared-helpers/jest.config.js @@ -19,9 +19,12 @@ module.exports = { preset: "ts-jest", globals: { "ts-jest": { - tsConfig: "tsconfig.json", + tsconfig: "tsconfig.json", }, }, + moduleNameMapper: { + "\\.(scss|css|less)$": "identity-obj-proxy", + }, rootDir: "..", roots: ["/shared-helpers"], transform: { diff --git a/shared-helpers/package.json b/shared-helpers/package.json index 60894fbd98..3947aa9c65 100644 --- a/shared-helpers/package.json +++ b/shared-helpers/package.json @@ -1,18 +1,40 @@ { "name": "@bloom-housing/shared-helpers", - "version": "1.0.5", + "version": "4.4.0", "description": "Shared helpers for Bloom affordable housing system", "homepage": "https://github.com/bloom-housing/bloom/tree/master/shared-helpers", "main": "index.js", "license": "Apache-2.0", - "private": false, + "private": true, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "scripts": { "test": "jest -w 1", "test:coverage": "jest -w 1 --coverage --watchAll=false", "prettier": "prettier --write \"**/*.ts\"" }, + "dependencies": { + "@bloom-housing/backend-core": "^4.4.0", + "@bloom-housing/ui-components": "^12.0.11", + "axios": "0.21.2" + }, "devDependencies": { + "@testing-library/jest-dom": "5.16.4", + "@testing-library/react": "14.0.0", + "@types/jest": "^26.0.14", + "@types/node-polyglot": "^2.4.1", + "@types/react-tabs": "^2.3.2", + "@types/react-test-renderer": "18.0.0", + "@types/react-transition-group": "^4.4.0", + "identity-obj-proxy": "^3.0.0", "jest": "^26.5.3", - "typescript": "^3.9.7" + "react": "18.2.0", + "react-dom": "18.2.0", + "react-test-renderer": "18.2.0", + "ts-jest": "^26.4.1", + "ts-loader": "^8.0.4", + "typescript": "^4.5.5" } } diff --git a/ui-components/src/authentication/AuthContext.ts b/shared-helpers/src/AuthContext.ts similarity index 78% rename from ui-components/src/authentication/AuthContext.ts rename to shared-helpers/src/AuthContext.ts index 0f0e1ea23b..a18658bcae 100644 --- a/ui-components/src/authentication/AuthContext.ts +++ b/shared-helpers/src/AuthContext.ts @@ -7,6 +7,7 @@ import { UserBasic, UserCreate, UserService, + UserProfileService, serviceOptions, Status, AmiChartsService, @@ -14,6 +15,12 @@ import { UnitAccessibilityPriorityTypesService, UnitTypesService, PreferencesService, + JurisdictionsService, + ProgramsService, + RequestMfaCodeResponse, + EnumRequestMfaCodeMfaType, + EnumLoginMfaType, + UserPreferencesService, } from "@bloom-housing/backend-core/types" import { createContext, @@ -23,26 +30,36 @@ import { useEffect, useMemo, useReducer, + useCallback, } from "react" import qs from "qs" import axiosStatic from "axios" -import { ConfigContext } from "../config/ConfigContext" +import { ConfigContext } from "./ConfigContext" import { createAction, createReducer } from "typesafe-actions" import { clearToken, getToken, getTokenTtl, setToken } from "./token" -import { NavigationContext } from "../config/NavigationContext" +import { NavigationContext } from "@bloom-housing/ui-components" type ContextProps = { amiChartsService: AmiChartsService applicationsService: ApplicationsService applicationFlaggedSetsService: ApplicationFlaggedSetsService listingsService: ListingsService + jurisdictionsService: JurisdictionsService userService: UserService + userProfileService: UserProfileService authService: AuthService preferencesService: PreferencesService + programsService: ProgramsService reservedCommunityTypeService: ReservedCommunityTypesService unitPriorityService: UnitAccessibilityPriorityTypesService unitTypesService: UnitTypesService - login: (email: string, password: string) => Promise + loadProfile: (redirect?: string) => void + login: ( + email: string, + password: string, + mfaCode?: string, + mfaType?: EnumLoginMfaType + ) => Promise loginWithToken: (token: string) => Promise resetPassword: ( token: string, @@ -58,6 +75,14 @@ type ContextProps = { initialStateLoaded: boolean loading: boolean profile?: User + userPreferencesService: UserPreferencesService + requestMfaCode: ( + email: string, + password: string, + mfaType: EnumRequestMfaCodeMfaType, + phoneNumber?: string + ) => Promise + updateProfile: (profile: User) => void } // Internal Provider State @@ -79,7 +104,7 @@ const saveToken = createAction("SAVE_TOKEN")<{ accessToken: string dispatch: DispatchType }>() -const saveProfile = createAction("SAVE_PROFILE")() +const saveProfile = createAction("SAVE_PROFILE")() const startLoading = createAction("START_LOADING")() const stopLoading = createAction("STOP_LOADING")() const signOut = createAction("SIGN_OUT")() @@ -110,6 +135,7 @@ const reducer = createReducer( initialStateLoaded: false, storageType: "session", language: "en", + accessToken: undefined, } as AuthState, { SAVE_TOKEN: (state, { payload }) => { @@ -154,7 +180,7 @@ const reducer = createReducer( ) export const AuthContext = createContext>({}) -export const AuthProvider: FunctionComponent = ({ children }) => { +export const AuthProvider: FunctionComponent = ({ children }) => { const { apiUrl, storageType } = useContext(ConfigContext) const { router } = useContext(NavigationContext) const [state, dispatch] = useReducer(reducer, { @@ -198,32 +224,42 @@ export const AuthProvider: FunctionComponent = ({ children }) => { } }, [apiUrl, storageType]) + const loadProfile = useCallback( + async (redirect?: string) => { + try { + const profile = await userService?.userControllerProfile() + if (profile) { + dispatch(saveProfile(profile)) + } + } finally { + dispatch(stopLoading()) + + if (redirect) { + router.push(redirect) + } + } + }, + [userService, router] + ) + // Load our profile as soon as we have an access token available useEffect(() => { if (!state.profile && state.accessToken && !state.loading) { - const loadProfile = async () => { - dispatch(startLoading()) - try { - const profile = await userService?.userControllerProfile() - if (profile) { - dispatch(saveProfile(profile)) - } - } finally { - dispatch(stopLoading()) - } - } void loadProfile() } - }, [state.profile, state.accessToken, apiUrl, userService, state.loading]) + }, [state.profile, state.accessToken, apiUrl, userService, state.loading, loadProfile]) const contextValues: ContextProps = { amiChartsService: new AmiChartsService(), applicationsService: new ApplicationsService(), applicationFlaggedSetsService: new ApplicationFlaggedSetsService(), listingsService: new ListingsService(), + jurisdictionsService: new JurisdictionsService(), userService: new UserService(), + userProfileService: new UserProfileService(), authService: new AuthService(), preferencesService: new PreferencesService(), + programsService: new ProgramsService(), reservedCommunityTypeService: new ReservedCommunityTypesService(), unitPriorityService: new UnitAccessibilityPriorityTypesService(), unitTypesService: new UnitTypesService(), @@ -231,14 +267,23 @@ export const AuthProvider: FunctionComponent = ({ children }) => { accessToken: state.accessToken, initialStateLoaded: state.initialStateLoaded, profile: state.profile, - login: async (email, password) => { + userPreferencesService: new UserPreferencesService(), + loadProfile, + login: async ( + email, + password, + mfaCode: string | undefined = undefined, + mfaType: EnumLoginMfaType | undefined = undefined + ) => { dispatch(signOut()) dispatch(startLoading()) try { - const response = await authService?.login({ body: { email, password } }) + const response = await authService?.login({ body: { email, password, mfaCode, mfaType } }) if (response) { dispatch(saveToken({ accessToken: response.accessToken, apiUrl, dispatch })) - const profile = await userService?.userControllerProfile() + const profile = await userService?.userControllerProfile({ + headers: { Authorization: `Bearer ${response.accessToken}` }, + }) if (profile) { dispatch(saveProfile(profile)) return profile @@ -251,7 +296,9 @@ export const AuthProvider: FunctionComponent = ({ children }) => { }, loginWithToken: async (token: string) => { dispatch(saveToken({ accessToken: token, apiUrl, dispatch })) - const profile = await userService?.userControllerProfile() + const profile = await userService?.userControllerProfile({ + headers: { Authorization: `Bearer ${token}` }, + }) if (profile) { dispatch(saveProfile(profile)) return profile @@ -272,7 +319,9 @@ export const AuthProvider: FunctionComponent = ({ children }) => { }) if (response) { dispatch(saveToken({ accessToken: response.accessToken, apiUrl, dispatch })) - const profile = await userService?.userControllerProfile() + const profile = await userService?.userControllerProfile({ + headers: { Authorization: `Bearer ${response.accessToken}` }, + }) if (profile) { dispatch(saveProfile(profile)) return profile @@ -289,7 +338,9 @@ export const AuthProvider: FunctionComponent = ({ children }) => { const response = await userService?.confirm({ body: { token } }) if (response) { dispatch(saveToken({ accessToken: response.accessToken, apiUrl, dispatch })) - const profile = await userService?.userControllerProfile() + const profile = await userService?.userControllerProfile({ + headers: { Authorization: `Bearer ${response.accessToken}` }, + }) if (profile) { dispatch(saveProfile(profile)) return profile @@ -333,6 +384,19 @@ export const AuthProvider: FunctionComponent = ({ children }) => { dispatch(stopLoading()) } }, + requestMfaCode: async (email, password, mfaType, phoneNumber) => { + dispatch(startLoading()) + try { + return await authService?.requestMfaCode({ + body: { email, password, mfaType, phoneNumber }, + }) + } finally { + dispatch(stopLoading()) + } + }, + updateProfile: (profile) => { + dispatch(saveProfile(profile)) + }, } return createElement(AuthContext.Provider, { value: contextValues }, children) } diff --git a/ui-components/src/config/ConfigContext.tsx b/shared-helpers/src/ConfigContext.tsx similarity index 96% rename from ui-components/src/config/ConfigContext.tsx rename to shared-helpers/src/ConfigContext.tsx index a7c4c148ec..069ade0e97 100644 --- a/ui-components/src/config/ConfigContext.tsx +++ b/shared-helpers/src/ConfigContext.tsx @@ -19,6 +19,7 @@ export const ConfigProvider: FunctionComponent<{ apiUrl: string storageType?: ConfigContextProps["storageType"] idleTimeout?: number + children?: React.ReactNode }> = ({ apiUrl, storageType = "session", idleTimeout = defaultTimeout, children }) => { return createElement( ConfigContext.Provider, diff --git a/shared-helpers/src/Icons.tsx b/shared-helpers/src/Icons.tsx new file mode 100644 index 0000000000..1565e22d42 --- /dev/null +++ b/shared-helpers/src/Icons.tsx @@ -0,0 +1,955 @@ +import * as React from "react" + +export interface IconProps { + fill?: string + className?: string +} + +export const Accessible = (props: IconProps) => { + return ( + + + + + + ) +} + +export const Application = (props: IconProps) => { + return ( + + + + + + + + + + + ) +} + +export const ArrowBack = (props: IconProps) => { + return ( + + + + ) +} + +export const ArrowForward = (props: IconProps) => { + return ( + + + + + + ) +} + +export const ArrowDown = (props: IconProps) => { + return ( + + + + ) +} + +export const Assistance = (props: IconProps) => { + return ( + + + + + ) +} + +export const Asterisk = (props: IconProps) => { + return ( + + + + ) +} + +export const BadgeCheck = (props: IconProps) => { + return ( + + + + ) +} + +export const Bed = (props: IconProps) => { + return ( + + + + ) +} + +export const Browse = (props: IconProps) => { + return ( + + + + + + ) +} + +export const Building = (props: IconProps) => { + return ( + + + + + + + ) +} + +export const Calendar = (props: IconProps) => { + return ( + + + + ) +} + +export const CalendarBlock = (props: IconProps) => { + return ( + + + + ) +} + +export const Check = (props: IconProps) => { + return ( + + + + ) +} + +export const Clock = (props: IconProps) => { + return ( + + + + + ) +} + +export const Close = (props: IconProps) => { + return ( + + + + + + ) +} + +export const CloseRound = (props: IconProps) => { + return ( + + + + ) +} + +export const CloseSmall = (props: IconProps) => { + return ( + + + + + + + + + + ) +} + +export const Cross = (props: IconProps) => { + return ( + + + + ) +} + +export const Document = (props: IconProps) => { + return ( + + + + ) +} + +export const DoubleHouse = (props: IconProps) => { + return ( + + + + + + + + + + ) +} + +export const Down = (props: IconProps) => { + return ( + + + + + + ) +} + +export const Download = (props: IconProps) => { + return ( + + + + ) +} + +export const Draggable = (props: IconProps) => { + return ( + + + + ) +} + +export const Edit = (props: IconProps) => { + return ( + + + + ) +} + +export const Eligibility = (props: IconProps) => { + return ( + + + + + + + + + + + + ) +} + +export const Envelope = (props: IconProps) => { + return ( + + + + ) +} + +export const EnvelopeThin = (props: IconProps) => { + return ( + + + + ) +} + +export const Eye = (props: IconProps) => { + return ( + + + + ) +} + +export const Favorite = (props: IconProps) => { + return ( + + + + ) +} + +export const File = (props: IconProps) => { + return ( + + + + ) +} + +export const Filter = (props: IconProps) => { + return ( + + + + + + + + ) +} + +export const Forward = (props: IconProps) => { + return ( + + + + ) +} + +export const FrontDoor = (props: IconProps) => { + return ( + + + + ) +} + +export const Globe = (props: IconProps) => { + return ( + + + + ) +} + +export const Hamburger = (props: IconProps) => { + return ( + + + + + + + + + + ) +} + +export const House = (props: IconProps) => { + return ( + + + + ) +} + +export const HouseThin = (props: IconProps) => { + return ( + + + + ) +} +export const Info = (props: IconProps) => { + return ( + + + + ) +} + +export const Left = (props: IconProps) => { + return ( + + + + {" "} + + ) +} + +export const Lightbulb = (props: IconProps) => { + return ( + + + + ) +} + +export const Like = (props: IconProps) => { + return ( + + + + ) +} + +export const LikeFill = (props: IconProps) => { + return ( + + + + ) +} + +export const Link = (props: IconProps) => { + return ( + + + + ) +} + +export const List = (props: IconProps) => { + return ( + + + + ) +} + +export const Lock = (props: IconProps) => { + return ( + + + + ) +} + +export const Mail = (props: IconProps) => { + return ( + + + + ) +} + +export const MailThin = (props: IconProps) => { + return ( + + + + + + + + + + + + + + + + ) +} + +export const Map = (props: IconProps) => { + return ( + + + + ) +} + +export const MapThin = (props: IconProps) => { + return ( + + + + + ) +} + +export const Menu = (props: IconProps) => { + return ( + + + + ) +} + +export const Messages = (props: IconProps) => { + return ( + + + + ) +} + +export const Oval = (props: IconProps) => { + return ( + + + + ) +} + +export const Phone = (props: IconProps) => { + return ( + + + + ) +} + +export const Plus = (props: IconProps) => { + return ( + + + + ) +} + +export const Polygon = (props: IconProps) => { + return ( + + + + ) +} + +export const Profile = (props: IconProps) => { + return ( + + + + + ) +} + +export const Question = (props: IconProps) => { + return ( + + + + ) +} + +export const QuestionThin = (props: IconProps) => { + return ( + + + + ) +} + +export const CircleQuestionThin = (props: IconProps) => { + return ( + + {" "} + + ) +} + +export const Result = (props: IconProps) => { + return ( + + + + + + + + ) +} + +export const Right = (props: IconProps) => { + return ( + + + + + + ) +} + +export const Search = (props: IconProps) => { + return ( + + + + ) +} + +export const Settings = (props: IconProps) => { + return ( + + + + + ) +} + +export const Spinner = (props: IconProps) => { + return ( + + + + ) +} + +export const Star = (props: IconProps) => { + return ( + + + + ) +} + +export const Ticket = (props: IconProps) => { + return ( + + + + ) +} + +export const Trash = (props: IconProps) => { + return ( + + + + ) +} +export const UniversalAccess = (props: IconProps) => { + return ( + + + + ) +} + +export const Warning = (props: IconProps) => { + return ( + + + + ) +} + +export const DetroitIconMap = { + accessible: Accessible, + application: Application, + arrowBack: ArrowBack, + arrowForward: ArrowForward, + arrowDown: ArrowDown, + assistance: Assistance, + asterisk: Asterisk, + badgeCheck: BadgeCheck, + bed: Bed, + browse: Browse, + building: Building, + calendar: Calendar, + calendarBlock: CalendarBlock, + check: Check, + clock: Clock, + close: Close, + closeRound: CloseRound, + closeSmall: CloseSmall, + cross: Cross, + document: Document, + doubleHouse: DoubleHouse, + down: Down, + download: Download, + draggable: Draggable, + edit: Edit, + eligibility: Eligibility, + envelope: Envelope, + envelopeThin: EnvelopeThin, + eye: Eye, + favorite: Favorite, + file: File, + filter: Filter, + forward: Forward, + frontDoor: FrontDoor, + globe: Globe, + hamburger: Hamburger, + house: House, + houseThin: HouseThin, + info: Info, + left: Left, + lightbulb: Lightbulb, + like: Like, + likeFill: LikeFill, + link: Link, + list: List, + lock: Lock, + mail: Mail, + mailThin: MailThin, + map: Map, + mapThin: MapThin, + menu: Menu, + messages: Messages, + oval: Oval, + phone: Phone, + plus: Plus, + polygon: Polygon, + profile: Profile, + question: Question, + questionThin: QuestionThin, + circleQuestionThin: CircleQuestionThin, + result: Result, + right: Right, + search: Search, + settings: Settings, + spinner: Spinner, + star: Star, + ticket: Ticket, + trash: Trash, + universalAccess: UniversalAccess, + warning: Warning, +} + +export type DetroitIconTypes = keyof typeof DetroitIconMap diff --git a/shared-helpers/src/RequireLogin.tsx b/shared-helpers/src/RequireLogin.tsx new file mode 100644 index 0000000000..046c4e1b36 --- /dev/null +++ b/shared-helpers/src/RequireLogin.tsx @@ -0,0 +1,92 @@ +import React, { FunctionComponent, useContext, useEffect, useState } from "react" +import { + clearSiteAlertMessage, + setSiteAlertMessage, + NavigationContext, +} from "@bloom-housing/ui-components" +import { AuthContext } from "./AuthContext" +// See https://github.com/Microsoft/TypeScript/issues/14094 +type Without = { [P in Exclude]?: never } +type XOR = T | U extends Record + ? (Without & U) | (Without & T) + : T | U + +type RequireLoginProps = { + signInPath: string + signInMessage: string + termsPath?: string // partners portal required accepted terms after sign-in + children?: React.ReactNode +} & XOR<{ requireForRoutes?: string[] }, { skipForRoutes: string[] }> + +/** + * Require a login to render children. Will redirect to `signInPath` if not logged in. + * + * Props can be specified with either an "allowlist" (list of routes to skip check for) or a "blocklist" (list of + * routes to apply test on). If no list of routes is provided, then will always apply check. + */ +const RequireLogin: FunctionComponent = ({ + children, + signInPath, + signInMessage, + termsPath, + ...rest +}) => { + const { router } = useContext(NavigationContext) + const { profile, initialStateLoaded } = useContext(AuthContext) + const [hasTerms, setHasTerms] = useState(false) + + // Parse just the pathname portion of the signInPath (in case we want to pass URL params) + const [signInPathname] = signInPath.split("?") + + // Check if this route requires a login or not (can be specified as an allowlist or a blocklist). + const loginRequiredForPath = + // by definition, we shouldn't require login on the sign in page itself + router.pathname !== signInPathname && + ("requireForRoutes" in rest + ? rest.requireForRoutes + ? rest.requireForRoutes.some((path) => new RegExp(path).exec(router.pathname)) + : true + : rest.skipForRoutes + ? !rest.skipForRoutes.some((path) => new RegExp(path).exec(router.pathname)) + : true) + + useEffect(() => { + if (profile?.jurisdictions?.some((jurisdiction) => jurisdiction.partnerTerms)) { + setHasTerms(true) + } + }, [profile]) + + useEffect(() => { + if (loginRequiredForPath && initialStateLoaded && !profile) { + setSiteAlertMessage(signInMessage, "notice") + void router.push(signInPath) + } else { + clearSiteAlertMessage("notice") + } + + if (termsPath && profile && !profile?.agreedToTermsOfService && hasTerms) { + void router.push(termsPath) + } + }, [ + loginRequiredForPath, + initialStateLoaded, + profile, + router, + signInPath, + signInMessage, + termsPath, + hasTerms, + ]) + + if ( + loginRequiredForPath && + (!profile || (hasTerms && termsPath && !profile.agreedToTermsOfService)) + ) { + return null + } + + // Login either isn't required, or the user object is loaded successfully, continue rendering as normal. + return <>{children} +} + +export { RequireLogin as default, RequireLogin } diff --git a/shared-helpers/src/TransitionBanner.tsx b/shared-helpers/src/TransitionBanner.tsx new file mode 100644 index 0000000000..5c5daed36a --- /dev/null +++ b/shared-helpers/src/TransitionBanner.tsx @@ -0,0 +1,18 @@ +import React from "react" +import Markdown from "markdown-to-jsx" +import { AlertBox, t } from "@bloom-housing/ui-components" + +export const TransitionBanner = () => ( +
+ + {t("alert.unavailable")} + +
+) + +export { TransitionBanner as default } diff --git a/shared-helpers/src/blankApplication.ts b/shared-helpers/src/blankApplication.ts new file mode 100644 index 0000000000..1d6f4877d1 --- /dev/null +++ b/shared-helpers/src/blankApplication.ts @@ -0,0 +1,112 @@ +import { + ApplicationStatus, + ApplicationSubmissionType, + Language, + ApplicationPreference, + ApplicationProgram, +} from "@bloom-housing/backend-core/types" + +export const blankApplication = { + loaded: false, + autofilled: false, + completedSections: 0, + submissionType: ApplicationSubmissionType.electronical, + language: Language.en, + acceptedTerms: false, + status: ApplicationStatus.submitted, + applicant: { + orderId: undefined, + firstName: "", + middleName: "", + lastName: "", + birthMonth: "", + birthDay: "", + birthYear: "", + emailAddress: null, + noEmail: false, + phoneNumber: "", + phoneNumberType: "", + noPhone: false, + workInRegion: null, + address: { + street: "", + street2: "", + city: "", + state: "", + zipCode: "", + county: "", + latitude: null, + longitude: null, + }, + workAddress: { + street: "", + street2: "", + city: "", + state: "", + zipCode: "", + county: "", + latitude: null, + longitude: null, + }, + }, + additionalPhone: false, + additionalPhoneNumber: "", + additionalPhoneNumberType: "", + contactPreferences: [], + householdSize: 0, + housingStatus: "", + sendMailToMailingAddress: false, + mailingAddress: { + street: "", + street2: "", + city: "", + state: "", + zipCode: "", + }, + alternateAddress: { + street: "", + street2: "", + city: "", + state: "", + zipCode: "", + }, + alternateContact: { + type: "", + otherType: "", + firstName: "", + lastName: "", + agency: "", + phoneNumber: "", + emailAddress: null, + mailingAddress: { + street: "", + street2: "", + city: "", + state: "", + zipCode: "", + }, + }, + accessibility: { + mobility: null, + vision: null, + hearing: null, + }, + householdExpectingChanges: null, + householdStudent: null, + incomeVouchers: null, + income: null, + incomePeriod: null, + householdMembers: [], + preferredUnit: [], + demographics: { + ethnicity: "", + race: [], + gender: "", + sexualOrientation: "", + howDidYouHear: [], + }, + preferences: [] as ApplicationPreference[], + programs: [] as ApplicationProgram[], + confirmationCode: "", + id: "", +} diff --git a/shared-helpers/src/catchNetworkError.ts b/shared-helpers/src/catchNetworkError.ts new file mode 100644 index 0000000000..2b91279684 --- /dev/null +++ b/shared-helpers/src/catchNetworkError.ts @@ -0,0 +1,92 @@ +import { useState } from "react" +import { t, AlertTypes } from "@bloom-housing/ui-components" +import axios, { AxiosError } from "axios" + +export type NetworkStatus = { + content: NetworkStatusContent + type?: NetworkStatusType + reset: NetworkErrorReset +} + +export type NetworkStatusType = AlertTypes + +export type NetworkStatusError = AxiosError + +export type NetworkStatusContent = { + title: string + description: string + error?: AxiosError +} | null + +export type NetworkErrorDetermineError = ( + status: number, + error: AxiosError, + mfaEnabled?: boolean +) => void + +export type NetworkErrorReset = () => void + +export enum NetworkErrorMessage { + PasswordOutdated = "passwordOutdated", + MfaUnauthorized = "mfaUnauthorized", +} + +/** + * This helper can be used in the catch part for each network request. It determines a proper title and message for AlertBox + AlertNotice components depending on error status code. + */ +export const useCatchNetworkError = () => { + const [networkError, setNetworkError] = useState(null) + + const check401Error = (message: string, error: AxiosError) => { + if (message === NetworkErrorMessage.PasswordOutdated) { + setNetworkError({ + title: t("authentication.signIn.passwordOutdated"), + description: t("authentication.signIn.changeYourPassword"), + error, + }) + } else if (message === NetworkErrorMessage.MfaUnauthorized) { + setNetworkError({ + title: t("authentication.signIn.enterValidEmailAndPasswordAndMFA"), + description: t("authentication.signIn.afterFailedAttempts"), + error, + }) + } else { + setNetworkError({ + title: t("authentication.signIn.enterValidEmailAndPassword"), + description: t("authentication.signIn.afterFailedAttempts"), + error, + }) + } + } + + const determineNetworkError: NetworkErrorDetermineError = (status, error) => { + const responseMessage = axios.isAxiosError(error) ? error.response?.data.message : "" + + switch (status) { + case 401: + check401Error(responseMessage, error) + break + case 429: + setNetworkError({ + title: t("authentication.signIn.accountHasBeenLocked"), + description: t("authentication.signIn.youHaveToWait"), + error, + }) + break + default: + setNetworkError({ + title: t("errors.somethingWentWrong"), + description: t("authentication.signIn.errorGenericMessage"), + error, + }) + } + } + + const resetNetworkError: NetworkErrorReset = () => setNetworkError(null) + + return { + networkError, + determineNetworkError, + resetNetworkError, + } +} diff --git a/shared-helpers/src/filters.ts b/shared-helpers/src/filters.ts new file mode 100644 index 0000000000..c69ab401c0 --- /dev/null +++ b/shared-helpers/src/filters.ts @@ -0,0 +1,225 @@ +import { + EnumListingFilterParamsComparison, + ListingFilterKeys, +} from "@bloom-housing/backend-core/types" +import { ParsedUrlQuery } from "querystring" +import { Region } from "./regions" +import { HomeTypeEnum } from "../../backend/core/src/listings/types/home-type-enum" + +// TODO(#629): Refactor filter state storage strategy +// Currently, the knowledge of "what a filter is" is spread across multiple +// places: getComparisonForFilter(), ListingFilterState, FrontendListingFilterStateKeys, +// ListingFilterKeys, the encode/decode methods, and the various enums with options +// for the filters. It could be worth unifying this into a ListingFilterStateManager +// class that can hold all this in one place. Work toward this is in +// https://github.com/CityOfDetroit/bloom/pull/484, but was set aside. + +// On the frontend, we assume a filter will always use the same comparison. (For +// example, that minRent will always use a >= comparison.) The backend doesn't +// make this assumption, so we need to tell it what comparison to use. +function getComparisonForFilter(filterKey: ListingFilterKeys) { + switch (filterKey) { + case ListingFilterKeys.name: + case ListingFilterKeys.status: + case ListingFilterKeys.leasingAgents: + case ListingFilterKeys.elevator: + case ListingFilterKeys.wheelchairRamp: + case ListingFilterKeys.serviceAnimalsAllowed: + case ListingFilterKeys.accessibleParking: + case ListingFilterKeys.parkingOnSite: + case ListingFilterKeys.inUnitWasherDryer: + case ListingFilterKeys.laundryInBuilding: + case ListingFilterKeys.barrierFreeEntrance: + case ListingFilterKeys.rollInShower: + case ListingFilterKeys.grabBars: + case ListingFilterKeys.heatingInUnit: + case ListingFilterKeys.acInUnit: + case ListingFilterKeys.jurisdiction: + case ListingFilterKeys.favorited: + case ListingFilterKeys.isVerified: + case ListingFilterKeys.hearing: + case ListingFilterKeys.mobility: + case ListingFilterKeys.visual: + case ListingFilterKeys.vacantUnits: + case ListingFilterKeys.openWaitlist: + case ListingFilterKeys.closedWaitlist: + case ListingFilterKeys.Families: + case ListingFilterKeys.ResidentswithDisabilities: + case ListingFilterKeys.Seniors55: + case ListingFilterKeys.Seniors62: + case ListingFilterKeys.SupportiveHousingfortheHomeless: + case ListingFilterKeys.Veterans: + case ListingFilterKeys.barrierFreeUnitEntrance: + case ListingFilterKeys.loweredLightSwitch: + case ListingFilterKeys.barrierFreeBathroom: + case ListingFilterKeys.wideDoorways: + case ListingFilterKeys.loweredCabinets: + case ListingFilterKeys.section8Acceptance: + return EnumListingFilterParamsComparison["="] + case ListingFilterKeys.minRent: + return EnumListingFilterParamsComparison[">="] + case ListingFilterKeys.maxRent: + return EnumListingFilterParamsComparison["<="] + case ListingFilterKeys.bedrooms: + case ListingFilterKeys.bedRoomSize: + case ListingFilterKeys.communityPrograms: + case ListingFilterKeys.region: + case ListingFilterKeys.homeType: + case ListingFilterKeys.accessibility: + case ListingFilterKeys.availability: + case ListingFilterKeys.zipcode: + return EnumListingFilterParamsComparison["IN"] + default: { + const _exhaustiveCheck: any = filterKey + return _exhaustiveCheck + } + } +} + +// Define the keys we expect to see in the frontend URL. These are also used for +// the filter state object, ListingFilterState. +// We exclude bedrooms, since that is constructed from studio, oneBdrm, and so on +const { bedrooms, ...IncludedBackendKeys } = ListingFilterKeys + +enum BedroomFields { + studio = "studio", + oneBdrm = "oneBdrm", + twoBdrm = "twoBdrm", + threeBdrm = "threeBdrm", + fourBdrm = "fourBdrm", +} +export const FrontendListingFilterStateKeys = { + ...IncludedBackendKeys, + ...BedroomFields, + ...Region, + ...HomeTypeEnum, + favorited: "favorited" as const, + bedRoomSize: "bedRoomSize" as const, + communityPrograms: "communityPrograms" as const, + region: "region" as const, + accessibility: "accessibility" as const, +} + +// The types in this interface are `string | ...` because we don't currently parse +// the values pulled from the URL querystring to their types, so they could be +// strings or the type the form fields set them to be. +// TODO: Update `decodeFiltersFromFrontendUrl` to parse each filter into its +// correct type, so we can remove the `string` type from these fields. +export interface ListingFilterState { + // confirmedListings & listing status + [FrontendListingFilterStateKeys.status]?: string + [FrontendListingFilterStateKeys.isVerified]?: string | boolean + // availability + [FrontendListingFilterStateKeys.vacantUnits]?: string | boolean + [FrontendListingFilterStateKeys.openWaitlist]?: string | boolean + [FrontendListingFilterStateKeys.closedWaitlist]?: string | boolean + [FrontendListingFilterStateKeys.availability]?: string + // bedRoomSize + [FrontendListingFilterStateKeys.bedRoomSize]?: string + [FrontendListingFilterStateKeys.studio]?: string | boolean + [FrontendListingFilterStateKeys.oneBdrm]?: string | boolean + [FrontendListingFilterStateKeys.twoBdrm]?: string | boolean + [FrontendListingFilterStateKeys.threeBdrm]?: string | boolean + [FrontendListingFilterStateKeys.fourBdrm]?: string | boolean + // rentRange + [FrontendListingFilterStateKeys.minRent]?: string | number + [FrontendListingFilterStateKeys.maxRent]?: string | number + // communityPrograms + [FrontendListingFilterStateKeys.communityPrograms]?: string + [FrontendListingFilterStateKeys.ResidentswithDisabilities]?: string | number + [FrontendListingFilterStateKeys.Seniors55]?: string | number + [FrontendListingFilterStateKeys.Seniors62]?: string | number + [FrontendListingFilterStateKeys.SupportiveHousingfortheHomeless]?: string | number + // region + [FrontendListingFilterStateKeys.region]?: string + [FrontendListingFilterStateKeys.GreaterDowntown]?: string | boolean + [FrontendListingFilterStateKeys.Eastside]?: string | boolean + [FrontendListingFilterStateKeys.Southwest]?: string | boolean + [FrontendListingFilterStateKeys.Westside]?: string | boolean + // accessibility + [FrontendListingFilterStateKeys.accessibility]?: string + [FrontendListingFilterStateKeys.elevator]?: string | boolean + [FrontendListingFilterStateKeys.wheelchairRamp]?: string | boolean + [FrontendListingFilterStateKeys.serviceAnimalsAllowed]?: string | boolean + [FrontendListingFilterStateKeys.accessibleParking]?: string | boolean + [FrontendListingFilterStateKeys.parkingOnSite]?: string | boolean + [FrontendListingFilterStateKeys.inUnitWasherDryer]?: string | boolean + [FrontendListingFilterStateKeys.laundryInBuilding]?: string | boolean + [FrontendListingFilterStateKeys.barrierFreeEntrance]?: string | boolean + [FrontendListingFilterStateKeys.rollInShower]?: string | boolean + [FrontendListingFilterStateKeys.grabBars]?: string | boolean + [FrontendListingFilterStateKeys.heatingInUnit]?: string | boolean + [FrontendListingFilterStateKeys.acInUnit]?: string | boolean + [FrontendListingFilterStateKeys.hearing]?: string | boolean + [FrontendListingFilterStateKeys.mobility]?: string | boolean + [FrontendListingFilterStateKeys.visual]?: string | boolean + [FrontendListingFilterStateKeys.barrierFreeUnitEntrance]?: string | boolean + [FrontendListingFilterStateKeys.loweredLightSwitch]?: string | boolean + [FrontendListingFilterStateKeys.barrierFreeBathroom]?: string | boolean + [FrontendListingFilterStateKeys.wideDoorways]?: string | boolean + [FrontendListingFilterStateKeys.loweredCabinets]?: string | boolean + [FrontendListingFilterStateKeys.section8Acceptance]?: string | boolean + + // home type + [FrontendListingFilterStateKeys.homeType]?: string + [FrontendListingFilterStateKeys.apartment]?: string | boolean + [FrontendListingFilterStateKeys.duplex]?: string | boolean + [FrontendListingFilterStateKeys.house]?: string | boolean + [FrontendListingFilterStateKeys.townhome]?: string | boolean + + // favorites + [FrontendListingFilterStateKeys.favorited]?: string | boolean + + // misc + [FrontendListingFilterStateKeys.zipcode]?: string +} + +export function encodeToBackendFilterArray(filterState: ListingFilterState) { + const filterArray: { + [x: string]: any + $comparison: EnumListingFilterParamsComparison + bedrooms?: string + }[] = [] + if (filterState === undefined) { + return filterArray + } + // Only include things that are a backend filter type. The keys of + // ListingFilterState are a superset of ListingFilterKeys that may include + // keys not recognized by the backend, so we check against ListingFilterKeys + // here. + for (const filterType in ListingFilterKeys) { + if (filterType in filterState && filterState[filterType] !== null) { + const comparison = getComparisonForFilter(ListingFilterKeys[filterType]) + filterArray.push({ + $comparison: comparison, + [filterType]: filterState[filterType], + }) + } + } + return filterArray +} + +export function encodeToFrontendFilterString(filterState: ListingFilterState) { + let queryString = "" + for (const filterType in filterState) { + const value = filterState[filterType] + if (filterType in FrontendListingFilterStateKeys && value !== undefined && value) { + queryString += `&${filterType}=${value}` + } + } + return queryString +} + +export function decodeFiltersFromFrontendUrl( + query: ParsedUrlQuery +): ListingFilterState | undefined { + const filterState: ListingFilterState = {} + let foundFilterKey = false + for (const queryKey in query) { + if (queryKey in FrontendListingFilterStateKeys && query[queryKey] !== "") { + filterState[queryKey] = query[queryKey] + foundFilterKey = true + } + } + return foundFilterKey ? filterState : undefined +} diff --git a/shared-helpers/src/formKeys.ts b/shared-helpers/src/formKeys.ts index 49c7346d5a..11368551be 100644 --- a/shared-helpers/src/formKeys.ts +++ b/shared-helpers/src/formKeys.ts @@ -1,3 +1,5 @@ +import { Language } from "@bloom-housing/backend-core/types" + export const stateKeys = [ "", "AL", @@ -68,6 +70,8 @@ export const contactPreferencesKeys = [ }, ] +export const adaFeatureKeys = ["mobility", "vision", "hearing"] + export const relationshipKeys = [ "", "spouse", @@ -97,17 +101,31 @@ export const altContactRelationshipKeys = [ export const ethnicityKeys = ["hispanicLatino", "notHispanicLatino"] -export const raceKeys = [ +export const rootRaceKeys = [ "americanIndianAlaskanNative", "asian", "blackAfricanAmerican", "nativeHawaiianOtherPacificIslander", "white", - "americanIndianAlaskanNativeAndBlackAfricanAmerican", - "americanIndianAlaskanNativeAndWhite", - "asianAndWhite", - "blackAfricanAmericanAndWhite", - "otherMutliracial", + "otherMultiracial", + "declineToRespond", +] + +export const asianKeys = [ + "asianIndian", + "chinese", + "filipino", + "japanese", + "korean", + "vietnamese", + "otherAsian", +] + +export const nativeHawaiianOtherPacificIslanderKeys = [ + "nativeHawaiian", + "guamanianOrChamorro", + "samoan", + "otherPacificIslander", ] export const genderKeys = [ @@ -127,9 +145,54 @@ export const sexualOrientation = [ "notListed", ] +export const prependRoot = (root: string, subKeys: string[]) => { + return subKeys.map((key) => `${root}-${key}`) +} + +interface subCheckboxes { + [key: string]: string[] +} + +// Transform an object with keys that may be prepended with a string to an array of only the values with the string +export const fieldGroupObjectToArray = ( + formObject: { [key: string]: any }, + rootKey: string +): string[] => { + const modifiedArray: string[] = [] + const getValue = (elem: string) => { + const formSubKey = elem.substring(elem.indexOf("-") + 1) + return formSubKey === formObject[elem] ? formSubKey : `${formSubKey}: ${formObject[elem]}` + } + Object.keys(formObject) + .filter((formValue) => formValue.split("-")[0] === rootKey && formObject[formValue]) + .forEach((elem) => { + if (formObject[elem].isArray) { + formObject[elem].forEach(() => { + modifiedArray.push(getValue(elem)) + }) + } else { + modifiedArray.push(getValue(elem)) + } + }) + return modifiedArray +} + +export const raceKeys: subCheckboxes = { + americanIndianAlaskanNative: [], + asian: prependRoot("asian", asianKeys), + blackAfricanAmerican: [], + nativeHawaiianOtherPacificIslander: prependRoot( + "nativeHawaiianOtherPacificIslander", + nativeHawaiianOtherPacificIslanderKeys + ), + white: [], + otherMultiracial: [], + declineToRespond: [], +} + export const howDidYouHear = [ { - id: "alamedaCountyHCDWebsite", + id: "jurisdictionWebsite", }, { id: "developerWebsite", @@ -178,3 +241,46 @@ export const preferredUnit = [ ] export const bedroomKeys = ["studio", "oneBdrm", "twoBdrm", "threeBdrm"] + +export const listingFeatures = [ + "wheelchairRamp", + "elevator", + "serviceAnimalsAllowed", + "accessibleParking", + "parkingOnSite", + "inUnitWasherDryer", + "laundryInBuilding", + "barrierFreeEntrance", + "rollInShower", + "grabBars", + "heatingInUnit", + "acInUnit", + "hearing", + "mobility", + "visual", + "barrierFreeUnitEntrance", + "loweredLightSwitch", + "barrierFreeBathroom", + "wideDoorways", + "loweredCabinets", +] + +export const applicationLanguageKeys = [Language.en, Language.es, Language.zh, Language.vi] + +export enum RoleOption { + Administrator = "administrator", + Partner = "partner", +} + +export const roleKeys = Object.values(RoleOption) + +export const listingUtilities = [ + "water", + "gas", + "trash", + "sewer", + "electricity", + "cable", + "phone", + "internet", +] diff --git a/shared-helpers/src/formatRange.ts b/shared-helpers/src/formatRange.ts new file mode 100644 index 0000000000..8c100a92e4 --- /dev/null +++ b/shared-helpers/src/formatRange.ts @@ -0,0 +1,15 @@ +export function formatRange( + min: string | number, + max: string | number, + prefix: string, + postfix: string +): string { + if (!isDefined(min) && !isDefined(max)) return "" + if (min == max || !isDefined(max)) return `${prefix}${min}${postfix}` + if (!isDefined(min)) return `${prefix}${max}${postfix}` + return `${prefix}${min}${postfix} - ${prefix}${max}${postfix}` +} + +export function isDefined(item: number | string): boolean { + return item !== null && item !== undefined && item !== "" +} diff --git a/shared-helpers/src/formatRentRange.ts b/shared-helpers/src/formatRentRange.ts new file mode 100644 index 0000000000..2e89fe1561 --- /dev/null +++ b/shared-helpers/src/formatRentRange.ts @@ -0,0 +1,16 @@ +import { MinMax } from "@bloom-housing/backend-core/types" +import { formatRange } from "./formatRange" + +export function formatRentRange(rent: MinMax, percent: MinMax): string { + let toReturn = "" + if (rent) { + toReturn += formatRange(rent.min, rent.max, "$", "") + } + if (rent && percent) { + toReturn += ", " + } + if (percent) { + toReturn += formatRange(percent.min, percent.max, "", "%") + } + return toReturn +} diff --git a/shared-helpers/src/gtm.ts b/shared-helpers/src/gtm.ts new file mode 100644 index 0000000000..c1f8e86ee3 --- /dev/null +++ b/shared-helpers/src/gtm.ts @@ -0,0 +1,36 @@ +import { ListingReviewOrder } from "@bloom-housing/backend-core/types" + +declare global { + interface Window { + dataLayer: DataLayerArgsUnion[] + } +} + +export type PageView = { + event: string + pageTitle: string + status: string +} + +export type ListingList = PageView & { + numberOfListings: number + listingIds: string[] +} + +export type ListingDetail = PageView & { + listingStartDate: string + listingStatus: string + listingID: string + listingType: ListingReviewOrder + applicationDueDate: string + digitalApplication: boolean + paperApplication: boolean +} + +type DataLayerArgsUnion = PageView | ListingList | ListingDetail + +export function pushGtmEvent(args: T): void { + if (!window) return + window.dataLayer = window.dataLayer || [] + window.dataLayer.push(args) +} diff --git a/shared-helpers/src/locales/ar.json b/shared-helpers/src/locales/ar.json new file mode 100644 index 0000000000..cd72ebbf51 --- /dev/null +++ b/shared-helpers/src/locales/ar.json @@ -0,0 +1,1287 @@ +{ + "config.routePrefix": "ar", + "about.body1": "نحن نعلم أن العثور على منزل يلبي احتياجاتك قد يكون أمرًا صعبًا ومحبطًا. Detroit Home Connect هي يدك المساعدة في العثور على مكان جديد للاتصال بالمنزل.", + "about.body2": "Detroit Home Connect هي خدمة جديدة لمدينة ديترويت توفر لك خطوة أولى مركزية في العثور على سكن في ديترويت يلبي القدرة على تحمل التكاليف والاحتياجات المنزلية. يمكنك فهم أهليتك لوحدات الإيجار من خلال استكشاف الخيارات بناءً على حجم عائلتك وعمرك ودخلك. Detroit Home Connect هي مبادرة من قسم الإسكان والتنشيط في مدينة ديترويت. يعتمد تصميم وميزات موقع الويب على التعليقات والرؤى من سكان المنطقة والمنظمات المجتمعية ومديري العقارات ومالكي العقارات.", + "about.moreInfoContact": "لمزيد من المعلومات ، يرجى الاتصال بموظفي City على detroithomeconnect@detroitmi.gov.", + "about.thankYouPartners": "تود إدارة الإسكان والتعمير في مدينة ديترويت التوجه بالشكر إلى الشركاء التالين على دعمهم وشراكتهم خلال تطوير Detroit Home Connect، بمن فيهم:", + "account.accountSettings": "إعدادت الحساب", + "account.errorFetchingApplications": "خطأ في جلب التطبيقات", + "account.noApplications": "يبدو أنك لم تنطبق على أي قوائم حتى الآن.", + "account.accountSettingsSubtitle": "إعدادات الحساب والبريد الإلكتروني وكلمة المرور", + "account.createAccount": "إنشاء حساب", + "account.haveAnAccount": "هل لديك حساب؟", + "account.myApplications": "تطبيقاتي", + "account.myApplicationsSubtitle": "اطلع على تواريخ وقوائم اليانصيب للممتلكات التي قمت بتقديم طلب للحصول عليها", + "account.myFavorites": "المفضلة", + "account.myFavoritesSubtitle": "حفظ القوائم والتحقق مرة أخرى من التحديثات", + "account.application.confirmation": "تأكيد", + "account.application.error": "خطأ", + "account.application.noAccessError": "لا يوجد تطبيق بهذا المعرف", + "account.application.noApplicationError": "لا يوجد تطبيق بهذا المعرف", + "account.application.return": "العودة إلى التطبيقات", + "account.settings.passwordSuccess": "تم تحديث كلمة المرور بنجاح", + "account.settings.update": "تحديث", + "account.settings.passwordRemember": "عند تغيير كلمة المرور الخاصة بك ، تأكد من تدوينها حتى تتذكرها في المستقبل.", + "account.settings.currentPassword": "كلمة المرور الحالي", + "account.settings.newPassword": "كلمة المرور الجديدة", + "account.settings.confirmNewPassword": "تأكيد كلمة المرور الجديدة", + "account.settings.alerts.genericError": "كان هناك خطأ. يرجى المحاولة مرة أخرى ، أو الاتصال بالدعم للمساعدة.", + "account.settings.alerts.nameSuccess": "تم تحديث الاسم بنجاح", + "account.settings.alerts.dobSuccess": "تم تحديث تاريخ الميلاد بنجاح", + "account.settings.alerts.emailSuccess": "تم التحديث عبر البريد الإلكتروني بنجاح", + "account.settings.alerts.phoneNumberSuccess": "تم تحديث رقم الهاتف بنجاح", + "account.settings.alerts.currentPassword": "كلمة مرور غير صحيحة. حاول مرة اخرى.", + "account.settings.alerts.passwordSuccess": "تم تحديث كلمة المرور بنجاح", + "account.settings.alerts.passwordMatch": "حقول كلمة المرور الجديدة غير متطابقة", + "account.settings.alerts.passwordEmpty": "لا يجوز أن تكون حقول كلمة المرور فارغة", + "account.settings.placeholders.month": "مم", + "account.settings.placeholders.day": "DD", + "account.settings.placeholders.year": "YYYY", + "alert.unavailable": "قد يكون موقع Detroit Home Connect غير متاح مؤقتًا خلال أسبوع 2 سبتمبر 2025 لإجراء ترقية مجدولة. نتوقع أن يظل الموقع معطلاً لمدة تصل إلى 48 ساعة خلال هذه الفترة. للاستفسارات أو الاستفسارات، يُرجى التواصل معنا عبر البريد الإلكتروني: detroithomeconnect@detroitmi.gov", + "applications.begin.en": "يبدأ", + "applications.begin.es": "يبدأ", + "applications.begin.zh": "يبدأ", + "applications.begin.vi": "يبدأ", + "applications.totalApplications": "إجمالي التطبيقات", + "applications.totalSets": "مجموع المجموعات", + "applications.addApplication": "أضف التطبيق", + "applications.newApplication": "تطبيق جديد", + "applications.editApplication": "تحرير التطبيق", + "applications.applicationsReceived": "تم استلام الطلبات", + "applications.table.applicationSubmissionDate": "تاريخ تقديم الطلب", + "applications.table.declaredAnnualIncome": "الدخل السنوي المعلن", + "applications.table.declaredMonthlyIncome": "الدخل الشهري المعلن", + "applications.table.subsidyOrVoucher": "الدعم أو القسيمة", + "applications.table.requestAda": "طلب ADA", + "applications.table.preferenceClaimed": "تمت المطالبة بالأفضلية", + "applications.table.primaryDob": "DOB الأساسي", + "applications.table.phoneType": "نوع الهاتف", + "applications.table.additionalPhoneType": "نوع الهاتف الإضافي", + "applications.table.residenceStreet": "عنوان شارع السكن", + "applications.table.residenceCity": "مدينة الإقامة", + "applications.table.residenceState": "دولة الإقامة", + "applications.table.residenceZip": "الإقامة الرمز البريدي", + "applications.table.mailingStreet": "عنوان الشارع البريدي", + "applications.table.mailingCity": "المدينة البريدية", + "applications.table.mailingState": "الدولة البريدية", + "applications.table.mailingZip": "البريدي البريدي", + "applications.table.workStreet": "عنوان شارع العمل", + "applications.table.workCity": "مدينة العمل", + "applications.table.workState": "دولة العمل", + "applications.table.workZip": "العمل البريدي", + "applications.table.altContactFirstName": "الاسم الأول جهة الاتصال البديلة", + "applications.table.altContactLastName": "الاسم الأخير لجهة الاتصال البديلة", + "applications.table.altContactRelationship": "علاقة جهة اتصال بديلة", + "applications.table.altContactAgency": "وكالة الاتصال البديل", + "applications.table.altContactEmail": "بديل البريد الإلكتروني لجهة الاتصال", + "applications.table.altContactPhone": "بديل هاتف جهة الاتصال", + "applications.table.altContactStreetAddress": "عنوان شارع الاتصال البديل", + "applications.table.altContactCity": "Alt Contact City", + "applications.table.altContactState": "بديل حالة جهة الاتصال", + "applications.table.altContactZip": "Alt Contact Zip", + "applications.table.householdFirstName": "الاسم الأول للأسرة", + "applications.table.householdLastName": "الاسم الأخير للأسرة", + "applications.table.householdRelationship": "العلاقة الأسرية", + "applications.table.householdDob": "DOB المنزلية", + "applications.table.householdStreetAddress": "عنوان شارع الأسرة", + "applications.table.householdCity": "المدينة المنزلية", + "applications.table.householdState": "دولة الأسرة", + "applications.table.householdZip": "الرمز المنزلي", + "applications.table.applicationType": "نوع التطبيق", + "application.add.applicationAddError": "ستحتاج إلى حل أي أخطاء قبل المضي قدمًا.", + "application.add.workInRegion": "العمل في المنطقة؟", + "application.add.mobility": "ضعف الحركة", + "application.add.vision": "ضعف البصر", + "application.add.hearing": "ضعف السمع", + "application.add.preferences.liveIn": "يعيش في", + "application.add.preferences.workIn": "العمل في", + "application.add.preferences.optedOut": "تم الانسحاب من التفضيل", + "application.add.incomePeriod": "فترة الدخل", + "application.add.demographicsInformation": "المعلومات الديموغرافية", + "application.add.ethnicity": "الأصل العرقي", + "application.add.race": "العنصر", + "application.add.gender": "جنس تذكير أو تأنيث", + "application.add.howDidYouHearAboutUs": "كيف سمعت عنا؟", + "application.add.sexualOrientation": "التوجه الجنسي", + "application.add.addHouseholdMember": "إضافة فرد في الأسرة", + "application.add.sameAddressAsPrimary": "نفس العنوان الأساسي", + "application.add.sameResidence": "نفس السكن", + "application.add.languageSubmittedIn": "اللغة المقدمة بـ", + "application.add.timeSubmitted": "وقت التقديم", + "application.add.dateSubmitted": "تاريخ التقديم", + "application.add.applicationSubmitted": "تم تقديم الطلب", + "application.add.applicationUpdated": "تم تحديث التطبيق", + "application.add.saveAndExit": "احفظ المخرج", + "application.add.claimant": "المدعي", + "application.add.displacedAddress": "عنوان النازحين", + "application.referralApplication.instructions": "يتم إحالة الوحدات السكنية المساندة الدائمة مباشرة من خلال نظام الدخول المنسق . يمكن للأسر التي تعاني من التشرد الاتصال بالرقم من أجل الاتصال بنقطة وصول لمعرفة المزيد حول نظام الدخول المنسق والوصول إلى الموارد والمعلومات المتعلقة بالسكن.", + "application.referralApplication.furtherInformation": "لمزيد من المعلومات", + "application.referralApplication.phoneNumber": "٢١١", + "application.details.applicationData": "بيانات الطلب", + "application.details.number": "رقم الطلب", + "application.details.type": "نوع تقديم الطلب", + "application.details.submittedDate": "تاريخ تقديم الطلب", + "application.details.timeDate": "وقت تقديم الطلب", + "application.details.language": "لغة التطبيق", + "application.details.householdSize": "حجم الأسرة", + "application.details.totalSize": "إجمالي حجم الأسرة", + "application.details.submittedBy": "مقدم من", + "application.details.agency": "الوكالة إذا كان ذلك ممكنًا", + "application.details.adaPriorities": "تم تحديد أولويات ADA", + "application.details.preferences": "تفضيلات التطبيق", + "application.details.liveOrWorkIn": "العيش أو العمل في", + "application.details.householdIncome": "دخل الأسرة المعلن", + "application.details.annualIncome": "الدخل السنوي", + "application.details.monthlyIncome": "الدخل الشهري", + "application.details.vouchers": "قسيمة السكن أو الدعم", + "application.details.preferredContact": "الاتصال المفضل", + "application.details.residenceAddress": "عنوان السكن", + "application.details.workInRegion": "العمل في المنطقة", + "application.details.signatureOnTerms": "التوقيع على شروط الاتفاقية", + "application.details.submissionType.electronical": "إلكتروني", + "application.details.submissionType.paper": "ورق", + "application.details.applicationStatus.draft": "مسودة", + "application.details.applicationStatus.submitted": "مقدم", + "application.details.applicationStatus.removed": "إزالة", + "application.details.preferredUnitSizes": "أحجام الوحدات المفضلة", + "application.details.householdMemberDetails": "تفاصيل أفراد الأسرة", + "application.form.general.saveAndReturn": "حفظ والعودة للمراجعة", + "application.form.general.saveAndFinishLater": "احفظها وأكملها لاحقًا", + "application.form.options.relationship.spouse": "زوج", + "application.form.options.relationship.registeredDomesticPartner": "الشريك المحلي المسجل", + "application.form.options.relationship.parent": "الأبوين", + "application.form.options.relationship.child": "طفل", + "application.form.options.relationship.sibling": "أخ أو أخت", + "application.form.options.relationship.cousin": "ولد عم", + "application.form.options.relationship.aunt": "عمة", + "application.form.options.relationship.uncle": "خال", + "application.form.options.relationship.nephew": "ابن الأخ", + "application.form.options.relationship.niece": "ابنة الاخ", + "application.form.options.relationship.grandparent": "الجد", + "application.form.options.relationship.greatGrandparent": "الجد العظيم", + "application.form.options.relationship.inLaw": "إن لو", + "application.form.options.relationship.friend": "صديق", + "application.form.options.relationship.other": "آخر", + "application.chooseLanguage.letsGetStarted": "لنبدأ في تطبيقك", + "application.chooseLanguage.chooseYourLanguage": "اختر لغتك", + "application.chooseLanguage.signInSaveTime": "يمكن أن يوفر لك تسجيل الدخول الوقت من خلال البدء بتفاصيل تطبيقك الأخير ، ويسمح لك بالتحقق من حالة هذا التطبيق في أي وقت.", + "application.autofill.saveTime": "وفر الوقت باستخدام التفاصيل من آخر تطبيق لك", + "application.autofill.prefillYourApplication": "سنقوم ببساطة بملء طلبك مسبقًا بالتفاصيل التالية ، ويمكنك إجراء التحديثات كما تذهب.", + "application.autofill.start": "ابدأ بهذه التفاصيل", + "application.autofill.reset": "أعد التعيين وابدأ من جديد", + "application.name.title": "ما اسمك؟", + "application.name.yourName": "اسمك", + "application.name.firstName": "الاسم الاول", + "application.name.middleNameOptional": "الاسم الأوسط (اختياري)", + "application.name.middleName": "الاسم الأوسط", + "application.name.lastName": "الكنية", + "application.name.yourDateOfBirth": "تاريخ ميلادك", + "application.name.yourEmailAddress": "عنوان بريدك الإلكتروني", + "application.name.emailPrivacy": "سنستخدم عنوان بريدك الإلكتروني فقط للاتصال بك بخصوص طلبك.", + "application.name.noEmailAddress": "ليس لدي عنوان بريد إلكتروني", + "application.contact.title": "شكرًا٪ {firstName}. الآن نحن بحاجة لمعرفة كيفية الاتصال بك.", + "application.contact.yourPhoneNumber": "رقم تليفونك", + "application.contact.phoneNumberTypes.prompt": "ما نوع هذا الرقم؟", + "application.contact.phoneNumberTypes.work": "عمل", + "application.contact.phoneNumberTypes.home": "الصفحة الرئيسية", + "application.contact.phoneNumberTypes.cell": "زنزانة", + "application.contact.noPhoneNumber": "ليس لدي رقم هاتف", + "application.contact.yourAdditionalPhoneNumber": "رقم هاتفك الثاني", + "application.contact.additionalPhoneNumber": "لدي رقم هاتف إضافي", + "application.contact.address": "تبوك", + "application.contact.addressWhereYouCurrentlyLive": "نحتاج إلى العنوان الذي تعيش فيه حاليًا. إذا كنت بلا مأوى ، أدخل إما عنوان المأوى أو عنوانًا قريبًا من مكان إقامتك.", + "application.contact.streetAddress": "عنوان الشارع", + "application.contact.apt": "شقة أو وحدة #", + "application.contact.city": "مدينة", + "application.contact.cityName": "اسم المدينة", + "application.contact.contactPreference": "كيف تفضل أن يتم الاتصال بك؟", + "application.contact.preferredContactType": "نوع الاتصال المفضل", + "application.contact.state": "حالة", + "application.contact.zip": "أزيز", + "application.contact.zipCode": "الرمز البريدي", + "application.contact.sendMailToMailingAddress": "أرسل بريدي إلى عنوان مختلف", + "application.contact.mailingAddress": "عنوان المراسلات", + "application.contact.provideAMailingAddress": "قدم عنوانًا يمكنك من خلاله تلقي التحديثات والمواد المتعلقة بتطبيقك.", + "application.contact.doYouWorkIn": "هل تعمل في مقاطعة٪ {County}؟", + "application.contact.doYouWorkInDescription": "يحدد لاحقًا", + "application.contact.workAddress": "عنوان العمل", + "application.alternateContact.type.title": "هل هناك شخص آخر ترغب في تفويضنا للاتصال به إذا لم نتمكن من الوصول إليك؟", + "application.alternateContact.type.description": "من خلال توفير جهة اتصال بديلة ، فإنك تسمح لنا بمناقشة المعلومات حول طلبك معهم.", + "application.alternateContact.type.label": "بديل الاتصال", + "application.alternateContact.type.options.familyMember": "فرد من العائلة", + "application.alternateContact.type.options.friend": "صديق", + "application.alternateContact.type.options.caseManager": "مدير الحالة أو مستشار الإسكان", + "application.alternateContact.type.options.other": "آخر", + "application.alternateContact.type.options.noContact": "ليس لدي جهة اتصال بديلة", + "application.alternateContact.type.otherTypeFormPlaceholder": "ما هي علاقتك؟", + "application.alternateContact.type.otherTypeValidationErrorMessage": "الرجاء إدخال نوع العلاقة", + "application.alternateContact.type.validationErrorMessage": "الرجاء تحديد جهة اتصال بديلة", + "application.alternateContact.name.title": "من هو الاتصال البديل الخاص بك؟", + "application.alternateContact.name.alternateContactFormLabel": "اسم جهة الاتصال البديلة", + "application.alternateContact.name.caseManagerAgencyFormLabel": "أين يعمل مدير حالتك أو مستشار الإسكان؟", + "application.alternateContact.name.caseManagerAgencyFormPlaceHolder": "وكالة", + "application.alternateContact.name.caseManagerAgencyValidationErrorMessage": "الرجاء إدخال وكالة", + "application.alternateContact.contact.title": "دعنا نعرف كيفية الوصول إلى جهة الاتصال البديلة الخاصة بك", + "application.alternateContact.contact.description": "سنستخدم هذه المعلومات فقط للاتصال بهم بخصوص طلبك.", + "application.alternateContact.contact.phoneNumberFormLabel": "الاتصال رقم الهاتف", + "application.alternateContact.contact.emailAddressFormLabel": "عنوان البريد الإلكتروني للاتصال", + "application.alternateContact.contact.contactMailingAddressLabel": "العنوان البريدي للاتصال", + "application.alternateContact.contact.contactMailingAddressHelperText": "اختر عنوانًا يمكنهم من خلاله تلقي التحديثات والمواد المتعلقة بتطبيقك", + "application.household.assistanceUrl": "هتبص://إكسيج.كوم/", + "application.household.dontQualifyHeader": "للأسف يبدو أنك غير مؤهل لهذه القائمة.", + "application.household.dontQualifyInfo": "يرجى إجراء تغييرات إذا كنت تعتقد أنك قد ارتكبت خطأ. اعلم أنه إذا قمت بتزوير أي معلومات في طلبك ، فسيتم استبعادك. إذا كانت المعلومات التي أدخلتها دقيقة ، فنحن نشجعك على إعادة التحقق في المستقبل مع توفر المزيد من الخصائص.", + "application.household.addMembers.addHouseholdMember": "+ إضافة فرد من أفراد الأسرة", + "application.household.addMembers.done": "تمت إضافة الأشخاص", + "application.household.addMembers.title": "أخبرنا عن منزلك.", + "application.household.addMembers.doubleCheck": "يرجى التحقق مرة أخرى من المعلومات الخاصة بكل فرد من أفراد الأسرة.", + "application.household.householdMember": "فرد من الأسرة", + "application.household.householdMembers": "أفراد الأسرة", + "application.household.liveAlone.title": "بعد ذلك نود أن نعرف عن الآخرين الذين سيعيشون معك في الوحدة", + "application.household.liveAlone.willLiveAlone": "سأعيش وحدي", + "application.household.liveAlone.liveWithOtherPeople": "الناس الآخرون سيعيشون معي", + "application.household.preferredUnit.preferredUnitType": "نوع الوحدة المفضل", + "application.household.preferredUnit.title": "ما هي أحجام الوحدات التي تهتم بها؟", + "application.household.preferredUnit.subTitle": "سيكون نوع الوحدة التي اخترتها خاضعًا للتوافر.", + "application.household.preferredUnit.legend": "نوع الوحدة المفضل", + "application.household.preferredUnit.optionsLabel": "تحقق من كل ما ينطبق:", + "application.household.preferredUnit.options.studio": "ستوديو", + "application.household.preferredUnit.options.oneBedroom": "1 غرفة نوم", + "application.household.preferredUnit.options.twoBedroom": "2 غرفة نوم", + "application.household.preferredUnit.options.threeBedroom": "3 غرف نوم", + "application.household.preferredUnit.options.moreThanThreeBedroom": "3+ غرف نوم", + "application.household.member.cancelAddingThisPerson": "إلغاء إضافة هذا الشخص", + "application.household.member.deleteThisPerson": "احذف هذا الشخص", + "application.household.member.dateOfBirth": "تاريخ الولادة", + "application.household.member.name": "اسم فرد الأسرة", + "application.household.member.haveSameAddress": "هل لديهم نفس عنوانك؟", + "application.household.member.whatIsTheirRelationship": "ماذا تعني علاقتهم لك؟", + "application.household.member.saveHouseholdMember": "حفظ فرد من الأسرة", + "application.household.member.subTitle": "سيكون لديك فرصة لإضافة المزيد من أفراد الأسرة على الشاشة التالية", + "application.household.member.title": "أخبرنا عن هذا الشخص", + "application.household.member.updateHouseholdMember": "تحديث أفراد الأسرة", + "application.household.member.whatReletionship": "ماذا تعني علاقتهم لك", + "application.household.member.workInRegion": "هل يعملون في مقاطعة٪ {County}؟", + "application.household.member.workInRegionNote": "يحدد لاحقًا", + "application.household.membersInfo.title": "قبل إضافة أشخاص آخرين ، تأكد من عدم ذكر أسمائهم في أي تطبيق آخر لهذه القائمة.", + "application.household.primaryApplicant": "متقدم أولي", + "application.ada.label": "وحدات ADA سهلة الوصول", + "application.ada.title": "هل أنت أو أي شخص في منزلك بحاجة إلى أي من ميزات الوصول ADA التالية؟", + "application.ada.subTitle": "إذا تم اختيارك لوحدة ما ، فسيعمل العقار على تلبية حاجتك إلى أقصى حد ممكن. إذا تم اختيار طلبك ، فاستعد لتقديم المستندات الداعمة من طبيبك.", + "application.ada.mobility": "لضعف الحركة", + "application.ada.vision": "لضعف البصر", + "application.ada.hearing": "لضعف السمع", + "application.financial.income.title": "دعنا ننتقل إلى الدخل.", + "application.financial.income.instruction1": "اجمع إجمالي دخل الأسرة (قبل الضريبة) من الأجور والمزايا والمصادر الأخرى من جميع أفراد الأسرة.", + "application.financial.income.instruction2": "ما عليك سوى تقديم إجمالي تقديري الآن. سيتم احتساب الإجمالي الفعلي إذا تم اختيارك.", + "application.financial.income.prompt": "ما هو إجمالي دخل أسرتك قبل الضريبة؟", + "application.financial.income.placeholder": "اجمع كل مصادر دخلك", + "application.financial.income.legend": "تردد الدخل", + "application.financial.income.validationError.title": "للأسف يبدو أنك غير مؤهل لهذه القائمة.", + "application.financial.income.validationError.reason.low": "دخل أسرتك منخفض للغاية.", + "application.financial.income.validationError.reason.high": "دخل أسرتك مرتفع للغاية.", + "application.financial.income.validationError.instruction1": "يرجى إجراء تغييرات إذا كنت تعتقد أنك قد ارتكبت خطأ. اعلم أنه إذا قمت بتزوير أي معلومات في طلبك ، فسيتم استبعادك.", + "application.financial.income.validationError.instruction2": "إذا كانت المعلومات التي أدخلتها دقيقة ، فنحن نشجعك على إعادة التحقق في المستقبل مع توفر المزيد من الخصائص.", + "application.financial.vouchers.title": "هل تتلقى أنت أو أي شخص في هذا التطبيق أيًا مما يلي؟", + "application.financial.vouchers.housingVouchers.strong": "قسائم الإسكان", + "application.financial.vouchers.housingVouchers.text": "مثل القسم 8", + "application.financial.vouchers.nonTaxableIncome.strong": "الدخل غير الخاضع للضريبة", + "application.financial.vouchers.nonTaxableIncome.text": "مثل SSI أو SSDI أو مدفوعات إعالة الطفل أو مزايا تعويض العمال", + "application.financial.vouchers.rentalSubsidies.strong": "إعانات الإيجار", + "application.financial.vouchers.rentalSubsidies.text": "مثل VASH و HSA و HOPWA والجمعيات الخيرية الكاثوليكية ومؤسسة الإيدز ، إلخ.", + "application.financial.vouchers.legend": "قسائم الإسكان والدخل غير الخاضع للضريبة أو إعانات الإيجار", + "application.preferences.title": "قد تكون أسرتك مؤهلة للحصول على تفضيلات الإسكان التالية.", + "application.preferences.preamble": "إذا كنت مؤهلاً لهذا التفضيل ، فستحصل على ترتيب أعلى.", + "application.preferences.selectBelow": "إذا كان لديك أحد تفضيلات السكن هذه ، فحدده أدناه:", + "application.preferences.dontWant": "لا أريد هذه التفضيلات", + "application.preferences.stillHaveOpportunity": "ستظل لديك الفرصة للمطالبة بالتفضيلات الأخرى.", + "application.preferences.youHaveClaimed": "لقد طالبت بما يلي:", + "application.preferences.liveWork.title": "العيش أو العمل في مقاطعة٪ {County}؟", + "application.preferences.liveWork.live.label": "العيش في تفضيلات المقاطعة٪ {County}", + "application.preferences.liveWork.live.description": "يعيش في٪ {County} نسخة هنا…", + "application.preferences.liveWork.live.link": "حطب://دومين.كوم", + "application.preferences.liveWork.work.label": "العمل في تفضيلات المقاطعة٪ {County}", + "application.preferences.liveWork.work.description": "العمل في٪ {County} نسخة يذهب هنا…", + "application.preferences.liveWork.work.link": "حطب://دومين.كوم", + "application.preferences.PBV.title": "٪ {County} نسخة تذهب هنا…", + "application.preferences.PBV.residency.label": "الإقامة", + "application.preferences.PBV.residency.description": "٪ {County} نسخة تذهب هنا…", + "application.preferences.PBV.family.label": "أسرة", + "application.preferences.PBV.family.description": "٪ {County} نسخة تذهب هنا…", + "application.preferences.PBV.veteran.label": "محارب قديم", + "application.preferences.PBV.veteran.description": "٪ {County} نسخة تذهب هنا…", + "application.preferences.PBV.homeless.label": "بلا مأوى", + "application.preferences.PBV.homeless.description": "٪ {County} نسخة تذهب هنا…", + "application.preferences.PBV.noneApplyButConsider.label": "لا تنطبق أي من هذه التفضيلات علي ، لكني أود أن يتم أخذها في الاعتبار", + "application.preferences.PBV.doNotConsider.label": "لا أريد أن أكون في الاعتبار لوحدات القسائم القائمة على مشروع [سلطة الإسكان]", + "application.preferences.HOPWA.title": "فرص السكن للأشخاص المصابين بالإيدز", + "application.preferences.HOPWA.hopwa.label": "فرص السكن للأشخاص المصابين بالإيدز", + "application.preferences.HOPWA.hopwa.description": "٪ {County} نسخة تذهب هنا…", + "application.preferences.HOPWA.doNotConsider.label": "لا أريد أن أعتبر", + "application.preferences.displacedTenant.title": "تفضيل السكن المستأجر النازح", + "application.preferences.displacedTenant.whichHouseholdMember": "أي فرد من أفراد الأسرة يدعي هذا التفضيل؟", + "application.preferences.displacedTenant.whatAddress": "ما العنوان الذي نزح منه فرد الأسرة؟", + "application.preferences.displacedTenant.general.label": "تفضيل السكن المستأجر النازح", + "application.preferences.displacedTenant.general.description": "نسخة المستأجر النازح تذهب هنا ...", + "application.preferences.displacedTenant.general.link": "حطب://دومين.كوم", + "application.preferences.displacedTenant.missionCorridor.label": "ممر البعثة", + "application.preferences.displacedTenant.missionCorridor.description": "نسخة ممر المهمة تذهب هنا ...", + "application.preferences.general.title": "بناءً على المعلومات التي أدخلتها ، لم تطالب أسرتك بأي تفضيلات للسكن.", + "application.preferences.general.preamble": "ستكون في المجموعة العامة للمتقدمين.", + "application.preferences.options.address": "عنوان النازحين", + "application.preferences.options.name": "المدعي", + "application.review.takeAMomentToReview": "توقف لحظة لمراجعة معلوماتك قبل تقديم طلبك.", + "application.review.sameAddressAsApplicant": "نفس عنوان مقدم الطلب", + "application.review.noAdditionalMembers": "لا يوجد أفراد إضافيون في الأسرة", + "application.review.householdDetails": "التفاصيل المنزلية", + "application.review.voucherOrSubsidy": "قسيمة السكن أو دعم الإيجار", + "application.review.lastChanceToEdit": "هذه فرصتك الأخيرة للتعديل قبل التقديم.", + "application.review.terms.title": "مصطلحات", + "application.review.terms.text": "يجب تقديم هذا الطلب في موعد أقصاه٪ {applicationDueDate}.

سيتصل الوكيل بالمتقدمين في ترتيب اليانصيب وترتيب التفضيل حتى يتم ملء الشواغر.

جميع المعلومات التي قدمتها سيتم التحقق منها وتأكيد أهليتك. ستتم إزالة طلبك من اليانصيب إذا قدمت أي بيانات احتيالية ، أو إذا ظهر أي فرد من أفراد الأسرة في أكثر من طلب واحد لهذه القائمة. إذا لم نتمكن من التحقق من تفضيل يانصيب الإسكان الذي طالبت به ، فلن تتلقى التفضيل ولكن لن يتم معاقبتك بطريقة أخرى.

سيتم التحقق من جميع المعلومات التي قدمتها وتأكيد أهليتك. ستتم إزالة طلبك من قائمة الانتظار إذا قدمت أي بيانات احتيالية ، أو إذا ظهر أي فرد من أفراد الأسرة في أكثر من طلب واحد لهذه القائمة. إذا لم نتمكن من التحقق من تفضيل السكن الذي طالبت به ، فلن تتلقى التفضيل ولكن لن يتم معاقبتك بطريقة أخرى.

إذا تم اختيار طلبك من اليانصيب ، فكن مستعدًا لملء طلب أكثر تفصيلاً و تقديم المستندات الداعمة المطلوبة في غضون 5 أيام عمل من الاتصال بك. لمزيد من المعلومات ، يرجى الاتصال بالمطور أو الوكيل المدرج في القائمة. لا يؤهلك إكمال طلب اليانصيب هذا للحصول على سكن أو يشير إلى أنك مؤهل للحصول على سكن. سيتم فحص جميع المتقدمين على النحو المبين في معايير اختيار المقيمين في مكان الإقامة.

لا يمكنك تغيير طلبك عبر الإنترنت بعد الإرسال.

أقر بأن ما سبق صحيح ودقيق ، وأقر بأن أي خطأ سيؤدي إجراء احتيال أو إهمال على هذا التطبيق إلى الإزالة من اليانصيب.

", + "application.review.terms.confirmCheckboxText": "أوافق وأفهم أنه لا يمكنني تغيير أي شيء بعد الإرسال.", + "application.review.demographics.title": "ساعدنا في التأكد من أننا نحقق هدفنا المتمثل في خدمة جميع الناس.", + "application.review.demographics.subTitle": "هذه الأسئلة اختيارية ولن تؤثر على أهليتك للسكن. ستبقى إجاباتك سرية.", + "application.review.demographics.ethnicityLabel": "ما هو أفضل وصف لعرقك؟", + "application.review.demographics.raceLabel": "ما أفضل وصف لعرقك؟", + "application.review.demographics.genderLabel": "ما هو جنسك؟", + "application.review.demographics.genderInfo": "اختر أفضل وصف لهويتك الجنسية الحالية.", + "application.review.demographics.sexualOrientationLabel": "كيف تصفين ميولك الجنسية أو هويتك الجنسية؟", + "application.review.demographics.howDidYouHearLabel": "كيف سمعت عن هذه القائمة؟", + "application.review.demographics.ethnicityOptions.hispanicLatino": "اسباني / لاتيني", + "application.review.demographics.ethnicityOptions.notHispanicLatino": "ليس من أصل اسباني / لاتيني", + "application.review.demographics.raceOptions.americanIndianAlaskanNative": "الهنود الحمر / سكان ألاسكا الأصليين", + "application.review.demographics.raceOptions.asian": "آسيا", + "application.review.demographics.raceOptions.blackAfricanAmerican": "أسود / أمريكي من أصل أفريقي", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander": "من سكان هاواي الأصليين / سكان جزر المحيط الهادئ الأخرى", + "application.review.demographics.raceOptions.white": "أبيض", + "application.review.demographics.raceOptions.americanIndianAlaskanNativeAndBlackAfricanAmerican": "الهنود الأمريكيون / سكان ألاسكا الأصليين والأسود / الأمريكيون من أصل أفريقي", + "application.review.demographics.raceOptions.americanIndianAlaskanNativeAndWhite": "الهنود الحمر / سكان ألاسكا الأصليين والأبيض", + "application.review.demographics.raceOptions.asianAndWhite": "آسيوي وأبيض", + "application.review.demographics.raceOptions.blackAfricanAmericanAndWhite": "أسود / أمريكي من أصل أفريقي وأبيض", + "application.review.demographics.raceOptions.otherMutliracial": "أخرى / متعدد الأعراق", + "application.review.demographics.genderOptions.female": "أنثى", + "application.review.demographics.genderOptions.male": "ذكر", + "application.review.demographics.genderOptions.genderqueerGenderNon-Binary": "Genderqueer / الجنس غير ثنائي", + "application.review.demographics.genderOptions.transFemale": "عبر أنثى", + "application.review.demographics.genderOptions.transMale": "ذكر عبر", + "application.review.demographics.genderOptions.notListed": "غير مدرج", + "application.review.demographics.sexualOrientationOptions.bisexual": "ثنائي الجنس", + "application.review.demographics.sexualOrientationOptions.gayLesbianSameGenderLoving": "مثلي الجنس / السحاقيات / نفس الجنس المحبة", + "application.review.demographics.sexualOrientationOptions.questioningUnsure": "استجواب / غير متأكد", + "application.review.demographics.sexualOrientationOptions.straightHeterosexual": "مستقيم / متغاير الجنس", + "application.review.demographics.sexualOrientationOptions.notListed": "غير مدرج", + "application.review.demographics.howDidYouHearOptions.alamedaCountyHCDWebsite": "موقع ويب Alameda County HCD", + "application.review.demographics.howDidYouHearOptions.developerWebsite": "موقع المطور", + "application.review.demographics.howDidYouHearOptions.flyer": "فلاير", + "application.review.demographics.howDidYouHearOptions.emailAlert": "تنبيه عبر البريد الإلكتروني", + "application.review.demographics.howDidYouHearOptions.friend": "صديق", + "application.review.demographics.howDidYouHearOptions.housingCounselor": "مستشار الإسكان", + "application.review.demographics.howDidYouHearOptions.radioAd": "إعلان راديو", + "application.review.demographics.howDidYouHearOptions.busAd": "إعلان الحافلة", + "application.review.demographics.howDidYouHearOptions.other": "آخر", + "application.review.confirmation.title": "شكرا. لقد تلقينا طلبك ل", + "application.review.confirmation.lotteryNumber": "هذا هو رقم تأكيد طلبك", + "application.review.confirmation.pleaseWriteNumber": "يرجى كتابة رقم الطلب الخاص بك والاحتفاظ به في مكان آمن. لقد قمنا أيضًا بإرسال هذا الرقم إليك عبر البريد الإلكتروني إذا قدمت عنوان بريد إلكتروني.", + "application.review.confirmation.whatExpectTitle": "ماذا تتوقع بعد ذلك", + "application.review.confirmation.whatExpectFirstParagraph.held": "سيقام اليانصيب يوم", + "application.review.confirmation.whatExpectFirstParagraph.attend": "لا تحتاج لحضور يانصيب الإسكان. سيتم نشر النتائج", + "application.review.confirmation.whatExpectFirstParagraph.listing": "في القائمة.", + "application.review.confirmation.whatExpectFirstParagraph.refer": "يرجى الرجوع إلى القائمة لتاريخ نتائج اليانصيب.", + "application.review.confirmation.whatExpectSecondparagraph": "سيتم الاتصال بالمتقدمين بالترتيب حتى يتم ملء الوظائف الشاغرة. إذا تم اختيار طلبك ، فكن مستعدًا لملء طلب أكثر تفصيلاً وتقديم المستندات الداعمة المطلوبة.", + "application.review.confirmation.doNotSubmitTitle": "لا تقدم طلبًا آخر لهذه القائمة.", + "application.review.confirmation.needToUpdate": "إذا كنت بحاجة إلى تحديث المعلومات في التطبيق الخاص بك ، فلا تقدم مرة أخرى. اتصل بالوكيل إذا لم تتلق رسالة تأكيد بالبريد الإلكتروني.", + "application.review.confirmation.createAccountTitle": "هل ترغب في إنشاء حساب؟", + "application.review.confirmation.createAccountParagraph": "سيؤدي إنشاء حساب إلى حفظ معلوماتك للتطبيقات المستقبلية ، ويمكنك التحقق من حالة هذا التطبيق في أي وقت.", + "application.review.confirmation.imdone": "لا شكرا ، لقد انتهيت.", + "application.review.confirmation.browseMore": "تصفح المزيد من القوائم", + "application.review.confirmation.print": "عرض الطلب المقدم وطباعة نسخة.", + "application.confirmation.viewOriginalListing": "عرض القائمة الأصلية", + "application.confirmation.informationSubmittedTitle": "هذه هي المعلومات التي قدمتها.", + "application.confirmation.submitted": "مقدم:", + "application.confirmation.lotteryNumber": "رقم التأكيد الخاص بك", + "application.confirmation.preferences": "التفضيلات", + "application.confirmation.generalLottery": "بناءً على المعلومات التي أدخلتها ، لم تطالب أسرتك بأي تفضيلات يانصيب الإسكان. ستكون في اليانصيب العام.", + "application.confirmation.printCopy": "اطبع نسخة لسجلاتك", + "application.start.whatToExpect.title": "إليك ما يمكن توقعه من هذا التطبيق.", + "application.start.whatToExpect.info1": "أولاً ، سنسألك عنك وعن الأشخاص الذين تخطط للعيش معهم. بعد ذلك ، سوف نسأل عن دخلك. أخيرًا ، سنرى ما إذا كنت مؤهلاً لأي تفضيل يانصيب للإسكان ميسور التكلفة.", + "application.start.whatToExpect.info2": "يرجى العلم أن كل فرد من أفراد الأسرة يمكن أن يظهر فقط في طلب واحد لكل قائمة.", + "application.start.whatToExpect.info3": "ستؤدي أي بيانات احتيالية إلى إزالة طلبك.", + "application.timeout.text": "لحماية هويتك ، ستنتهي جلستك في دقيقة واحدة بسبب عدم النشاط. ستفقد أي معلومات غير محفوظة إذا اخترت عدم الرد.", + "application.timeout.action": "مستمر بالعمل", + "application.timeout.afterMessage": "نحن نهتم بسلامتك. لقد أنهينا جلستك بسبب عدم النشاط. الرجاء بدء تطبيق جديد للمتابعة.", + "application.continueApplication": "متابعة التطبيق", + "application.applicationNeverSubmitted": "لم يتم تقديم طلبك قط", + "application.deleteThisApplication": "هل تريد حذف هذا التطبيق؟", + "application.deleteThisMember": "حذف هذا العضو؟", + "application.deleteMemberDescription": "هل تريد حقا حذف هذا العضو؟", + "application.deleteApplicationDescription": "يعني حذف هذا التطبيق أنك ستفقد جميع المعلومات التي أدخلتها.", + "application.edited": "تم تحريره", + "application.status": "حالة", + "application.statuses.inProgress": "في تقدم", + "application.statuses.neverSubmitted": "لم ترسل أبدا", + "application.statuses.submitted": "مقدم", + "application.viewApplication": "مشاهدة التطبيق", + "application.yourLotteryNumber": "رقم التأكيد الخاص بك هو", + "users.confirmed": "مؤكد", + "users.unconfirmed": "غير مؤكد", + "users.totalUsers": "إجمالي المستخدمين", + "users.administrator": "مدير", + "users.partner": "شريك", + "flags.flaggedSet": "مجموعة مميزة", + "flags.ruleName": "اسم القاعدة", + "flags.pendingReview": "في انتظار المراجعة", + "flags.totalSets": "مجموع المجموعات", + "flags.resolveFlag": "حل العلم", + "flags.markedAsDuplicate": "تم وضع علامة على٪ {quantity} من التطبيقات على أنها مكررة", + "authentication.forgotPassword.changePassword": "تغيير كلمة السر", + "authentication.forgotPassword.message": "إذا كان هناك حساب تم إنشاؤه باستخدام هذا البريد الإلكتروني ، فستتلقى بريدًا إلكترونيًا به ارتباط لإعادة تعيين كلمة المرور الخاصة بك.", + "authentication.forgotPassword.sendEmail": "ارسل بريد الكتروني", + "authentication.forgotPassword.errors.tokenExpired": "انتهت صلاحية رمز إعادة تعيين كلمة المرور. الرجاء طلب واحدة جديدة.", + "authentication.forgotPassword.errors.tokenMissing": "لم يتم العثور على الرمز. الرجاء طلب واحدة جديدة.", + "authentication.forgotPassword.errors.generic": "كان هناك خطأ. يرجى المحاولة مرة أخرى ، أو الاتصال بالدعم للمساعدة.", + "authentication.forgotPassword.errors.emailNotFound": "البريد الإلكتروني غير موجود. يرجى التأكد من أن بريدك الإلكتروني لديه حساب معنا وأنه تم تأكيده.", + "authentication.timeout.text": "لحماية هويتك ، ستنتهي جلستك في دقيقة واحدة بسبب عدم النشاط. ستفقد أي معلومات غير محفوظة وسيتم تسجيل خروجك إذا اخترت عدم الرد.", + "authentication.timeout.action": "ابق متصلا", + "authentication.timeout.signOutMessage": "نحن نهتم بسلامتك. لقد قمنا بتسجيل الخروج بسبب عدم النشاط. من فضلك سجل دخولك للمتابعة.", + "authentication.signIn.loginError": "يرجى إدخال عنوان بريد إلكتروني صالح", + "authentication.signIn.passwordError": "الرجاء إدخال كلمة السر الصحيحة", + "authentication.signIn.phoneError": "الرجاء إدخال رقم هاتف أمريكي صالح.", + "authentication.signIn.cantFindAccount": "لم نتمكن من العثور على حساب بعنوان البريد الإلكتروني / كلمة المرور هذا.", + "authentication.signIn.error": "حدث خطأ أثناء تسجيل دخولك", + "authentication.signIn.errorGenericMessage": "يرجى المحاولة مرة أخرى ، أو الاتصال بالدعم للمساعدة.", + "authentication.signIn.forgotPassword": "هل نسيت كلمة السر؟", + "authentication.signIn.success": "مرحبًا بعودتك، %{name}", + "authentication.createAccount.accountConfirmed": "تم تأكيد حسابك بنجاح.", + "authentication.createAccount.anEmailHasBeenSent": "تم إرسال بريد إلكتروني إلى٪ {email}", + "authentication.createAccount.confirmationInstruction": "الرجاء النقر فوق الارتباط الموجود في البريد الإلكتروني الذي أرسلناه لك لإكمال إنشاء الحساب.", + "authentication.createAccount.confirmationNeeded": "مطلوب تأكيد", + "authentication.createAccount.emailSent": "تم إرسال البريد الإلكتروني للتأكيد. يرجى التحقق من البريد الوارد الخاص بك.", + "authentication.createAccount.yourName": "اسمك", + "authentication.createAccount.firstName": "الاسم الاول", + "authentication.createAccount.middleNameOptional": "الاسم الأوسط (اختياري)", + "authentication.createAccount.middleName": "الاسم الأوسط", + "authentication.createAccount.lastName": "الكنية", + "authentication.createAccount.yourDateOfBirth": "تاريخ ميلادك", + "authentication.createAccount.email": "بريد إلكتروني", + "authentication.createAccount.emailPrivacy": "سنستخدم عنوان بريدك الإلكتروني فقط للاتصال بك بخصوص طلبك.", + "authentication.createAccount.reEnterEmail": "أعد إدخال البريد الإلكتروني", + "authentication.createAccount.noAccount": "ليس لديك حساب؟", + "authentication.createAccount.phone": "هاتف", + "authentication.createAccount.reEnterPassword": "إعادة إدخال كلمة المرور الخاصة بك", + "authentication.createAccount.resendTheEmail": "إعادة إرسال البريد الإلكتروني", + "authentication.createAccount.emailSubscription": "راسلني كلما تم نشر قائمة أو تحديثها.", + "authentication.createAccount.smsSubscription": "أرسل لي رسالة نصية كلما تم نشر أو تحديث قائمة.", + "authentication.createAccount.linkExpired": "انتهت صلاحية الرابط الخاص بك", + "authentication.createAccount.mustBe8Chars": "يجب أن يتكون من 8 أحرف", + "authentication.createAccount.password": "كلمة المرور", + "authentication.createAccount.passwordInfo": "يجب أن يتكون من 8 أحرف على الأقل وأن يتضمن حرفًا واحدًا على الأقل ورقمًا واحدًا على الأقل.", + "authentication.createAccount.resendEmailInfo": "يرجى النقر فوق الارتباط الموجود في البريد الإلكتروني الذي نرسله إليك في غضون 24 ساعة لإكمال إنشاء الحساب.", + "authentication.createAccount.resendAnEmailTo": "إعادة إرسال بريد إلكتروني إلى", + "authentication.createAccount.errors.accountConfirmed": "تم تأكيد حسابك بالفعل.", + "authentication.createAccount.errors.emailInUse": "البريد الالكتروني قيد الاستخدام بالفعل", + "authentication.createAccount.errors.emailMismatch": "رسائل البريد الإلكتروني لا تتطابق", + "authentication.createAccount.errors.emailNotFound": "البريد الإلكتروني غير موجود. الرجاء التسجيل أولا.", + "authentication.createAccount.errors.passwordMismatch": "كلمات السر لا تتطابق", + "authentication.createAccount.errors.passwordTooWeak": "كلمة المرور ضعيفة جدا. يجب أن يتكون من 8 أحرف على الأقل وأن يتضمن حرفًا واحدًا على الأقل ورقمًا واحدًا على الأقل.", + "authentication.createAccount.errors.tokenExpired": "انتهت صلاحية الرابط الخاص بك.", + "authentication.createAccount.errors.tokenMissing": "تم تقديم رمز خاطئ.", + "authentication.signOut.success": "لقد قمت بتسجيل الخروج بنجاح من حسابك.", + "errors.alert.timeoutPleaseTryAgain": "وجه الفتاة! يبدو أنه حدث خطأ ما. حاول مرة اخرى.", + "errors.notFound.title": "الصفحة غير موجودة", + "errors.notFound.message": "عذرًا ، يبدو أننا لا نستطيع العثور على الصفحة التي تبحث عنها. حاول العودة إلى الصفحة السابقة أو انقر أدناه لتصفح القوائم.", + "errors.unauthorized.title": "غير مصرح", + "errors.unauthorized.message": "أه أوه ، غير مسموح لك بالوصول إلى هذه الصفحة.", + "errors.agreeError": "يجب أن توافق على الشروط من أجل المتابعة", + "errors.firstNameError": "الرجاء إدخال الاسم الأول", + "errors.lastNameError": "الرجاء إدخال اسم العائلة", + "errors.dateOfBirthError": "من فضلك ادخل تاريخ ميلاد صحيح", + "errors.dateOfBirthErrorAge": "الرجاء إدخال تاريخ ميلاد صالح ، يجب أن يكون 18 أو أكبر", + "errors.emailAddressError": "الرجاء إدخال عنوان البريد الإلكتروني", + "errors.phoneNumberError": "الرجاء إدخال رقم هاتف", + "errors.phoneNumberTypeError": "الرجاء إدخال نوع رقم الهاتف", + "errors.streetError": "يرجى إدخال عنوان", + "errors.timeError": "الرجاء إدخال وقت صالح", + "errors.cityError": "الرجاء إدخال مدينة", + "errors.stateError": "الرجاء إدخال الدولة", + "errors.zipCodeError": "الرجاء إدخال الرمز البريدي", + "errors.multipleZipCodeError": "الرجاء إدخال رمز بريدي واحد أو أكثر مفصول بفواصل", + "errors.errorsToResolve": "هناك أخطاء ستحتاج إلى حلها قبل المضي قدمًا.", + "errors.numberError": "الرجاء إدخال رقم صالح أكبر من 0.", + "errors.selectAllThatApply": "يرجى اختيار كل ما ينطبق.", + "errors.selectAtLeastOne": "الرجاء تحديد خيار واحد على الأقل.", + "errors.selectAnOption": "الرجاء تحديد خيار.", + "errors.selectOption": "الرجاء تحديد أحد الخيارات أعلاه.", + "errors.urlError": "أدخل رابط صحيح من فضلك", + "errors.householdTooBig": "حجم أسرتك كبير جدًا.", + "errors.householdTooSmall": "حجم أسرتك صغير جدًا.", + "errors.dateError": "ارجوك ادخل تاريخ صحيح", + "errors.rateLimitExceeded": "تم تجاوز حد المعدل ، حاول مرة أخرى لاحقًا.", + "errors.requiredFieldError": "هذه الخانة مطلوبه.", + "errors.noData": "لا تتوافر بيانات.", + "footer.srHeading": "تذييل", + "footer.srProjectInformation": "معلومات المشروع", + "footer.srContactInformation": "معلومات للتواصل", + "footer.srLegalInformation": "المعلومات القانونية", + "footer.contact": "اتصال", + "footer.terms": "تنصل", + "footer.forGeneralQuestions": "للاستفسارات العامة عن البرنامج ، يمكنك الاتصال بنا على 000-000-0000.", + "footer.giveFeedback": "إعطاء ردود الفعل", + "footer.privacyPolicy": "سياسة الخصوصية", + "footer.copyright": "مظاهرة مقاطعة © 2020 • جميع الحقوق محفوظة", + "housingCounselors.subtitle": "تحدث مع مستشار الإسكان المحلي الخاص باحتياجاتك.", + "housingCounselors.languageServices": "خدمات اللغة:", + "housingCounselors.call": "اتصل بالرقم}", + "housingCounselors.visitWebsite": "قم بزيارة٪ {name}", + "homeType.apartment": "شقة", + "homeType.duplex": "دوبلكس", + "homeType.house": "بيت لعائلة واحدة", + "homeType.townhome": "تاون هوم", + "languages.srHeading": "اللغات", + "languages.srNavigation": "لغة", + "languages.en": "English", + "languages.es": "Español", + "languages.zh": "中文", + "languages.vi": "Tiếng Việt", + "languages.ar": "عربى", + "languages.bn": "বাংলা", + "leasingAgent.contact": "اتصل بوكيل التأجير", + "leasingAgent.dueToHighCallVolume": "نظرًا لارتفاع حجم المكالمات ، قد تسمع رسالة.", + "leasingAgent.name": "اسم وكيل التأجير", + "leasingAgent.namePlaceholder": "الاسم بالكامل", + "leasingAgent.title": "عنوان وكيل التأجير", + "leasingAgent.officeHours": "ساعات العمل", + "leasingAgent.officeHoursPlaceholder": "على سبيل المثال: 9:00 صباحًا - 5:00 مساءً ، من الاثنين إلى الجمعة", + "listingFilters.program.Seniors 55+": "لكبار السن فوق 55 سنة", + "listingFilters.program.Seniors 62+": "لكبار السن فوق 62 سنة", + "listingFilters.program.Residents with Disabilities": "المقيمون من ذوي الإعاقة", + "listingFilters.program.Families": "الأُسر", + "listingFilters.program.Supportive Housing for the Homeless": "إسكان دعم المشردين", + "listingFilters.program.Veterans": "المحاربون القدامى", + "listingFilters.clear": "مسح", + "listingFilters.section8": "يقبل قسائم اختيار المسكن بالقسم 8", + "listings.error": "كانت هناك مشكلة في إرسال النموذج.", + "listings.closeThisListing": "هل تريد حقًا إغلاق هذه القائمة؟", + "listings.active": "قبول الطلبات", + "listings.pending": "قريبًا", + "listings.closed": "مغلق", + "listings.actions.publish": "ينشر", + "listings.actions.draft": "حفظ كمسودة", + "listings.actions.preview": "معاينة", + "listings.actions.close": "يغلق", + "listings.actions.viewListing": "عرض القائمة", + "listings.actions.unpublish": "إلغاء النشر", + "listings.actions.postResults": "نتائج ما بعد", + "listings.actions.resultsPosted": "تم نشر النتائج", + "listings.actions.previewLotteryResults": "معاينة نتائج اليانصيب", + "listings.activePreferences": "التفضيلات النشطة", + "listings.addListing": "إضافة قائمة", + "listings.addPhoto": "إضافة صورة", + "listings.addPreference": "إضافة تفضيل", + "listings.addPreferences": "إضافة التفضيلات", + "listings.additionalApplicationSubmissionNotes": "ملاحظات إضافية حول تقديم الطلب", + "listings.additionalInformation": "معلومة اضافية", + "listings.allUnits": "جميع الوحدات", + "listings.allUnitsReservedFor": "جميع الوحدات محجوزة لـ٪ {type}", + "listings.annualIncome": "٪ {الدخل} في السنة", + "listings.annualIncomeRange": "٪ {from} إلى٪ {to} سنويًا", + "listings.applicationTitle": "بيانات الطلب", + "listings.applicationAddress": "تبوك", + "listings.applicationDeadline": "تاريخ استحقاق الطلب", + "listings.applicationDueTime": "وقت التقديم", + "listings.applicationFCFS": "الخدمة بأسبقية الوصول", + "listings.applicationFee": "رسم الإستمارة", + "listings.applicationFeeDueAt": "موعد المقابلة", + "listings.applicationOpenPeriod": "التطبيقات مفتوحة", + "listings.applicationPerApplicantAgeDescription": "لكل متقدم يبلغ من العمر 18 عامًا أو أكثر", + "listings.applicationPickupQuestion": "هل يمكن التقاط الطلبات؟", + "listings.applicationsClosed": "التطبيقات مغلقة", + "listings.applicationDropOffQuestion": "هل يمكن إسقاط الطلبات؟", + "listings.apply.applicationsMustBeReceivedByDeadline": "يجب استلام الطلبات بحلول الموعد النهائي ولن يتم النظر في العلامات البريدية.", + "listings.apply.applicationSeason": "يجب على السكان التقديم في", + "listings.apply.applicationWillBeAvailableOn": "سيكون التطبيق متاحًا للتنزيل والاستلام في٪ {openDate}", + "listings.apply.applyOnline": "يقدم طلب على الإنترنت", + "listings.apply.downloadApplication": "تحميل التطبيق", + "listings.apply.dropOffApplication": "إسقاط التطبيق", + "listings.apply.dropOffApplicationOrMail": "إسقاط التطبيق أو الإرسال عن طريق البريد الأمريكي", + "listings.apply.getAPaperApplication": "احصل على تطبيق ورقي", + "listings.apply.howToApply": "كيفية التقديم", + "listings.apply.paperApplicationsMustBeMailed": "يجب إرسال الطلبات الورقية عن طريق البريد الأمريكي ولا يمكن تقديمها شخصيًا.", + "listings.apply.pickUpAnApplication": "التقط طلبًا", + "listings.apply.postmarkedApplicationsMustBeReceivedByDate": "يجب استلام الطلبات قبل الموعد النهائي. في حالة الإرسال عن طريق بريد الولايات المتحدة ، يجب ختم الطلب بختم بريد٪ {applicationDueDate} واستلامه بالبريد في موعد لا يتجاوز٪ {postmarkReceivedByDate}. لن يتم قبول الطلبات التي يتم استلامها بعد٪ {postmarkReceivedByDate} عبر البريد حتى إذا تم ختمها بواسطة٪ {applicationDueDate}. ٪ {developer} غير مسئول عن البريد المفقود أو المتأخر.", + "listings.apply.sendByUsMail": "أرسل الطلب عن طريق البريد الأمريكي", + "listings.apply.submitAPaperApplication": "تقديم طلب ورقي", + "listings.apply.contactManagment": "شركة إدارة الاتصال", + "listings.atAnotherAddress": "في عنوان آخر", + "listings.atLeasingAgentAddress": "في عنوان وكيل التأجير", + "listings.atMailingAddress": "في العنوان البريدي", + "listings.availableAndWaitlist": "الوحدات المتاحة وفتح قائمة الانتظار", + "listings.availableUnits": "الوحدات المتاحة", + "listings.availableUnitsAndWaitlist": "الوحدات المتاحة وقائمة الانتظار", + "listings.availableUnitsAndWaitlistDesc": "بمجرد ملء المتقدمين لجميع الوحدات المتاحة ، سيتم وضع المتقدمين الإضافيين في قائمة الانتظار لـ ٪ {number} من الوحدات ", + "listings.bath": "بث", + "listings.browseListings": "تصفح القوائم", + "listings.buildingImageAltText": "صورة المبنى", + "listings.closedListings": "القوائم المغلقة", + "listings.communityProgramsDescription": "يتضمن هذا البرنامج فرصًا لأعضاء مجتمعات معينة", + "listings.confirmedPreferenceList": "تم تأكيد قائمة٪ {preference}", + "listings.creditHistory": "تاريخ الرصيد", + "listings.criminalBackground": "خلفية جنائية", + "listings.deleteListingDescription": "حذف هذه القائمة يعني أنك ستفقد جميع المعلومات التي أدخلتها.", + "listings.depositMax": "إيداع ماكس", + "listings.depositMin": "ديبوزيت من", + "listings.depositOrMonthsRent": "أو إيجار شهر", + "listings.depositMayBeHigherForLowerCredit": "قد تكون أعلى للحصول على درجات ائتمانية أقل", + "listings.details.listingData": "بيانات القائمة", + "listings.details.createdDate": "تاريخ الإنشاء", + "listings.details.updatedDate": "تاريخ تحديث", + "listings.details.id": "معرف القائمة", + "listings.developmentalDisabilities": "الأشخاص الذين يعانون من إعاقات في النمو", + "listings.developmentalDisabilitiesDescription": "تم تخصيص عدد من الوحدات في هذا المبنى للأشخاص الذين يعانون من إعاقات في النمو. يرجى زيارة Housingchoices.org للحصول على معلومات حول الأهلية والمتطلبات وكيفية الحصول على طلب وللحصول على إجابات لأية أسئلة أخرى قد يكون لديك حول هذه العملية.", + "listings.dropOffAddress": "عنوان التسليم", + "listings.dueDateQuestion": "هل يوجد موعد لتقديم الطلب؟", + "listings.editPreferences": "تعديل التفضيلات", + "listings.enterLotteryForWaitlist": "قم بإرسال طلب للحصول على خانة مفتوحة في قائمة الانتظار لـ٪ {Units} من الوحدات.", + "listings.firstComeFirstServe": "الخدمة بأسبقية الوصول", + "listings.forIncomeCalculations": "بالنسبة لحسابات الدخل ، يشمل حجم الأسرة جميع (جميع الأعمار) الذين يعيشون في الوحدة.", + "listings.forIncomeCalculationsBMR": "تعتمد حسابات الدخل على نوع الوحدة", + "listings.hideClosedListings": "إخفاء القوائم المغلقة", + "listings.householdMaximumIncome": "الحد الأقصى لدخل الأسرة", + "listings.householdSize": "حجم الأسرة", + "listings.homeType": "نوع المنزل", + "listings.importantProgramRules": "قواعد البرنامج الهامة", + "listings.includesPriorityUnits": "يتضمن وحدات ذات أولوية بالنسبة لـ٪ {أولويات}", + "listings.latitude": "خط العرض", + "listings.leasingAgentAddress": "عنوان وكيل التأجير", + "listings.listingPreviewOnly": "هذه معاينة القائمة فقط.", + "listings.listingStatus.active": "يفتح", + "listings.listingStatus.pending": "مسودة", + "listings.listingStatus.closed": "مغلق", + "listings.listingSubmitted": "تم إرسال القائمة", + "listings.listingUpdated": "تم تحديث القائمة", + "listings.longitude": "خط الطول", + "listings.lottery": "اليانصيب", + "listings.lotteryDateNotes": "ملاحظات تاريخ اليانصيب", + "listings.lotteryDateQuestion": "متى سيتم تشغيل اليانصيب؟", + "listings.lotteryEndTime": "وقت انتهاء اليانصيب", + "listings.lotteryResults.completeResultsWillBePosted": "سيتم نشر نتائج اليانصيب الكاملة قريبًا.", + "listings.lotteryResults.downloadResults": "تحميل النتائج", + "listings.lotteryResults.header": "نتائج اليانصيب", + "listings.lotteryStartTime": "وقت بدء اليانصيب", + "listings.mapPinAutomaticDescription": "يعتمد موضع دبوس الخريطة على العنوان المقدم", + "listings.mapPinCustomDescription": "اسحب الدبوس لتحديث موقع العلامة", + "listings.mapPinPosition": "خريطة الموقع", + "listings.mapPreview": "معاينة الخريطة", + "listings.mapPreviewNoAddress": "أدخل عنوانًا لمعاينة الخريطة", + "listings.maxIncomeMonth": "الحد الأقصى للدخل / الشهر", + "listings.maxIncomeYear": "الحد الأقصى للدخل / السنة", + "listings.monthlyIncome": "٪ {الدخل} شهريًا", + "listings.monthlyIncomeRange": "٪ {from} إلى٪ {to} شهريًا", + "listings.moreBuildingSelectionCriteria": "اكتشف المزيد حول معايير اختيار المبنى", + "listings.newListing": "قائمة جديدة", + "listings.noAvailableUnits": "لا توجد وحدات متاحة في هذا الوقت.", + "listings.noOpenListings": "لا توجد قوائم لديها تطبيقات مفتوحة حاليا.", + "listings.occupancyDescriptionNoSro": "تعتمد حدود الإشغال لهذا المبنى على نوع الوحدة.", + "listings.openHouseEvent.header": "البيوت المفتوحة", + "listings.openHouseEvent.seeVideo": "شاهد الفيديو", + "listings.paperDifferentAddress": "يتم إرسال الطلبات الورقية بالبريد إلى عنوان آخر", + "listings.percentAMIUnit": "٪ {percent}٪ وحدة AMI", + "listings.pickupAddress": "عنوان الاستلام", + "listings.postmarkByDate": "ختم البريد حسب التاريخ", + "listings.postmarksConsideredQuestion": "هل يتم أخذ العلامات البريدية في الاعتبار؟", + "listings.priorityUnits": "وحدات الأولوية", + "listings.priorityUnitsDescription": "يحتوي هذا المبنى على وحدات موضوعة جانباً إذا كان أي مما يلي ينطبق عليك أو على أحد أفراد أسرتك:", + "listings.title": "بيانات الملكية", + "listings.developer": "مطور إسكان", + "listings.buildingAddress": "تبوك", + "listings.publicLottery.header": "اليانصيب العام", + "listings.publicLottery.seeVideo": "شاهد الفيديو", + "listings.remainingUnitsAfterPreferenceConsideration": "بعد النظر في جميع أصحاب التفضيلات ، ستكون أي وحدات متبقية متاحة لمقدمي الطلبات المؤهلين الآخرين.", + "listings.rentalHistory": "تاريخ الإيجار", + "listings.requiredDocuments": "الوتائق المطلوبة", + "listings.reservedCommunityBuilding": "٪ {type} مبنى", + "listings.reservedCommunityDescription": "وصف المجتمع المحجوز", + "listings.reservedCommunitySeniorTitle": "مبنى كبير", + "listings.reservedCommunityTitleDefault": "مبنى محجوز", + "listings.reservedCommunityTypes.senior": "كبار السن", + "listings.reservedCommunityTypes.senior55": "لكبار السن فوق 55 سنة", + "listings.reservedCommunityTypes.senior62": "لكبار السن فوق 62 سنة", + "listings.reservedCommunityTypes.partiallySenior": "كبار السن جزئيا", + "listings.reservedCommunityTypes.specialNeeds": "يمكن الوصول", + "listings.reservedFor": "محجوز لـ٪ {type}", + "listings.reservedCommunityType": "نوع المجتمع المحجوز", + "listings.reservedTypePlural.family": "العائلات", + "listings.reservedTypePlural.senior": "كبار السن", + "listings.reservedTypePlural.veteran": "قدامى المحاربين", + "listings.reservedTypePlural.specialNeeds": "الاحتياجات الخاصة", + "listings.reservedUnits": "الوحدات المحجوزة", + "listings.reservedUnitsDescription": "من أجل التأهل لهذه الوحدات ، يجب أن ينطبق أحد ما يلي عليك أو على أحد أفراد أسرتك:", + "listings.reservedUnitsForWhoAre": "محجوز لـ٪ {communityType} من٪ {reservedType}", + "listings.reviewOrderQuestion": "كيف يتم تحديد أمر مراجعة الطلب؟", + "listings.sections.additionalDetails": "تفاصيل اضافية", + "listings.sections.additionalDetailsSubtitle": "هل هناك أي مستندات ومعايير اختيار أخرى مطلوبة؟", + "listings.sections.additionalEligibilitySubtext": "دع المتقدمين يعرفون أي قواعد أخرى للمبنى.", + "listings.sections.additionalEligibilitySubtitle": "يجب على المتقدمين أيضًا التأهل وفقًا لقواعد المبنى.", + "listings.sections.additionalEligibilityTitle": "قواعد الأهلية الإضافية", + "listings.sections.additionalFees": "رسوم اضافية", + "listings.sections.additionalFeesSubtitle": "أخبرنا عن أي رسوم أخرى يطلبها مقدم الطلب.", + "listings.sections.additionalInformationSubtitle": "المستندات المطلوبة ومعايير الاختيار", + "listings.sections.additionalInformationTitle": "معلومة اضافية", + "listings.sections.applicationAddressSubtitle": "في حالة التطبيقات الورقية ، أين تريد إسقاط الطلبات أو إرسالها بالبريد؟", + "listings.sections.applicationAddressTitle": "عنوان التطبيق", + "listings.sections.buildingDetailsSubtitle": "أخبرنا أين يقع المبنى.", + "listings.sections.buildingDetailsTitle": "تفاصيل البناء", + "listings.sections.buildingFeaturesSubtitle": "قدم تفاصيل عن أي وسائل الراحة وتفاصيل الوحدة.", + "listings.sections.buildingFeaturesTitle": "ميزات المبنى", + "listings.sections.applicationDatesTitle": "مواعيد التقديم", + "listings.sections.applicationDatesSubtitle": "أخبرنا عن التواريخ المهمة المتعلقة بهذه القائمة.", + "listings.sections.communityType": "نوع المجتمع", + "listings.sections.communityTypeSubtitle": "هل هناك أي متطلبات يجب على المتقدمين الوفاء بها؟", + "listings.sections.costsNotIncluded": "التكاليف غير مشمولة", + "listings.sections.eligibilitySubtitle": "الدخل والإشغال والتفضيلات والإعانات", + "listings.sections.eligibilityTitle": "جدارة - أهلية", + "listings.sections.featuresSubtitle": "وسائل الراحة وتفاصيل الوحدة والرسوم الإضافية", + "listings.sections.featuresTitle": "سمات", + "listings.sections.housingPreferencesSubtitle": "سيتم منح أصحاب الأفضلية أعلى مرتبة.", + "listings.sections.housingPreferencesSubtext": "أخبرنا عن أي تفضيلات سيتم استخدامها لتصنيف المتقدمين المؤهلين.", + "listings.sections.housingPreferencesTitle": "تفضيلات السكن", + "listings.sections.introSubtitle": "لنبدأ ببعض المعلومات الأساسية حول قائمتك.", + "listings.sections.introTitle": "سرد مقدمة", + "listings.sections.leasingAgentSubtitle": "قدم تفاصيل حول وكيل التأجير الذي سيتولى إدارة عملية تقديم الطلب.", + "listings.sections.leasingAgentTitle": "وكيل تأجير", + "listings.sections.neighborhoodSubtitle": "الموقع والمواصلات", + "listings.sections.neighborhoodTitle": "حي", + "listings.sections.photoTitle": "قائمة الصورة", + "listings.sections.photoSubtitle": "قم بتحميل صورة للقائمة التي سيتم استخدامها كمعاينة.", + "listings.sections.processSubtitle": "التواريخ الهامة ومعلومات الاتصال", + "listings.sections.processTitle": "معالجة", + "listings.sections.publicProgramNote": "غالبًا ما تتلقى عقارات الإسكان الميسور التكلفة تمويلًا لإسكان مجموعات سكانية محددة ، مثل كبار السن ، والمقيمين ذوي الإعاقة ، وما إلى ذلك. يمكن للممتلكات أن تخدم أكثر من مجموعة سكانية واحدة. اتصل بهذا العقار إذا لم تكن متأكدًا مما إذا كنت مؤهلاً.", + "listings.sections.rankingsResultsTitle": "الترتيب والنتائج", + "listings.sections.rankingsResultsSubtitle": "قدم تفاصيل حول ما يحدث للطلبات بمجرد تقديمها.", + "listings.sections.rentalAssistanceSubtitle": "سيتم النظر في قسائم اختيار السكن ، والقسم 8 وبرامج مساعدة الإيجار السارية الأخرى لهذه المنشأة. في حالة وجود دعم إيجار ساري المفعول ، سيعتمد الحد الأدنى المطلوب للدخل على جزء من الإيجار الذي يدفعه المستأجر بعد استخدام الدعم.", + "listings.sections.rentalAssistanceTitle": "المساعدة في التأجير", + "listings.sections.addOpenHouse": "أضف البيت المفتوح", + "listings.sections.openHouse": "بيت مفتوح", + "listings.sections.utilities": "شاملة المرافق", + "listings.seeMaximumIncomeInformation": "انظر معلومات الحد الأقصى للدخل", + "listings.seePreferenceInformation": "انظر معلومات التفضيل", + "listings.seeUnitInformation": "انظر معلومات الوحدة", + "listings.selectPreferences": "حدد التفضيلات", + "listings.showClosedListings": "إظهار القوائم المغلقة", + "listings.specialNotes": "ملاحظات خاصة", + "listings.streetAddressOrPOBox": "عنوان الشارع أو صندوق البريد", + "listings.totalListings": "إجمالي القوائم", + "listings.underConstruction": "تحت التشيد", + "listings.unitSummaryGroupMessage": "لكل نوع من أنواع الوحدات ، لا يمكنك تحقيق أكثر من حد الدخل المرتبط بحجم أسرتك ، كما هو موضح في جدول الحد الأقصى لدخل الأسرة أدناه.", + "listings.section8Message": "إذا كان لديك قسيمة اختيار الإسكان من القسم 8 ، فلن تنطبق متطلبات الدخل وستدفع الإيجار على أساس دخلك.", + "listings.section8MessageOpening": "إذا كان لديك", + "listings.section8FullName": "قسيمة اختيار الإسكان من القسم 8", + "listings.section8MessageClosing": "، فلن تنطبق متطلبات الدخل وستدفع الإيجار على أساس دخلك.", + "listings.unitTypes.oneBdrm": "1 غرف نوم", + "listings.unitTypes.twoBdrm": "2 غرف نوم", + "listings.unitTypes.threeBdrm": "3 غرف نوم", + "listings.unitTypes.fourBdrm": "4 غرف نوم", + "listings.unitTypes.fiveBdrm": "5 غرف نوم", + "listings.unitTypes.studio": "ستوديو", + "listings.unitsAreFor": "هذه الوحدات خاصة بـ٪ {type}.", + "listings.unitsHaveAccessibilityFeaturesFor": "تحتوي هذه الوحدات على ميزات إمكانية الوصول للأشخاص الذين لديهم٪ {type}.", + "listings.upcomingLotteries.hide": "إخفاء القوائم المغلقة", + "listings.upcomingLotteries.noResults": "لا توجد قوائم مغلقة مع اليانصيب القادمة في هذا الوقت.", + "listings.upcomingLotteries.show": "إظهار القوائم المغلقة", + "listings.upcomingLotteries.title": "القوائم المغلقة", + "listings.vacantUnits": "الوحدات الشاغرة", + "listings.verifiedListing": "مؤكدة حسب العقار", + "listings.waitlist.closed": "قائمة الانتظار مغلقة", + "listings.waitlist.currentSizeQuestion": "كم عدد الأشخاص في القائمة الحالية؟", + "listings.waitlist.label": "قائمة الانتظار", + "listings.waitlist.isOpen": "قائمة الانتظار مفتوحة", + "listings.waitlist.currentSize": "حجم قائمة الانتظار الحالية", + "listings.waitlist.finalSize": "حجم قائمة الانتظار النهائية", + "listings.waitlist.maxSize": "الحد الأقصى لحجم قائمة الانتظار", + "listings.waitlist.maxSizeQuestion": "ما هو الحجم الأقصى لقائمة الانتظار؟", + "listings.waitlist.open": "افتح قائمة الانتظار", + "listings.waitlist.openQuestion": "هل قائمة الانتظار مفتوحة؟", + "listings.waitlist.openSize": "عدد الفتحات", + "listings.waitlist.openSizeQuestion": "كم عدد المواقع المفتوحة في القائمة؟", + "listings.waitlist.openSlots": "فتح فتحات قائمة الانتظار", + "listings.waitlist.sizeQuestion": "هل تريد إظهار حجم قائمة الانتظار؟", + "listings.waitlist.submitAnApplication": "بمجرد ملء المتقدمين المصنفين لجميع الوحدات المتاحة ، سيتم وضع المتقدمين المصنفين المتبقين في قائمة انتظار لتلك الوحدات نفسها.", + "listings.waitlist.submitForWaitlist": "أرسل طلبًا للحصول على فتحة مفتوحة في قائمة الانتظار.", + "listings.waitlist.unitsAndWaitlist": "الوحدات المتاحة وقائمة الانتظار", + "listings.whatToExpectLabel": "أخبر مقدم الطلب بما يمكن توقعه من العملية", + "listings.whereDropOffQuestion": "أين يتم تسليم الطلبات؟", + "listings.wherePickupQuestion": "من أين يتم استلام الطلبات؟", + "listings.yearBuilt": "بنيت عام", + "listings.whenApplicationsClose": "عندما تكون التطبيقات قريبة من الجمهور", + "listings.cc&r": "العهود والشروط والقيود (CC & R's)", + "listings.cc&rDescription": "يشرح CC & R قواعد رابطة أصحاب المنازل ، ويقيدون كيفية تعديل الملكية.", + "listings.downloadPdf": "تحميل PDF", + "listings.rePricing": "إعادة التسعير", + "listings.eligibilityNotebook": "مفكرة الأهلية", + "listings.processInfo": "معلومات عملية", + "listings.featuresCards": "بطاقات الميزات", + "listings.neighborhoodBuildings": "مباني الحي", + "listings.additionalInformationEnvelope": "مغلف معلومات إضافية", + "listings.listingName": "اسم القائمة", + "listings.applicationsSubmitted": "الطلبات المقدمة", + "listings.listingStatusText": "حالة الاستماع", + "listings.applications": "التطبيقات", + "listings.unit.title": "وحدة", + "listings.unit.add": "أضف وحدة", + "listings.unit.number": "وحدة #", + "listings.unit.unitNumber": "رقم الوحدة", + "listings.unit.type": "نوع الوحدة", + "listings.unit.ami": "أي", + "listings.unit.amiChart": "أنا أخطط", + "listings.unit.amiPercentage": "نسبة AMI", + "listings.unit.rent": "إيجار", + "listings.unit.sqft": "قدم مربع", + "listings.unit.squareFootage": "لقطات مربعة", + "listings.unit.priorityType": "يوجد", + "listings.unit.reservedType": "محجوز", + "listings.unit.status": "حالة", + "listings.unit.unitStatus": "حالة الوحدة", + "listings.unit.details": "تفاصيل", + "listings.unit.numBathrooms": "عدد الحمامات", + "listings.unit.floor": "وحدة أرضية", + "listings.unit.minOccupancy": "الحد الأدنى من الإشغال", + "listings.unit.maxOccupancy": "الحد الأقصى للإشغال", + "listings.unit.rentType": "كيف يتم تحديد الإيجار؟", + "listings.unit.fixed": "مبلغ ثابت", + "listings.unit.percentage": "٪ من الدخل", + "listings.unit.monthlyRent": "الإيجار الشهري", + "listings.unit.%incomeRent": "نسبة إيجار الدخل", + "listings.unit.accessibilityPriorityType": "نوع أولوية الوصول", + "listings.unit.unitTypes": "أنواع الوحدات", + "listings.unit.individualUnits": "الوحدات الفردية", + "listings.unit.delete": "احذف هذه الوحدة", + "listings.unit.deleteConf": "هل تريد حقًا حذف هذه الوحدة؟", + "listings.unit.eligibility": "جدارة - أهلية", + "listings.unit.statusOptions.unknown": "مجهول", + "listings.unit.statusOptions.available": "متوفرة", + "listings.unit.statusOptions.occupied": "احتل", + "listings.unit.statusOptions.unavailable": "غير متوفره", + "listings.unitsSummary.add": "أضف الملخص", + "listings.unitsSummary.occupancy": "شغل", + "listings.unitsSummary.floorMin": "الحد الأدنى من الطابق", + "listings.unitsSummary.floorMax": "أقصى حد", + "listings.unitsSummary.monthlyRentMin": "الحد الأدنى للإيجار الشهري", + "listings.unitsSummary.monthlyRentMax": "ايجار شهري كحد أقصى", + "listings.unitsSummary.sqFeetMin": "الحد الأدنى من اللقطات المربعة", + "listings.unitsSummary.sqFeetMax": "أقصى حد بالقدم المربع", + "listings.unitsSummary.availability": "التوفر", + "listings.unitsSummary.count": "العدد الإجمالي", + "listings.unitsSummary.available": "مجموعه متاحة", + "listings.unitsSummary.delete": "احذف هذا الملخص", + "listings.unitsSummary.deleteConf": "هل تريد حقًا حذف هذا الملخص؟", + "listings.events.deleteThisEvent": "احذف هذا الحدث", + "listings.events.deleteConf": "هل تريد حقًا حذف هذا الحدث؟", + "listings.events.openHouseNotes": "ملاحظات البيت المفتوح", + "listings.units": "وحدات القيد", + "listings.unitsDescription": "حدد وحدات البناء المتوفرة من خلال القائمة.", + "listings.unitTypesOrIndividual": "هل تريد إظهار أنواع الوحدات أم الوحدات الفردية؟", + "listings.utilities.water": "الماء", + "listings.utilities.gas": "الغاز", + "listings.utilities.trash": "القمامة", + "listings.utilities.sewer": "الصرف الصحي", + "listings.utilities.electricity": "الكهرباء", + "listings.utilities.cable": "اشتراك التلفاز", + "listings.utilities.phone": "الهاتف", + "listings.utilities.internet": "الإنترنت", + "lottery.applicationsThatQualifyForPreference": "سيتم إعطاء الطلبات المؤهلة لهذا التفضيل أولوية أعلى.", + "lottery.viewPreferenceList": "عرض قائمة التفضيلات", + "nav.srHeading": "قائمة الإبحار", + "nav.srNavigation": "الأساسية", + "nav.accountSettings": "إعدادت الحساب", + "nav.browseProperties": "تصفح الخصائص", + "nav.getFeedback": "هذه معاينة لموقعنا الجديد. لقد بدأنا للتو. يسعدنا تلقي تعليقاتك. ", + "nav.listings": "القوائم", + "nav.properties": "ملكيات", + "nav.applications": "التطبيقات", + "nav.myAccount": "حسابي", + "nav.myApplications": "تطبيقاتي", + "nav.myDashboard": "لوحة القيادة الخاصة بي", + "nav.mySettings": "اعداداتي", + "nav.rentals": "الإيجارات", + "nav.signIn": "تسجيل الدخول", + "nav.signOut": "خروج", + "nav.signUp": "اشتراك", + "nav.siteTitle": "بوابة الإسكان", + "nav.siteTitlePartners": "بوابة الشركاء", + "nav.skip": "تخطي إلى المحتوى الرئيسي", + "nav.flags": "أعلام", + "nav.users": "المستخدمون", + "pageTitle.additionalResources": "المزيد من فرص السكن", + "pageTitle.accessibilityStatement": "بيان إمكانية الوصول", + "pageTitle.terms": "إخلاء المسؤولية عن المصادقة", + "pageTitle.housingCounselors": "مستشاري الإسكان", + "pageTitle.getAssistance": "الحصول على المساعدة", + "pageTitle.rentalListings": "انظر الإيجارات", + "pageTitle.rent": "إيجار مساكن ميسورة التكلفة", + "pageTitle.privacy": "سياسة الخصوصية", + "pageTitle.welcomeEnglish": "أهلا بك", + "pageTitle.welcomeSpanish": "اهلا", + "pageTitle.welcomeVietnamese": "فيتنامي", + "pageTitle.about": "عن", + "pageTitle.feedback": "مشاركة ملاحظات الموقع", + "pageTitle.resources": "موارد", + "pageTitle.housingBasics": "أساسيات السكن بأسعار معقولة", + "pageDescription.welcome": "ابحث وتقدم للحصول على سكن ميسور التكلفة على بوابة إسكان٪ {regionName}", + "pageDescription.listing": "تقدم بطلب للحصول على سكن ميسور التكلفة في٪ {listName} في٪ {regionName} ، تم بناؤه بالشراكة مع Exygy.", + "pageDescription.getAssistance": "لمساعدتك في رحلتك للعثور على سكن مستقر، الرجاء تصفّح الموارد والخدمات أدناه، أو التعرف على المزيد بشأن السكن بأسعار معقولة.", + "pageDescription.housingBasics": "نعلم أن العثور على سكن قد يمثل عملية صعبة ومخيفة. يمكنك العثور على عدة موارد في هذه الصفحة لمساعدتك على تحديد السكن بأسعار معقولة وعملية تقديم الطلبات للحصول على هذه العقارات.", + "region.name": "المنطقة المحلية", + "progressNav.srHeading": "تقدم", + "progressNav.current": "الخطوة الحالية: ", + "publicFilter.confirmedListings": "القوائم المؤكدة", + "publicFilter.confirmedListingsFieldLabel": "أظهر فقط القوائم المؤكدة حسب العقار", + "publicFilter.bedRoomSize": "حجم غرفة النوم", + "publicFilter.rentRange": "نطاق الإيجار الشهري", + "publicFilter.rentRangeMin": "بدون حد أدنى للإيجار", + "publicFilter.rentRangeMax": "بدون حد أقصى للإيجار", + "publicFilter.communityTypes": "أنواع المجتمع", + "publicFilter.waitlist.open": "قائمة الانتظار المفتوحة", + "publicFilter.waitlist.closed": "قائمة الانتظار المكتملة", + "resources.affordableHousingSubtitle": "معرفة المزيد عن كيفية التأهل وتقديم طلب للحصول على سكن بأسعار معقولة", + "seasons.fall": "الخريف", + "seasons.spring": "الربيع", + "seasons.summer": "الصيف", + "seasons.winter": "الشتاء", + "states.AL": "ألاباما", + "states.AK": "ألاسكا", + "states.AZ": "أريزونا", + "states.AR": "أركنساس", + "states.CA": "كاليفورنيا", + "states.CO": "كولورادو", + "states.CT": "كونيتيكت", + "states.DE": "ديلاوير", + "states.DC": "مقاطعة كولومبيا", + "states.FL": "فلوريدا", + "states.GA": "جورجيا", + "states.HI": "هاواي", + "states.ID": "ايداهو", + "states.IL": "إلينوي", + "states.IN": "إنديانا", + "states.IA": "ايوا", + "states.KS": "كانساس", + "states.KY": "كنتاكي", + "states.LA": "لويزيانا", + "states.ME": "مين", + "states.MD": "ماريلاند", + "states.MA": "ماساتشوستس", + "states.MI": "ميشيغان", + "states.MN": "مينيسوتا", + "states.MS": "ميسيسيبي", + "states.MO": "ميسوري", + "states.MT": "مونتانا", + "states.NE": "نبراسكا", + "states.NV": "نيفادا", + "states.NH": "نيو هامبشاير", + "states.NJ": "نيو جيرسي", + "states.NM": "المكسيك جديدة", + "states.NY": "نيويورك", + "states.NC": "شمال كارولينا", + "states.ND": "شمال داكوتا", + "states.OH": "أوهايو", + "states.OK": "أوكلاهوما", + "states.OR": "أوريغون", + "states.PA": "بنسلفانيا", + "states.RI": "جزيرة رود", + "states.SC": "كارولينا الجنوبية", + "states.SD": "جنوب داكوتا", + "states.TN": "تينيسي", + "states.TX": "تكساس", + "states.UT": "يوتا", + "states.VT": "فيرمونت", + "states.VA": "فرجينيا", + "states.WA": "واشنطن", + "states.WV": "فرجينيا الغربية", + "states.WI": "ويسكونسن", + "states.WY": "وايومنغ", + "t.areYouSure": "هل أنت متأكد؟", + "t.addNotes": "أضف ملاحظات", + "t.at": "في", + "t.additionalPhone": "هاتف إضافي", + "t.area": "منطقة", + "t.areYouStillWorking": "هل ما زلت تعمل؟", + "t.accessibility": "سهولة الوصول", + "t.additionalAccessibility": "تفاصيل الوصول الإضافية", + "t.am": "صباحا", + "t.availability": "التوافر", + "t.automatic": "تلقائي", + "t.back": "عودة", + "t.built": "مبني", + "t.call": "مكالمة", + "t.cancel": "يلغي", + "t.confirm": "يتأكد", + "t.contactPropertyManagement": "اتصل بإدارة الممتلكات", + "t.chooseFromFolder": "اختر من المجلد", + "t.custom": "مخصص", + "t.day": "يوم", + "t.date": "تاريخ", + "t.delete": "حذف", + "t.deposit": "إيداع", + "t.done": "تم", + "t.descriptionTitle": "وصف", + "t.description": "أدخل الوصف", + "t.emailAddressPlaceholder": "يو@ميعمل.كوم", + "t.end": "نهاية", + "t.dragFilesHere": "اسحب الملفات هنا", + "t.dropFilesHere": "قم بوضع الملفات هنا…", + "t.export": "يصدر", + "t.enterAmount": "أدخل المبلغ", + "t.fileName": "اسم الملف", + "t.filter": "منقي", + "t.edit": "يحرر", + "t.email": "بريد إلكتروني", + "t.finish": "ينهي", + "t.floor": "الأرض", + "t.floors": "طوابق", + "t.getDirections": "احصل على الاتجاهات", + "t.hour": "ساعة", + "t.household": "أسرة", + "t.income": "دخل", + "t.jumpTo": "اقفز إلى", + "t.jurisdiction": "الاختصاص القضائي", + "t.label": "ملصق", + "t.lastUpdated": "آخر تحديث", + "t.letter": "رسالة", + "t.less": "أقل", + "t.link": "وصلة", + "t.listing": "القوائم", + "t.loginIsRequired": "مطلوب تسجيل الدخول لمشاهدة هذه الصفحة.", + "t.max": "أقصى", + "t.menu": "قائمة الطعام", + "t.min": "الحد الأدنى", + "t.minimumIncome": "الحد الأدنى للدخل", + "t.minutes": "دقائق", + "t.month": "شهر", + "t.more": "أكثر", + "t.n/a": "غير متوفر", + "t.name": "اسم", + "t.neighborhood": "حي", + "t.next": "التالي", + "t.no": "رقم", + "t.none": "لا أحد", + "t.noneFound": "لا شيء وجد.", + "t.notes": "ملحوظات", + "t.occupancy": "شغل", + "t.ok": "حسنا", + "t.optional": "اختياري", + "t.or": "أو", + "t.order": "ترتيب", + "t.pageXofY": "الصفحة٪ {number} من٪ {total}", + "t.people": "اشخاص", + "t.person": "شخص", + "t.perMonth": "كل شهر", + "t.perYear": "كل سنة", + "t.petsPolicy": "سياسة الحيوانات الأليفة", + "t.phone": "هاتف", + "t.phoneNumberPlaceholder": "(خخخ) خخخخ", + "t.pleaseSelectOne": "رجاءا اختر واحدة.", + "t.pm": "مساء", + "t.post": "بريد", + "t.preferences": "التفضيلات", + "t.preview": "معاينة", + "t.previous": "السابق", + "t.propertyAmenities": "مزايا الملكية", + "t.range": "٪ {from} إلى٪ {to}", + "t.readLess": "أقرأ أقل", + "t.readMore": "قراءة المزيد", + "t.region": "منطقة", + "t.relationship": "صلة", + "t.otherRelationShip": "علاقة أخرى", + "t.rent": "إيجار", + "t.review": "إعادة النظر", + "t.role": "دور", + "t.secondPhone": "الهاتف الثاني", + "t.seconds": "ثواني", + "t.seeDetails": "انظر التفاصيل", + "t.seeListing": "انظر القائمة", + "t.selectOne": "حدد واحد", + "t.servicesOffered": "الخدمات المقدمة", + "t.show": "تبين", + "t.showLess": "تظهر أقل", + "t.showMore": "أظهر المزيد", + "t.skipToMainContent": "تخطي إلى المحتوى الرئيسي", + "t.smokingPolicy": "سياسة التدخين", + "t.sort": "نوع", + "t.sqFeet": "قدم مربع", + "t.squareFeet": "قدم مربع", + "t.statusHistory": "محفوظات الحالة", + "t.startTime": "وقت البدء", + "t.endTime": "وقت النهاية", + "t.street": "شارع", + "t.submit": "يقدم", + "t.submitNew": "إرسال & جديد", + "t.copyNew": "نسخ & جديد", + "t.save": "يحفظ", + "t.saveNew": "حفظ & جديد", + "t.saveExit": "احفظ المخرج", + "t.time": "الوقت", + "t.text": "نص", + "t.to": "ل", + "t.totalCount": "العدد الإجمالي", + "t.unit": "وحدة", + "t.units": "الوحدات", + "t.unitAmenities": "مرافق الوحدة", + "t.unitFeatures": "ميزات الوحدة", + "t.unitType": "نوع الوحدة", + "t.url": "URL", + "t.view": "رأي", + "t.viewListings": "عرض القوائم", + "t.viewMap": "إعرض الخريطة", + "t.viewOnMap": "عرض على الخريطة", + "t.website": "موقع الكتروني", + "t.year": "عام", + "t.yes": "نعم", + "t.you": "أنت", + "welcome.allApplicationClosed": "جميع التطبيقات مغلقة حاليًا ، ولكن يمكنك عرض القوائم المغلقة.", + "welcome.seeRentalListings": "انظر جميع الايجارات", + "welcome.title": "تقدم بطلب للحصول على سكن ميسور التكلفة في", + "welcome.seeMoreOpportunities": "شاهد المزيد من فرص الإسكان الإيجاري والملكية", + "welcome.viewAdditionalHousing": "عرض فرص وموارد الإسكان الإضافية", + "welcome.bedrooms.studios": "استوديو متاح(%{smart_count}) |||| استوديو متاح(%{smart_count})", + "welcome.bedrooms.numBed": "(%{smart_count}) %{num_bed} غرفة نوم المتاحة |||| (%{smart_count}) %{num_bed} غرف نوم المتاحة", + "welcome.bedrooms.fourPlusBed": "(%{smart_count}) 4+ غرفة نوم المتاحة |||| (%{smart_count}) 4+ غرف نوم المتاحة", + "welcome.checkEligibility": "هل أنا مؤهل؟", + "welcome.checkEligibilityDescription": "تحقق في دقائق إذا كنت مؤهلاً!", + "welcome.findRentalsForMe": "البحث عن الايجارات بالنسبة لي", + "welcome.latestListings": "أحدث القوائم", + "welcome.lastUpdated": "آخر تحديث %{date}", + "welcome.cityRegions": "مناطق المدينة", + "welcome.signUp": "الحصول على تنبيهات كلما تم نشر قائمة جديدة", + "welcome.signUpToday": "سجّل اليوم", + "welcome.subTitle": "انقر فوق الزر أدناه للعثور على سكن مستأجر بناءً على دخلك واحتياجات أسرتك", + "welcome.underConstructionButton": "رؤية كل ما تحت الإنشاء", + "welcome.learnMore": "معرفة المزيد", + "welcome.learnHousingBasics": "معرفة كيف يمكنك التأهل وتقديم طلب للحصول على سكن بأسعار معقولة", + "whatToExpect.label": "ماذا تتوقع", + "whatToExpect.default": "سيتم الاتصال بالمتقدمين من قبل وكيل الملكية بترتيب الترتيب حتى يتم ملء الشواغر. سيتم التحقق من جميع المعلومات التي قدمتها وتأكيد أهليتك. ستتم إزالة طلبك من قائمة الانتظار إذا قدمت أي بيانات احتيالية. إذا لم نتمكن من التحقق من تفضيل السكن الذي طالبت به ، فلن تحصل على الأفضلية ولكن لن يتم معاقبتك بطريقة أخرى. إذا تم اختيار طلبك ، فكن مستعدًا لملء طلب أكثر تفصيلاً وتقديم المستندات الداعمة المطلوبة.", + "listingFilters.allRentals": "جميع الإيجارات", + "listingFilters.buttonTitle": "منقي", + "listingFilters.buttonTitleExtended": "البحث عن أماكن للإيجار لك", + "listingFilters.loading": "جار التحميل...", + "listingFilters.rentalsFound": "تم العثور على %{smart_count} إيجار |||| تم العثور على %{smart_count} إيجارات", + "listingFilters.resetButton": "إعادة ضبط", + "listingFilters.modalTitle": "تصفية النتائج", + "listingFilters.modalHeader": "استخدم هذه الخيارات لتحسين قائمة الخصائص الخاصة بك.", + "listingFilters.neighborhood": "حي", + "listingFilters.bedrooms": "غرف نوم", + "listingFilters.bedroomsOptions.studioPlus": "ستوديو", + "listingFilters.bedroomsOptions.onePlus": "غرفة نوم واحدة", + "listingFilters.bedroomsOptions.twoPlus": "غرفتا نوم", + "listingFilters.bedroomsOptions.threePlus": "3 غرف نوم", + "listingFilters.bedroomsOptions.fourPlus": "4 غرف نوم أو أكثر", + "listingFilters.zipCode": "الرمز البريدي", + "listingFilters.zipCodeDescription": "أدخل الرمز البريدي", + "listingFilters.region": "منطقة", + "listingFilters.region.GreaterDowntown": "وسط المدينة الكبرى", + "listingFilters.region.Eastside": "الجانب الشرقي", + "listingFilters.region.Westside": "الجانب الغربي", + "listingFilters.region.Southwest": "الجانب الجنوبي الغربي", + "listingFilters.adaCompliant": "الميزات التي يمكن الوصول إليها للمقيمين ذوي الإعاقة؟", + "listingFilters.rentRange": "نطاق الإيجار", + "listingFilters.availability": "توفر الوحدة", + "listingFilters.hasAvailability": "لديها توافر", + "listingFilters.noAvailability": "لا يوجد توافر", + "listingFilters.waitlist": "قائمة الانتظار", + "listingFilters.applyFilter": "تطبيق مرشح", + "listingFilters.noResults": "لا نتائج", + "listingFilters.includeUnknowns": "عرض المنازل مع المعلومات المفقودة", + "listingFilters.senior": "مساكن كبار السن (62+)", + "listingFilters.independentLivingHousing": "স্বাধীন জীবিত সম্প্রদায়", + "listingFilters.minAmiPercentageLabel": "الوحدات المخصصة لذوي الدخل", + "listingFilters.minAmiPercentageOptions.amiOption20": "20٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption25": "25٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption30": "30٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption35": "35٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption40": "40٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption45": "45٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption50": "50٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption55": "55٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption60": "60٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption70": "70٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption80": "80٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption100": "100٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption120": "120٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption125": "125٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption140": "140٪ من AMI أو أكثر", + "listingFilters.minAmiPercentageOptions.amiOption150": "150٪ من AMI أو أكثر", + "eligibility.progress.header": "البحث عن الايجارات بالنسبة لي", + "eligibility.progress.sections.welcome": "أهلا بك", + "eligibility.progress.sections.household": "أسرة", + "eligibility.progress.sections.age": "عمر", + "eligibility.progress.sections.disability": "عجز", + "eligibility.progress.sections.accessibility": "إمكانية الوصول", + "eligibility.progress.sections.income": "دخل", + "eligibility.progress.sections.terms": "تنصل", + "eligibility.welcome.header": "أهلا بك", + "eligibility.welcome.description": "مرحبًا بك في Detroit Home Connect! لمعرفة الإيجارات التي قد تكون مؤهلاً لها ، ما عليك سوى الإجابة على أربعة أسئلة.", + "eligibility.household.prompt": "كم عدد الأشخاص الذين سيعيشون في إيجارك القادم ، بما في ذلك أنت؟", + "eligibility.household.srCountLabel": "حجم الأسرة", + "eligibility.household.ranges.one": "1 عضو", + "eligibility.household.ranges.two": "2 أعضاء", + "eligibility.household.ranges.three": "3 أعضاء", + "eligibility.household.ranges.four": "4 أعضاء", + "eligibility.household.ranges.five": "5 أعضاء", + "eligibility.household.ranges.six": "6 أعضاء", + "eligibility.household.ranges.seven": "7 أعضاء", + "eligibility.household.ranges.eight": "8+ أعضاء", + "eligibility.age.prompt": "كم عمرك؟", + "eligibility.age.description": "يمكنك اختيار فئات عمرية متعددة. بعض الإيجارات لها متطلبات حد أدنى للسن.", + "eligibility.age.lessThan55": "55 >", + "eligibility.age.55to61": "55 - 61", + "eligibility.age.62plus": "+62", + "eligibility.disability.prompt": "هل يوجد أي شخص في أسرتك لديه إعاقة؟", + "eligibility.disability.description": "قد تحتاج إلى تقديم دليل على الإعاقة عند التقدم بطلب للحصول على بعض المنازل.", + "eligibility.income.prompt": "ما هو إجمالي الدخل السنوي التقديري لكل من سيعيش معك بما فيهم أنت؟", + "eligibility.income.description": "قم بتضمين الدخل من آخر 12 شهرًا لنفسك ولكل شخص سيعيش معك. عند التقدم بطلب للحصول على إيجار ، ستأخذ العقارات أيضًا في الاعتبار عوامل أخرى لتحديد ما إذا كان دخلك السنوي المقدر يفي بمتطلبات الأهلية. أمثلة على الدخل:", + "eligibility.income.examples.wages": "الأجور والإكراميات", + "eligibility.income.examples.socialSecurity": "الضمان الاجتماعي", + "eligibility.income.examples.retirement": "دخل التقاعد", + "eligibility.income.examples.unemployment": "تعويض البطالة", + "eligibility.income.label": "مجموعة الدخل", + "eligibility.income.ranges.below10k": "0 دولار - 9999 دولار", + "eligibility.income.ranges.10kTo20k": "10000 دولار - 19999 دولار", + "eligibility.income.ranges.20kTo30k": "20000 دولار - 29999 دولار", + "eligibility.income.ranges.30kTo40k": "30000 دولار - 39999 دولار", + "eligibility.income.ranges.40kTo50k": "40000 دولار - 49999 دولار", + "eligibility.income.ranges.over50k": "50000 دولار أو أكثر", + "eligibility.accessibility.accessibleParking": "أماكن وقوف السيارات التي يمكن الوصول إليها", + "eligibility.accessibility.acInUnit": "مكيف في الوحدة", + "eligibility.accessibility.barrierFreeBathroom": "حمامات خالية من العوائق", + "eligibility.accessibility.barrierFreeEntrance": "مدخل ملكية خالي من العوائق (بدون خطوات)", + "eligibility.accessibility.barrierFreeUnitEntrance": "مداخل وحدة خالية من العوائق (بدون خطوات)", + "eligibility.accessibility.description": "تحتوي بعض الخصائص على ميزات إمكانية الوصول قد لا يمتلكها الآخرون.", + "eligibility.accessibility.elevator": "مصعد", + "eligibility.accessibility.grabBars": "انتزاع القضبان في الحمامات", + "eligibility.accessibility.hearing": "وحدات لذوي الإعاقة السمعية", + "eligibility.accessibility.heatingInUnit": "تدفئة في الوحدة", + "eligibility.accessibility.inUnitWasherDryer": "غسالة / مجفف في الوحدة", + "eligibility.accessibility.laundryInBuilding": "مغسلة في المبنى", + "eligibility.accessibility.loweredCabinets": "خزائن منخفضة وأسطح العمل", + "eligibility.accessibility.loweredLightSwitch": "مفاتيح الإضاءة المنخفضة", + "eligibility.accessibility.mobility": "وحدات لذوي الإعاقة الحركية", + "eligibility.accessibility.parkingOnSite": "وقوف السيارات في الموقع", + "eligibility.accessibility.prompt": "هل تحتاج إلى ميزات وصول إضافية؟", + "eligibility.accessibility.rollInShower": "دوشات دوارة", + "eligibility.accessibility.serviceAnimalsAllowed": "مسموح بحيوانات الخدمة", + "eligibility.accessibility.title": "ميزات إمكانية الوصول", + "eligibility.accessibility.visual": "وحدات لذوي الإعاقة البصرية", + "eligibility.accessibility.wheelchairRamp": "منحدر للكراسي المتحركة", + "eligibility.accessibility.wideDoorways": "مداخل وحدة واسعة للكراسي المتحركة", + "eligibility.disclaimer.description": "شكرا لك على الإجابة على هذه الأسئلة. عندما تنقر أو تضغط على 'عرض النتائج الآن' ، سترى إيجارات قد تناسب احتياجاتك بناءً على إجاباتك. يمكن أن تؤثر عوامل متعددة على الأهلية لتأجيرات معينة ، لذلك إذا رأيت إيجارًا تهتم به ، فاتصل بوكيل العقارات في القائمة. يمكنهم مساعدتك في تحديد ما إذا كنت مؤهلاً لهذا الإيجار.", + "eligibility.preferNotToSay": "افضل عدم القول", + "resources.body1": "قد تحتاج إلى مساعدة إضافية في بحثك عن سكن. قامت إدارة الموارد البشرية بتجميع قائمة من الموارد لمساعدتك في العثور على سكنك والحفاظ عليه.", + "resources.evictionAssistance": "مساعدة الإخلاء", + "resources.detroitHousingNetwork": "شبكة الإسكان في ديترويت", + "resources.detroitHousingNetworkBody": "شبكة الإسكان في ديترويت عبارة عن مجموعة من المنظمات المجتمعية في ديترويت التي تقدم مجموعة متنوعة من الخدمات لأصحاب المنازل والمستأجرين ، بما في ذلك المساعدة في المرافق ، وتقديم المشورة للمستأجرين والمالكين ، واستشارات الإخلاء ، وحلول ضريبة الممتلكات. للمزيد من المعلومات قم بزيارة:", + "resources.utilityAssistance": "مساعدة المرافق", + "resources.homelessnessServices": "خدمات التشرد", + "resources.detroitLandBankAuthority": "هيئة مصرف ديترويت العقاري", + "resources.homeRepairResources": "موارد إصلاح المنزل", + "resources.affordableHousingTitle": "أساسيات السكن", + "resources.affordableHousingLinkLabel": "قراءة المزيد لمعرفة كيف يسير الأمر", + "resources.housingResourcesTitle": "موارد سكن إضافية", + "resources.housingResourcesSubtitle": "تصفّح الموارد والخدمات المحلية في بحثك عن سكن", + "resources.housingResourcesLinkLabel": "عرض الموارد المجتمعية" +} diff --git a/shared-helpers/src/locales/bn.json b/shared-helpers/src/locales/bn.json new file mode 100644 index 0000000000..5ceab6443b --- /dev/null +++ b/shared-helpers/src/locales/bn.json @@ -0,0 +1,1285 @@ +{ + "config.routePrefix": "bn", + "about.body1": "আমরা জানি যে আপনার চাহিদা পূরণ করে এমন একটি বাড়ি খুঁজে পাওয়া কঠিন এবং হতাশাজনক হতে পারে। ডেট্রয়েট হোম কানেক্ট হল বাড়িতে কল করার জন্য একটি নতুন জায়গা খুঁজে পেতে আপনার সাহায্যকারী হাত।", + "about.body2": "ডেট্রয়েট হোম কানেক্ট হল একটি নতুন সিটি অফ ডেট্রয়েট পরিষেবা যা আপনাকে ডেট্রয়েটে আবাসন খোঁজার একটি কেন্দ্রীয় প্রথম ধাপ প্রদান করে যা আপনার সামর্থ্য এবং পরিবারের চাহিদা পূরণ করে। আপনি আপনার পরিবারের আকার, বয়স এবং আয়ের উপর ভিত্তি করে বিকল্পগুলি অন্বেষণ করে ভাড়া ইউনিটের জন্য আপনার যোগ্যতা বুঝতে পারেন। ডেট্রয়েট হোম কানেক্ট হল ডেট্রয়েট শহরের আবাসন ও পুনরুজ্জীবন বিভাগের একটি উদ্যোগ। ওয়েবসাইটের নকশা এবং বৈশিষ্ট্যগুলি এলাকার বাসিন্দা, সম্প্রদায়-ভিত্তিক সংস্থা, সম্পত্তি পরিচালক এবং সম্পত্তির মালিকদের প্রতিক্রিয়া এবং অন্তর্দৃষ্টির উপর ভিত্তি করে।", + "about.moreInfoContact": "আরও তথ্যের জন্য, দয়া করে detroithomeconnect@detroitmi.gov-এ সিটি কর্মীদের সাথে যোগাযোগ করুন।", + "about.thankYouPartners": "ডেট্রয়েট সিটির হাউজিং এবং রিভাইটালাইজেশন ডিপার্টমেন্ট নিম্নলিখিত অংশীদারসহ বিভিন্ন সংস্থাকে ডেট্রয়েট হোম কানেক্ট তৈরির সময় তাদের সহায়তা এবং অংশীদারিত্বের জন্য ধন্যবাদ জানাতে চায়:", + "account.accountSettings": "অ্যাকাউন্ট সেটিংস", + "account.errorFetchingApplications": "অ্যাপ্লিকেশন আনতে ত্রুটি", + "account.noApplications": "মনে হচ্ছে আপনি এখনও কোনো তালিকাতে আবেদন করেননি।", + "account.accountSettingsSubtitle": "অ্যাকাউন্ট সেটিংস, ইমেইল এবং পাসওয়ার্ড", + "account.createAccount": "হিসাব তৈরি কর", + "account.haveAnAccount": "ইতিমধ্যে একটি সদস্যপদ আছে?", + "account.myApplications": "আমার অ্যাপ্লিকেশন", + "account.myApplicationsSubtitle": "যেসব বৈশিষ্ট্যের জন্য আপনি আবেদন করেছেন তার জন্য লটারির তারিখ এবং তালিকা দেখুন", + "account.myFavorites": "আমার পছন্দ", + "account.myFavoritesSubtitle": "তালিকা সংরক্ষণ করুন এবং আপডেটের জন্য ফিরে আসুন", + "account.application.confirmation": "নিশ্চিতকরণ", + "account.application.error": "ত্রুটি", + "account.application.noAccessError": "সেই আইডি সহ কোন আবেদন নেই", + "account.application.noApplicationError": "সেই আইডি সহ কোন আবেদন নেই", + "account.application.return": "অ্যাপ্লিকেশনগুলিতে ফিরে আসুন", + "account.settings.passwordSuccess": "পাসওয়ার্ড সফলভাবে আপডেট হয়েছে", + "account.settings.update": "হালনাগাদ", + "account.settings.passwordRemember": "আপনার পাসওয়ার্ড পরিবর্তন করার সময় নিশ্চিত করুন যে আপনি এটি নোট করেছেন যাতে আপনি ভবিষ্যতে এটি মনে রাখবেন।", + "account.settings.currentPassword": "বর্তমান পাসওয়ার্ড", + "account.settings.newPassword": "নতুন পাসওয়ার্ড", + "account.settings.confirmNewPassword": "নিশ্চিত কর নতুন গোপননম্বর", + "account.settings.alerts.genericError": "সেখানে একটা ভুল ছিল. অনুগ্রহ করে আবার চেষ্টা করুন, অথবা সাহায্যের জন্য সহায়তার সাথে যোগাযোগ করুন।", + "account.settings.alerts.nameSuccess": "নাম আপডেট সফল", + "account.settings.alerts.dobSuccess": "জন্ম তারিখ আপডেট সফল", + "account.settings.alerts.emailSuccess": "ইমেল আপডেট সফল", + "account.settings.alerts.phoneNumberSuccess": "ফোন নম্বর আপডেট সফল হয়েছে৷", + "account.settings.alerts.currentPassword": "অবৈধ বর্তমান পাসওয়ার্ড। অনুগ্রহপূর্বক আবার চেষ্টা করুন.", + "account.settings.alerts.passwordSuccess": "পাসওয়ার্ড আপডেট সফল", + "account.settings.alerts.passwordMatch": "নতুন পাসওয়ার্ড ক্ষেত্র মিলছে না", + "account.settings.alerts.passwordEmpty": "পাসওয়ার্ড ক্ষেত্রগুলি খালি নাও থাকতে পারে", + "account.settings.placeholders.month": "এমএম", + "account.settings.placeholders.day": "ডিডি", + "account.settings.placeholders.year": "হ্যাঁ", + "alert.unavailable": "ডেট্রয়েট হোম কানেক্ট ২রা সেপ্টেম্বর, ২০২৫ তারিখের সপ্তাহে একটি নির্ধারিত আপগ্রেডের জন্য অস্থায়ীভাবে অনুপলব্ধ হতে পারে। আমরা আশা করছি এই সময়ের মধ্যে সাইটটি ৪৮ ঘন্টা পর্যন্ত বন্ধ থাকবে। কোন প্রশ্ন বা উদ্বেগ থাকলে, অনুগ্রহ করে আমাদের সাথে যোগাযোগ করুন: detroithomeconnect@detroitmi.gov", + "applications.begin.en": "শুরু", + "applications.begin.es": "শুরু করুন", + "applications.begin.zh": "শুরু করুন", + "applications.begin.vi": "শুরু করুন", + "applications.totalApplications": "মোট অ্যাপ্লিকেশন", + "applications.totalSets": "মোট সেট", + "applications.addApplication": "আবেদন যোগ করুন", + "applications.newApplication": "নূতন আবেদনপত্র", + "applications.editApplication": "আবেদন সম্পাদনা করুন", + "applications.applicationsReceived": "আবেদনপত্র গৃহীত হয়েছে", + "applications.table.applicationSubmissionDate": "আবেদন জমা দেওয়ার তারিখ", + "applications.table.declaredAnnualIncome": "ঘোষিত বার্ষিক আয়", + "applications.table.declaredMonthlyIncome": "ঘোষিত মাসিক আয়", + "applications.table.subsidyOrVoucher": "ভর্তুকি বা ভাউচার", + "applications.table.requestAda": "ADA অনুরোধ করুন", + "applications.table.preferenceClaimed": "পছন্দ দাবি করা হয়েছে", + "applications.table.primaryDob": "প্রাথমিক ডিওবি", + "applications.table.phoneType": "ফোনের ধরন", + "applications.table.additionalPhoneType": "অতিরিক্ত ফোনের ধরণ", + "applications.table.residenceStreet": "বাসস্থান রাস্তার ঠিকানা", + "applications.table.residenceCity": "রেসিডেন্স সিটি", + "applications.table.residenceState": "আবাসস্থল", + "applications.table.residenceZip": "আবাসস্থল জিপ", + "applications.table.mailingStreet": "মেইলিং রাস্তার ঠিকানা", + "applications.table.mailingCity": "মেইলিং সিটি", + "applications.table.mailingState": "মেইলিং স্টেট", + "applications.table.mailingZip": "মেলিং জিপ", + "applications.table.workStreet": "কাজের রাস্তার ঠিকানা", + "applications.table.workCity": "কাজের শহর", + "applications.table.workState": "কাজের রাজ্য", + "applications.table.workZip": "কাজের জিপ", + "applications.table.altContactFirstName": "Alt যোগাযোগের প্রথম নাম", + "applications.table.altContactLastName": "Alt যোগাযোগের শেষ নাম", + "applications.table.altContactRelationship": "Alt যোগাযোগ সম্পর্ক", + "applications.table.altContactAgency": "Alt যোগাযোগ এজেন্সি", + "applications.table.altContactEmail": "Alt যোগাযোগ ইমেইল", + "applications.table.altContactPhone": "Alt যোগাযোগ ফোন", + "applications.table.altContactStreetAddress": "Alt যোগাযোগ রাস্তার ঠিকানা", + "applications.table.altContactCity": "Alt যোগাযোগ শহর", + "applications.table.altContactState": "Alt যোগাযোগ রাজ্য", + "applications.table.altContactZip": "Alt যোগাযোগ জিপ", + "applications.table.householdFirstName": "পরিবারের প্রথম নাম", + "applications.table.householdLastName": "পরিবারের শেষ নাম", + "applications.table.householdRelationship": "গৃহস্থালীর সম্পর্ক", + "applications.table.householdDob": "পারিবারিক ডিওবি", + "applications.table.householdStreetAddress": "বাড়ির রাস্তার ঠিকানা", + "applications.table.householdCity": "পারিবারিক শহর", + "applications.table.householdState": "গৃহস্থালী রাষ্ট্র", + "applications.table.householdZip": "গৃহস্থালী জিপ", + "applications.table.applicationType": "দরখাস্তের প্রকার", + "application.add.applicationAddError": "এগিয়ে যাওয়ার আগে আপনাকে কোন ত্রুটি সমাধান করতে হবে।", + "application.add.workInRegion": "অঞ্চলে কাজ?", + "application.add.mobility": "গতিশীলতা দুর্বলতা", + "application.add.vision": "দৃষ্টি প্রতিবন্ধকতা", + "application.add.hearing": "শ্রবণ প্রতিবন্ধকতা", + "application.add.preferences.liveIn": "বাস করা", + "application.add.preferences.workIn": "মধ্যে কাজ", + "application.add.preferences.optedOut": "পছন্দ ছাড়াই বেছে নেওয়া হয়েছে", + "application.add.incomePeriod": "আয়ের সময়কাল", + "application.add.demographicsInformation": "জনসংখ্যাতাত্ত্বিক তথ্য", + "application.add.ethnicity": "জাতিসত্তা", + "application.add.race": "রেস", + "application.add.gender": "লিঙ্গ", + "application.add.howDidYouHearAboutUs": "তুমি কিভাবে আমাদের সম্বন্ধে শুনেছো?", + "application.add.sexualOrientation": "যৌন অভিমুখ", + "application.add.addHouseholdMember": "পরিবারের সদস্য যোগ করুন", + "application.add.sameAddressAsPrimary": "প্রাথমিক হিসাবে একই ঠিকানা", + "application.add.sameResidence": "একই আবাস", + "application.add.languageSubmittedIn": "ভাষা জমা দেওয়া হয়েছে", + "application.add.timeSubmitted": "জমা দেওয়া সময়", + "application.add.dateSubmitted": "জন্ম জমা", + "application.add.applicationSubmitted": "আবেদন জমা দেওয়া হয়েছে", + "application.add.applicationUpdated": "আবেদন আপডেট করা হয়েছে", + "application.add.saveAndExit": "সংরক্ষণ করুন এবং প্রস্থান করুন", + "application.add.claimant": "দাবিদার", + "application.add.displacedAddress": "স্থানচ্যুত ঠিকানা", + "application.referralApplication.instructions": "স্থায়ী সহায়ক আবাসন ইউনিটগুলিকে সরাসরি কোঅর্ডিনেটেড এন্ট্রি সিস্টেমের মাধ্যমে উল্লেখ করা হয়। গৃহহীনতার সম্মুখীন গৃহস্থরা এ কল করতে পারেন যাতে সমন্বিত এন্ট্রি সিস্টেম এবং আবাসন-সংক্রান্ত সম্পদ এবং তথ্য অ্যাক্সেস করতে অ্যাক্সেস পয়েন্টের সাথে সংযুক্ত হতে পারে।", + "application.referralApplication.furtherInformation": "আরো তথ্যের জন্য", + "application.referralApplication.phoneNumber": "২গ", + "application.details.applicationData": "আবেদনের উপাত্ত", + "application.details.number": "আবেদন সংখ্যা", + "application.details.type": "আবেদন জমা দেওয়ার ধরন", + "application.details.submittedDate": "আবেদন জমা দেওয়ার তারিখ", + "application.details.timeDate": "আবেদন জমা দেওয়ার সময়", + "application.details.language": "আবেদনের ভাষা", + "application.details.householdSize": "পরিবারের আকার", + "application.details.totalSize": "মোট পরিবারের আকার", + "application.details.submittedBy": "জমাদানকারী", + "application.details.agency": "প্রযোজ্য হলে এজেন্সি", + "application.details.adaPriorities": "ADA অগ্রাধিকার নির্বাচিত", + "application.details.preferences": "আবেদন পছন্দ", + "application.details.liveOrWorkIn": "বাস বা কাজ", + "application.details.householdIncome": "ঘোষিত পারিবারিক আয়", + "application.details.annualIncome": "বার্ষিক আয়", + "application.details.monthlyIncome": "মাসিক আয়", + "application.details.vouchers": "হাউজিং ভাউচার বা ভর্তুকি", + "application.details.preferredContact": "পছন্দের যোগাযোগ", + "application.details.residenceAddress": "বাসার ঠিকানা", + "application.details.workInRegion": "অঞ্চলে কাজ", + "application.details.signatureOnTerms": "চুক্তির শর্তাবলীতে স্বাক্ষর", + "application.details.submissionType.electronical": "বৈদ্যুতিক", + "application.details.submissionType.paper": "কাগজ", + "application.details.applicationStatus.draft": "খসড়া", + "application.details.applicationStatus.submitted": "জমা দেওয়া হয়েছে", + "application.details.applicationStatus.removed": "সরানো হয়েছে", + "application.details.preferredUnitSizes": "পছন্দের ইউনিট সাইজ", + "application.details.householdMemberDetails": "পরিবারের সদস্যদের বিবরণ", + "application.form.general.saveAndReturn": "সংরক্ষণ করুন এবং পর্যালোচনায় ফিরে আসুন", + "application.form.general.saveAndFinishLater": "সংরক্ষণ করুন এবং পরে শেষ করুন", + "application.form.options.relationship.spouse": "পত্নী", + "application.form.options.relationship.registeredDomesticPartner": "নিবন্ধিত গার্হস্থ্য অংশীদার", + "application.form.options.relationship.parent": "পিতামাতা", + "application.form.options.relationship.child": "শিশু", + "application.form.options.relationship.sibling": "ভাইবোন", + "application.form.options.relationship.cousin": "কাজিন", + "application.form.options.relationship.aunt": "খালা", + "application.form.options.relationship.uncle": "চাচা", + "application.form.options.relationship.nephew": "ভাতিজা", + "application.form.options.relationship.niece": "ভাতিজি", + "application.form.options.relationship.grandparent": "দাদা -দাদি", + "application.form.options.relationship.greatGrandparent": "মহান দাদা", + "application.form.options.relationship.inLaw": "আইন", + "application.form.options.relationship.friend": "বন্ধু", + "application.form.options.relationship.other": "অন্যান্য", + "application.chooseLanguage.letsGetStarted": "আপনার আবেদন শুরু করা যাক", + "application.chooseLanguage.chooseYourLanguage": "আপনার ভাষা নির্বাচন করুন", + "application.chooseLanguage.signInSaveTime": "সাইন ইন করা আপনার শেষ আবেদনের বিবরণ দিয়ে শুরু করে আপনার সময় বাঁচাতে পারে এবং যে কোন সময় আপনাকে এই অ্যাপ্লিকেশনের অবস্থা পরীক্ষা করতে দেয়।", + "application.autofill.saveTime": "আপনার শেষ আবেদন থেকে বিস্তারিত ব্যবহার করে সময় বাঁচান", + "application.autofill.prefillYourApplication": "আমরা কেবল নিম্নলিখিত বিবরণ দিয়ে আপনার আবেদনটি পূর্বে পূরণ করব এবং আপনি যেতে যেতে আপডেট করতে পারবেন।", + "application.autofill.start": "এই বিবরণ দিয়ে শুরু করুন", + "application.autofill.reset": "পুনরায় সেট করুন এবং নতুন করে শুরু করুন", + "application.name.title": "তোমার নাম কি?", + "application.name.yourName": "তোমার নাম", + "application.name.firstName": "নামের প্রথম অংশ", + "application.name.middleNameOptional": "মধ্য নাম (ঐচ্ছিক)", + "application.name.middleName": "মধ্য নাম", + "application.name.lastName": "নামের শেষাংশ", + "application.name.yourDateOfBirth": "তোমার জন্ম তারিখ", + "application.name.yourEmailAddress": "আপনার ইমেইল ঠিকানা", + "application.name.emailPrivacy": "আমরা আপনার আবেদন সম্পর্কে আপনার সাথে যোগাযোগ করতে শুধুমাত্র আপনার ইমেইল ঠিকানা ব্যবহার করব।", + "application.name.noEmailAddress": "আমার কোন ইমেইল ঠিকানা নেই", + "application.contact.title": "ধন্যবাদ %{firstName}। এখন আমাদের জানতে হবে কিভাবে আপনার সাথে যোগাযোগ করতে হয়।", + "application.contact.yourPhoneNumber": "আপনার ফোন নম্বর", + "application.contact.phoneNumberTypes.prompt": "এটা কোন ধরনের নম্বর?", + "application.contact.phoneNumberTypes.work": "কাজ", + "application.contact.phoneNumberTypes.home": "বাড়ি", + "application.contact.phoneNumberTypes.cell": "সেল", + "application.contact.noPhoneNumber": "আমার টেলিফোন নম্বর নেই", + "application.contact.yourAdditionalPhoneNumber": "আপনার দ্বিতীয় ফোন নম্বর", + "application.contact.additionalPhoneNumber": "আমার একটি অতিরিক্ত ফোন নম্বর আছে", + "application.contact.address": "ঠিকানা", + "application.contact.addressWhereYouCurrentlyLive": "আপনি বর্তমানে যেখানে থাকেন সেই ঠিকানা আমাদের প্রয়োজন। আপনি যদি গৃহহীন হন, আশ্রয়ের ঠিকানা অথবা আপনি যেখানে থাকেন তার কাছাকাছি ঠিকানা লিখুন।", + "application.contact.streetAddress": "রাস্তার ঠিকানা", + "application.contact.apt": "Apt বা ইউনিট #", + "application.contact.city": "শহর", + "application.contact.cityName": "শহরের নাম", + "application.contact.contactPreference": "তোমার সাথে কিভাবে যোগাযোগ করা হলে পছন্দ করবে?", + "application.contact.preferredContactType": "পছন্দের যোগাযোগের ধরন", + "application.contact.state": "রাষ্ট্র", + "application.contact.zip": "জিপ", + "application.contact.zipCode": "জিপকোড", + "application.contact.sendMailToMailingAddress": "আমার মেইল ​​অন্য ঠিকানায় পাঠান", + "application.contact.mailingAddress": "চিঠি পাঠানোর ঠিকানা", + "application.contact.provideAMailingAddress": "একটি ঠিকানা দিন যেখানে আপনি আপনার আবেদন সম্পর্কে আপডেট এবং উপকরণ পেতে পারেন।", + "application.contact.doYouWorkIn": "আপনি কি %{county} কাউন্টিতে কাজ করেন?", + "application.contact.doYouWorkInDescription": "টিবিডি", + "application.contact.workAddress": "কাজের ঠিকানা", + "application.alternateContact.type.title": "আমরা যদি আপনার কাছে পৌঁছাতে না পারি তাহলে অন্য কেউ কি আপনি আমাদের সাথে যোগাযোগ করার অনুমতি দিতে চান?", + "application.alternateContact.type.description": "বিকল্প যোগাযোগ প্রদান করে, আপনি আমাদের তাদের সাথে আপনার আবেদনের তথ্য আলোচনা করার অনুমতি দিচ্ছেন।", + "application.alternateContact.type.label": "বিকল্প যোগাযোগ", + "application.alternateContact.type.options.familyMember": "পরিবারের সদস্য", + "application.alternateContact.type.options.friend": "বন্ধু", + "application.alternateContact.type.options.caseManager": "কেস ম্যানেজার বা হাউজিং কাউন্সেলর", + "application.alternateContact.type.options.other": "অন্যান্য", + "application.alternateContact.type.options.noContact": "আমার কোন বিকল্প যোগাযোগ নেই", + "application.alternateContact.type.otherTypeFormPlaceholder": "আপনার সম্পর্ক কি?", + "application.alternateContact.type.otherTypeValidationErrorMessage": "দয়া করে সম্পর্কের ধরন লিখুন", + "application.alternateContact.type.validationErrorMessage": "একটি বিকল্প পরিচিতি নির্বাচন করুন", + "application.alternateContact.name.title": "আপনার বিকল্প পরিচিতি কে?", + "application.alternateContact.name.alternateContactFormLabel": "বিকল্প যোগাযোগের নাম", + "application.alternateContact.name.caseManagerAgencyFormLabel": "আপনার কেস ম্যানেজার বা হাউজিং কাউন্সেলর কোথায় কাজ করেন?", + "application.alternateContact.name.caseManagerAgencyFormPlaceHolder": "সংস্থা", + "application.alternateContact.name.caseManagerAgencyValidationErrorMessage": "অনুগ্রহ করে একটি এজেন্সি লিখুন", + "application.alternateContact.contact.title": "আপনার বিকল্প যোগাযোগে কিভাবে পৌঁছাবেন তা আমাদের জানান", + "application.alternateContact.contact.description": "আমরা আপনার আবেদন সম্পর্কে তাদের সাথে যোগাযোগ করার জন্য এই তথ্য ব্যবহার করব।", + "application.alternateContact.contact.phoneNumberFormLabel": "যোগাযোগের ফোন নম্বর", + "application.alternateContact.contact.emailAddressFormLabel": "যোগাযোগের ইমেল ঠিকানা", + "application.alternateContact.contact.contactMailingAddressLabel": "যোগাযোগের ঠিকানা", + "application.alternateContact.contact.contactMailingAddressHelperText": "একটি ঠিকানা বেছে নিন যেখানে তারা আপনার আবেদন সম্পর্কে আপডেট এবং উপকরণ পেতে পারে", + "application.household.assistanceUrl": "হটপস://এক্সিজি.কম/", + "application.household.dontQualifyHeader": "দুর্ভাগ্যক্রমে মনে হচ্ছে আপনি এই তালিকার জন্য যোগ্য নন।", + "application.household.dontQualifyInfo": "দয়া করে পরিবর্তন করুন যদি আপনি বিশ্বাস করেন যে আপনি ভুল করেছেন। সচেতন থাকুন যে আপনি যদি আপনার আবেদনের কোন তথ্য মিথ্যা প্রমাণ করেন তাহলে আপনি অযোগ্য হবেন। আপনার দেওয়া তথ্য যদি সঠিক হয়, ভবিষ্যতে আরও বৈশিষ্ট্য উপলব্ধ হওয়ায় আমরা আপনাকে আবার পরীক্ষা করতে উৎসাহিত করি।", + "application.household.addMembers.addHouseholdMember": "+ পরিবারের সদস্য যোগ করুন", + "application.household.addMembers.done": "লোক যোগ করা শেষ", + "application.household.addMembers.title": "আপনার পরিবারের কথা বলুন।", + "application.household.addMembers.doubleCheck": "দয়া করে পরিবারের প্রতিটি সদস্যের তথ্য দুবার পরীক্ষা করুন।", + "application.household.householdMember": "পরিবারের সদস্য", + "application.household.householdMembers": "পরিবারের সদস্যগন", + "application.household.liveAlone.title": "পরবর্তীতে আমরা অন্যদের সম্পর্কে জানতে চাই যারা আপনার সাথে ইউনিটে থাকবে", + "application.household.liveAlone.willLiveAlone": "আমি একা থাকব", + "application.household.liveAlone.liveWithOtherPeople": "অন্য লোকেরা আমার সাথে থাকবে", + "application.household.preferredUnit.preferredUnitType": "পছন্দের ইউনিটের ধরণ", + "application.household.preferredUnit.title": "আপনি কোন ইউনিটের আকারে আগ্রহী?", + "application.household.preferredUnit.subTitle": "আপনার নির্বাচিত ইউনিটের ধরন প্রাপ্যতা সাপেক্ষে হবে।", + "application.household.preferredUnit.legend": "পছন্দের ইউনিটের ধরণ", + "application.household.preferredUnit.optionsLabel": "সকল আবেদন যাচাই কর:", + "application.household.preferredUnit.options.studio": "স্টুডিও", + "application.household.preferredUnit.options.oneBedroom": "1 বেডরুম", + "application.household.preferredUnit.options.twoBedroom": "2 বেডরুম", + "application.household.preferredUnit.options.threeBedroom": "3 বেডরুম", + "application.household.preferredUnit.options.moreThanThreeBedroom": "3+ বেডরুম", + "application.household.member.cancelAddingThisPerson": "এই ব্যক্তিকে যোগ করা বাতিল করুন", + "application.household.member.deleteThisPerson": "এই ব্যক্তিকে মুছে দিন", + "application.household.member.dateOfBirth": "জন্ম তারিখ", + "application.household.member.name": "পরিবারের সদস্যের নাম", + "application.household.member.haveSameAddress": "তাদের কি আপনার মত একই ঠিকানা আছে?", + "application.household.member.whatIsTheirRelationship": "আপনার সাথে তাদের সম্পর্ক কি?", + "application.household.member.saveHouseholdMember": "পরিবারের সদস্যকে বাঁচান", + "application.household.member.subTitle": "আপনি পরবর্তী পর্দায় আরো পরিবারের সদস্যদের যোগ করার সুযোগ পাবেন", + "application.household.member.title": "এই ব্যক্তির সম্পর্কে আমাদের বলুন", + "application.household.member.updateHouseholdMember": "পরিবারের সদস্য আপডেট করুন", + "application.household.member.whatReletionship": "আপনার সাথে তাদের সম্পর্ক কি", + "application.household.member.workInRegion": "তারা কি %{county} কাউন্টিতে কাজ করে?", + "application.household.member.workInRegionNote": "টিবিডি", + "application.household.membersInfo.title": "অন্য লোকেদের যোগ করার আগে, নিশ্চিত করুন যে এই তালিকার জন্য অন্য কোনো অ্যাপ্লিকেশনে তাদের নাম নেই।", + "application.household.primaryApplicant": "প্রাথমিক আবেদনকারী", + "application.ada.label": "ADA অ্যাক্সেসযোগ্য ইউনিট", + "application.ada.title": "আপনি বা আপনার পরিবারের কারও কি নিম্নলিখিত ADA অ্যাক্সেসিবিলিটি বৈশিষ্ট্যগুলির প্রয়োজন?", + "application.ada.subTitle": "যদি আপনি একটি ইউনিটের জন্য নির্বাচিত হন, তাহলে সম্পত্তি আপনার প্রয়োজন অনুযায়ী তাদের সামর্থ্য অনুযায়ী কাজ করবে। আপনার আবেদন নির্বাচন করা উচিত, আপনার চিকিত্সকের কাছ থেকে সহায়ক ডকুমেন্টেশন প্রদান করতে প্রস্তুত থাকুন।", + "application.ada.mobility": "গতিশীলতা দুর্বলতার জন্য", + "application.ada.vision": "দৃষ্টি প্রতিবন্ধীদের জন্য", + "application.ada.hearing": "শ্রবণ প্রতিবন্ধীদের জন্য", + "application.financial.income.title": "আসুন আয়ের দিকে এগিয়ে যাই।", + "application.financial.income.instruction1": "পরিবারের মোট সদস্যদের মজুরি, সুবিধা এবং অন্যান্য উৎস থেকে আপনার মোট মোট (কর-পূর্ব) পরিবারের আয় যোগ করুন।", + "application.financial.income.instruction2": "আপনাকে এখনই একটি আনুমানিক মোট প্রদান করতে হবে। আপনি নির্বাচিত হলে প্রকৃত মোট গণনা করা হবে।", + "application.financial.income.prompt": "আপনার পরিবারের মোট কর-পূর্ব আয় কত?", + "application.financial.income.placeholder": "আপনার আয়ের উৎসের মোট", + "application.financial.income.legend": "আয়ের ফ্রিকোয়েন্সি", + "application.financial.income.validationError.title": "দুর্ভাগ্যক্রমে মনে হচ্ছে আপনি এই তালিকার জন্য যোগ্য নন।", + "application.financial.income.validationError.reason.low": "আপনার পরিবারের আয় খুবই কম।", + "application.financial.income.validationError.reason.high": "আপনার পরিবারের আয় খুব বেশি।", + "application.financial.income.validationError.instruction1": "দয়া করে পরিবর্তন করুন যদি আপনি বিশ্বাস করেন যে আপনি ভুল করেছেন। সচেতন থাকুন যে আপনি যদি আপনার আবেদনের কোন তথ্য মিথ্যা প্রমাণ করেন তাহলে আপনি অযোগ্য হবেন।", + "application.financial.income.validationError.instruction2": "আপনার দেওয়া তথ্য যদি সঠিক হয়, ভবিষ্যতে আরও বৈশিষ্ট্য উপলব্ধ হওয়ায় আমরা আপনাকে আবার পরীক্ষা করতে উৎসাহিত করি।", + "application.financial.vouchers.title": "আপনি বা এই অ্যাপ্লিকেশনে কেউ নিচের কোনটি পান?", + "application.financial.vouchers.housingVouchers.strong": "হাউজিং ভাউচার", + "application.financial.vouchers.housingVouchers.text": "অধ্যায় 8 মত", + "application.financial.vouchers.nonTaxableIncome.strong": "অ-করযোগ্য আয়", + "application.financial.vouchers.nonTaxableIncome.text": "যেমন SSI, SSDI, চাইল্ড সাপোর্ট পেমেন্ট, অথবা শ্রমিকের ক্ষতিপূরণ সুবিধা", + "application.financial.vouchers.rentalSubsidies.strong": "ভাড়া ভর্তুকি", + "application.financial.vouchers.rentalSubsidies.text": "যেমন VASH, HSA, HOPWA, Catholic Charities, AIDS Foundation, ইত্যাদি।", + "application.financial.vouchers.legend": "হাউজিং ভাউচার, ননটেক্সেবল আয় বা ভাড়া ভর্তুকি", + "application.preferences.title": "আপনার পরিবার নিম্নলিখিত আবাসন পছন্দগুলির জন্য যোগ্যতা অর্জন করতে পারে।", + "application.preferences.preamble": "আপনি যদি এই পছন্দের জন্য যোগ্যতা অর্জন করেন, তাহলে আপনি একটি উচ্চতর র ranking্যাঙ্কিং পাবেন।", + "application.preferences.selectBelow": "আপনার যদি এই আবাসন পছন্দগুলির মধ্যে একটি থাকে তবে নীচে এটি নির্বাচন করুন:", + "application.preferences.dontWant": "আমি এই পছন্দগুলি চাই না", + "application.preferences.stillHaveOpportunity": "আপনি এখনও অন্যান্য পছন্দ দাবি করার সুযোগ পাবেন।", + "application.preferences.youHaveClaimed": "আপনি দাবি করেছেন:", + "application.preferences.liveWork.title": "%{County} কাউন্টিতে থাকেন বা কাজ করেন?", + "application.preferences.liveWork.live.label": "%{County} কাউন্টি প্রেফারেন্সে বাস করুন", + "application.preferences.liveWork.live.description": "%{County} এ লাইভ কপি এখানে যায় ...", + "application.preferences.liveWork.live.link": "এইচটিটিপি://ডোমেইন.কম", + "application.preferences.liveWork.work.label": "%{County} কাউন্টি প্রেফারেন্সে কাজ করুন", + "application.preferences.liveWork.work.description": "%{County} কপিতে কাজ এখানে যায় ...", + "application.preferences.liveWork.work.link": "এইচটিটিপি://ডোমেইন.কম", + "application.preferences.PBV.title": "%{county} কপি এখানে যায় ...", + "application.preferences.PBV.residency.label": "রেসিডেন্সি", + "application.preferences.PBV.residency.description": "%{county} কপি এখানে যায় ...", + "application.preferences.PBV.family.label": "পরিবার", + "application.preferences.PBV.family.description": "%{county} কপি এখানে যায় ...", + "application.preferences.PBV.veteran.label": "প্রবীণ", + "application.preferences.PBV.veteran.description": "%{county} কপি এখানে যায় ...", + "application.preferences.PBV.homeless.label": "গৃহহীন", + "application.preferences.PBV.homeless.description": "%{county} কপি এখানে যায় ...", + "application.preferences.PBV.noneApplyButConsider.label": "এই পছন্দগুলির কোনটিই আমার জন্য প্রযোজ্য নয়, তবে আমি বিবেচনা করা চাই", + "application.preferences.PBV.doNotConsider.label": "আমি [আবাসন কর্তৃপক্ষ] প্রকল্প ভিত্তিক ভাউচার ইউনিটের জন্য বিবেচিত হতে চাই না", + "application.preferences.HOPWA.title": "এইডস আক্রান্ত ব্যক্তিদের জন্য আবাসন সুযোগ", + "application.preferences.HOPWA.hopwa.label": "এইডস আক্রান্ত ব্যক্তিদের জন্য আবাসন সুযোগ", + "application.preferences.HOPWA.hopwa.description": "%{county} কপি এখানে যায় ...", + "application.preferences.HOPWA.doNotConsider.label": "আমি বিবেচিত হতে চাই না", + "application.preferences.displacedTenant.title": "স্থানচ্যুত ভাড়াটে আবাসন পছন্দ", + "application.preferences.displacedTenant.whichHouseholdMember": "পরিবারের কোন সদস্য এই অগ্রাধিকার দাবি করছেন?", + "application.preferences.displacedTenant.whatAddress": "পরিবারের সদস্য কোন ঠিকানা থেকে বাস্তুচ্যুত হয়েছিল?", + "application.preferences.displacedTenant.general.label": "স্থানচ্যুত ভাড়াটে আবাসন পছন্দ", + "application.preferences.displacedTenant.general.description": "স্থানচ্যুত ভাড়াটে কপি এখানে যায়…", + "application.preferences.displacedTenant.general.link": "এইচটিটিপি://ডোমেইন.কম", + "application.preferences.displacedTenant.missionCorridor.label": "মিশন করিডর", + "application.preferences.displacedTenant.missionCorridor.description": "মিশন করিডরের কপি এখানে যায় ...", + "application.preferences.general.title": "আপনার প্রবেশ করা তথ্যের ভিত্তিতে, আপনার পরিবার কোন আবাসন পছন্দ দাবি করেনি।", + "application.preferences.general.preamble": "আপনি আবেদনকারীদের সাধারণ পুলে থাকবেন।", + "application.preferences.options.address": "স্থানচ্যুত ঠিকানা", + "application.preferences.options.name": "দাবিদার", + "application.review.takeAMomentToReview": "আপনার আবেদন জমা দেওয়ার আগে আপনার তথ্য পর্যালোচনা করার জন্য একটু সময় নিন।", + "application.review.sameAddressAsApplicant": "আবেদনকারীর একই ঠিকানা", + "application.review.noAdditionalMembers": "বাড়ির অতিরিক্ত সদস্য নেই", + "application.review.householdDetails": "পরিবারের বিবরণ", + "application.review.voucherOrSubsidy": "হাউজিং ভাউচার বা ভাড়া ভর্তুকি", + "application.review.lastChanceToEdit": "জমা দেওয়ার আগে এডিট করার এটাই আপনার শেষ সুযোগ।", + "application.review.terms.title": "শর্তাবলী", + "application.review.terms.text": "এই আবেদনটি অবশ্যই %{applicationDueDate} দ্বারা জমা দিতে হবে।

শূন্যপদ পূরণ না হওয়া পর্যন্ত আবেদনকারীদের লটারি র rank্যাঙ্ক এবং অগ্রাধিকার আদেশে এজেন্টের সাথে যোগাযোগ করা হবে।

আপনার দেওয়া সমস্ত তথ্য যাচাই করা হবে এবং আপনি যোগ্যতা নিশ্চিত করেছেন। যদি আপনি কোন প্রতারণামূলক বিবৃতি দেন, অথবা এই তালিকার জন্য একাধিক আবেদনে পরিবারের কোনো সদস্য উপস্থিত হন তাহলে আপনার আবেদন লটারি থেকে সরিয়ে দেওয়া হবে। আপনি যে হাউজিং লটারির পছন্দটি দাবি করেছেন তা যদি আমরা যাচাই করতে না পারি, তাহলে আপনি অগ্রাধিকার পাবেন না কিন্তু অন্যথায় শাস্তি পাবেন না।

আপনার দেওয়া সমস্ত তথ্য যাচাই করা হবে এবং আপনার যোগ্যতা নিশ্চিত করা হবে। যদি আপনি কোন প্রতারণামূলক বিবৃতি দিয়ে থাকেন, অথবা এই তালিকার জন্য একাধিক আবেদনে পরিবারের কোনো সদস্য উপস্থিত হন তাহলে আপনার আবেদনটি প্রতীক্ষা তালিকা থেকে সরিয়ে দেওয়া হবে। যদি আমরা আপনার দাবি করা আবাসন পছন্দ যাচাই করতে না পারি, তাহলে আপনি অগ্রাধিকার পাবেন না কিন্তু অন্যথায় শাস্তি পাবেন না।

আপনার আবেদন যদি লটারি থেকে বেছে নেওয়া হয়, তাহলে আরো বিস্তারিত আবেদন পূরণ করার জন্য প্রস্তুত থাকুন এবং যোগাযোগের 5 দিনের মধ্যে প্রয়োজনীয় সহায়ক নথি সরবরাহ করুন। আরও তথ্যের জন্য, তালিকাভুক্ত ডেভেলপার বা এজেন্টের সাথে যোগাযোগ করুন। এই লটারির আবেদন পূরণ করা আপনাকে আবাসনের অধিকারী করে না বা নির্দেশ করে না যে আপনি আবাসনের জন্য যোগ্য। সমস্ত আবেদনকারীদের সম্পত্তির আবাসিক নির্বাচন মানদণ্ডে বর্ণিত হিসাবে পরীক্ষা করা হবে।

আপনি জমা দেওয়ার পরে আপনি আপনার অনলাইন আবেদন পরিবর্তন করতে পারবেন না।

আমি ঘোষণা করছি যে পূর্বোক্ত সত্য এবং নির্ভুল, এবং স্বীকার করি যে কোন ভুল ব্যাখ্যা এই অ্যাপ্লিকেশনটিতে প্রতারণামূলক বা অবহেলা করলে লটারি থেকে অপসারণ করা হবে।

", + "application.review.terms.confirmCheckboxText": "আমি একমত এবং বুঝি যে আমি জমা দেওয়ার পরে আমি কিছু পরিবর্তন করতে পারি না।", + "application.review.demographics.title": "সকল মানুষের সেবা করার লক্ষ্যে আমরা আমাদের লক্ষ্য পূরণ করছি তা নিশ্চিত করতে আমাদের সাহায্য করুন।", + "application.review.demographics.subTitle": "এই প্রশ্নগুলি alচ্ছিক এবং আবাসনের জন্য আপনার যোগ্যতাকে প্রভাবিত করবে না। আপনার উত্তর গোপন রাখা হবে।", + "application.review.demographics.ethnicityLabel": "কোনটি আপনার জাতিসত্তাকে সবচেয়ে ভালভাবে বর্ণনা করে?", + "application.review.demographics.raceLabel": "কোনটি আপনার জাতি সম্পর্কে সেরা বর্ণনা করে?", + "application.review.demographics.genderLabel": "আপনার লিঙ্গ কি?", + "application.review.demographics.genderInfo": "আপনার বর্তমান লিঙ্গ পরিচয়কে সর্বোত্তমভাবে বর্ণনা করে এমন একটি নির্বাচন করুন।", + "application.review.demographics.sexualOrientationLabel": "আপনি কিভাবে আপনার যৌন প্রবণতা বা যৌন পরিচয় বর্ণনা করবেন?", + "application.review.demographics.howDidYouHearLabel": "আপনি এই তালিকা সম্পর্কে কিভাবে শুনেছেন?", + "application.review.demographics.ethnicityOptions.hispanicLatino": "হিস্পানিক / ল্যাটিনো", + "application.review.demographics.ethnicityOptions.notHispanicLatino": "হিস্পানিক / ল্যাটিনো নয়", + "application.review.demographics.raceOptions.americanIndianAlaskanNative": "আমেরিকান ইন্ডিয়ান / আলাস্কান নেটিভ", + "application.review.demographics.raceOptions.asian": "এশিয়ান", + "application.review.demographics.raceOptions.blackAfricanAmerican": "কালো / আফ্রিকান আমেরিকান", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander": "নেটিভ হাওয়াইয়ান / অন্যান্য প্রশান্ত মহাসাগরীয় দ্বীপপুঞ্জ", + "application.review.demographics.raceOptions.white": "সাদা", + "application.review.demographics.raceOptions.americanIndianAlaskanNativeAndBlackAfricanAmerican": "আমেরিকান ভারতীয় / আলাস্কান নেটিভ এবং কালো / আফ্রিকান আমেরিকান", + "application.review.demographics.raceOptions.americanIndianAlaskanNativeAndWhite": "আমেরিকান ইন্ডিয়ান / আলাস্কান নেটিভ এবং হোয়াইট", + "application.review.demographics.raceOptions.asianAndWhite": "এশিয়ান এবং হোয়াইট", + "application.review.demographics.raceOptions.blackAfricanAmericanAndWhite": "কালো / আফ্রিকান আমেরিকান এবং সাদা", + "application.review.demographics.raceOptions.otherMutliracial": "অন্যান্য / বহুজাতি", + "application.review.demographics.genderOptions.female": "মহিলা", + "application.review.demographics.genderOptions.male": "পুরুষ", + "application.review.demographics.genderOptions.genderqueerGenderNon-Binary": "Genderqueer / লিঙ্গ অ-বাইনারি", + "application.review.demographics.genderOptions.transFemale": "ট্রান্স ফিমেল", + "application.review.demographics.genderOptions.transMale": "ট্রান্স পুরুষ", + "application.review.demographics.genderOptions.notListed": "তালিকাভুক্ত না", + "application.review.demographics.sexualOrientationOptions.bisexual": "উভলিঙ্গ", + "application.review.demographics.sexualOrientationOptions.gayLesbianSameGenderLoving": "সমকামী / সমকামী / সমলিঙ্গ প্রেমী", + "application.review.demographics.sexualOrientationOptions.questioningUnsure": "প্রশ্ন করা / অনিশ্চিত", + "application.review.demographics.sexualOrientationOptions.straightHeterosexual": "সোজা / বিষমকামী", + "application.review.demographics.sexualOrientationOptions.notListed": "তালিকাভুক্ত না", + "application.review.demographics.howDidYouHearOptions.alamedaCountyHCDWebsite": "আলমেদা কাউন্টি এইচসিডি ওয়েবসাইট", + "application.review.demographics.howDidYouHearOptions.developerWebsite": "ডেভেলপার ওয়েবসাইট", + "application.review.demographics.howDidYouHearOptions.flyer": "ফ্লায়ার", + "application.review.demographics.howDidYouHearOptions.emailAlert": "ইমেল সতর্কতা", + "application.review.demographics.howDidYouHearOptions.friend": "বন্ধু", + "application.review.demographics.howDidYouHearOptions.housingCounselor": "হাউজিং কাউন্সিলর", + "application.review.demographics.howDidYouHearOptions.radioAd": "রেডিও বিজ্ঞাপন", + "application.review.demographics.howDidYouHearOptions.busAd": "বাস বিজ্ঞাপন", + "application.review.demographics.howDidYouHearOptions.other": "অন্যান্য", + "application.review.confirmation.title": "ধন্যবাদ। আমরা আপনার জন্য আবেদন পেয়েছি", + "application.review.confirmation.lotteryNumber": "এখানে আপনার আবেদন নিশ্চিতকরণ নম্বর", + "application.review.confirmation.pleaseWriteNumber": "অনুগ্রহ করে আপনার আবেদন নম্বর লিখুন এবং এটি একটি নিরাপদ স্থানে রাখুন। যদি আপনি একটি ইমেল ঠিকানা প্রদান করেন তবে আমরা আপনাকে এই নম্বরটি ইমেল করেছি।", + "application.review.confirmation.whatExpectTitle": "এরপর কি আশা করা যায়", + "application.review.confirmation.whatExpectFirstParagraph.held": "লটারি অনুষ্ঠিত হবে", + "application.review.confirmation.whatExpectFirstParagraph.attend": "আপনাকে হাউজিং লটারিতে অংশ নেওয়ার দরকার নেই। ফলাফল পোস্ট করা হবে", + "application.review.confirmation.whatExpectFirstParagraph.listing": "তালিকায়।", + "application.review.confirmation.whatExpectFirstParagraph.refer": "লটারি ফলাফলের তারিখের জন্য তালিকা দেখুন।", + "application.review.confirmation.whatExpectSecondparagraph": "শূন্যপদ পূরণ না হওয়া পর্যন্ত আবেদনকারীদের সাথে যোগাযোগ করা হবে। আপনার আবেদন নির্বাচন করা উচিত, আরো বিস্তারিত আবেদন পূরণ এবং প্রয়োজনীয় সহায়ক নথি প্রদান করার জন্য প্রস্তুত থাকুন।", + "application.review.confirmation.doNotSubmitTitle": "এই তালিকার জন্য অন্য আবেদন জমা দেবেন না।", + "application.review.confirmation.needToUpdate": "আপনার আবেদনের তথ্য আপডেট করার প্রয়োজন হলে, আবার আবেদন করবেন না। এজেন্টের সাথে যোগাযোগ করুন যদি আপনি একটি ইমেল নিশ্চিতকরণ না পান।", + "application.review.confirmation.createAccountTitle": "আপনি কি একটি অ্যাকাউন্ট তৈরি করতে চান?", + "application.review.confirmation.createAccountParagraph": "একটি অ্যাকাউন্ট তৈরি করা ভবিষ্যতের অ্যাপ্লিকেশনের জন্য আপনার তথ্য সংরক্ষণ করবে, এবং আপনি যে কোনো সময় এই অ্যাপ্লিকেশনের অবস্থা পরীক্ষা করতে পারেন।", + "application.review.confirmation.imdone": "না ধন্যবাদ, আমি শেষ করেছি।", + "application.review.confirmation.browseMore": "আরও তালিকা ব্রাউজ করুন", + "application.review.confirmation.print": "জমা দেওয়া আবেদন দেখুন এবং একটি অনুলিপি মুদ্রণ করুন।", + "application.confirmation.viewOriginalListing": "মূল তালিকা দেখুন", + "application.confirmation.informationSubmittedTitle": "আপনার জমা দেওয়া তথ্য এখানে।", + "application.confirmation.submitted": "জমা দেওয়া হয়েছে:", + "application.confirmation.lotteryNumber": "আপনার কনফার্মেশন নম্বর", + "application.confirmation.preferences": "পছন্দ", + "application.confirmation.generalLottery": "আপনার প্রবেশ করা তথ্যের উপর ভিত্তি করে, আপনার পরিবার কোন আবাসন লটারি পছন্দ দাবি করেনি। আপনি সাধারণ লটারিতে থাকবেন।", + "application.confirmation.printCopy": "আপনার রেকর্ডের জন্য একটি কপি মুদ্রণ করুন", + "application.start.whatToExpect.title": "এই অ্যাপ্লিকেশন থেকে কি আশা করা যায় তা এখানে।", + "application.start.whatToExpect.info1": "প্রথমে আমরা আপনাকে এবং যাদের সাথে আপনি বাস করার পরিকল্পনা করছেন তাদের সম্পর্কে জিজ্ঞাসা করব। তারপর, আমরা আপনার আয় সম্পর্কে জিজ্ঞাসা করব। পরিশেষে, আমরা দেখব আপনি কোন সাশ্রয়ী মূল্যের হাউজিং লটারি পছন্দের জন্য যোগ্য কিনা।", + "application.start.whatToExpect.info2": "দয়া করে সচেতন থাকুন যে প্রতিটি পরিবারের সদস্য প্রতিটি তালিকার জন্য শুধুমাত্র একটি আবেদনে উপস্থিত হতে পারে।", + "application.start.whatToExpect.info3": "যেকোনো প্রতারণামূলক বিবৃতি আপনার আবেদন সরিয়ে ফেলবে।", + "application.timeout.text": "আপনার পরিচয় রক্ষা করার জন্য, নিষ্ক্রিয়তার কারণে আপনার অধিবেশন এক মিনিটের মধ্যে শেষ হয়ে যাবে। যদি আপনি সাড়া না দেওয়া বেছে নেন তাহলে আপনি কোন সংরক্ষিত তথ্য হারাবেন।", + "application.timeout.action": "কাজ করতে থাকো", + "application.timeout.afterMessage": "আমরা আপনার নিরাপত্তার কথা চিন্তা করি। নিষ্ক্রিয়তার কারণে আমরা আপনার সেশন শেষ করেছি। চালিয়ে যাওয়ার জন্য অনুগ্রহ করে একটি নতুন অ্যাপ্লিকেশন শুরু করুন।", + "application.continueApplication": "আবেদন চালিয়ে যান", + "application.applicationNeverSubmitted": "আপনার আবেদন কখনো জমা দেওয়া হয়নি", + "application.deleteThisApplication": "এই অ্যাপ্লিকেশনটি মুছবেন?", + "application.deleteThisMember": "এই সদস্যকে মুছবেন?", + "application.deleteMemberDescription": "আপনি কি সত্যিই এই সদস্যকে মুছে ফেলতে চান?", + "application.deleteApplicationDescription": "এই অ্যাপ্লিকেশনটি মুছে ফেলার অর্থ আপনি প্রবেশ করা সমস্ত তথ্য হারাবেন।", + "application.edited": "সম্পাদিত", + "application.status": "স্থিতি", + "application.statuses.inProgress": "চলমান", + "application.statuses.neverSubmitted": "কখনো জমা দেওয়া হয়নি", + "application.statuses.submitted": "জমা দেওয়া হয়েছে", + "application.viewApplication": "আবেদন দেখুন", + "application.yourLotteryNumber": "আপনার কনফার্মেশন নম্বর হল", + "users.confirmed": "নিশ্চিত করা হয়েছে", + "users.unconfirmed": "অনিশ্চিত", + "users.totalUsers": "মোট ব্যবহারকারী", + "users.administrator": "প্রশাসক", + "users.partner": "অংশীদার", + "flags.flaggedSet": "পতাকাযুক্ত সেট", + "flags.ruleName": "নিয়মের নাম", + "flags.pendingReview": "মুলতুবি পর্যালোচনা", + "flags.totalSets": "মোট সেট", + "flags.resolveFlag": "পতাকা সমাধান করুন", + "flags.markedAsDuplicate": "%{amount} অ্যাপ্লিকেশনগুলি সদৃশ হিসাবে চিহ্নিত", + "authentication.forgotPassword.changePassword": "পাসওয়ার্ড পরিবর্তন করুন", + "authentication.forgotPassword.message": "যদি সেই ইমেল দিয়ে একটি অ্যাকাউন্ট তৈরি করা হয়, তাহলে আপনি আপনার পাসওয়ার্ড রিসেট করার জন্য একটি লিঙ্ক সহ একটি ইমেল পাবেন।", + "authentication.forgotPassword.sendEmail": "ইমেইল পাঠান", + "authentication.forgotPassword.errors.tokenExpired": "পাসওয়ার্ড টোকেনের মেয়াদ শেষ হয়ে গেছে। অনুগ্রহ করে নতুনের জন্য অনুরোধ করুন।", + "authentication.forgotPassword.errors.tokenMissing": "টোকেন পাওয়া যায়নি। অনুগ্রহ করে নতুনের জন্য অনুরোধ করুন।", + "authentication.forgotPassword.errors.generic": "সেখানে একটা ভুল ছিল. অনুগ্রহ করে আবার চেষ্টা করুন, অথবা সাহায্যের জন্য সহায়তার সাথে যোগাযোগ করুন।", + "authentication.forgotPassword.errors.emailNotFound": "ইমেল পাওয়া যায়নি। অনুগ্রহ করে নিশ্চিত করুন যে আপনার ইমেলের আমাদের সাথে একটি অ্যাকাউন্ট আছে এবং নিশ্চিত করা হয়েছে।", + "authentication.timeout.text": "আপনার পরিচয় রক্ষা করার জন্য, নিষ্ক্রিয়তার কারণে আপনার অধিবেশন এক মিনিটের মধ্যে শেষ হয়ে যাবে। আপনি যদি কোন সাশ্রয় না করা তথ্য হারাবেন এবং আপনি যদি সাড়া না দেওয়া বেছে নেন তবে লগ আউট হয়ে যাবেন।", + "authentication.timeout.action": "লগ ইন করে থাকুন", + "authentication.timeout.signOutMessage": "আমরা আপনার নিরাপত্তার কথা চিন্তা করি। নিষ্ক্রিয়তার কারণে আমরা আপনাকে লগ আউট করেছি। অবিরত সাইন ইন করুন।", + "authentication.signIn.loginError": "একটি বৈধ ইমেইল ঠিকানা লিখুন", + "authentication.signIn.passwordError": "একটি বৈধ পাসওয়ার্ড দিন", + "authentication.signIn.phoneError": "একটি বৈধ US ফোন নম্বর লিখুন.", + "authentication.signIn.cantFindAccount": "আমরা সেই ইমেল ঠিকানা/পাসওয়ার্ড সহ একটি অ্যাকাউন্ট খুঁজে পাইনি।", + "authentication.signIn.error": "আপনাকে সাইন ইন করার সময় একটি ত্রুটি হয়েছে", + "authentication.signIn.errorGenericMessage": "অনুগ্রহ করে আবার চেষ্টা করুন, অথবা সাহায্যের জন্য সহায়তার সাথে যোগাযোগ করুন।", + "authentication.signIn.forgotPassword": "পাসওয়ার্ড ভুলে গেছেন?", + "authentication.signIn.success": "আবার স্বাগতম, %{name}", + "authentication.createAccount.accountConfirmed": "আপনার অ্যাকাউন্ট সফলভাবে নিশ্চিত করা হয়েছে।", + "authentication.createAccount.anEmailHasBeenSent": "%{Email} এ একটি ইমেল পাঠানো হয়েছে", + "authentication.createAccount.confirmationInstruction": "অ্যাকাউন্ট তৈরি সম্পূর্ণ করার জন্য আমরা আপনাকে যে ইমেইলটি পাঠিয়েছি সেই লিঙ্কে ক্লিক করুন।", + "authentication.createAccount.confirmationNeeded": "নিশ্চিতকরণ প্রয়োজন", + "authentication.createAccount.emailSent": "নিশ্চিতকরণ ইমেইল পাঠানো হয়েছে। অনুগ্রহ করে আপনার ইনবক্স চেক করুন।", + "authentication.createAccount.yourName": "তোমার নাম", + "authentication.createAccount.firstName": "নামের প্রথম অংশ", + "authentication.createAccount.middleNameOptional": "মধ্য নাম (ঐচ্ছিক)", + "authentication.createAccount.middleName": "মধ্য নাম", + "authentication.createAccount.lastName": "নামের শেষাংশ", + "authentication.createAccount.yourDateOfBirth": "তোমার জন্ম তারিখ", + "authentication.createAccount.email": "ইমেইল", + "authentication.createAccount.emailPrivacy": "আমরা আপনার আবেদন সম্পর্কে আপনার সাথে যোগাযোগ করতে শুধুমাত্র আপনার ইমেইল ঠিকানা ব্যবহার করব।", + "authentication.createAccount.reEnterEmail": "ই - মেইল ​​এর ঠিকানা পুনঃ - প্রবেশ করান", + "authentication.createAccount.noAccount": "কোনো অ্যাকাউন্ট নেই?", + "authentication.createAccount.phone": "ফোন", + "authentication.createAccount.reEnterPassword": "আপনার পাসওয়ার্ড পুনরায় লিখুন", + "authentication.createAccount.resendTheEmail": "ইমেলটি আবার পাঠান", + "authentication.createAccount.emailSubscription": "जब भी कोई लिस्टिंग पोस्ट या अपडेट की जाती है तो मुझे ईमेल करें।", + "authentication.createAccount.smsSubscription": "जब भी कोई लिस्टिंग पोस्ट या अपडेट की जाती है तो मुझे टेक्स्ट करें।", + "authentication.createAccount.linkExpired": "আপনার লিঙ্কের মেয়াদ শেষ হয়ে গেছে", + "authentication.createAccount.mustBe8Chars": "8 টি অক্ষর হতে হবে", + "authentication.createAccount.password": "পাসওয়ার্ড", + "authentication.createAccount.passwordInfo": "কমপক্ষে 8 টি অক্ষর এবং কমপক্ষে 1 টি অক্ষর এবং কমপক্ষে একটি সংখ্যা থাকতে হবে।", + "authentication.createAccount.resendEmailInfo": "অ্যাকাউন্ট তৈরি সম্পূর্ণ করার জন্য অনুগ্রহ করে আমরা আপনাকে ২ hours ঘন্টার মধ্যে যে ইমেইলটি পাঠাবো সেই লিঙ্কে ক্লিক করুন।", + "authentication.createAccount.resendAnEmailTo": "একটি ইমেল পুনরায় পাঠান", + "authentication.createAccount.errors.accountConfirmed": "আপনার অ্যাকাউন্ট ইতিমধ্যে নিশ্চিত করা হয়.", + "authentication.createAccount.errors.emailInUse": "ইমেইল ইতোমধ্যে ব্যবহৃত হচ্ছে", + "authentication.createAccount.errors.emailMismatch": "ইমেল মেলে না", + "authentication.createAccount.errors.emailNotFound": "ইমেল পাওয়া যায়নি। দয়া করে প্রথমে নিবন্ধন করুন।", + "authentication.createAccount.errors.passwordMismatch": "পাসওয়ার্ড মেলে না", + "authentication.createAccount.errors.passwordTooWeak": "পাসওয়ার্ড খুব দুর্বল। কমপক্ষে 8 টি অক্ষর এবং কমপক্ষে 1 টি অক্ষর এবং কমপক্ষে একটি সংখ্যা থাকতে হবে।", + "authentication.createAccount.errors.tokenExpired": "আপনার লিঙ্কের মেয়াদ শেষ হয়ে গেছে।", + "authentication.createAccount.errors.tokenMissing": "ভুল টোকেন দেওয়া হয়েছে।", + "authentication.signOut.success": "আপনি আপনার অ্যাকাউন্ট থেকে সফলভাবে লগ আউট করেছেন।", + "errors.alert.timeoutPleaseTryAgain": "উফ! মনে হচ্ছে কিছু ভুল হয়েছে। অনুগ্রহপূর্বক আবার চেষ্টা করুন.", + "errors.notFound.title": "পৃষ্ঠা খুঁজে পাওয়া যায়নি", + "errors.notFound.message": "ওহ, আপনি যে পৃষ্ঠাটি খুঁজছেন তা আমরা খুঁজে পাচ্ছি না। পূর্ববর্তী পৃষ্ঠায় ফিরে যাওয়ার চেষ্টা করুন অথবা তালিকাগুলি ব্রাউজ করতে নীচে ক্লিক করুন।", + "errors.unauthorized.title": "অননুমোদিত", + "errors.unauthorized.message": "ওহ, আপনাকে এই পৃষ্ঠাটি অ্যাক্সেস করার অনুমতি নেই।", + "errors.agreeError": "চালিয়ে যাওয়ার জন্য আপনাকে অবশ্যই শর্তাবলীর সাথে সম্মত হতে হবে", + "errors.firstNameError": "দয়া করে একটি প্রথম নাম লিখুন", + "errors.lastNameError": "দয়া করে একটি শেষ নাম লিখুন", + "errors.dateOfBirthError": "অনুগ্রহ করে একটি বৈধ জন্ম তারিখ প্রবেশ করান", + "errors.dateOfBirthErrorAge": "দয়া করে একটি বৈধ জন্ম তারিখ লিখুন, তার বয়স 18 বা তার বেশি হতে হবে", + "errors.emailAddressError": "একটি ইমেল ঠিকানা লিখুন", + "errors.phoneNumberError": "দয়া করে একটি ফোন নম্বর লিখুন", + "errors.phoneNumberTypeError": "দয়া করে একটি ফোন নম্বর টাইপ করুন", + "errors.streetError": "দয়া করে একটি ঠিকানা লিখুন", + "errors.timeError": "দয়া করে একটি বৈধ সময় লিখুন", + "errors.cityError": "দয়া করে একটি শহরে প্রবেশ করুন", + "errors.stateError": "দয়া করে একটি রাজ্যে প্রবেশ করুন", + "errors.zipCodeError": "একটি জিপ কোড প্রবেশ করুন", + "errors.multipleZipCodeError": "অনুগ্রহ করে এক বা একাধিক কমা দ্বারা পৃথক পিন কোড লিখুন", + "errors.errorsToResolve": "এগিয়ে যাওয়ার আগে আপনাকে ত্রুটিগুলি সমাধান করতে হবে।", + "errors.numberError": "দয়া করে 0 এর চেয়ে বড় একটি বৈধ সংখ্যা লিখুন।", + "errors.selectAllThatApply": "প্রযোজ্য সমস্ত নির্বাচন করুন।", + "errors.selectAtLeastOne": "কমপক্ষে একটি বিকল্প নির্বাচন করুন।", + "errors.selectAnOption": "একটি বিকল্প নির্বাচন করুন.", + "errors.selectOption": "দয়া করে উপরের বিকল্পগুলির মধ্যে একটি নির্বাচন করুন।", + "errors.urlError": "একটি বৈধ URL প্রবেশ করুন", + "errors.householdTooBig": "আপনার পরিবারের আকার অনেক বড়।", + "errors.householdTooSmall": "আপনার পরিবারের আকার খুব ছোট।", + "errors.dateError": "একটি বৈধ তারিখ লিখুন দয়া করে", + "errors.rateLimitExceeded": "রেটের সীমা অতিক্রম করেছে, পরে আবার চেষ্টা করুন।", + "errors.requiredFieldError": "ঘরটি অবশ্যই পূরণ করতে হবে.", + "errors.noData": "কোন তথ্য নেই.", + "footer.srHeading": "পাদলেখ", + "footer.srProjectInformation": "প্রকল্প সম্পর্কিত তথ্য", + "footer.srContactInformation": "যোগাযোগের তথ্য", + "footer.srLegalInformation": "আইনি তথ্য", + "footer.contact": "যোগাযোগ", + "footer.terms": "অস্বীকৃতি", + "footer.forGeneralQuestions": "সাধারণ প্রোগ্রাম অনুসন্ধানের জন্য, আপনি আমাদের 000-000-0000 এ কল করতে পারেন।", + "footer.giveFeedback": "মতামত দিন", + "footer.privacyPolicy": "গোপনীয়তা নীতি", + "footer.copyright": "বিক্ষোভ কাউন্টি © 2020 • সর্বস্বত্ব সংরক্ষিত", + "housingCounselors.subtitle": "আপনার প্রয়োজনের জন্য নির্দিষ্ট স্থানীয় হাউজিং কাউন্সেলরের সাথে কথা বলুন।", + "housingCounselors.languageServices": "ভাষা পরিষেবা:", + "housingCounselors.call": "কল সংখ্যা}", + "housingCounselors.visitWebsite": "%{Name} এ যান", + "homeType.apartment": "অ্যাপার্টমেন্ট", + "homeType.duplex": "ডুপ্লেক্স", + "homeType.house": "একক-পরিবার ঘর", + "homeType.townhome": "টাউনহোম", + "languages.srHeading": "ভাষা", + "languages.srNavigation": "ভাষা", + "languages.en": "English", + "languages.es": "Español", + "languages.zh": "中文", + "languages.vi": "Tiếng Việt", + "languages.ar": "عربى", + "languages.bn": "বাংলা", + "leasingAgent.contact": "লিজিং এজেন্টের সাথে যোগাযোগ করুন", + "leasingAgent.dueToHighCallVolume": "উচ্চ কল ভলিউমের কারণে আপনি একটি বার্তা শুনতে পারেন।", + "leasingAgent.name": "লিজিং এজেন্টের নাম", + "leasingAgent.namePlaceholder": "পুরো নাম", + "leasingAgent.title": "লিজিং এজেন্ট শিরোনাম", + "leasingAgent.officeHours": "অফিসের সময়সূচি", + "leasingAgent.officeHoursPlaceholder": "উদা: সকাল :00:০০ - বিকাল ৫ টা, সোমবার থেকে শুক্রবার", + "listingFilters.program.Seniors 55+": "প্রবীণ 55+", + "listingFilters.program.Seniors 62+": "প্রবীণ 62+", + "listingFilters.program.Residents with Disabilities": "প্রতিবন্ধিত্ব সম্পন্ন বাসিন্দা", + "listingFilters.program.Families": "পরিবার", + "listingFilters.program.Supportive Housing for the Homeless": "গৃহহীনদের জন্য সহায়ক আবাসন", + "listingFilters.program.Veterans": "প্রাক্তন সমরকর্মী", + "listingFilters.clear": "পরিষ্কার", + "listingFilters.section8": "সেকশন 8 হাউজিং চয়েস ভাউচার গ্রহণ করে", + "listingFilters.region.GreaterDowntown": "শহর কেন্দ্রের আশেপাশে", + "listingFilters.region.Eastside": "পূর্ব অংশে", + "listingFilters.region.Westside": "পশ্চিম অংশে", + "listingFilters.region.Southwest": "দক্ষিণ-পশ্চিম অংশে", + "listings.error": "ফর্ম জমা দিতে সমস্যা হয়েছে।", + "listings.closeThisListing": "আপনি কি সত্যিই এই তালিকাটি বন্ধ করতে চান?", + "listings.active": "আবেদন গ্রহণ করা", + "listings.pending": "শীঘ্রই আসছে", + "listings.closed": "বন্ধ", + "listings.actions.publish": "প্রকাশ করুন", + "listings.actions.draft": "খসড়া হিসেবে সংরক্ষণ করুন", + "listings.actions.preview": "প্রিভিউ", + "listings.actions.close": "বন্ধ", + "listings.actions.viewListing": "তালিকা দেখুন", + "listings.actions.unpublish": "অপ্রকাশিত", + "listings.actions.postResults": "ফলাফল পোস্ট করুন", + "listings.actions.resultsPosted": "ফলাফল পোস্ট করা হয়েছে", + "listings.actions.previewLotteryResults": "লটারি ফলাফলের পূর্বরূপ দেখুন", + "listings.activePreferences": "সক্রিয় পছন্দ", + "listings.addListing": "তালিকা যোগ করুন", + "listings.addPhoto": "ছবি জগকরুন", + "listings.addPreference": "পছন্দ যোগ করুন", + "listings.addPreferences": "পছন্দ যোগ করুন", + "listings.additionalApplicationSubmissionNotes": "অতিরিক্ত আবেদন জমা নোট", + "listings.additionalInformation": "অতিরিক্ত তথ্য", + "listings.allUnits": "সব ইউনিট", + "listings.allUnitsReservedFor": "সমস্ত ইউনিট %{type} এর জন্য সংরক্ষিত", + "listings.annualIncome": "%{আয়} প্রতি বছর", + "listings.annualIncomeRange": "%{থেকে} থেকে %{থেকে} প্রতি বছর", + "listings.applicationTitle": "আবেদনের উপাত্ত", + "listings.applicationAddress": "ঠিকানা", + "listings.applicationDeadline": "আবেদনের শেষ তারিখ", + "listings.applicationDueTime": "আবেদনের নির্ধারিত সময়", + "listings.applicationFCFS": "আগে আসলে আগে পাবেন", + "listings.applicationFee": "আবেদন ফী", + "listings.applicationFeeDueAt": "সাক্ষাৎকারের সময়", + "listings.applicationOpenPeriod": "অ্যাপ্লিকেশন খোলা", + "listings.applicationPerApplicantAgeDescription": "প্রতি আবেদনকারীর বয়স 18 এবং তার বেশি", + "listings.applicationPickupQuestion": "আবেদনগুলি কি নেওয়া যাবে?", + "listings.applicationsClosed": "অ্যাপ্লিকেশন বন্ধ", + "listings.applicationDropOffQuestion": "আবেদন কি বাদ দেওয়া যাবে?", + "listings.apply.applicationsMustBeReceivedByDeadline": "আবেদনপত্র সময়সীমার মধ্যে গ্রহণ করতে হবে এবং পোস্টমার্ক বিবেচনা করা হবে না।", + "listings.apply.applicationSeason": "বাসিন্দাদের আবেদন করতে হবে", + "listings.apply.applicationWillBeAvailableOn": "%{OpenDate} এ ডাউনলোড এবং পিক -আপের জন্য আবেদন পাওয়া যাবে", + "listings.apply.applyOnline": "অনলাইনে আবেদন", + "listings.apply.downloadApplication": "অ্যাপ্লিকেশন ডাউনলোড করুন", + "listings.apply.dropOffApplication": "ড্রপ অফ অ্যাপ্লিকেশন", + "listings.apply.dropOffApplicationOrMail": "ড্রপ অফ অ্যাপ্লিকেশন বা ইউএস মেইল ​​দ্বারা পাঠান", + "listings.apply.getAPaperApplication": "একটি কাগজ অ্যাপ্লিকেশন পান", + "listings.apply.howToApply": "কিভাবে আবেদন করতে হবে", + "listings.apply.paperApplicationsMustBeMailed": "কাগজের আবেদনগুলি অবশ্যই ইউএস মেইল ​​দ্বারা পাঠানো উচিত এবং ব্যক্তিগতভাবে জমা দেওয়া যাবে না।", + "listings.apply.pickUpAnApplication": "একটি আবেদন সংগ্রহ করুন", + "listings.apply.postmarkedApplicationsMustBeReceivedByDate": "নির্দিষ্ট সময়সীমার মধ্যে আবেদনপত্র গ্রহণ করতে হবে। যদি ইউএস মেইল ​​দ্বারা প্রেরণ করা হয়, তাহলে আবেদনটি অবশ্যই %{applicationDueDate} দ্বারা পোস্টমার্ক করা এবং %{postmarkReceivedByDate} এর পরে মেইলে প্রাপ্ত হতে হবে। মেইলের মাধ্যমে %{postmarkReceivedByDate} এর পরে প্রাপ্ত আবেদনপত্রগুলি %{applicationDueDate} দ্বারা পোস্টমার্ক করা হলেও গ্রহণ করা হবে না। %{developer} হারিয়ে যাওয়া বা বিলম্বিত মেইলের জন্য দায়ী নয়।", + "listings.apply.sendByUsMail": "ইউএস মেইলের মাধ্যমে আবেদন পাঠান", + "listings.apply.submitAPaperApplication": "একটি কাগজ আবেদন জমা দিন", + "listings.apply.contactManagment": "ম্যানেজমেন্ট কোম্পানির সাথে যোগাযোগ করুন", + "listings.atAnotherAddress": "অন্য ঠিকানায়", + "listings.atLeasingAgentAddress": "লিজিং এজেন্টের ঠিকানায়", + "listings.atMailingAddress": "মেইলিং ঠিকানায়", + "listings.availableAndWaitlist": "উপলব্ধ ইউনিট এবং ওপেন ওয়েস্টলিস্ট", + "listings.availableUnits": "উপলব্ধ ইউনিট", + "listings.availableUnitsAndWaitlist": "উপলব্ধ ইউনিট এবং অপেক্ষা তালিকা", + "listings.availableUnitsAndWaitlistDesc": "একবার আবেদনকারীরা সমস্ত উপলব্ধ ইউনিট পূরণ করলে, অতিরিক্ত আবেদনকারীদের %{number} ইউনিট এর অপেক্ষার তালিকায় রাখা হবে", + "listings.bath": "স্নান", + "listings.browseListings": "তালিকা ব্রাউজ করুন", + "listings.buildingImageAltText": "ভবনের একটি ছবি", + "listings.closedListings": "বন্ধ তালিকা", + "listings.communityProgramsDescription": "এই প্রোগ্রামে নির্দিষ্ট সম্প্রদায়ের সদস্যদের জন্য সুযোগ রয়েছে", + "listings.confirmedPreferenceList": "%{Preference} তালিকা নিশ্চিত করা হয়েছে", + "listings.creditHistory": "ঋনের ইতিহাস", + "listings.criminalBackground": "অপরাধমূলক পটভূমি", + "listings.deleteListingDescription": "এই তালিকাটি মুছে ফেলার অর্থ আপনি প্রবেশ করা সমস্ত তথ্য হারাবেন।", + "listings.depositMax": "আমানত সর্বোচ্চ", + "listings.depositMin": "আমানত ন্যূনতম", + "listings.depositOrMonthsRent": "অথবা এক মাসের ভাড়া", + "listings.depositMayBeHigherForLowerCredit": "কম ক্রেডিট স্কোরের জন্য উচ্চতর হতে পারে", + "listings.details.listingData": "তালিকা ডেটা", + "listings.details.createdDate": "তারিখ তৈরী", + "listings.details.updatedDate": "তারিখ আপডেট করা হয়েছে", + "listings.details.id": "তালিকা আইডি", + "listings.developmentalDisabilities": "বিকাশের অক্ষম ব্যক্তিরা", + "listings.developmentalDisabilitiesDescription": "এই ভবনের একক সংখ্যক ইউনিট উন্নয়নশীল প্রতিবন্ধীদের জন্য আলাদা রাখা হয়েছে। যোগ্যতা, প্রয়োজনীয়তা, কিভাবে আবেদন পেতে হয় এবং অন্য কোন প্রশ্নের উত্তরের জন্য তথ্যের জন্য দয়া করে housechoices.org দেখুন আপনি প্রক্রিয়া সম্পর্কে থাকতে পারে।", + "listings.dropOffAddress": "ঠিকানা বন্ধ করুন", + "listings.dueDateQuestion": "আবেদনের শেষ তারিখ আছে কি?", + "listings.editPreferences": "সম্পাদনা করুন পছন্দ", + "listings.enterLotteryForWaitlist": "%{Units} ইউনিটের অপেক্ষার তালিকায় একটি খোলা স্লটের জন্য একটি আবেদন জমা দিন।", + "listings.firstComeFirstServe": "আগে আসলে আগে পাবেন", + "listings.forIncomeCalculations": "আয়ের হিসাবের জন্য, পরিবারের আকারে ইউনিটে বসবাসকারী সবাই (সব বয়সী) অন্তর্ভুক্ত।", + "listings.forIncomeCalculationsBMR": "আয়ের হিসাব ইউনিটের প্রকারভিত্তিক", + "listings.hideClosedListings": "বন্ধ তালিকা লুকান", + "listings.householdMaximumIncome": "পরিবারের সর্বোচ্চ আয়", + "listings.householdSize": "পরিবারের আকার", + "listings.homeType": "হোম টাইপ", + "listings.importantProgramRules": "গুরুত্বপূর্ণ প্রোগ্রামের নিয়ম", + "listings.includesPriorityUnits": "%{Priorities} এর জন্য অগ্রাধিকার ইউনিট অন্তর্ভুক্ত", + "listings.latitude": "অক্ষাংশ", + "listings.leasingAgentAddress": "লিজিং এজেন্টের ঠিকানা", + "listings.listingPreviewOnly": "এটি শুধুমাত্র একটি তালিকা পূর্বরূপ।", + "listings.listingStatus.active": "খোলা", + "listings.listingStatus.pending": "খসড়া", + "listings.listingStatus.closed": "বন্ধ", + "listings.listingSubmitted": "তালিকা জমা দেওয়া হয়েছে", + "listings.listingUpdated": "তালিকা আপডেট করা হয়েছে", + "listings.longitude": "দ্রাঘিমাংশ", + "listings.lottery": "লটারি", + "listings.lotteryDateNotes": "লটারির তারিখের নোট", + "listings.lotteryDateQuestion": "কবে লটারি চালানো হবে?", + "listings.lotteryEndTime": "লটারির শেষ সময়", + "listings.lotteryResults.completeResultsWillBePosted": "সম্পূর্ণ লটারির ফলাফল শীঘ্রই পোস্ট করা হবে।", + "listings.lotteryResults.downloadResults": "ফলাফল ডাউনলোড করুন", + "listings.lotteryResults.header": "লটারির ফলাফল", + "listings.lotteryStartTime": "লটারি শুরুর সময়", + "listings.mapPinAutomaticDescription": "ম্যাপ পিনের অবস্থান প্রদান করা ঠিকানার উপর ভিত্তি করে", + "listings.mapPinCustomDescription": "মার্কারের অবস্থান আপডেট করতে পিনটি টেনে আনুন", + "listings.mapPinPosition": "ম্যাপ পিন অবস্থান", + "listings.mapPreview": "মানচিত্রের পূর্বরূপ", + "listings.mapPreviewNoAddress": "মানচিত্রের পূর্বরূপ দেখতে একটি ঠিকানা লিখুন", + "listings.maxIncomeMonth": "সর্বোচ্চ আয় / মাস", + "listings.maxIncomeYear": "সর্বোচ্চ আয় / বছর", + "listings.monthlyIncome": "%{আয়} প্রতি মাসে", + "listings.monthlyIncomeRange": "%{থেকে} থেকে %{থেকে} প্রতি মাসে", + "listings.moreBuildingSelectionCriteria": "বিল্ডিং নির্বাচনের মানদণ্ড সম্পর্কে আরও জানুন", + "listings.newListing": "নতুন তালিকীকরণ", + "listings.noAvailableUnits": "এই সময়ে কোন উপলব্ধ ইউনিট নেই।", + "listings.noOpenListings": "বর্তমানে কোন তালিকা খোলা অ্যাপ্লিকেশন নেই।", + "listings.occupancyDescriptionNoSro": "এই ভবনের জন্য দখলের সীমাগুলি ইউনিটের প্রকারের উপর ভিত্তি করে।", + "listings.openHouseEvent.header": "খোলা ঘর", + "listings.openHouseEvent.seeVideo": "ভিডিও দেখুন", + "listings.paperDifferentAddress": "কাগজের আবেদনপত্র অন্য ঠিকানায় পাঠানো হয়", + "listings.percentAMIUnit": "% {শতাংশ}% AMI ইউনিট", + "listings.pickupAddress": "পিক আপের ঠিকানা", + "listings.postmarkByDate": "তারিখ দ্বারা পোস্টমার্ক", + "listings.postmarksConsideredQuestion": "পোস্টমার্ক কি বিবেচনা করা হয়?", + "listings.priorityUnits": "অগ্রাধিকার ইউনিট", + "listings.priorityUnitsDescription": "এই বিল্ডিংটিতে ইউনিট আলাদা রাখা আছে যদি নিচের কোনটি আপনার বা আপনার পরিবারের কারো জন্য প্রযোজ্য হয়:", + "listings.title": "সম্পত্তির তথ্য", + "listings.developer": "হাউজিং ডেভেলপার", + "listings.buildingAddress": "ঠিকানা", + "listings.publicLottery.header": "পাবলিক লটারি", + "listings.publicLottery.seeVideo": "ভিডিও দেখুন", + "listings.remainingUnitsAfterPreferenceConsideration": "সমস্ত অগ্রাধিকার হোল্ডারদের বিবেচনা করার পরে, বাকি যোগ্য ইউনিট অন্যান্য যোগ্য আবেদনকারীদের জন্য উপলব্ধ হবে।", + "listings.rentalHistory": "ভাড়ার ইতিহাস", + "listings.requiredDocuments": "প্রয়োজনীয় কাগজপত্র", + "listings.reservedCommunityBuilding": "%{type} বিল্ডিং", + "listings.reservedCommunityDescription": "সংরক্ষিত সম্প্রদায়ের বর্ণনা", + "listings.reservedCommunitySeniorTitle": "সিনিয়র বিল্ডিং", + "listings.reservedCommunityTitleDefault": "সংরক্ষিত ভবন", + "listings.reservedCommunityTypes.senior": "সিনিয়র", + "listings.reservedCommunityTypes.senior55": "প্রবীণ 55+", + "listings.reservedCommunityTypes.senior62": "প্রবীণ 62+", + "listings.reservedCommunityTypes.partiallySenior": "আংশিক সিনিয়র", + "listings.reservedCommunityTypes.specialNeeds": "অ্যাক্সেসযোগ্য", + "listings.reservedFor": "%{Type} এর জন্য সংরক্ষিত", + "listings.reservedCommunityType": "সংরক্ষিত সম্প্রদায়ের ধরন", + "listings.reservedTypePlural.family": "পরিবার", + "listings.reservedTypePlural.senior": "প্রবীণরা", + "listings.reservedTypePlural.veteran": "ভেটেরান্স", + "listings.reservedTypePlural.specialNeeds": "বিশেষ প্রয়োজন", + "listings.reservedUnits": "সংরক্ষিত ইউনিট", + "listings.reservedUnitsDescription": "এই ইউনিটগুলির জন্য যোগ্যতা অর্জনের জন্য নিম্নলিখিতগুলির মধ্যে একটি আপনার বা আপনার পরিবারের কারো জন্য আবেদন করতে হবে:", + "listings.reservedUnitsForWhoAre": "%{CommunityType} এর জন্য সংরক্ষিত যারা %{ReserveType}", + "listings.reviewOrderQuestion": "কিভাবে আবেদন পর্যালোচনা আদেশ নির্ধারিত হয়?", + "listings.sections.additionalDetails": "অতিরিক্ত তথ্য", + "listings.sections.additionalDetailsSubtitle": "অন্য কোন প্রয়োজনীয় কাগজপত্র এবং নির্বাচনের মানদণ্ড আছে?", + "listings.sections.additionalEligibilitySubtext": "ভবনের অন্য কোন নিয়ম আবেদনকারীদের জানাতে দিন।", + "listings.sections.additionalEligibilitySubtitle": "আবেদনকারীদের অবশ্যই ভবনের নিয়ম অনুযায়ী যোগ্যতা অর্জন করতে হবে।", + "listings.sections.additionalEligibilityTitle": "অতিরিক্ত যোগ্যতা বিধি", + "listings.sections.additionalFees": "অতিরিক্ত খরচ", + "listings.sections.additionalFeesSubtitle": "আবেদনকারীর প্রয়োজনীয় অন্যান্য ফি সম্পর্কে আমাদের বলুন।", + "listings.sections.additionalInformationSubtitle": "প্রয়োজনীয় কাগজপত্র এবং নির্বাচনের মানদণ্ড", + "listings.sections.additionalInformationTitle": "অতিরিক্ত তথ্য", + "listings.sections.applicationAddressSubtitle": "কাগজের আবেদনের ক্ষেত্রে, আপনি কোথায় আবেদনগুলি বাদ দিতে বা মেইল ​​করতে চান?", + "listings.sections.applicationAddressTitle": "আবেদনের ঠিকানা", + "listings.sections.buildingDetailsSubtitle": "ভবনটি কোথায় অবস্থিত তা আমাদের বলুন।", + "listings.sections.buildingDetailsTitle": "বিল্ডিং বিবরণ", + "listings.sections.buildingFeaturesSubtitle": "যে কোন সুবিধা এবং ইউনিটের বিশদ বিবরণ প্রদান করুন।", + "listings.sections.buildingFeaturesTitle": "বিল্ডিং বৈশিষ্ট্য", + "listings.sections.applicationDatesTitle": "আবেদনের তারিখ", + "listings.sections.applicationDatesSubtitle": "এই তালিকা সম্পর্কিত গুরুত্বপূর্ণ তারিখগুলি সম্পর্কে আমাদের বলুন।", + "listings.sections.communityType": "সম্প্রদায়ের ধরন", + "listings.sections.communityTypeSubtitle": "আবেদনকারীদের কোন প্রয়োজনীয়তা পূরণ করতে হবে?", + "listings.sections.costsNotIncluded": "খরচ অন্তর্ভুক্ত নয়", + "listings.sections.eligibilitySubtitle": "আয়, দখল, পছন্দ এবং ভর্তুকি", + "listings.sections.eligibilityTitle": "যোগ্যতা", + "listings.sections.featuresSubtitle": "সুবিধা, ইউনিটের বিবরণ এবং অতিরিক্ত ফি", + "listings.sections.featuresTitle": "বৈশিষ্ট্য", + "listings.sections.housingPreferencesSubtitle": "অগ্রাধিকারীদের সর্বোচ্চ র ranking্যাঙ্কিং দেওয়া হবে।", + "listings.sections.housingPreferencesSubtext": "যোগ্যতা সম্পন্ন আবেদনকারীদের র rank্যাঙ্ক করতে ব্যবহার করা হবে এমন কোন পছন্দ সম্পর্কে আমাদের বলুন।", + "listings.sections.housingPreferencesTitle": "আবাসন পছন্দ", + "listings.sections.introSubtitle": "আপনার তালিকা সম্পর্কে কিছু প্রাথমিক তথ্য দিয়ে শুরু করা যাক।", + "listings.sections.introTitle": "তালিকা ভূমিকা", + "listings.sections.leasingAgentSubtitle": "লিজিং এজেন্ট সম্পর্কে বিস্তারিত তথ্য প্রদান করুন যিনি আবেদন প্রক্রিয়া পরিচালনা করবেন।", + "listings.sections.leasingAgentTitle": "লিজিং এজেন্ট", + "listings.sections.neighborhoodSubtitle": "অবস্থান এবং পরিবহন", + "listings.sections.neighborhoodTitle": "প্রতিবেশ", + "listings.sections.photoTitle": "তালিকা তালিকা", + "listings.sections.photoSubtitle": "তালিকাটির জন্য একটি ছবি আপলোড করুন যা পূর্বরূপ হিসাবে ব্যবহৃত হবে।", + "listings.sections.processSubtitle": "গুরুত্বপূর্ণ তারিখ এবং যোগাযোগের তথ্য", + "listings.sections.processTitle": "প্রক্রিয়া", + "listings.sections.publicProgramNote": "সাশ্রয়ী মূল্যের আবাসন সম্পত্তি প্রায়শই নির্দিষ্ট জনসংখ্যার জন্য তহবিল পায়, যেমন বয়স্ক, প্রতিবন্ধী বাসিন্দা ইত্যাদি। সম্পত্তি একাধিক জনসংখ্যাকে সেবা দিতে পারে। আপনি যোগ্য কিনা তা নিশ্চিত না হলে এই সম্পত্তির সাথে যোগাযোগ করুন।", + "listings.sections.rankingsResultsTitle": "র R্যাঙ্কিং এবং ফলাফল", + "listings.sections.rankingsResultsSubtitle": "একবার আবেদন জমা দিলে কি হয় সে সম্পর্কে বিস্তারিত তথ্য প্রদান করুন।", + "listings.sections.rentalAssistanceSubtitle": "এই সম্পত্তির জন্য হাউজিং চয়েস ভাউচার, বিভাগ 8 এবং অন্যান্য বৈধ ভাড়া সহায়তা প্রোগ্রাম বিবেচনা করা হবে। বৈধ ভাড়া ভর্তুকির ক্ষেত্রে, প্রয়োজনীয় ন্যূনতম আয় ভাড়ার অংশের উপর ভিত্তি করে হবে যা ভাড়াটিয়া ভর্তুকি ব্যবহারের পরে পরিশোধ করে।", + "listings.sections.rentalAssistanceTitle": "ভাড়া সহায়তা", + "listings.sections.addOpenHouse": "ওপেন হাউস যোগ করুন", + "listings.sections.openHouse": "খোলা বাড়ি", + "listings.sections.utilities": "যেসব পরিষেবা অন্তর্ভুক্ত", + "listings.seeMaximumIncomeInformation": "সর্বোচ্চ আয়ের তথ্য দেখুন", + "listings.seePreferenceInformation": "পছন্দ তথ্য দেখুন", + "listings.seeUnitInformation": "ইউনিটের তথ্য দেখুন", + "listings.selectPreferences": "পছন্দগুলি নির্বাচন করুন", + "listings.showClosedListings": "বন্ধ তালিকা দেখান", + "listings.specialNotes": "বিশেষ নোট", + "listings.streetAddressOrPOBox": "রাস্তার ঠিকানা বা পিও বক্স", + "listings.totalListings": "মোট তালিকা", + "listings.underConstruction": "নির্মানাধীন", + "listings.unitSummaryGroupMessage": "প্রতিটি ধরণের ইউনিটের জন্য, আপনি আপনার পরিবারের আকারের সাথে সম্পর্কিত আয়ের সীমার বেশি করতে পারবেন না, যেমনটি নীচের পরিবারের সর্বাধিক আয়ের সারণীতে দেখানো হয়েছে।", + "listings.section8MessageOpening": "আপনার যদি ", + "listings.section8FullName": "সেকশন 8 হাউজিং চয়েস ভাউচার থাকে", + "listings.section8MessageClosing": ", তাহলে আয়ের প্রয়োজনীয়তা প্রযোজ্য হবে না এবং আপনি আপনার আয়ের উপর ভিত্তি করে ভাড়া দেবেন।", + "listings.unitTypes.twoBdrm": "2 বিআর", + "listings.unitTypes.threeBdrm": "3 বিআর", + "listings.unitTypes.fourBdrm": "4 বিআর", + "listings.unitTypes.fiveBdrm": "5 বিআর", + "listings.unitTypes.studio": "স্টুডিও", + "listings.unitsAreFor": "এই ইউনিটগুলি %{type} এর জন্য।", + "listings.unitsHaveAccessibilityFeaturesFor": "এই ইউনিটে %{type} থাকা ব্যক্তিদের জন্য অ্যাক্সেসযোগ্যতার বৈশিষ্ট্য রয়েছে।", + "listings.upcomingLotteries.hide": "বন্ধ তালিকা লুকান", + "listings.upcomingLotteries.noResults": "এই সময়ে আসন্ন লটারির সাথে কোন বন্ধ তালিকা নেই।", + "listings.upcomingLotteries.show": "বন্ধ তালিকা দেখান", + "listings.upcomingLotteries.title": "বন্ধ তালিকা", + "listings.vacantUnits": "খালি ইউনিটসমূহ", + "listings.verifiedListing": "সম্পত্তির ভিত্তিতে নিশ্চিতকৃত", + "listings.waitlist.closed": "অপেক্ষার তালিকা বন্ধ", + "listings.waitlist.currentSizeQuestion": "বর্তমান তালিকায় কতজন আছেন?", + "listings.waitlist.label": "প্রতীক্ষার তালিকা", + "listings.waitlist.isOpen": "ওয়েটলিস্ট খোলা আছে", + "listings.waitlist.currentSize": "বর্তমান অপেক্ষার তালিকার আকার", + "listings.waitlist.finalSize": "চূড়ান্ত প্রতীক্ষার আকার", + "listings.waitlist.maxSize": "সর্বাধিক ওয়েটলিস্ট সাইজ", + "listings.waitlist.maxSizeQuestion": "অপেক্ষার তালিকার সর্বোচ্চ আকার কত?", + "listings.waitlist.open": "ওয়েটলিস্ট খুলুন", + "listings.waitlist.openQuestion": "ওয়েটলিস্ট কি খোলা আছে?", + "listings.waitlist.openSize": "খোলার সংখ্যা", + "listings.waitlist.openSizeQuestion": "তালিকায় কয়টি দাগ খোলা আছে?", + "listings.waitlist.openSlots": "ওয়েটলিস্ট স্লট খুলুন", + "listings.waitlist.sizeQuestion": "আপনি একটি অপেক্ষার তালিকার আকার দেখাতে চান?", + "listings.waitlist.submitAnApplication": "একবার র‍্যাঙ্ককৃত আবেদনকারীরা সমস্ত উপলভ্য ইউনিট পূরণ করলে, বাকি র‍্যাঙ্কিং আবেদনকারীদের সেই একই ইউনিটের জন্য অপেক্ষার তালিকায় রাখা হবে।", + "listings.waitlist.submitForWaitlist": "অপেক্ষার তালিকায় একটি খোলা স্লটের জন্য একটি আবেদন জমা দিন।", + "listings.waitlist.unitsAndWaitlist": "উপলব্ধ ইউনিট এবং অপেক্ষা তালিকা", + "listings.whatToExpectLabel": "আবেদনকারীকে প্রক্রিয়া থেকে কী আশা করতে হবে তা বলুন", + "listings.whereDropOffQuestion": "আবেদনগুলি কোথায় ফেলে দেওয়া হয়?", + "listings.wherePickupQuestion": "আবেদনগুলি কোথায় নেওয়া হয়?", + "listings.yearBuilt": "বছর নির্মিত", + "listings.whenApplicationsClose": "যখন অ্যাপ্লিকেশন জনসাধারণের কাছাকাছি", + "listings.cc&r": "চুক্তি, শর্ত এবং বিধিনিষেধ (CC & R's)", + "listings.cc&rDescription": "CC & R- এর মালিকদের সমিতির নিয়ম ব্যাখ্যা করে, এবং আপনি কিভাবে সম্পত্তি পরিবর্তন করতে পারেন তা সীমাবদ্ধ করুন।", + "listings.downloadPdf": "পিডিএফ ডাউনলোড করুন", + "listings.rePricing": "পুনরায় মূল্য নির্ধারণ", + "listings.eligibilityNotebook": "যোগ্যতা নোটবুক", + "listings.processInfo": "প্রক্রিয়া তথ্য", + "listings.featuresCards": "বৈশিষ্ট্য কার্ড", + "listings.neighborhoodBuildings": "প্রতিবেশী ভবন", + "listings.additionalInformationEnvelope": "অতিরিক্ত তথ্য খাম", + "listings.listingName": "তালিকার নাম", + "listings.applicationsSubmitted": "আবেদন জমা দেওয়া হয়েছে", + "listings.listingStatusText": "তালিকা অবস্থা", + "listings.applications": "অ্যাপ্লিকেশন", + "listings.unit.title": "ইউনিট", + "listings.unit.add": "ইউনিট যোগ করুন", + "listings.unit.number": "ইউনিট #", + "listings.unit.unitNumber": "একক সংখ্যা", + "listings.unit.type": "ইউনিটের প্রকার", + "listings.unit.ami": "আমি", + "listings.unit.amiChart": "আমি চার্ট", + "listings.unit.amiPercentage": "পার্সেন্টেজে অফ আমি", + "listings.unit.rent": "ভাড়া", + "listings.unit.sqft": "বর্গফুট", + "listings.unit.squareFootage": "বর্গক্ষেত্র ফুটেজ", + "listings.unit.priorityType": "এখানে", + "listings.unit.reservedType": "সংরক্ষিত", + "listings.unit.status": "স্থিতি", + "listings.unit.unitStatus": "ইউনিটের স্থিতি", + "listings.unit.details": "বিস্তারিত", + "listings.unit.numBathrooms": "বাথরুমের সংখ্যা", + "listings.unit.floor": "ইউনিট ফ্লোর", + "listings.unit.minOccupancy": "ন্যূনতম দখল", + "listings.unit.maxOccupancy": "সর্বোচ্চ দখল", + "listings.unit.rentType": "ভাড়া কিভাবে নির্ধারিত হয়?", + "listings.unit.fixed": "নির্দিষ্ট পরিমাণ", + "listings.unit.percentage": "% আয়ের", + "listings.unit.monthlyRent": "মাসিক ভাড়া", + "listings.unit.%incomeRent": "আয় ভাড়ার শতাংশ", + "listings.unit.accessibilityPriorityType": "অ্যাক্সেসযোগ্যতার অগ্রাধিকার প্রকার", + "listings.unit.unitTypes": "ইউনিটের প্রকারভেদ", + "listings.unit.individualUnits": "পৃথক ইউনিট", + "listings.unit.delete": "এই ইউনিটটি মুছুন", + "listings.unit.deleteConf": "আপনি কি সত্যিই এই ইউনিটটি মুছে ফেলতে চান?", + "listings.unit.eligibility": "যোগ্যতা", + "listings.unit.statusOptions.unknown": "অজানা", + "listings.unit.statusOptions.available": "পাওয়া যায়", + "listings.unit.statusOptions.occupied": "অধিকৃত", + "listings.unit.statusOptions.unavailable": "অনুপলব্ধ", + "listings.unitsSummary.add": "সারাংশ যোগ করুন", + "listings.unitsSummary.occupancy": "দখল", + "listings.unitsSummary.floorMin": "ন্যূনতম মেঝে", + "listings.unitsSummary.floorMax": "সর্বোচ্চ মেঝে", + "listings.unitsSummary.monthlyRentMin": "সর্বনিম্ন মাসিক ভাড়া", + "listings.unitsSummary.monthlyRentMax": "সর্বোচ্চ মাসিক ভাড়া", + "listings.unitsSummary.sqFeetMin": "ন্যূনতম স্কয়ার ফুটেজ", + "listings.unitsSummary.sqFeetMax": "সর্বোচ্চ স্কয়ার ফুটেজ", + "listings.unitsSummary.availability": "উপস্থিতি", + "listings.unitsSummary.count": "মোট গণনা", + "listings.unitsSummary.available": "মোট উপলব্ধ", + "listings.unitsSummary.delete": "এই সারাংশ মুছে ফেলুন", + "listings.unitsSummary.deleteConf": "আপনি কি সত্যিই এই সারাংশ মুছে ফেলতে চান?", + "listings.events.deleteThisEvent": "এই ইভেন্টটি মুছুন", + "listings.events.deleteConf": "আপনি কি সত্যিই এই ইভেন্টটি মুছে ফেলতে চান?", + "listings.events.openHouseNotes": "ওপেন হাউস নোট", + "listings.units": "তালিকা ইউনিট", + "listings.unitsDescription": "তালিকার মাধ্যমে যে বিল্ডিং ইউনিটগুলি পাওয়া যায় তা নির্বাচন করুন।", + "listings.unitTypesOrIndividual": "আপনি কি ইউনিটের ধরন বা স্বতন্ত্র ইউনিট দেখাতে চান?", + "listings.utilities.water": "পানি", + "listings.utilities.gas": "গ্যাস", + "listings.utilities.trash": "আবর্জনা", + "listings.utilities.sewer": "পয়ঃনিষ্কাশন", + "listings.utilities.electricity": "বিদ্যুৎ", + "listings.utilities.cable": "কেবল টিভি", + "listings.utilities.phone": "ফোন", + "listings.utilities.internet": "ইন্টারনেট", + "lottery.applicationsThatQualifyForPreference": "এই অগ্রাধিকার জন্য যোগ্যতা অর্জনকারী অ্যাপ্লিকেশনগুলিকে একটি উচ্চ অগ্রাধিকার দেওয়া হবে।", + "lottery.viewPreferenceList": "পছন্দ তালিকা দেখুন", + "nav.srHeading": "ন্যাভিগেশন মেনু", + "nav.srNavigation": "প্রধান", + "nav.accountSettings": "অ্যাকাউন্ট সেটিংস", + "nav.browseProperties": "বৈশিষ্ট্য ব্রাউজ করুন", + "nav.getFeedback": "এটি আমাদের নতুন ওয়েবসাইটের একটি প্রিভিউ। আমরা শুধু শুরু করছি। আমরা আপনার মতামত পেতে চাই। ", + "nav.listings": "তালিকা", + "nav.properties": "বৈশিষ্ট্য", + "nav.applications": "অ্যাপ্লিকেশন", + "nav.myAccount": "আমার অ্যাকাউন্ট", + "nav.myApplications": "আমার অ্যাপ্লিকেশন", + "nav.myDashboard": "আমার ড্যাশবোর্ড", + "nav.mySettings": "আমার সেটিংস", + "nav.rentals": "ভাড়া", + "nav.signIn": "সাইন ইন করুন", + "nav.signOut": "সাইন আউট", + "nav.signUp": "নিবন্ধন করুন", + "nav.siteTitle": "হাউজিং পোর্টাল", + "nav.siteTitlePartners": "অংশীদারদের পোর্টাল", + "nav.skip": "স্কিপ করে মূল কন্টেন্ট এ যাও", + "nav.flags": "পতাকা", + "nav.users": "ব্যবহারকারীরা", + "pageTitle.additionalResources": "আরও আবাসন সুযোগ", + "pageTitle.accessibilityStatement": "অ্যাক্সেসিবিলিটি স্টেটমেন্ট", + "pageTitle.terms": "অনুমোদন অস্বীকৃতি", + "pageTitle.housingCounselors": "হাউজিং কাউন্সিলর", + "pageTitle.getAssistance": "সহায়তা নিন", + "pageTitle.rentalListings": "ভাড়া দেখুন", + "pageTitle.rent": "সাশ্রয়ী মূল্যের আবাসন ভাড়া নিন", + "pageTitle.privacy": "গোপনীয়তা নীতি", + "pageTitle.welcomeEnglish": "স্বাগত", + "pageTitle.welcomeSpanish": "স্বাগত", + "pageTitle.welcomeVietnamese": "ভিয়েতনামী", + "pageTitle.about": "সম্পর্কিত", + "pageTitle.feedback": "ওয়েবসাইট প্রতিক্রিয়া শেয়ার করুন", + "pageTitle.resources": "সম্পদ", + "pageTitle.housingBasics": "সাশ্রয়ী আবাসন সংক্রান্ত সাধারণ তথ্য", + "pageDescription.welcome": "%{RegionName} এর হাউজিং পোর্টালে সাশ্রয়ী মূল্যের আবাসনের জন্য অনুসন্ধান করুন এবং আবেদন করুন", + "pageDescription.listing": "Exygy- এর সাথে অংশীদারিতে নির্মিত %{regionName} এ %{লিগিংনাম} -এ সাশ্রয়ী মূল্যের আবাসনের জন্য আবেদন করুন।", + "pageDescription.getAssistance": "স্থিতিশীল আবাসন খুঁজে পেতে আপনার প্রচেষ্টায় সহায়তা পেতে, নিম্নের রিসোর্স ও পরিষেবাগুলো ব্রাউজ করুন, অথবা সাশ্রয়ী আবাসন সম্পর্কে আরও জেনে নিন।", + "pageDescription.housingBasics": "আমরা বুঝি যে বাসা খোঁজা একটি জটিল ও ভীতিকর প্রক্রিয়া। সাশ্রয়ী আবাসন ও এগুলোর জন্য আবেদন করার প্রক্রিয়া সম্পর্কে আরও জানতে আপনি এই পেজটিতে বিভিন্ন রিসোর্স খুঁজে পাবেন।", + "region.name": "স্থানীয় অঞ্চল", + "resources.affordableHousingSubtitle": "আপনি সাশ্রয়ী আবাসনের জন্য কীভাবে উপযুক্ত হবেন ও আবেদন করবেন সেটি সম্পর্কে আরও জানুন", + "progressNav.srHeading": "অগ্রগতি", + "progressNav.current": "বর্তমান ধাপ: ", + "publicFilter.confirmedListings": "নিশ্চিত তালিকাসমূহ", + "publicFilter.confirmedListingsFieldLabel": "শুধুমাত্র সম্পত্তির ভিত্তিতে নিশ্চিতকৃত তালিকাসমূহ দেখান", + "publicFilter.bedRoomSize": "বেডরুমের আকার", + "publicFilter.rentRange": "মাসিক ভাড়ার পরিসর", + "publicFilter.rentRangeMin": "কোনো সর্বনিম্ন ভাড়া নেই", + "publicFilter.rentRangeMax": "কোনো সর্বোচ্চ ভাড়া নেই", + "publicFilter.communityTypes": "সম্প্রদায়ের ধরন", + "publicFilter.waitlist.open": "খোলা অপেক্ষমান তালিকা", + "publicFilter.waitlist.closed": "বন্ধ অপেক্ষমান তালিকা", + "seasons.fall": "শরৎ", + "seasons.spring": "বসন্ত", + "seasons.summer": "গ্রীষ্ম", + "seasons.winter": "শীত", + "states.AL": "আলাবামা", + "states.AK": "আলাস্কা", + "states.AZ": "অ্যারিজোনা", + "states.AR": "আরকানসাস", + "states.CA": "ক্যালিফোর্নিয়া", + "states.CO": "কলোরাডো", + "states.CT": "কানেকটিকাট", + "states.DE": "ডেলাওয়্যার", + "states.DC": "কলম্বিয়া জেলা", + "states.FL": "ফ্লোরিডা", + "states.GA": "জর্জিয়া", + "states.HI": "হাওয়াই", + "states.ID": "আইডাহো", + "states.IL": "ইলিনয়", + "states.IN": "ইন্ডিয়ানা", + "states.IA": "আইওয়া", + "states.KS": "কানসাস", + "states.KY": "কেনটাকি", + "states.LA": "লুইসিয়ানা", + "states.ME": "মেইন", + "states.MD": "মেরিল্যান্ড", + "states.MA": "ম্যাসাচুসেটস", + "states.MI": "মিশিগান", + "states.MN": "মিনেসোটা", + "states.MS": "মিসিসিপি", + "states.MO": "মিসৌরি", + "states.MT": "মন্টানা", + "states.NE": "নেব্রাস্কা", + "states.NV": "নেভাদা", + "states.NH": "নিউ হ্যাম্পশায়ার", + "states.NJ": "নতুন জার্সি", + "states.NM": "নতুন মেক্সিকো", + "states.NY": "নিউইয়র্ক", + "states.NC": "উত্তর ক্যারোলিনা", + "states.ND": "উত্তর ডাকোটা", + "states.OH": "ওহিও", + "states.OK": "ওকলাহোমা", + "states.OR": "ওরেগন", + "states.PA": "পেনসিলভেনিয়া", + "states.RI": "রোড আইল্যান্ড", + "states.SC": "সাউথ ক্যারোলিনা", + "states.SD": "দক্ষিন ডাকোটা", + "states.TN": "টেনেসি", + "states.TX": "টেক্সাস", + "states.UT": "উটাহ", + "states.VT": "ভারমন্ট", + "states.VA": "ভার্জিনিয়া", + "states.WA": "ওয়াশিংটন", + "states.WV": "পশ্চিম ভার্জিনিয়া", + "states.WI": "উইসকনসিন", + "states.WY": "ওয়াইমিং", + "t.areYouSure": "তুমি কি নিশ্চিত?", + "t.addNotes": "নোট যোগ করুন", + "t.at": "এ", + "t.additionalPhone": "অতিরিক্ত ফোন", + "t.area": "এলাকা", + "t.areYouStillWorking": "তুমি কি এখনও কাজ করছো?", + "t.accessibility": "প্রবেশযোগ্যতা", + "t.additionalAccessibility": "অতিরিক্ত অ্যাক্সেসযোগ্যতার বিবরণ", + "t.am": "এএম", + "t.availability": "প্রাপ্যতা", + "t.automatic": "স্বয়ংক্রিয়", + "t.back": "পেছনে", + "t.built": "নির্মিত", + "t.call": "ডাক", + "t.cancel": "বাতিল করুন", + "t.confirm": "নিশ্চিত করুন", + "t.contactPropertyManagement": "সম্পত্তি পরিচালনার সাথে যোগাযোগ করুন", + "t.chooseFromFolder": "ফোল্ডার থেকে বেছে নিন", + "t.custom": "কাস্টম", + "t.day": "দিন", + "t.date": "তারিখ", + "t.delete": "মুছে ফেলা", + "t.deposit": "আমানত", + "t.done": "সম্পন্ন", + "t.descriptionTitle": "বর্ণনা", + "t.description": "বিবরণ লিখুন", + "t.emailAddressPlaceholder": "ইউ@মেয়েমাঈল.কম", + "t.end": "শেষ", + "t.dragFilesHere": "ফাইলগুলি এখানে টেনে আনুন", + "t.dropFilesHere": "ফাইলগুলো এখানে ফেলে দিন ...", + "t.export": "রপ্তানি", + "t.enterAmount": "পরিমান লিখুন", + "t.fileName": "ফাইলের নাম", + "t.filter": "ছাঁকনি", + "t.edit": "সম্পাদনা করুন", + "t.email": "ইমেইল", + "t.finish": "শেষ করুন", + "t.floor": "মেঝে", + "t.floors": "মেঝে", + "t.getDirections": "দিকনির্দেশ পান", + "t.hour": "ঘন্টা", + "t.household": "গৃহস্থালি", + "t.income": "আয়", + "t.jumpTo": "লাফ দাও", + "t.jurisdiction": "এখতিয়ার", + "t.label": "লেবেল", + "t.lastUpdated": "সর্বশেষ সংষ্করণ", + "t.letter": "চিঠি", + "t.less": "কম", + "t.link": "লিঙ্ক", + "t.listing": "তালিকা", + "t.loginIsRequired": "এই পৃষ্ঠাটি দেখতে লগইন প্রয়োজন।", + "t.max": "সর্বোচ্চ", + "t.menu": "তালিকা", + "t.min": "ন্যূনতম", + "t.minimumIncome": "ন্যূনতম আয়", + "t.minutes": "মিনিট", + "t.month": "মাস", + "t.more": "আরো", + "t.n/a": "n/a", + "t.name": "নাম", + "t.neighborhood": "প্রতিবেশ", + "t.next": "পরবর্তী", + "t.no": "না", + "t.none": "কোনটিই নয়", + "t.noneFound": "কোন পাওয়া যায়নি।", + "t.notes": "মন্তব্য", + "t.occupancy": "দখল", + "t.ok": "ঠিক আছে", + "t.optional": "চ্ছিক", + "t.or": "অথবা", + "t.order": "আদেশ", + "t.pageXofY": "%{Total} এর %{number} পৃষ্ঠা", + "t.people": "মানুষ", + "t.person": "ব্যক্তি", + "t.perMonth": "প্রতি মাসে", + "t.perYear": "প্রতি বছরে", + "t.petsPolicy": "পোষা প্রাণী নীতি", + "t.phone": "ফোন", + "t.phoneNumberPlaceholder": "(৫৫৫) ৫৫৫-৫৫৫৫", + "t.pleaseSelectOne": "অনুগ্রহপূর্বক একটা নির্বাচন করুন.", + "t.pm": "PM", + "t.post": "পোস্ট", + "t.preferences": "পছন্দ", + "t.preview": "প্রিভিউ", + "t.previous": "পূর্ববর্তী", + "t.propertyAmenities": "সম্পত্তি সুবিধা", + "t.range": "%{থেকে} থেকে %{থেকে}", + "t.readLess": "কম পড়ুন", + "t.readMore": "আরো পড়ুন", + "t.region": "অঞ্চল", + "t.relationship": "সম্পর্ক", + "t.otherRelationShip": "অন্যান্য সম্পর্ক", + "t.rent": "ভাড়া", + "t.review": "পুনঃমূল্যায়ন", + "t.role": "ভূমিকা", + "t.secondPhone": "দ্বিতীয় ফোন", + "t.seconds": "সেকেন্ড", + "t.seeDetails": "বিস্তারিত দেখুন", + "t.seeListing": "তালিকা দেখুন", + "t.selectOne": "একটা নির্বাচন করুন", + "t.servicesOffered": "প্রস্তাবিত সেবাসমূহ", + "t.show": "দেখান", + "t.showLess": "প্রদর্শন কম", + "t.showMore": "আরো দেখুন", + "t.skipToMainContent": "স্কিপ করে মূল কন্টেন্ট এ যাও", + "t.smokingPolicy": "ধূমপান নীতি", + "t.sort": "সাজান", + "t.sqFeet": "বর্গফুট", + "t.squareFeet": "বর্গফুট", + "t.statusHistory": "স্থিতির ইতিহাস", + "t.startTime": "সময় শুরু", + "t.endTime": "শেষ সময়", + "t.street": "রাস্তা", + "t.submit": "জমা দিন", + "t.submitNew": "জমা দিন এবং নতুন", + "t.copyNew": "কপি এবং নতুন", + "t.save": "সংরক্ষণ", + "t.saveNew": "সেভ করুন এবং নতুন", + "t.saveExit": "সংরক্ষণ করুন এবং প্রস্থান করুন", + "t.time": "সময়", + "t.text": "পাঠ্য", + "t.to": "প্রতি", + "t.totalCount": "মোট গণনা", + "t.unit": "ইউনিট", + "t.units": "ইউনিট", + "t.unitAmenities": "ইউনিট সুবিধা", + "t.unitFeatures": "ইউনিটের বৈশিষ্ট্য", + "t.unitType": "ইউনিটের প্রকার", + "t.url": "URL", + "t.view": "দেখুন", + "t.viewListings": "তালিকা দেখুন", + "t.viewMap": "মানচিত্র দেখুন", + "t.viewOnMap": "মানচিত্রে দেখুন", + "t.website": "ওয়েবসাইট", + "t.year": "বছর", + "t.yes": "হ্যাঁ", + "t.you": "আপনি", + "welcome.allApplicationClosed": "সমস্ত অ্যাপ্লিকেশন বর্তমানে বন্ধ, কিন্তু আপনি বন্ধ তালিকা দেখতে পারেন।", + "welcome.seeRentalListings": "সব ভাড়া দেখুন", + "welcome.title": "সাশ্রয়ী মূল্যের আবাসনের জন্য আবেদন করুন", + "welcome.seeMoreOpportunities": "আরও ভাড়া এবং মালিকানার আবাসন সুযোগ দেখুন", + "welcome.viewAdditionalHousing": "অতিরিক্ত আবাসন সুযোগ এবং সম্পদ দেখুন", + "welcome.bedrooms.studios": "(%{smart_count})টি স্টুডিও উপলব্ধ |||| (%{smart_count}) studios available", + "welcome.bedrooms.numBed": "(%{smart_count}) %{num_bed} বেডরুম উপলব্ধ |||| (%{smart_count}) %{num_bed} বেডরুম উপলব্ধ", + "welcome.bedrooms.fourPlusBed": "(%{smart_count}) 1 4+ বেডরুম উপলব্ধ |||| (%{smart_count}) 4+ বেডরুম উপলব্ধ", + "welcome.checkEligibility": "আমি কি যোগ্যতা অর্জন করি?", + "welcome.checkEligibilityDescription": "আপনি যোগ্য হলে মিনিট চেক করুন!", + "welcome.findRentalsForMe": "আমার জন্য ভাড়া খুঁজুন", + "welcome.latestListings": "সর্বশেষ তালিকা", + "welcome.lastUpdated": "সর্বশেষ আপডেট করা হয়েছে %{date} তারিখে", + "welcome.cityRegions": "শহরের অঞ্চলগুলি", + "welcome.signUp": "কোনো নতুন তালিকা পোস্ট করার সাথে-সাথে বার্তা পান", + "welcome.signUpToday": "আজই সাইন-আপ করুন", + "welcome.subTitle": "আপনার আয় এবং গৃহস্থালি চাহিদার উপর ভিত্তি করে ভাড়া আবাসন খুঁজে পেতে নীচের বোতামে ক্লিক করুন", + "welcome.underConstructionButton": "নির্মাণাধীন সব দেখুন", + "welcome.learnMore": "আরও জেনে নিন", + "welcome.learnHousingBasics": "আপনি সাশ্রয়ী আবাসনের জন্য কীভাবে উপযুক্ত হবেন ও আবেদন করবেন সেটি জানুন", + "whatToExpect.label": "কি আশা করছ", + "whatToExpect.default": "শূন্যপদ পূরণ না হওয়া পর্যন্ত আবেদনকারীরা র agent্যাঙ্ক ক্রমে সম্পত্তি এজেন্টের সাথে যোগাযোগ করবে। আপনার দেওয়া সমস্ত তথ্য যাচাই করা হবে এবং আপনার যোগ্যতা নিশ্চিত করা হবে। যদি আপনি কোন প্রতারণামূলক বিবৃতি দিয়ে থাকেন তাহলে আপনার আবেদনটি প্রতীক্ষার তালিকা থেকে সরিয়ে দেওয়া হবে। যদি আমরা আপনার দাবি করা আবাসন পছন্দ যাচাই করতে না পারি, তাহলে আপনি অগ্রাধিকার পাবেন না কিন্তু অন্যথায় শাস্তি পাবেন না। আপনার আবেদন নির্বাচন করা উচিত, আরো বিস্তারিত আবেদন পূরণ এবং প্রয়োজনীয় সহায়ক নথি প্রদান করার জন্য প্রস্তুত থাকুন।", + "listingFilters.allRentals": "সমস্ত ভাড়া", + "listingFilters.buttonTitle": "ছাঁকনি", + "listingFilters.buttonTitleExtended": "আপনার জন্য ভাড়া বাড়ি খুঁজুন", + "listingFilters.loading": "লোড হচ্ছে...", + "listingFilters.rentalsFound": "%{smart_count} ভাড়া পাওয়া গেছে |||| %{smart_count} ভাড়া পাওয়া গেছে", + "listingFilters.resetButton": "রিসেট", + "listingFilters.modalTitle": "ফিল্টার ফল", + "listingFilters.modalHeader": "আপনার বৈশিষ্ট্যের তালিকা পরিমার্জন করতে এই বিকল্পগুলি ব্যবহার করুন।", + "listingFilters.neighborhood": "প্রতিবেশ", + "listingFilters.bedrooms": "বেডরুম", + "listingFilters.bedroomsOptions.studioPlus": "স্টুডিও", + "listingFilters.bedroomsOptions.onePlus": "1 বেডরুম", + "listingFilters.bedroomsOptions.twoPlus": "2 বেডরুম", + "listingFilters.bedroomsOptions.threePlus": "3 বেডরুম", + "listingFilters.bedroomsOptions.fourPlus": "4 বা তার বেশি বেডরুম", + "listingFilters.zipCode": "জিপ কোড", + "listingFilters.zipCodeDescription": "জিপ কোড প্রবেশ", + "listingFilters.region": "অঞ্চল", + "listingFilters.adaCompliant": "विकलांग निवासियों के लिए सुलभ सुविधाएँ?", + "listingFilters.rentRange": "ভাড়া পরিসীমা", + "listingFilters.availability": "ইউনিট প্রাপ্যতা", + "listingFilters.hasAvailability": "প্রাপ্যতা আছে", + "listingFilters.noAvailability": "সক্ষমতা নেই", + "listingFilters.waitlist": "প্রতীক্ষার তালিকা", + "listingFilters.applyFilter": "ফিল্টার প্রয়োগ করুন", + "listingFilters.noResults": "কোন ফলাফল নেই", + "listingFilters.includeUnknowns": "অনুপস্থিত তথ্য সহ বাড়ি দেখান", + "listingFilters.senior": "সিনিয়র হাউজিং (62+)", + "listingFilters.independentLivingHousing": "স্বাধীন জীবিত সম্প্রদায়", + "listingFilters.minAmiPercentageLabel": "যাদের আয় আছে তাদের জন্য ইউনিট আলাদা রাখা হয়েছে", + "listingFilters.minAmiPercentageOptions.amiOption20": "এএমআই এর 20% বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption25": "25% AMI বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption30": "এএমআই এর 30% বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption35": "35% AMI বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption40": "40% AMI বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption45": "45% AMI বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption50": "এএমআই এর 50% বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption55": "55% AMI বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption60": "এএমআই এর 60% বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption70": "এএমআই এর 70% বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption80": "80% AMI বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption100": "এএমআই এর 100% বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption120": "120% AMI বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption125": "125% AMI বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption140": "140% AMI বা তার বেশি", + "listingFilters.minAmiPercentageOptions.amiOption150": "150% AMI বা তার বেশি", + "eligibility.progress.header": "আমার জন্য ভাড়া খুঁজুন", + "eligibility.progress.sections.welcome": "স্বাগত", + "eligibility.progress.sections.household": "পরিবার", + "eligibility.progress.sections.age": "আগে", + "eligibility.progress.sections.disability": "অক্ষমতা", + "eligibility.progress.sections.accessibility": "অ্যাক্সেসযোগ্যতা", + "eligibility.progress.sections.income": "আয়", + "eligibility.progress.sections.terms": "দাবিত্যাগ", + "eligibility.welcome.header": "স্বাগত", + "eligibility.welcome.description": "Detroit Home Connect এ স্বাগতম! আপনি যে ভাড়ার জন্য যোগ্য হতে পারেন তা দেখতে, শুধু চারটি প্রশ্নের উত্তর দিন।", + "eligibility.household.prompt": "আপনার পরবর্তী ভাড়ায় আপনি সহ কতজন লোক বাস করবে?", + "eligibility.household.srCountLabel": "পরিবারের আকার", + "eligibility.household.ranges.one": "1 সদস্য", + "eligibility.household.ranges.two": "2 সদস্য", + "eligibility.household.ranges.three": "3 সদস্য", + "eligibility.household.ranges.four": "4 সদস্য", + "eligibility.household.ranges.five": "5 সদস্য", + "eligibility.household.ranges.six": "6 সদস্য", + "eligibility.household.ranges.seven": "7 সদস্য", + "eligibility.household.ranges.eight": "8+ সদস্য", + "eligibility.age.prompt": "আপনার বয়স কত?", + "eligibility.age.description": "আপনি একাধিক বয়সের সীমা বেছে নিতে পারেন। কিছু ভাড়ার ন্যূনতম বয়সের প্রয়োজনীয়তা রয়েছে।", + "eligibility.age.lessThan55": "< 55", + "eligibility.age.55to61": "55 - 61", + "eligibility.age.62plus": "62+", + "eligibility.disability.prompt": "আপনার পরিবারের কেউ কি অক্ষমতা আছে?", + "eligibility.disability.description": "আপনি যখন কিছু বাড়ির জন্য আবেদন করেন তখন আপনাকে অক্ষমতার প্রমাণ দিতে হতে পারে।", + "eligibility.income.prompt": "আপনি সহ যারা আপনার সাথে বসবাস করবেন তাদের প্রত্যেকের আনুমানিক মোট বার্ষিক আয় কত?", + "eligibility.income.description": "আপনার এবং যারা আপনার সাথে বসবাস করবে তাদের জন্য গত 12 মাসের আয় অন্তর্ভুক্ত করুন। আপনি যখন ভাড়ার জন্য আবেদন করেন, আপনার আনুমানিক বার্ষিক আয় যোগ্যতার প্রয়োজনীয়তা পূরণ করে কিনা তা নির্ধারণ করতে সম্পত্তিগুলি অন্যান্য বিষয়গুলিও বিবেচনা করবে। আয়ের উদাহরণ:", + "eligibility.income.examples.wages": "মজুরি এবং টিপস", + "eligibility.income.examples.socialSecurity": "সামাজিক নিরাপত্তা", + "eligibility.income.examples.retirement": "অবসর আয়", + "eligibility.income.examples.unemployment": "বেকার ভাতা", + "eligibility.income.label": "আয়ের পরিসীমা", + "eligibility.income.ranges.below10k": "$0 - $9,999", + "eligibility.income.ranges.10kTo20k": "$10,000 - $19,999", + "eligibility.income.ranges.20kTo30k": "$20,000 - $29,999", + "eligibility.income.ranges.30kTo40k": "$30,000 - $39,999", + "eligibility.income.ranges.40kTo50k": "$40,000 - $49,999", + "eligibility.income.ranges.over50k": "$50,000 বা তার বেশি", + "eligibility.accessibility.accessibleParking": "প্রবেশযোগ্য পার্কিং স্পট", + "eligibility.accessibility.acInUnit": "ইউনিট এ এসি", + "eligibility.accessibility.barrierFreeBathroom": "বাধা মুক্ত বাথরুম", + "eligibility.accessibility.barrierFreeEntrance": "বাধা-মুক্ত (কোন-পদক্ষেপ) সম্পত্তি প্রবেশদ্বার", + "eligibility.accessibility.barrierFreeUnitEntrance": "বাধা-মুক্ত (কোন-পদক্ষেপ) ইউনিট প্রবেশদ্বার", + "eligibility.accessibility.description": "কিছু বৈশিষ্ট্যে অ্যাক্সেসিবিলিটি বৈশিষ্ট্য রয়েছে যা অন্যদের নাও থাকতে পারে।", + "eligibility.accessibility.elevator": "লিফট", + "eligibility.accessibility.grabBars": "বাথরুম মধ্যে বার দখল", + "eligibility.accessibility.hearing": "শ্রবণ প্রতিবন্ধীদের জন্য ইউনিট", + "eligibility.accessibility.heatingInUnit": "হিটিং ইউনিট", + "eligibility.accessibility.inUnitWasherDryer": "ইন-ইউনিট ওয়াশার/ড্রায়ার", + "eligibility.accessibility.laundryInBuilding": "বিল্ডিংয়ে লন্ড্রি", + "eligibility.accessibility.loweredCabinets": "নীচের ক্যাবিনেট এবং কাউন্টারটপ", + "eligibility.accessibility.loweredLightSwitch": "কম আলোর সুইচ", + "eligibility.accessibility.mobility": "যারা চলাফেরায় অক্ষম তাদের জন্য ইউনিট", + "eligibility.accessibility.parkingOnSite": "সাইটে পার্কিং", + "eligibility.accessibility.prompt": "আপনার কি অতিরিক্ত অ্যাক্সেসিবিলিটি বৈশিষ্ট্য প্রয়োজন?", + "eligibility.accessibility.rollInShower": "রোল ইন ঝরনা", + "eligibility.accessibility.serviceAnimalsAllowed": "পরিষেবা অনুমোদিত", + "eligibility.accessibility.title": "অভিগম্যতা বৈশিষ্ট্য", + "eligibility.accessibility.visual": "দৃষ্টি প্রতিবন্ধী ব্যক্তিদের জন্য ইউনিট", + "eligibility.accessibility.wheelchairRamp": "হুইলচেয়ার র‌্যাম্প", + "eligibility.accessibility.wideDoorways": "হুইলচেয়ারের জন্য প্রশস্ত ইউনিট দরজা", + "eligibility.disclaimer.description": "এই প্রশ্নের উত্তর দেওয়ার জন্য আপনাকে ধন্যবাদ. আপনি যখন \"এখন ফলাফল দেখুন\" ক্লিক করেন বা ট্যাপ করেন, তখন আপনি ভাড়া দেখতে পাবেন যা আপনার উত্তরের উপর ভিত্তি করে আপনার প্রয়োজনের সাথে মানানসই হতে পারে। একাধিক কারণ নির্দিষ্ট ভাড়ার জন্য যোগ্যতাকে প্রভাবিত করতে পারে, তাই আপনি যদি এমন একটি ভাড়া দেখতে পান যা আপনি আগ্রহী, তালিকায় থাকা সম্পত্তি এজেন্টের সাথে যোগাযোগ করুন৷ আপনি সেই ভাড়ার জন্য যোগ্য কিনা তা নির্ধারণ করতে তারা আপনাকে সাহায্য করতে পারে।", + "eligibility.preferNotToSay": "না বলা পছন্দ", + "resources.body1": "আবাসন অনুসন্ধানে আপনার অতিরিক্ত সহায়তার প্রয়োজন হতে পারে। আপনার আবাসন খুঁজে পেতে এবং বজায় রাখতে সাহায্য করার জন্য HRD সম্পদের একটি তালিকা তৈরি করেছে।", + "resources.evictionAssistance": "উচ্ছেদ সহায়তা", + "resources.detroitHousingNetwork": "ডেট্রয়েট হাউজিং নেটওয়ার্ক", + "resources.detroitHousingNetworkBody": "ডেট্রয়েট হাউজিং নেটওয়ার্ক হল ডেট্রয়েট কমিউনিটি সংস্থাগুলির একটি সংগ্রহ যা বাড়ির মালিক এবং ভাড়াটেদের জন্য বিভিন্ন পরিষেবা প্রদান করে, যার মধ্যে ইউটিলিটি সহায়তা, ভাড়াটে এবং বাড়িওয়ালা কাউন্সেলিং, উচ্ছেদ কাউন্সেলিং এবং সম্পত্তি ট্যাক্স সমাধান রয়েছে৷ আরও তথ্যের জন্য, ভিজিট করুন ", + "resources.utilityAssistance": "ইউটিলিটি সহায়তা", + "resources.homelessnessServices": "গৃহহীনতা সেবা", + "resources.detroitLandBankAuthority": "ডেট্রয়েট ল্যান্ড ব্যাংক কর্তৃপক্ষ", + "resources.homeRepairResources": "বাড়ি মেরামতের সংস্থান", + "resources.affordableHousingTitle": "আবাসন বিষয়ক সাধারণ তথ্য", + "resources.affordableHousingLinkLabel": "এটি কীভাবে কাজ করে তা জানতে আরও পড়ুন", + "resources.housingResourcesTitle": "আবাসন সংক্রান্ত অতিরিক্ত রিসোর্সসমূহ", + "resources.housingResourcesSubtitle": "আপনার আবাসনের খোঁজে স্থানীয় রিসোর্স ও পরিষেবাগুলো ব্রাউজ করুন", + "resources.housingResourcesLinkLabel": "কমিউনিটি রিসোর্সগুলো দেখুন" +} diff --git a/shared-helpers/src/locales/es.json b/shared-helpers/src/locales/es.json new file mode 100644 index 0000000000..c67babf6d3 --- /dev/null +++ b/shared-helpers/src/locales/es.json @@ -0,0 +1,742 @@ +{ + "about.body1": "Sabemos que encontrar una vivienda que satisfaga sus necesidades puede ser difícil y frustrante. Detroit Home Connect es su mano amiga para encontrar un nuevo lugar al que llamar hogar.", + "about.body2": "Detroit Home Connect es un nuevo servicio de la ciudad de Detroit que le brinda un primer paso central para encontrar una vivienda en Detroit que satisfaga sus necesidades económicas y familiares. Puede comprender su elegibilidad para unidades de alquiler explorando opciones basadas en el tamaño, la edad y los ingresos de su familia. Detroit Home Connect es una iniciativa del Departamento de Vivienda y Revitalización de la Ciudad de Detroit. El diseño y las funciones del sitio web se basan en los comentarios y las opiniones de los residentes del área, las organizaciones comunitarias, los administradores de propiedades y los propietarios.", + "about.moreInfoContact": "Para obtener más información, comuníquese con el personal de la Ciudad en detroithomeconnect@detroitmi.gov.", + "about.thankYouPartners": "El Departamento de Vivienda y Revitalización de la ciudad de Detroit desea expresar su agradecimiento a las siguientes organizaciones que colaboraron en el desarrollo de Detroit Home Connect:", + "account.accountSettings": "Configuraciones de la cuenta", + "account.accountSettingsSubtitle": "Configuraciones de la cuenta, email y contraseña", + "account.createAccount": "Crear una cuenta", + "account.haveAnAccount": "¿Ya tiene una cuenta?", + "account.myApplications": "Mis Solicitudes", + "account.myApplicationsSubtitle": "Vea las fechas de la lotería y los listados de las propiedades para las que ha presentado solicitudes", + "account.myFavorites": "Mis favoritos", + "account.myFavoritesSubtitle": "Guardar las ofertas y volver a buscar actualizaciones", + "alert.unavailable": "Detroit Home Connect podría no estar disponible temporalmente la semana del 2 de septiembre de 2025 debido a una actualización programada. Se prevé que el sitio web esté inactivo hasta 48 horas durante este período. Si tiene alguna pregunta o inquietud, contáctenos en: detroithomeconnect@detroitmi.gov", + "application.ada.hearing": "Para dificultades auditivas", + "application.ada.label": "Viviendas accesibles de conformidad con ADA", + "application.ada.mobility": "Para dificultades en la movilidad", + "application.ada.subTitle": "Si usted es seleccionado para una vivienda, la propiedad hará todo lo posible para satisfacer sus necesidades. Si su solicitud fuera elegida, esté preparado para proporcionar documentación de apoyo de su médico.", + "application.ada.title": "¿Necesita usted o algún miembro de su hogar alguna de las siguientes funciones de accesibilidad de ADA?", + "application.ada.vision": "Para dificultades en la visita", + "application.alternateContact.contact.contactMailingAddressHelperText": "Elija una dirección en donde su contacto pueda recibir información actualizada y materiales relacionados con su solicitud", + "application.alternateContact.contact.contactMailingAddressLabel": "Dirección postal del contacto", + "application.alternateContact.contact.description": "Solo utilizaremos esta información para ponernos en contacto con él o ella en relación con su solicitud.", + "application.alternateContact.contact.emailAddressFormLabel": "Dirección de email del contacto", + "application.alternateContact.contact.phoneNumberFormLabel": "Número telefónico del contacto", + "application.alternateContact.contact.title": "Díganos cómo comunicarnos con su contacto alternativo", + "application.alternateContact.name.alternateContactFormLabel": "Nombre del contacto alternativo", + "application.alternateContact.name.caseManagerAgencyFormLabel": "¿En dónde trabaja su administrador(a) de casos o asesor(a) sobre vivienda?", + "application.alternateContact.name.caseManagerAgencyFormPlaceHolder": "Agencia", + "application.alternateContact.name.caseManagerAgencyValidationErrorMessage": "Por favor ingrese una agencia", + "application.alternateContact.name.title": "¿Quién es su contacto alternativo?", + "application.alternateContact.type.description": "Al proporcionar un contacto alternativo nos permite discutir información acerca de su solicitud con él o ella.", + "application.alternateContact.type.label": "Contacto alternativo", + "application.alternateContact.type.options.caseManager": "Administrador(a) de casos o asesor(a) sobre vivienda", + "application.alternateContact.type.options.familyMember": "Familiar", + "application.alternateContact.type.options.friend": "Amigo(a)", + "application.alternateContact.type.options.noContact": "No tengo un contacto alternativo", + "application.alternateContact.type.options.other": "Otro", + "application.alternateContact.type.otherTypeFormPlaceholder": "¿Cuál es su parentesco o relación?", + "application.alternateContact.type.otherTypeValidationErrorMessage": "Por favor ingrese el tipo de parentesco o relación", + "application.alternateContact.type.title": "¿Hay alguna otra persona que usted desee autorizar para que nos comuniquemos con él o ella si no podemos ponernos en contacto con usted?", + "application.alternateContact.type.validationErrorMessage": "Por favor seleccione un contacto alternativo", + "application.autofill.prefillYourApplication": "Simplemente rellenaremos automáticamente su solicitud con los siguientes detalles, y usted puede actualizarlos mientras los vaya completando.", + "application.autofill.reset": "Empezar de cero", + "application.autofill.saveTime": "Ahorre tiempo cargando los detalles de su última solicitud", + "application.autofill.start": "Empezar con estos detalles", + "application.chooseLanguage.chooseYourLanguage": "Elija su idioma", + "application.chooseLanguage.letsGetStarted": "Empecemos su solicitud", + "application.chooseLanguage.signInSaveTime": "Iniciar sesión podría ahorrarle tiempo al empezar con los detalles de su última solicitud, y permitirle verificar el estatus de esta solicitud en cualquier momento.", + "application.confirmation.informationSubmittedTitle": "Esta es la información que usted envió.", + "application.confirmation.lotteryNumber": "Su número de confirmación", + "application.confirmation.printCopy": "Imprima una copia para sus archivos", + "application.confirmation.submitted": "Enviada: ", + "application.confirmation.viewOriginalListing": "Ver el listado original", + "application.contact.additionalPhoneNumber": "Tengo un número telefónico adicional", + "application.contact.address": "Dirección", + "application.contact.addressWhereYouCurrentlyLive": "Necesitamos saber cuál es la dirección en donde vive actualmente. Si no tiene hogar, ingrese ya sea la dirección del refugio o una dirección cercana a donde se esté quedando.", + "application.contact.apt": "# de Apto o unidad", + "application.contact.cityName": "Nombre de la Ciudad", + "application.contact.contactPreference": "¿Cómo prefiere que nos comuniquemos con usted?", + "application.contact.doYouWorkIn": "¿Trabaja usted en ?", + "application.contact.doYouWorkInDescription": "Por decidirse", + "application.contact.mailingAddress": "Dirección postal", + "application.contact.noPhoneNumber": "No tengo un número telefónico", + "application.contact.phoneNumberTypes.cell": "Celular", + "application.contact.phoneNumberTypes.home": "Casa", + "application.contact.phoneNumberTypes.prompt": "¿Qué tipo de número es este?", + "application.contact.phoneNumberTypes.work": "Trabajo", + "application.contact.preferredContactType": "Tipo de contacto preferido", + "application.contact.provideAMailingAddress": "Proporcione una dirección en donde pueda recibir información actualizada y materiales relacionados con su solicitud.", + "application.contact.sendMailToMailingAddress": "Envíenme mi correspondencia a una dirección diferente", + "application.contact.state": "Estado", + "application.contact.streetAddress": "Domicilio", + "application.contact.title": "Gracias %{firstName}. Ahora, necesitamos saber cómo comunicarnos con usted.", + "application.contact.workAddress": "Dirección del trabajo", + "application.contact.yourPhoneNumber": "Su número telefónico", + "application.contact.zip": "Código Postal", + "application.contact.zipCode": "Código Postal", + "application.edited": "Editada", + "application.financial.income.instruction1": "Sume los ingresos brutos totales (antes de impuestos) provenientes de salarios, beneficios y otras fuentes de todos los miembros del hogar.", + "application.financial.income.instruction2": "En este momento solo tiene que proporcionar un total aproximado. El total real será calculado si usted es seleccionado(a).", + "application.financial.income.placeholder": "Saque el total de todas sus fuentes de ingresos", + "application.financial.income.prompt": "¿Cuáles son los ingresos totales de su hogar antes de impuestos?", + "application.financial.income.title": "Pasemos a los ingresos.", + "application.financial.income.validationError.instruction1": "Por favor haga cambios si usted cree haber cometido algún error. Tenga presente que si usted falsifica cualquier tipo de información en su solicitud será descalificado(a).", + "application.financial.income.validationError.instruction2": "Si la información que usted ingresó es correcta, lo invitamos a visitarnos en el futuro a medida que otras propiedades están disponibles.", + "application.financial.income.validationError.reason.high": "Los ingresos de su hogar son demasiado altos.", + "application.financial.income.validationError.reason.low": "Los ingresos de su hogar son demasiado bajos.", + "application.financial.vouchers.housingVouchers.strong": "Cupones de viviendas", + "application.financial.vouchers.housingVouchers.text": "como Section 8", + "application.financial.vouchers.nonTaxableIncome.strong": "Ingresos no gravables de impuestos", + "application.financial.vouchers.nonTaxableIncome.text": "como SSI, SSDI, pagos de manutención infantil o beneficios de compensaciones del trabajador", + "application.financial.vouchers.rentalSubsidies.strong": "Subsidios en el alquiler", + "application.financial.vouchers.rentalSubsidies.text": "como VASH, HSA, HOPWA, Catholic Charities, AIDS Foundation, etc.", + "application.financial.vouchers.title": "¿Recibe usted o alguna otra persona de esta solicitud alguno de los siguientes?", + "application.form.general.saveAndFinishLater": "Guardar y terminar más tarde", + "application.form.general.saveAndReturn": "Guardar y regresar a revisión", + "application.form.options.relationship.aunt": "Tía", + "application.form.options.relationship.child": "Niño(a)", + "application.form.options.relationship.cousin": "Primo(a)", + "application.form.options.relationship.friend": "Amigo(a)", + "application.form.options.relationship.grandparent": "Abuelo(a)", + "application.form.options.relationship.greatGrandparent": "Bisabuelo(a)", + "application.form.options.relationship.inLaw": "Pariente político", + "application.form.options.relationship.nephew": "Sobrino", + "application.form.options.relationship.niece": "Sobrina", + "application.form.options.relationship.other": "Otro", + "application.form.options.relationship.parent": "Padre de familia", + "application.form.options.relationship.registeredDomesticPartner": "Pareja doméstica registrada", + "application.form.options.relationship.sibling": "Hermano(a)", + "application.form.options.relationship.spouse": "Cónyuge", + "application.form.options.relationship.uncle": "Tío", + "application.household.addMembers.addHouseholdMember": "+ Añadir a un miembro del hogar", + "application.household.addMembers.done": "Ya terminó de añadir personas", + "application.household.addMembers.doubleCheck": "Por favor revise la información de cada miembro del hogar.", + "application.household.addMembers.title": "Háblenos un poco acerca de su hogar.", + "application.household.assistanceUrl": "https://exygy.com/", + "application.household.dontQualifyHeader": "Desafortunadamente, parece que usted no reúne los requisitos de este listado.", + "application.household.dontQualifyInfo": "Por favor haga cambios si usted cree haber cometido algún error. Tenga presente que si usted falsifica cualquier tipo de información en su solicitud será descalificado(a). Si la información que usted ingresó es correcta, lo invitamos a visitarnos en el futuro a medida que otras propiedades están disponibles.", + "application.household.expectingChanges.question": "¿Prevé algún cambio en su hogar en los próximos 12 meses, como el número de personas?", + "application.household.expectingChanges.title": "Espera cambios en su grupo familiar", + "application.household.genericSubtitle": "Si se eligiera su solicitud, esté preparado para proporcionar documentación respaldatoria.", + "application.household.householdMember": "Miembro del hogar", + "application.household.householdMembers": "Miembros del hogar", + "application.household.householdStudent.question": "¿Alguien de su grupo familiar es estudiante a tiempo completo o cumplirá 18 años en los próximos 60 días?", + "application.household.householdStudent.title": "El grupo familiar incluye un estudiante o miembro que está por cumplir 18 años", + "application.household.liveAlone.liveWithOtherPeople": "Otras personas vivirán conmigo", + "application.household.liveAlone.title": "Ahora nos gustaría obtener información acerca de las otras personas que residirán con usted en la vivienda", + "application.household.liveAlone.willLiveAlone": "Viviré solo(a)", + "application.household.member.cancelAddingThisPerson": "Cancelar añadir a esta persona", + "application.household.member.dateOfBirth": "Fecha de nacimiento", + "application.household.member.deleteThisPerson": "Borrar a esta persona", + "application.household.member.haveSameAddress": "¿Tiene la misma dirección que usted?", + "application.household.member.name": "Nombre del miembro del hogar", + "application.household.member.saveHouseholdMember": "Guardar el miembro del hogar", + "application.household.member.subTitle": "Tendrá la oportunidad de añadir más miembros del hogar en la próxima pantalla", + "application.household.member.title": "Háblenos acerca de esta persona", + "application.household.member.updateHouseholdMember": "Actualizar al miembro del hogar", + "application.household.member.whatIsTheirRelationship": "¿Cuál es su parentesco o relación con usted?", + "application.household.member.whatReletionship": "¿Cuál es su parentesco o relación con usted?", + "application.household.member.workInRegion": "¿Trabaja él o ella en ?", + "application.household.member.workInRegionNote": "Esto significa que actualmente él o ella trabaja en al menos el 75% de sus horas de trabajo.", + "application.household.membersInfo.title": "Antes de añadir a otras personas, asegúrese de que no hayan sido nombradas en ninguna otra solicitud de este listado.", + "application.household.preferredUnit.options.fourBdrm": "+ de 3 dormitorios", + "application.household.preferredUnit.options.oneBdrm": "1 dormitorio", + "application.household.preferredUnit.options.studio": "Estudio", + "application.household.preferredUnit.options.threeBdrm": "3 dormitorios", + "application.household.preferredUnit.options.twoBdrm": "2 dormitorios", + "application.household.preferredUnit.optionsLabel": "Marque todas las opciones que correspondan:", + "application.household.preferredUnit.preferredUnitType": "Tipo de vivienda preferida", + "application.household.preferredUnit.subTitle": "Aunque los tamaños de las unidades en general se basen en la ocupación, indique el tamaño de unidad que desee para determinar su preferencia en esta oportunidad o establecer una lista de espera (solo por esta oportunidad).", + "application.household.preferredUnit.title": "¿Cuáles son los tamaños de vivienda que le interesan?", + "application.household.primaryApplicant": "Solicitante primario", + "application.name.emailPrivacy": "Solo utilizaremos su dirección de email para comunicarnos con usted en relación con su solicitud.", + "application.name.firstName": "Nombre", + "application.name.lastName": "Apellido", + "application.name.middleNameOptional": "Inicial media (opcional)", + "application.name.noEmailAddress": "No tengo dirección de email", + "application.name.title": "¿Cuál es su nombre?", + "application.name.yourDateOfBirth": "Su fecha de nacimiento", + "application.name.yourEmailAddress": "Su dirección de email", + "application.name.yourName": "Su nombre", + "application.preferences.displacedTenant.whatAddress": "¿De qué dirección fue desplazado el miembro del hogar?", + "application.preferences.displacedTenant.whichHouseholdMember": "¿Qué miembro del hogar solicita esta preferencia?", + "application.preferences.displaceeEastPaloAlto.codeEnforcement.label": "Actividad de Cumplimiento del Código de la Ciudad", + "application.preferences.displaceeEastPaloAlto.domesticViolence.label": "Violencia doméstica", + "application.preferences.displaceeEastPaloAlto.increasedRent.label": "Un aumento del 10% o más en el alquiler en los últimos 12 meses", + "application.preferences.displaceeEastPaloAlto.naturalDisaster.label": "Desastre natural declarado por el gobernador", + "application.preferences.displaceeEastPaloAlto.noFaultEviction.label": "Un desalojo \"Sin Culpa\" de una unidad de alquiler en East Palo Alto dentro del último año de esta solicitud", + "application.preferences.dontWant": "No deseo esta preferencia", + "application.preferences.dontWantSingular": "No deseo esta preferencia", + "application.preferences.dublinHousing.displacedResident.label": "Se requirió que al menos un miembro de mi hogar se mudara de la residencia actual de Dublín debido a la demolición de la vivienda o la conversión de la vivienda de alquiler a unidad de venta (1 punto)", + "application.preferences.dublinHousing.immediateFamily.label": "Al menos un miembro de mi hogar tiene un familiar directo en Dublín (1 punto)", + "application.preferences.dublinHousing.liveInDublin.label": "Al menos un miembro de mi hogar vive en Dublín (3 puntos)", + "application.preferences.dublinHousing.permanentlyDisabled.label": "Al menos un miembro de mi hogar tiene una discapacidad permanente (1 punto)", + "application.preferences.dublinHousing.publicServiceEmployee.label": "Al menos un miembro de mi hogar es un empleado del servicio público en Dublín (1 punto adicional)", + "application.preferences.dublinHousing.senior.label": "Al menos un miembro de mi hogar es una persona mayor, definida como de 62 años o más (1 punto)", + "application.preferences.dublinHousing.veteran.label": "Alguien en mi hogar ha servido en el ejército de los Estados Unidos (1 punto)", + "application.preferences.dublinHousing.worksInDublin.label": "Al menos un miembro de mi hogar trabaja a tiempo completo en Dublín (3 puntos)", + "application.preferences.fosterCityEmployee.employed.label": "Al menos un miembro de mi hogar es un empleado de la Ciudad de Foster City", + "application.preferences.fosterCitySchoolEmployee.employed.label": "Al menos un miembro de mi hogar es un empleado del distrito escolar", + "application.preferences.general.preamble": "Estará en el grupo general de solicitantes.", + "application.preferences.general.title": "En base a la información que usted ingresó, su hogar no ha solicitado ninguna preferencia de vivienda.", + "application.preferences.liveFosterCity.live.label": "Al menos un miembro de mi hogar vive en Foster City", + "application.preferences.liveWork.live.description": "El texto de vivir en va aquí...", + "application.preferences.liveWork.live.label": "Preferencia de vivir en ", + "application.preferences.liveWork.live.link": "http://domain.com", + "application.preferences.liveWork.work.description": "El texto de trabajar en va aquí...", + "application.preferences.liveWork.work.label": "Preferencia de trabajar en ", + "application.preferences.liveWork.work.link": "http://domain.com", + "application.preferences.liveWorkEastPaloAlto.live.label": "Al menos un miembro de mi hogar trabaja en la Ciudad de San Mateo", + "application.preferences.liveWorkEastPaloAlto.work.label": "Al menos un miembro de mi hogar trabaja 20 horas por semana o más en la ciudad de East Palo Alto", + "application.preferences.liveWorkFosterCity.live.label": "Al menos un miembro de mi hogar vive en Foster City", + "application.preferences.liveWorkFosterCity.work.label": "Al menos un miembro de mi hogar trabaja en Foster City", + "application.preferences.liveWorkSanMateo.live.label": "Al menos un miembro de mi hogar vive en la Ciudad de San Mateo", + "application.preferences.liveWorkSanMateo.work.label": "Al menos un miembro de mi hogar trabaja en la Ciudad de San Mateo", + "application.preferences.preamble": "Si reúne los requisitos de esta preferencia, recibirá una clasificación más alta.", + "application.preferences.rosefieldAUSD.title": "Empleado(a) del Distrito Escolar Unificado Alameda (AUSD)", + "application.preferences.rosefieldAUSD.yes.description": "Al menos un miembro de mi hogar es empleado del Distrito Escolar Unificado de Alameda", + "application.preferences.rosefieldAUSD.yes.label": "Al menos un miembro de mi hogar es empleado del Distrito Escolar Unificado de Alameda", + "application.preferences.rosefieldLive.title": "Anterior Residentes de Rosefield Village reubicados fuera de la ciudad de Alameda", + "application.preferences.rosefieldLive.yes.description": "Al menos un miembro de mi hogar fue residente anterior de Rosefield Village", + "application.preferences.rosefieldLive.yes.label": "Al menos un miembro de mi hogar fue residente anterior de Rosefield Village", + "application.preferences.selectBelow": "Si usted tiene una de estas preferencias de vivienda, selecciónela abajo:", + "application.preferences.stillHaveOpportunity": "Usted seguirá tendiendo la oportunidad de solicitar otras preferencias.", + "application.preferences.terminationOfAffordability.atLeastOne.label": "Al menos un miembro de mi hogar está sujeto a la terminación de las restricciones de asequibilidad", + "application.preferences.title": "Su hogar podría reunir los requisitos de las siguientes preferencias de vivienda.", + "application.preferences.workFosterCity.work.label": "Al menos un miembro de mi hogar trabaja en Foster City", + "application.preferences.youHaveClaimed": "Usted ha solicitado:", + "application.review.confirmation.browseMore": "Ver más listados", + "application.review.confirmation.createAccountParagraph": "Crear una cuenta le permite guardar su información para solicitudes futuras, y usted puede verificar el estatus de esta solicitud en cualquier momento.", + "application.review.confirmation.createAccountTitle": "¿Desea crear una cuenta?", + "application.review.confirmation.doNotSubmitTitle": "No envíe otra solicitud para este listado.", + "application.review.confirmation.imdone": "No gracias, ya terminé.", + "application.review.confirmation.lotteryNumber": "Este es el número de confirmación de su solicitud", + "application.review.confirmation.needToUpdate": "Si necesita actualizar información en su solicitud, no vuelva a hacer su solicitud. Comuníquese con el agente si no recibió una confirmación por email.", + "application.review.confirmation.pleaseWriteNumber": "Sírvase anotar su número de solicitud y consérvelo en un lugar seguro. También le enviamos este número por email si nos proporcionó una dirección de email.", + "application.review.confirmation.print": "Ver la solicitud enviada e imprimir una copia.", + "application.review.confirmation.title": "Muchas gracias. Hemos recibido su solicitud de ", + "application.review.confirmation.whatExpectFirstParagraph.attend": " Usted no tiene que asistir a la lotería de vivienda. Los resultados serán publicados ", + "application.review.confirmation.whatExpectFirstParagraph.held": "La lotería se llevará a cabo el ", + "application.review.confirmation.whatExpectFirstParagraph.listing": "en el listado. ", + "application.review.confirmation.whatExpectFirstParagraph.refer": "Sírvase consultar el listado para informarse sobre la fecha de los resultados de la lotería.", + "application.review.confirmation.whatExpectSecondparagraph": "Los solicitantes serán contactados en orden hasta que se hayan llenado todas las vacantes. Si su solicitud fuera elegida, esté preparado para llenar una solicitud más detallada y proporcionar los documentos de apoyo requeridos.", + "application.review.confirmation.whatExpectTitle": "Lo que puede esperar a continuación", + "application.review.demographics.ethnicityLabel": "¿Cuál de las siguientes opciones describe mejor su origen étnico?", + "application.review.demographics.ethnicityOptions.hispanicLatino": "Hispano o latino", + "application.review.demographics.ethnicityOptions.notHispanicLatino": "Ni hispano ni latino", + "application.review.demographics.genderInfo": "Seleccione uno que describa mejor su identidad de género actual.", + "application.review.demographics.genderLabel": "¿Cuál es su sexo?", + "application.review.demographics.genderOptions.female": "Mujer", + "application.review.demographics.genderOptions.genderqueerGenderNon-Binary": "Genderqueer / Género no binario", + "application.review.demographics.genderOptions.male": "Hombre", + "application.review.demographics.genderOptions.notListed": "No aparece en la lista", + "application.review.demographics.genderOptions.transFemale": "Mujer Trans", + "application.review.demographics.genderOptions.transMale": "Hombre Trans", + "application.review.demographics.howDidYouHearLabel": "¿Cómo se enteró de este listado?", + "application.review.demographics.howDidYouHearOptions.busAd": "Anuncio en el autobús", + "application.review.demographics.howDidYouHearOptions.developerWebsite": "Sitio web del constructor", + "application.review.demographics.howDidYouHearOptions.emailAlert": "Aviso por email", + "application.review.demographics.howDidYouHearOptions.flyer": "Folleto", + "application.review.demographics.howDidYouHearOptions.friend": "Amigo(a)", + "application.review.demographics.howDidYouHearOptions.housingCounselor": "Asesor en vivienda", + "application.review.demographics.howDidYouHearOptions.jurisdictionWebsite": "Sitio web del HCD del Condado de Alameda", + "application.review.demographics.howDidYouHearOptions.other": "Otro", + "application.review.demographics.howDidYouHearOptions.radioAd": "Comercial de radio", + "application.review.demographics.raceLabel": "¿Cuál de las siguientes opciones describe mejor su raza?", + "application.review.demographics.raceOptions.americanIndianAlaskanNative": "Indígena norteamericano o nativo de Alaska", + "application.review.demographics.raceOptions.americanIndianAlaskanNativeAndBlackAfricanAmerican": "Indígena norteamericano o nativo de Alaska y negro o afroamericano", + "application.review.demographics.raceOptions.americanIndianAlaskanNativeAndWhite": "Indígena norteamericano o nativo de Alaska y blanco", + "application.review.demographics.raceOptions.asian": "Asiático", + "application.review.demographics.raceOptions.asian-asianIndian": "Indio asiático", + "application.review.demographics.raceOptions.asian-chinese": "Chino", + "application.review.demographics.raceOptions.asian-filipino": "Filipino", + "application.review.demographics.raceOptions.asian-japanese": "Japonés", + "application.review.demographics.raceOptions.asian-korean": "Coreano", + "application.review.demographics.raceOptions.asian-otherAsian": "De otro país asiático", + "application.review.demographics.raceOptions.asian-vietnamese": "Vietnamita", + "application.review.demographics.raceOptions.asianAndWhite": "Asiático y blanco", + "application.review.demographics.raceOptions.blackAfricanAmerican": "Negro o afroamericano", + "application.review.demographics.raceOptions.blackAfricanAmericanAndWhite": "Negro o afroamericano y blanco", + "application.review.demographics.raceOptions.declineToRespond": "No desea responder", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander": "Indígena de Hawái o de otra isla del Pacífico", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander-guamanianOrChamorro": "Guameño o chamorro", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander-nativeHawaiian": "Nativo hawaiano", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander-otherPacificIslander": "Otro habitante de una isla del Pacífico", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander-samoan": "Samoano", + "application.review.demographics.raceOptions.otherMultiracial": "Otro/multirracial", + "application.review.demographics.raceOptions.otherMutliracial": "Otro / Multirracial", + "application.review.demographics.raceOptions.white": "Blanco", + "application.review.demographics.sexualOrientationLabel": "¿Cómo describe usted su orientación sexual o identidad sexual?", + "application.review.demographics.sexualOrientationOptions.bisexual": "Bisexual", + "application.review.demographics.sexualOrientationOptions.gayLesbianSameGenderLoving": "Gay / Lesbiana / Ama a su propio sexo", + "application.review.demographics.sexualOrientationOptions.notListed": "No aparece en la lista", + "application.review.demographics.sexualOrientationOptions.questioningUnsure": "Explorando su identidad sexual / No está seguro(a)", + "application.review.demographics.sexualOrientationOptions.straightHeterosexual": "Heterosexual", + "application.review.demographics.subTitle": "Estas preguntas son optativas y no afectarán su elegibilidad para la vivienda. Sus respuestas se mantendrán con carácter confidencial.", + "application.review.demographics.title": "Ayúdenos a estar seguros de que alcanzamos nuestro objetivo de servir a todas las personas.", + "application.review.householdDetails": "Detalles del hogar", + "application.review.lastChanceToEdit": "Esta es su última oportunidad de hacer cambios antes de enviar su solicitud.", + "application.review.noAdditionalMembers": "Sin miembros adicionales del hogar", + "application.review.sameAddressAsApplicant": "Misma dirección que el solicitante", + "application.review.takeAMomentToReview": "Dedique un momento a revisar su información antes de enviar su solicitud.", + "application.review.terms.confirmCheckboxText": "Convengo y comprendo que no puedo cambiar nada después de enviar la solicitud.", + "application.review.terms.title": "Términos", + "application.review.voucherOrSubsidy": "Cupón de vivienda o subsidio de alquiler", + "application.start.whatToExpect.info1": "Primero, le haremos preguntas sobre usted y las personas con las que piensa vivir. Luego, le haremos preguntas sobre sus ingresos. Finalmente, veremos si usted reúne los requisitos de alguna preferencia de lotería para vivienda de precio accesible.", + "application.start.whatToExpect.info2": "Por favor tome en cuenta que cada uno de los miembros del hogar solo puede aparecer en una solicitud en cada listado.", + "application.start.whatToExpect.info3": "Toda declaración fraudulenta ocasionará que su solicitud sea eliminada.", + "application.start.whatToExpect.title": "Esto es lo que puede esperar de esta solicitud.", + "application.status": "Estatus", + "application.statuses.inProgress": "En curso", + "application.statuses.neverSubmitted": "Nunca fue enviada", + "application.statuses.submitted": "Enviada", + "application.timeout.action": "Seguir trabajando", + "application.timeout.afterMessage": "Su seguridad es importante para nosotros. Concluimos su sesión debido a inactividad. Sírvase iniciar una nueva solicitud para continuar.", + "application.timeout.text": "Para proteger su identidad, su sesión concluirá en un minuto debido a inactividad. Si decide no responder, perderá toda la información que no haya guardado.", + "application.viewApplication": "Ver la solicitud", + "application.yourLotteryNumber": "Su número de confirmación es el", + "authentication.createAccount.accountConfirmed": "Su cuenta ha sido confirmada correctamente.", + "authentication.createAccount.anEmailHasBeenSent": "Se ha enviado un email a %{email}", + "authentication.createAccount.confirmationInstruction": "Por favor haga clic en el enlace del email que le hemos enviado para completar la creación de su cuenta.", + "authentication.createAccount.confirmationNeeded": "Se necesita confirmación", + "authentication.createAccount.mustBe8Chars": "debe tener 8 caracteres", + "authentication.createAccount.noAccount": "¿No tiene una cuenta?", + "authentication.createAccount.password": "Contraseña", + "authentication.createAccount.passwordInfo": "Debe tener al menos 8 caracteres e incluir al menos 1 letra y al menos un número", + "authentication.createAccount.reEnterEmail": "Vuelva a introducir la dirección de correo electrónico", + "authentication.createAccount.reEnterPassword": "Reingresa tu contraseña", + "authentication.createAccount.resendTheEmail": "Volver a enviar el email", + "authentication.forgotPassword.message": "Si hay una cuenta creada con ese correo electrónico, recibirá un correo electrónico con un enlace para restablecer su contraseña.", + "authentication.forgotPassword.sendEmail": "Inscribirse", + "authentication.signIn.error": "Hubo un error cuando usted inició sesión", + "authentication.signIn.errorGenericMessage": "Por favor inténtelo de nuevo, o comuníquese con servicio al cliente para recibir asistencia.", + "authentication.signIn.forgotPassword": "Olvidé la contraseña", + "authentication.signIn.success": "Bienvenido de nuevo, %{name}", + "authentication.signIn.changeYourPassword": "Puede cambiar su contraseña aquí", + "authentication.timeout.action": "Permanecer en la sesión", + "authentication.timeout.signOutMessage": "Su seguridad es importante para nosotros. Concluimos su sesión debido a inactividad. Sírvase iniciar sesión para continuar.", + "authentication.timeout.text": "Para proteger su identidad, su sesión concluirá en un minuto debido a inactividad. Si decide no responder, perderá toda la información que no haya guardado y concluirá su sesión.", + "config.routePrefix": "es", + "eligibility.accessibility.acInUnit": "CA en la unidad", + "eligibility.accessibility.accessibleParking": "Lugares de estacionamiento accesibles", + "eligibility.accessibility.barrierFreeBathroom": "Baños sin barreras", + "eligibility.accessibility.barrierFreeEntrance": "Entrada a la propiedad sin barreras (sin escalones)", + "eligibility.accessibility.barrierFreeUnitEntrance": "Entradas de unidades sin barreras (sin escalones)", + "eligibility.accessibility.description": "Algunas propiedades tienen características de accesibilidad que otras pueden no tener.", + "eligibility.accessibility.elevator": "Ascensor", + "eligibility.accessibility.grabBars": "Barras de apoyo en baños", + "eligibility.accessibility.hearing": "Unidades para personas con discapacidad auditiva", + "eligibility.accessibility.heatingInUnit": "Calefacción en Unidad", + "eligibility.accessibility.inUnitWasherDryer": "Lavadora/secadora en el apartamento", + "eligibility.accessibility.laundryInBuilding": "Lavandería en el edificio", + "eligibility.accessibility.loweredCabinets": "Armarios y encimeras rebajados", + "eligibility.accessibility.loweredLightSwitch": "Interruptores de luz bajados", + "eligibility.accessibility.mobility": "Unidades para personas con movilidad reducida", + "eligibility.accessibility.parkingOnSite": "Estacionamiento en el lugar", + "eligibility.accessibility.prompt": "¿Necesita funciones de accesibilidad adicionales?", + "eligibility.accessibility.rollInShower": "Duchas para sillas de ruedas", + "eligibility.accessibility.serviceAnimalsAllowed": "Se admiten animales de servicio", + "eligibility.accessibility.title": "Funciones de accesibilidad", + "eligibility.accessibility.visual": "Unidades para personas con discapacidad visual", + "eligibility.accessibility.wheelchairRamp": "Rampa para silla de ruedas", + "eligibility.accessibility.wideDoorways": "Entradas de unidades anchas para sillas de ruedas", + "errors.agreeError": "Debe estar de acuerdo con los términos para poder continuar", + "errors.alert.badRequest": "¡Oops! Parece que algo salió mal. Por favor, inténtelo de nuevo. \n\nComuníquese con su departamento de vivienda si sigue teniendo problemas.", + "errors.alert.timeoutPleaseTryAgain": "¡Oops! Parece que algo salió mal. Por favor, inténtelo de nuevo.", + "errors.cityError": "Por favor ingrese una ciudad", + "errors.dateOfBirthError": "Por favor ingrese una fecha de nacimiento válida", + "errors.emailAddressError": "Por favor ingrese una dirección de email", + "errors.errorsToResolve": "Hay errores que tendrá que corregir antes de poder seguir adelante.", + "errors.firstNameError": "Por favor ingrese un nombre", + "errors.householdTooBig": "El número de miembros de su hogar es demasiado alto.", + "errors.householdTooSmall": "El número de miembros de su hogar es demasiado bajo.", + "errors.lastNameError": "Por favor ingrese un apellido", + "errors.notFound.message": "Me temo que no podemos encontrar la página que está buscando. Intente regresar a la página anterior o haga clic abajo para ver listados.", + "errors.notFound.title": "No se encontró la página", + "errors.numberError": "Por favor ingrese un número válido mayor a 0.", + "errors.phoneNumberError": "Por favor ingrese un número telefónico", + "errors.phoneNumberTypeError": "Por favor ingrese un tipo de número telefónico", + "errors.selectAllThatApply": "Por favor seleccione todas las opciones que correspondan.", + "errors.selectAnOption": "Por favor seleccione una opción.", + "errors.selectAtLeastOne": "Por favor seleccione al menos una opción.", + "errors.selectOption": "Por favor seleccione una de las opciones de arriba.", + "errors.stateError": "Por favor ingrese un estado", + "errors.streetError": "Por favor ingrese una dirección", + "errors.zipCodeError": "Por favor ingrese un código postal", + "footer.contact": "Contacto", + "footer.copyright": "Demonstration Jurisdiction © 2021 • Todos los derechos reservados.", + "footer.terms": "Exención de responsabilidades", + "footer.forGeneralQuestions": "Si requiere información general sobre el programa, nos puede llamar al 000-000-0000.", + "footer.giveFeedback": "Proporcione sus comentarios", + "housingCounselors.call": "Llame al %{number}", + "housingCounselors.languageServices": "Servicios de idiomas: ", + "housingCounselors.subtitle": "Hable con un asesor de vivienda local en forma específica a sus necesidades.", + "housingCounselors.visitWebsite": "Visite a %{name}", + "homeType.apartment": "Apartamento", + "homeType.duplex": "Dúplex", + "homeType.house": "Casa para una sola familia", + "homeType.townhome": "Casa adosada", + "leasingAgent.contact": "Comuníquese con el Agente de alquiler", + "leasingAgent.dueToHighCallVolume": "Debido al alto volumen de llamadas, usted podría escuchar un mensaje.", + "leasingAgent.officeHours": "Horario de oficina", + "listingFilters.allRentals": "Todos los alquileres", + "listingFilters.buttonTitle": "Filtrar", + "listingFilters.program.Seniors 55+": "Adultos mayores de 55 años", + "listingFilters.program.Seniors 62+": "Adultos mayores de 62 años", + "listingFilters.program.Residents with Disabilities": "Residentes con discapacidades", + "listingFilters.program.Families": "Familias", + "listingFilters.program.Supportive Housing for the Homeless": "Vivienda de apoyo para personas sin hogar", + "listingFilters.program.Veterans": "Veteranos", + "listingFilters.clear": "Borrar", + "listingFilters.section8": "Acepta vales de elección de vivienda de la Sección 8", + "listingFilters.buttonTitleExtended": "Encuentre alquileres para usted", + "listingFilters.bedroomsOptions.studioPlus": "Estudio", + "listingFilters.bedroomsOptions.onePlus": "1 dormitorio", + "listingFilters.bedroomsOptions.twoPlus": "2 dormitorios", + "listingFilters.bedroomsOptions.threePlus": "3 dormitorios", + "listingFilters.bedroomsOptions.fourPlus": "4 o más dormitorios", + "listingFilters.region.GreaterDowntown": "Centro", + "listingFilters.region.Eastside": "Este", + "listingFilters.region.Westside": "Oeste", + "listingFilters.region.Southwest": "Sudoeste", + "listings.additionalInformation": "Información adicional", + "listings.additionalInformationEnvelope": "Sobre de Información adicional", + "listings.allUnits": "Todas las viviendas", + "listings.allUnitsReservedFor": "Todas las viviendas reservadas para %{type}", + "listings.annualIncome": "%{income} al año", + "listings.applicationDeadline": "Fecha límite de solicitud", + "listings.applicationFCFS": "Atención por orden de llegada", + "listings.applicationFee": "Cargo de solicitud", + "listings.applicationFeeDueAt": "Pagadero en la entrevista", + "listings.applicationOpenPeriod": "Solicitudes abiertas", + "listings.applicationPerApplicantAgeDescription": "por solicitante de 18 años o más", + "listings.applicationsClosed": "Solicitudes cerradas", + "listings.apply.applicationSeason": "Los residentes deben aplicar en", + "listings.apply.applicationWillBeAvailableOn": "La solicitud podrá ser descargada y recogida el %{openDate}", + "listings.apply.applyOnline": "Haga su solicitud por Internet", + "listings.apply.downloadApplication": "Descargar la Solicitud", + "listings.apply.dropOffApplication": "Lleve la Solicitud", + "listings.apply.dropOffApplicationOrMail": "Lleve la Solicitud o envíela por correo a través del Servicio Postal de los EE.UU.", + "listings.apply.getAPaperApplication": "Obtenga una Solicitud impresas", + "listings.apply.howToApply": "Cómo presentar una Solicitud", + "listings.apply.paperApplicationsMustBeMailed": "Las solicitudes impresas deben ser enviadas a través del Servicio Postal de los EE.UU. y no pueden ser entregadas en persona.", + "listings.apply.pickUpAnApplication": "Recoja una solicitud.", + "listings.apply.sendByUsMail": "Enviar la Solicitud a través del Servicio Postal de los EE.UU.", + "listings.apply.submitAPaperApplication": "Envíe una Solicitud impresa", + "listings.apply.submitPaperDueDatePostMark": "Las solicitudes deben recibirse para la fecha límite. Si envía la solicitud a través del Servicio Postal de los EE.UU., la solicitud debe llevar el matasellos a más tardar del %{applicationDueDate} y ser recibida por correo a más tardar el %{postmarkReceivedByDate}. Las solicitudes recibidas por correo después del %{postmarkReceivedByDate} no serán aceptadas incluso si el matasellos lleva una fecha de a más tardar el %{applicationDueDate}. %{developer} no es responsable de correo extraviado o demorado.", + "listings.availableAndWaitlist": "Viviendas disponibles y lista de espera abierta", + "listings.availableUnitsAndWaitlist": "Viviendas disponibles y lista de espera", + "listings.availableUnitsAndWaitlistDesc": "Una vez que los solicitantes llenen todas las viviendas disponibles, los solicitantes adicionales serán colocados en la lista de espera de %{number} viviendas", + "listings.bath": "baño", + "listings.browseListings": "Ver listados", + "listings.cc&r": "Convenios, Condiciones y Restricciones (CC&R's)", + "listings.cc&rDescription": "Los CC&R's explican las reglas de la asociación de propietarios de viviendas, y limitan la manera en que usted puede modificar la propiedad.", + "listings.closedListings": "Listados cerrados", + "listings.communityProgramsDescription": "Este programa incluye oportunidades para miembros de comunidades específicas", + "listings.confirmedPreferenceList": "Lista de %{preference} confirmada", + "listings.creditHistory": "Historial de crédito", + "listings.criminalBackground": "Antecedentes penales", + "listings.depositMayBeHigherForLowerCredit": "Puede ser mayor para calificaciones de crédito más bajas", + "listings.depositOrMonthsRent": "o el alquiler de un mes", + "listings.developmentalDisabilities": "Personas con discapacidades del desarrollo", + "listings.developmentalDisabilitiesDescription": "Una porción del número de viviendas en este edificio esta destinada a personas con discapacidades del desarrollo. Sírvase visitar housingchoices.org para obtener información sobre la elegibilidad, requisitos, cómo obtener una solicitud y obtener respuesta a cualquier pregunta que usted pudiera tener acerca del proceso.", + "listings.downloadPdf": "Descargar PDF", + "listings.eligibilityNotebook": "Cuaderno de elegibilidad", + "listings.enterLotteryForWaitlist": "Enviar una solicitud para un lugar abierto en la lista de espera de %{units} viviendas.", + "listings.featuresCards": "Tarjetas de características", + "listings.forIncomeCalculations": "Para realizar el cálculo de los ingresos, el número de miembros del hogar incluye a todas las personas (de todas las edades) que residan en la vivienda.", + "listings.forIncomeCalculationsBMR": "Los cálculos de los ingresos están basados en el tipo de vivienda", + "listings.hideClosedListings": "Ocultar los Listados cerrados", + "listings.householdMaximumIncome": "Ingresos máximos del hogar", + "listings.householdSize": "Tamaño del hogar", + "listings.homeType": "Tipo de casa", + "listings.importantProgramRules": "Reglas importantes del programa", + "listings.includesPriorityUnits": "Incluye viviendas prioritarias para %{priorities}", + "listings.listingUpdated": "Listado actualizado", + "listings.lotteryResults.completeResultsWillBePosted": "Los resultados de la lotería serán publicados pronto.", + "listings.lotteryResults.downloadResults": "Descargar resultados", + "listings.lotteryResults.header": "Resultados de la lotería", + "listings.maxIncomeMonth": "Ingresos máximos / Mes", + "listings.maxIncomeYear": "Ingresos máximos / Año", + "listings.monthlyIncome": "%{income} al mes", + "listings.moreBuildingSelectionCriteria": "Obtenga más información sobre los Criterios de Selección de Edificaciones", + "listings.neighborhoodBuildings": "Edificaciones en la comunidad", + "listings.noAvailableUnits": "No hay viviendas disponibles en este momento.", + "listings.noOpenListings": "Actualmente ningún listado tiene solicitudes abiertas.", + "listings.occupancyDescriptionNoSro": "Los límites de ocupación de esta edificación están basados en el tipo de vivienda.", + "listings.openHouseEvent.header": "Eventos de puerta abierta", + "listings.pending": "Próximamente", + "listings.percentAMIUnit": "Vivienda AMI de %{percent}%", + "listings.priorityUnits": "Viviendas prioritarias", + "listings.priorityUnitsDescription": "Esta edificación cuenta con viviendas apartadas si alguna de las siguientes condiciones se aplican a usted o a alguna persona de su hogar:", + "listings.processInfo": "Información sobre el proceso", + "listings.publicLottery.header": "Lotería pública", + "listings.rePricing": "Asignar un nuevo precio", + "listings.remainingUnitsAfterPreferenceConsideration": "Después de que se hayan tomado en consideración a los poseedores de preferencias, todas las viviendas restantes estarán a disposición de otros solicitantes calificados.", + "listings.rentalHistory": "Historial de alquiler", + "listings.requiredDocuments": "Documentos requeridos", + "listings.reservedCommunityBuilding": "Edificación %{type}", + "listings.reservedCommunityTypes.senior55": "Adultos mayores de 55 años", + "listings.reservedCommunityTypes.senior62": "Adultos mayores de 62 años", + "listings.reservedFor": "Reservada para %{type}", + "listings.reservedTypePlural.family": "familias", + "listings.reservedTypePlural.senior": "adultos mayores", + "listings.reservedTypePlural.veteran": "veteranos de guerra", + "listings.reservedUnits": "Viviendas reservadas", + "listings.reservedUnitsDescription": "Para poder reunir los requisitos de estas viviendas, una de las siguientes condiciones debe aplicarse a usted o a alguna persona de su hogar:", + "listings.reservedUnitsForWhoAre": "Reservada para %{communityType} que son %{reservedType}", + "listings.sections.additionalEligibilitySubtitle": "Los solicitantes también deben reunir los requisitos de conformidad con las reglas de la edificación.", + "listings.sections.additionalEligibilityTitle": "Reglas adicionales de elegibilidad", + "listings.sections.additionalFees": "Cargos adicionales", + "listings.sections.additionalInformationSubtitle": "Documentos requeridos y criterios de selección", + "listings.sections.eligibilitySubtitle": "Ingresos, ocupación, preferencias y subsidios", + "listings.sections.eligibilityTitle": "Elegibilidad", + "listings.sections.featuresSubtitle": "Servicios, detalles de la vivienda y cargos adicionales", + "listings.sections.featuresTitle": "Características", + "listings.sections.housingPreferencesSubtitle": "Los poseedores de preferencia recibirán la más alta clasificación", + "listings.sections.housingPreferencesTitle": "Preferencias de vivienda", + "listings.sections.neighborhoodSubtitle": "Ubicación y transporte", + "listings.sections.processSubtitle": "Fechas importantes e información de contacto", + "listings.sections.processTitle": "Proceso", + "listings.sections.publicProgramNote": "Las propiedades de viviendas asequibles a menudo reciben fondos para albergar a poblaciones específicas, como personas mayores, residentes con discapacidades, etc. Las propiedades pueden atender a más de una población. Comuníquese con esta propiedad si no está seguro si califica.", + "listings.sections.rentalAssistanceSubtitle": "Housing Choice Vouchers, Section 8 y otros programas válidos de asistencia en el alquiler serán considerados en esta propiedad. En el caso de un subsidio en el alquiler válido, los ingresos mínimos requeridos estarán basados en la porción del alquiler que pague el inquilino después de utilizar el subsidio.", + "listings.sections.rentalAssistanceTitle": "Asistencia en el alquiler", + "listings.sections.utilities": "Servicios incluidos", + "listings.seeMaximumIncomeInformation": "Ver Información sobre ingresos máximos", + "listings.seePreferenceInformation": "Ver Información sobre la preferencia", + "listings.seeUnitInformation": "Ver Información sobre la vivienda", + "listings.showClosedListings": "Mostrar los Listados cerrados", + "listings.underConstruction": "Bajo Construcción", + "listings.unitSummaryGroupMessage": "Para cada tipo de unidad, no puede ganar más que el límite de ingresos asociado con el tamaño de su hogar, como se muestra en la tabla de Ingresos máximos del hogar a continuación.", + "listings.section8MessageOpening": "Si tiene un ", + "listings.section8FullName": "vale de elección de vivienda de la Sección 8", + "listings.section8MessageClosing": ", los requisitos de ingresos no se aplican y pagará el alquiler en función de sus ingresos.", + "listings.unitTypes.oneBdrm": "1 dormitorio", + "listings.unitTypes.studio": "Estudio", + "listings.unitTypes.threeBdrm": "3 dormitorios", + "listings.unitTypes.twoBdrm": "2 dormitorios", + "listings.unitsAreFor": "Estas viviendas son para %{type}.", + "listings.unitsHaveAccessibilityFeaturesFor": "Estas viviendas tienen características de accesibilidad para personas con %{type}.", + "listings.upcomingLotteries.noResults": "No hay listados cerrados ni loterías próximas en este momento.", + "listings.vacantUnits": "Unidades vacantes", + "listings.verifiedListing": "Confirmado según propiedad", + "listings.waitlist.closed": "Lista de espera cerrada", + "listings.waitlist.currentSize": "Tamaño de la lista de espera actual", + "listings.waitlist.finalSize": "Tamaño final de la lista de espera", + "listings.waitlist.isOpen": "La lista de espera está abierta", + "listings.waitlist.label": "Lista de espera", + "listings.waitlist.open": "Lista de espera abierta", + "listings.waitlist.openSlots": "Lugares disponibles en la lista de espera", + "listings.waitlist.submitAnApplication": "Una vez que los solicitantes clasificados llenen todas las viviendas disponibles, los solicitantes clasificados restantes serán colocados en la lista de espera de esas mismas viviendas.", + "listings.waitlist.submitForWaitlist": "Enviar una solicitud para un lugar disponible en la lista de espera.", + "listings.waitlist.unitsAndWaitlist": "Viviendas disponibles y lista de espera", + "listings.utilities.water": "Agua", + "listings.utilities.gas": "Gas", + "listings.utilities.trash": "Recolección de residuos", + "listings.utilities.sewer": "Alcantarillado", + "listings.utilities.electricity": "Electricidad", + "listings.utilities.cable": "Cable", + "listings.utilities.phone": "Teléfono", + "listings.utilities.internet": "Internet", + "lottery.applicationsThatQualifyForPreference": "Las solicitudes que reúnan los requisitos de esta preferencia recibirán una prioridad más alta.", + "lottery.viewPreferenceList": "Ver la Lista de preferencias", + "nav.browseProperties": "Buscar Propiedades", + "nav.getFeedback": "Nos encantaría recibir sus comentarios", + "nav.listings": "Listados", + "nav.myAccount": "Mi Cuenta", + "nav.myDashboard": "Mi Tablero de control", + "nav.mySettings": "Mis Configuraciones", + "nav.properties": "Propiedades", + "nav.rentals": "Alquileres", + "nav.signIn": "Iniciar sesión", + "nav.signOut": "Cerrar sesión", + "nav.signUp": "Inscribirse", + "nav.siteTitle": "Portal de viviendas", + "pageDescription.listing": "Haga su solicitud de vivienda de precio accesible en %{listingName} en %{regionName}, construida en sociedad con Exygy.", + "pageDescription.welcome": "Busque y haga su solicitud de vivienda de precio accesible en el Portal de vivienda de %{regionName}", + "pageDescription.getAssistance": "Para ayudarlo en su búsqueda de una vivienda estable, consulte los recursos y servicios a continuación u obtenga más información sobre viviendas asequibles.", + "pageDescription.housingBasics": "Sabemos que encontrar vivienda puede ser un proceso difícil e intimidante. En esta página puede encontrar varios recursos que lo ayudarán a obtener información sobre viviendas asequibles y el proceso de solicitud para estas propiedades.", + "pageTitle.about": "Acerca de", + "pageTitle.resources": "Recursos", + "pageTitle.additionalResources": "Más oportunidades de vivienda", + "pageTitle.accessibilityStatement": "Declaración de Accesibilidad", + "pageTitle.terms": "Exenciones de respaldos", + "pageTitle.getAssistance": "Obtener asistencia", + "pageTitle.housingCounselors": "Asesores de vivienda", + "pageTitle.privacy": "Política de privacidad", + "pageTitle.rent": "Vivienda de alquiler razonable", + "pageTitle.feedback": "Compartir comentarios sobre el sitio web", + "pageTitle.welcomeEnglish": "Bienvenidos", + "pageTitle.welcomeSpanish": "Bienvenido", + "pageTitle.welcomeVietnamese": "Tiếng Việt", + "pageTitle.housingBasics": "Conceptos básicos sobre las viviendas asequibles", + "progressNav.current": "Paso actual: ", + "progressNav.srHeading": "Progreso", + "publicFilter.confirmedListings": "Ofertas confirmadas", + "publicFilter.confirmedListingsFieldLabel": "Mostrar únicamente ofertas confirmadas según propiedad", + "publicFilter.bedRoomSize": "Tamaño del dormitorio", + "publicFilter.rentRange": "Rango del alquiler mensual", + "publicFilter.rentRangeMin": "Sin costo de alquiler mínimo", + "publicFilter.rentRangeMax": "Sin costo de alquiler máximo", + "publicFilter.communityTypes": "Tipos de comunidad", + "publicFilter.waitlist.open": "Abrir lista de espera", + "publicFilter.waitlist.closed": "Cerrar lista de espera", + "region.name": "Región local", + "resources.affordableHousingTitle": "Conceptos básicos sobre vivienda", + "resources.affordableHousingSubtitle": "Obtenga más información sobre cómo calificar y solicitar una vivienda asequible", + "resources.affordableHousingLinkLabel": "Leer más para conocer el proceso", + "resources.housingResourcesTitle": "Recursos de vivienda adicionales", + "resources.housingResourcesSubtitle": "Consulte los recursos y servicios locales en su búsqueda de vivienda", + "resources.housingResourcesLinkLabel": "Ver recursos de la comunidad", + "seasons.fall": "Otoño", + "seasons.spring": "Primavera", + "seasons.summer": "Verano", + "seasons.winter": "Invierno", + "states.AK": "Alaska", + "states.AL": "Alabama", + "states.AR": "Arkansas", + "states.AZ": "Arizona", + "states.CA": "California", + "states.CO": "Colorado", + "states.CT": "Connecticut", + "states.DC": "Distrito de Columbia", + "states.DE": "Delaware", + "states.FL": "Florida", + "states.GA": "Georgia", + "states.HI": "Hawái", + "states.IA": "Iowa", + "states.ID": "Idaho", + "states.IL": "Illinois", + "states.IN": "Indiana", + "states.KS": "Kansas", + "states.KY": "Kentucky", + "states.LA": "Louisiana", + "states.MA": "Massachusetts", + "states.MD": "Maryland", + "states.ME": "Maine", + "states.MI": "Michigan", + "states.MN": "Minnesota", + "states.MO": "Missouri", + "states.MS": "Mississippi", + "states.MT": "Montana", + "states.NC": "North Carolina", + "states.ND": "North Dakota", + "states.NE": "Nebraska", + "states.NH": "New Hampshire", + "states.NJ": "Nueva Jersey", + "states.NM": "Nuevo México", + "states.NV": "Nevada", + "states.NY": "Nueva York", + "states.OH": "Ohio", + "states.OK": "Oklahoma", + "states.OR": "Oregon", + "states.PA": "Pennsylvania", + "states.RI": "Rhode Island", + "states.SC": "South Carolina", + "states.SD": "South Dakota", + "states.TN": "Tennessee", + "states.TX": "Texas", + "states.UT": "Utah", + "states.VA": "Virginia", + "states.VT": "Vermont", + "states.WA": "Washington", + "states.WI": "Wisconsin", + "states.WV": "West Virginia", + "states.WY": "Wyoming", + "t.accessibility": "Accesibilidad", + "t.additionalAccessibility": "Detalles adicionales de accesibilidad", + "t.additionalPhone": "Teléfono adicional", + "t.areYouStillWorking": "¿Sigue usted trabajando?", + "t.area": "área", + "t.availability": "Disponibilidad", + "t.back": "Atrás", + "t.built": "Construida", + "t.call": "Llame", + "t.cancel": "Cancelar", + "t.confirm": "Confirmar", + "t.day": "Día", + "t.delete": "Borrar", + "t.deposit": "Depósito", + "t.description": "Ingrese descripción", + "t.done": "Hecho", + "t.edit": "Editar", + "t.email": "Email", + "t.emailAddressPlaceholder": "you@myemail.com", + "t.finish": "Terminar", + "t.floor": "piso", + "t.floors": "pisos", + "t.getDirections": "Obtener instrucciones para llegar", + "t.household": "Hogar", + "t.income": "Ingresos", + "t.less": "Menos", + "t.letter": "Carta", + "t.loginIsRequired": "Es necesario iniciar sesión para poder ver esta página.", + "t.menu": "Menú", + "t.minimumIncome": "Ingresos mínimos", + "t.month": "Mes", + "t.more": "Más", + "t.name": "Nombre", + "t.neighborhood": "Comunidad", + "t.next": "Siguiente", + "t.no": "No", + "t.none": "Ninguno", + "t.noneFound": "No se encontró ninguno.", + "t.occupancy": "Ocupación", + "t.or": "o", + "t.people": "personas", + "t.perMonth": "al mes", + "t.perYear": "al año", + "t.person": "persona", + "t.petsPolicy": "Política de mascotas", + "t.phone": "Teléfono", + "t.phoneNumberPlaceholder": "(555) 555-5555", + "t.pleaseSelectOne": "Por favor seleccione una opción.", + "t.pleaseSelectYesNo": "Elija “sí” o “no”.", + "t.preferences": "Preferencias", + "t.previous": "Anterior", + "t.propertyAmenities": "Servicios en la propiedad", + "t.range": "%{from} a %{to}", + "t.readLess": "leer menos", + "t.readMore": "leer más", + "t.rent": "Alquiler", + "t.review": "Revisión", + "t.seeDetails": "Consulte los detalles", + "t.seeListing": "Ver el Listado", + "t.selectOne": "Elija una opción", + "t.showLess": "mostrar menos", + "t.showMore": "mostrar más", + "t.skipToMainContent": "Pasar al contenido principal", + "t.smokingPolicy": "Política de fumar", + "t.sqFeet": "pies cuadrados", + "t.squareFeet": "pies cuadrados", + "t.submit": "Enviar", + "t.text": "Texto", + "t.to": "a", + "t.unit": "vivienda", + "t.unitAmenities": "Servicios en la vivienda", + "t.unitFeatures": "Características de la vivienda", + "t.unitType": "Tipo de vivienda", + "t.units": "viviendas", + "t.viewMap": "Ver mapa", + "t.viewOnMap": "Ver en el mapa", + "t.year": "Año", + "t.yes": "Sí", + "t.you": "Usted", + "welcome.allApplicationClosed": "Todas las solicitudes están cerradas actualmente pero puede ver listados cerrados.", + "welcome.seeMoreOpportunities": "Ver más oportunidades de alquiler y propiedad de vivienda", + "welcome.seeMoreOpportunitiesTruncated": "Ver más oportunidades y recursos de vivienda", + "welcome.seeRentalListings": "Ver viviendas en alquiler", + "welcome.signUp": "Reciba alertas cuando se publique una nueva oferta", + "welcome.signUpToday": "Inscríbase hoy", + "welcome.title": "Haga su solicitud de vivienda de precio accesible en", + "welcome.viewAdditionalHousing": "Vea Oportunidades y recursos adicionales de vivienda", + "welcome.viewAdditionalHousingTruncated": "Ver oportunidades y recursos", + "welcome.underConstructionButton": "Ver todo en construcción", + "welcome.learnMore": "Obtener más información", + "welcome.learnHousingBasics": "Conozca cómo puede calificar y solicitar una vivienda asequible", + "welcome.findRentalsForMe": "Encuentre alquileres para usted", + "whatToExpect.default": "Los solicitantes serán contactados por el agente de la propiedad en orden de clasificación hasta que se hayan llenado todas las vacantes. Toda la información que usted ha proporcionado será verificada y su elegibilidad será confirmada. Su solicitud será retirada de la lista de espera si usted hizo alguna declaración fraudulenta. Si nos es imposible verificar una preferencia de vivienda que usted haya solicitado, no recibirá la preferencia pero no será penalizado de ninguna otra manera. Si su solicitud fuera elegida, esté preparado para llenar una solicitud más detallada y proporcionar los documentos de apoyo requeridos.", + "whatToExpect.label": "Lo que puede esperar" +} diff --git a/shared-helpers/src/locales/general.json b/shared-helpers/src/locales/general.json new file mode 100644 index 0000000000..701755b777 --- /dev/null +++ b/shared-helpers/src/locales/general.json @@ -0,0 +1,1605 @@ +{ + "about.body1": "We know that finding housing that meets your needs can be difficult and frustrating. Detroit Home Connect is your helping hand to find a new place to call home.", + "about.body2": "Detroit Home Connect is a new City of Detroit service that provides you a central first step in finding housing in Detroit that meets your affordability and household needs. You can understand your eligibility for rental units by exploring options based on your family size, age, and income. Detroit Home Connect is an initiative of the City of Detroit’s Housing and Revitalization Department. The design and features of the website is based on feedback and insight from area residents, community-based organizations, property managers, and property owners.", + "about.moreInfoContact": "For more information, please contact City staff at detroithomeconnect@detroitmi.gov.", + "about.thankYouPartners": "The City of Detroit Housing and Revitalization Department would like to thank the following partners for their support and partnership during the development of Detroit Home Connect, including:", + "about.partnersList": "Exygy, Google.org, City of Detroit Department of Innovation and Technology, City of Detroit Department of Neighborhoods, United Community Housing Coalition, Detroit Disability Power, Independent Management Services, Continental Management, KMG Prestige, Premier Property Management, Elite Management, The Associated Management Company, Ginosko Developement, The Platform, U Snap Bac, Central Detroit Christian CDC, Matrix Human Services, Wayne Metro, Bridging Communities, Southwest Solutions, Legal Aid and Defender Association, Ruth Ellis Center, Neighborhood Legal Services, COTS, Central City Integrated Health, Jefferson East Inc, Invest Detroit, Alternatives for Girls, Cass Community Social Services, and CSI Coop.", + "account.accountSettings": "Account Settings", + "account.errorFetchingApplications": "Error fetching applications", + "account.noApplications": "It looks like you haven't applied to any listings yet.", + "account.accountSettingsSubtitle": "Account Settings, email and password", + "account.myFavorites": "My Favorites", + "account.myFavoritesSubtitle": "Save listings and check back for updates", + "account.createAccount": "Create Account", + "account.haveAnAccount": "Already have an account?", + "account.myApplications": "My Applications", + "account.myApplicationsSubtitle": "See lottery dates and listings for properties for which you've applied", + "account.application.confirmation": "Confirmation", + "account.application.error": "Error", + "account.application.noAccessError": "No application with that ID exists", + "account.application.noApplicationError": "No application with that ID exists", + "account.application.return": "Return to applications", + "account.settings.passwordSuccess": "Password successfully updated", + "account.settings.update": "Update", + "account.settings.passwordRemember": "When changing your password make sure you make note of it so you remember it in the future.", + "account.settings.currentPassword": "Current password", + "account.settings.newPassword": "New password", + "account.settings.confirmNewPassword": "Confirm new password", + "account.settings.alerts.genericError": "There was an error. Please try again, or contact support for help.", + "account.settings.alerts.nameSuccess": "Name update successful", + "account.settings.alerts.dobSuccess": "Birthdate update successful", + "account.settings.alerts.emailSuccess": "Email update successful", + "account.settings.alerts.phoneNumberSuccess": "Phone number update successful", + "account.settings.alerts.currentPassword": "Invalid current password. Please try again.", + "account.settings.alerts.passwordSuccess": "Password update successful", + "account.settings.alerts.passwordMatch": "New password fields do not match", + "account.settings.alerts.passwordEmpty": "Password fields may not be empty", + "account.settings.placeholders.month": "MM", + "account.settings.placeholders.day": "DD", + "account.settings.placeholders.year": "YYYY", + "alert.unavailable": "Detroit Home Connect may be temporarily unavailable the week of September 2nd, 2025 for a scheduled upgrade. We expect the site to be down for up to 48 hours during this period. For questions or concerns, please contact us at: detroithomeconnect@detroitmi.gov", + "applications.begin.en": "Begin", + "applications.begin.es": "Empezar", + "applications.begin.zh": "開始", + "applications.begin.vi": "Bắt đầu", + "applications.totalApplications": "Total Applications", + "applications.totalSets": "Total Sets", + "applications.addApplication": "Add Application", + "applications.newApplication": "New Application", + "applications.editApplication": "Edit Application", + "applications.applicationsReceived": "Applications Received", + "applications.table.applicationSubmissionDate": "Application Submission Date", + "applications.table.declaredAnnualIncome": "Declared Annual Income", + "applications.table.declaredMonthlyIncome": "Declared Monthly Income", + "applications.table.subsidyOrVoucher": "Subsidy or Voucher", + "applications.table.requestAda": "Request ADA", + "applications.table.preferenceClaimed": "Preference Claimed", + "applications.table.primaryDob": "Primary DOB", + "applications.table.phoneType": "Phone Type", + "applications.table.additionalPhoneType": "Additional Phone Type", + "applications.table.residenceStreet": "Residence Street Address", + "applications.table.residenceCity": "Residence City", + "applications.table.residenceState": "Residence State", + "applications.table.residenceZip": "Residence Zip", + "applications.table.mailingStreet": "Mailing Street Address", + "applications.table.mailingCity": "Mailing City", + "applications.table.mailingState": "Mailing State", + "applications.table.mailingZip": "Mailing Zip", + "applications.table.workStreet": "Work Street Address", + "applications.table.workCity": "Work City", + "applications.table.workState": "Work State", + "applications.table.workZip": "Work Zip", + "applications.table.altContactFirstName": "Alt Contact First Name", + "applications.table.altContactLastName": "Alt Contact Last Name", + "applications.table.altContactRelationship": "Alt Contact Relationship", + "applications.table.altContactAgency": "Alt Contact Agency", + "applications.table.altContactEmail": "Alt Contact Email", + "applications.table.altContactPhone": "Alt Contact Phone", + "applications.table.altContactStreetAddress": "Alt Contact Street Address", + "applications.table.altContactCity": "Alt Contact City", + "applications.table.altContactState": "Alt Contact State", + "applications.table.altContactZip": "Alt Contact Zip", + "applications.table.householdFirstName": "Household First Name", + "applications.table.householdLastName": "Household Last Name", + "applications.table.householdRelationship": "Household Relationship", + "applications.table.householdDob": "Household DOB", + "applications.table.householdStreetAddress": "Household Street Address", + "applications.table.householdCity": "Household City", + "applications.table.householdState": "Household State", + "applications.table.householdZip": "Household Zip", + "applications.table.applicationType": "Application Type", + "application.add.applicationAddError": "You’ll need to resolve any errors before moving on.", + "application.add.workInRegion": "Work in the region?", + "application.add.mobility": "Mobility Impairments", + "application.add.vision": "Vision Impairments", + "application.add.hearing": "Hearing Impairments", + "application.add.preferences.liveIn": "Live in", + "application.add.preferences.workIn": "Work in", + "application.add.preferences.optedOut": "Opted out of preference", + "application.add.incomePeriod": "Income Period", + "application.add.demographicsInformation": "Demographic Information", + "application.add.ethnicity": "Ethnicity", + "application.add.race": "Race", + "application.add.gender": "Gender", + "application.add.howDidYouHearAboutUs": "How did you hear about us?", + "application.add.sexualOrientation": "Sexual Orientation", + "application.add.addHouseholdMember": "Add Household Member", + "application.add.sameAddressAsPrimary": "Same Address as Primary", + "application.add.sameResidence": "Same Residence", + "application.add.languageSubmittedIn": "Language Submitted In", + "application.add.timeSubmitted": "Time Submitted", + "application.add.dateSubmitted": "Date Submitted", + "application.add.applicationSubmitted": "Application Submitted", + "application.add.applicationUpdated": "Application Updated", + "application.add.saveAndExit": "Save & exit", + "application.add.claimant": "Claimant", + "application.add.displacedAddress": "Displaced Address", + "application.referralApplication.instructions": "The permanent supportive housing units are referred directly through Coordinated Entry System. Households experiencing homelessness can call in order to get connected to an Access Point to learn more about the coordinated entry system and access housing-related resources and information.", + "application.referralApplication.furtherInformation": "For further information", + "application.referralApplication.phoneNumber": "211", + "application.details.applicationData": "Application Data", + "application.details.number": "Application Number", + "application.details.type": "Application Submission Type", + "application.details.submittedDate": "Application Submitted Date", + "application.details.timeDate": "Application Submitted Time", + "application.details.language": "Application Language", + "application.details.householdSize": "Household Size", + "application.details.totalSize": "Total Household Size", + "application.details.submittedBy": "Submitted By", + "application.details.agency": "Agency if Applicable", + "application.details.adaPriorities": "ADA Priorities Selected", + "application.details.preferences": "Application Preferences", + "application.details.programs": "Application Programs", + "application.details.liveOrWorkIn": "Live or Work in", + "application.details.householdIncome": "Declared Household Income", + "application.details.annualIncome": "Annual Income", + "application.details.monthlyIncome": "Monthly Income", + "application.details.vouchers": "Housing Voucher or Subsidy", + "application.details.preferredContact": "Preferred Contact", + "application.details.residenceAddress": "Residence Address", + "application.details.workInRegion": "Work in Region", + "application.details.signatureOnTerms": "Signature on Terms of Agreement", + "application.details.submissionType.electronical": "Electronic", + "application.details.submissionType.paper": "Paper", + "application.details.applicationStatus.draft": "Draft", + "application.details.applicationStatus.submitted": "Submitted", + "application.details.applicationStatus.removed": "Removed", + "application.details.preferredUnitSizes": "Preferred Unit Sizes", + "application.details.householdMemberDetails": "Household Member Details", + "application.form.general.saveAndReturn": "Save and return to review", + "application.form.general.saveAndFinishLater": "Save and finish later", + "application.form.options.relationship.spouse": "Spouse", + "application.form.options.relationship.registeredDomesticPartner": "Registered Domestic Partner", + "application.form.options.relationship.parent": "Parent", + "application.form.options.relationship.child": "Child", + "application.form.options.relationship.sibling": "Sibling", + "application.form.options.relationship.cousin": "Cousin", + "application.form.options.relationship.aunt": "Aunt", + "application.form.options.relationship.uncle": "Uncle", + "application.form.options.relationship.nephew": "Nephew", + "application.form.options.relationship.niece": "Niece", + "application.form.options.relationship.grandparent": "Grandparent", + "application.form.options.relationship.greatGrandparent": "Great Grandparent", + "application.form.options.relationship.inLaw": "In Law", + "application.form.options.relationship.friend": "Friend", + "application.form.options.relationship.other": "Other", + "application.chooseLanguage.letsGetStarted": "Let’s get started on your application", + "application.chooseLanguage.chooseYourLanguage": "Choose Your Language", + "application.chooseLanguage.signInSaveTime": "Signing in could save you time by starting with details of your last application, and allow you to check the status of this application at any time.", + "application.autofill.saveTime": "Save time by using the details from your last application", + "application.autofill.prefillYourApplication": "We'll simply pre-fill your application with the following details, and you can make updates as you go.", + "application.autofill.start": "Start with these details", + "application.autofill.reset": "Reset and start fresh", + "application.name.title": "What's your name?", + "application.name.yourName": "Your Name", + "application.name.firstName": "First Name", + "application.name.middleNameOptional": "Middle Name (optional)", + "application.name.middleName": "Middle Name", + "application.name.lastName": "Last Name", + "application.name.yourDateOfBirth": "Your Date of Birth", + "application.name.yourEmailAddress": "Your Email Address", + "application.name.emailPrivacy": "We will only use your email address to contact you about your application.", + "application.name.noEmailAddress": "I don't have an email address", + "application.contact.title": "Thanks %{firstName}. Now we need to know how to contact you.", + "application.contact.yourPhoneNumber": "Your Phone Number", + "application.contact.phoneNumberTypes.prompt": "What type of number is this?", + "application.contact.phoneNumberTypes.work": "Work", + "application.contact.phoneNumberTypes.home": "Home", + "application.contact.phoneNumberTypes.cell": "Cell", + "application.contact.noPhoneNumber": "I don't have a telephone number", + "application.contact.yourAdditionalPhoneNumber": "Your Second Phone Number", + "application.contact.additionalPhoneNumber": "I have an additional phone number", + "application.contact.address": "Address", + "application.contact.addressWhereYouCurrentlyLive": "We need the address where you currently live. If you are homeless, enter either the shelter address or an address close to where you stay.", + "application.contact.streetAddress": "Street Address", + "application.contact.apt": "Apt or Unit #", + "application.contact.city": "City", + "application.contact.cityName": "City Name", + "application.contact.contactPreference": "How do you prefer to be contacted?", + "application.contact.preferredContactType": "Preferred Contact Type", + "application.contact.state": "State", + "application.contact.zip": "Zip", + "application.contact.zipCode": "Zipcode", + "application.contact.sendMailToMailingAddress": "Send my mail to a different address", + "application.contact.mailingAddress": "Mailing Address", + "application.contact.provideAMailingAddress": "Provide an address where you can receive updates and materials about your application.", + "application.contact.doYouWorkIn": "Do you work in %{county} County?", + "application.contact.doYouWorkInDescription": "TBD", + "application.contact.workAddress": "Work Address", + "application.contact.verifyAddressTitle": "We have located the following address. Please confirm it's correct.", + "application.contact.couldntLocateAddress": "We couldn't locate the address you entered. Please confirm it's correct.", + "application.contact.suggestedAddress": "Suggested Address:", + "application.contact.youEntered": "You Entered:", + "application.alternateContact.type.title": "Is there someone else you'd like to authorize us to contact if we can't reach you?", + "application.alternateContact.type.description": "By providing alternate contact, you are allowing us to discuss information on your application with them.", + "application.alternateContact.type.label": "Alternate Contact", + "application.alternateContact.type.options.familyMember": "Family member", + "application.alternateContact.type.options.friend": "Friend", + "application.alternateContact.type.options.caseManager": "Case manager or housing counselor", + "application.alternateContact.type.options.other": "Other", + "application.alternateContact.type.options.noContact": "I don't have an alternate contact", + "application.alternateContact.type.otherTypeFormPlaceholder": "What is your relationship?", + "application.alternateContact.type.otherTypeValidationErrorMessage": "Please enter relationship type", + "application.alternateContact.type.validationErrorMessage": "Please select an alternate contact", + "application.alternateContact.name.title": "Who is your alternate contact?", + "application.alternateContact.name.alternateContactFormLabel": "Name of alternate contact", + "application.alternateContact.name.caseManagerAgencyFormLabel": "Where does your case manager or housing counselor work?", + "application.alternateContact.name.caseManagerAgencyFormPlaceHolder": "Agency", + "application.alternateContact.name.caseManagerAgencyValidationErrorMessage": "Please enter an agency", + "application.alternateContact.contact.title": "Let us know how to reach your alternate contact", + "application.alternateContact.contact.description": "We'll only use this information to contact them about your application.", + "application.alternateContact.contact.phoneNumberFormLabel": "Contact phone number", + "application.alternateContact.contact.emailAddressFormLabel": "Contact email address", + "application.alternateContact.contact.contactMailingAddressLabel": "Contact mailing address", + "application.alternateContact.contact.contactMailingAddressHelperText": "Choose an address where they can receive updates and materials about your application", + "application.household.assistanceUrl": "https://exygy.com/", + "application.household.dontQualifyHeader": "Unfortunately it appears you do not qualify for this listing.", + "application.household.dontQualifyInfo": "Please make changes if you believe you might have made a mistake. Be aware that if you falsify any information on your application you will be disqualified. If the information you entered is accurate, we encourage you to check back in the future as more properties become available.", + "application.household.addMembers.addHouseholdMember": "+ Add Household Member", + "application.household.addMembers.done": "Done adding people", + "application.household.addMembers.title": "Tell us about your household.", + "application.household.addMembers.doubleCheck": "Please double-check the information for each household member.", + "application.household.expectingChanges.title": "Expecting Household Changes", + "application.household.expectingChanges.question": "Do you anticipate any changes in your household in the next 12 months?", + "application.household.householdStudent.title": "Household Includes Student or Member Nearing 18", + "application.household.householdStudent.question": "Is someone in your household a full time student or going to turn 18 years old within 60 days?", + "application.household.genericSubtitle": "Should your application be chosen, be prepared to provide supporting documentation.", + "application.household.householdMember": "Household Member", + "application.household.householdMembers": "Household Members", + "application.household.liveAlone.title": "Next we would like to know about the others who will live with you in the unit", + "application.household.liveAlone.willLiveAlone": "I will live alone", + "application.household.liveAlone.liveWithOtherPeople": "Other people will live with me", + "application.household.preferredUnit.preferredUnitType": "Preferred Unit Type", + "application.household.preferredUnit.title": "What unit sizes are you interested in?", + "application.household.preferredUnit.subTitle": "Although unit sizes will typically be based on occupancy, please provide your preferred unit size for determining your preference in this opportunity or establishing a waitlist (for this opportunity only)", + "application.household.preferredUnit.legend": "Preferred unit type", + "application.household.preferredUnit.optionsLabel": "Check all that apply:", + "application.household.preferredUnit.options.studio": "Studio", + "application.household.preferredUnit.options.oneBdrm": "1 Bedroom", + "application.household.preferredUnit.options.twoBdrm": "2 Bedroom", + "application.household.preferredUnit.options.threeBdrm": "3 Bedroom", + "application.household.preferredUnit.options.fourBdrm": "3+ Bedroom", + "application.household.member.cancelAddingThisPerson": "Cancel adding this person", + "application.household.member.deleteThisPerson": "Delete this person", + "application.household.member.dateOfBirth": "Date of Birth", + "application.household.member.name": "Household member's name", + "application.household.member.haveSameAddress": "Do they have same address as you?", + "application.household.member.whatIsTheirRelationship": "What is their relationship to you?", + "application.household.member.saveHouseholdMember": "Save household member", + "application.household.member.subTitle": "You will have an opportunity to add more household members on the next screen", + "application.household.member.title": "Tell us about this person", + "application.household.member.updateHouseholdMember": "Update Household Member", + "application.household.member.whatReletionship": "What is their relationship to you", + "application.household.member.workInRegion": "Do they work in %{county} County?", + "application.household.member.workInRegionNote": "TBD", + "application.household.membersInfo.title": "Before adding other people, make sure that they aren't named on any other application for this listing.", + "application.household.primaryApplicant": "Primary Applicant", + "application.ada.label": "ADA Accessible Units", + "application.ada.title": "Do you or anyone in your household need any of the following ADA accessibility features?", + "application.ada.subTitle": "If you are selected for a unit, the property will work to accommodate your need to the best of their ability. Should your application be chosen, be prepared to provide supporting documentation from your physician.", + "application.ada.mobility": "For Mobility Impairments", + "application.ada.vision": "For Vision Impairments", + "application.ada.hearing": "For Hearing Impairments", + "application.financial.income.title": "Let's move to income.", + "application.financial.income.instruction1": "Add up your total gross (pre-tax) household income from wages, benefits and other sources from all household members.", + "application.financial.income.instruction2": "You only need to provide an estimated total right now. The actual total will be calculated if you are selected.", + "application.financial.income.prompt": "What is your household total pre-tax income?", + "application.financial.income.placeholder": "Total all of your income sources", + "application.financial.income.legend": "Income frequency", + "application.financial.income.validationError.title": "Unfortunately it appears you do not qualify for this listing.", + "application.financial.income.validationError.reason.low": "Your household income is too low.", + "application.financial.income.validationError.reason.high": "Your household income is too high.", + "application.financial.income.validationError.instruction1": "Please make changes if you believe you might have made a mistake. Be aware that if you falsify any information on your application you will be disqualified.", + "application.financial.income.validationError.instruction2": "If the information you entered is accurate, we encourage you to check back in the future as more properties become available.", + "application.financial.vouchers.title": "Do you or anyone on this application receive any of the following?", + "application.financial.vouchers.housingVouchers.strong": "Housing vouchers", + "application.financial.vouchers.housingVouchers.text": "like Section 8", + "application.financial.vouchers.nonTaxableIncome.strong": "Non-taxable income", + "application.financial.vouchers.nonTaxableIncome.text": "like SSI, SSDI, child support payments, or worker's compensation benefits", + "application.financial.vouchers.rentalSubsidies.strong": "Rental subsidies", + "application.financial.vouchers.rentalSubsidies.text": "like VASH, HSA, HOPWA, Catholic Charities, AIDS Foundation, etc.", + "application.financial.vouchers.legend": "Housing vouchers, nontaxable income or rental subsidies", + "application.programs.servedInMilitary.summary": "Veteran of the US Military", + "application.programs.servedInMilitary.servedInMilitary.label": "Yes", + "application.programs.servedInMilitary.doNotConsider.label": "No", + "application.programs.tay.summary": "Transition Age Youth (TAY)", + "application.programs.tay.tay.label": "Yes", + "application.programs.tay.doNotConsider.label": "No", + "application.programs.disabilityOrMentalIllness.summary": "People with Developmental Disabilities or Mental Illness", + "application.programs.disabilityOrMentalIllness.disabilityOrMentalIllness.label": "Yes", + "application.programs.disabilityOrMentalIllness.doNotConsider.label": "No", + "application.programs.housingSituation.summary": "Temporarily Housed or Homeless", + "application.programs.housingSituation.notPermanent.label": "I have somewhere to stay, but it isn't permanent", + "application.programs.housingSituation.notPermanent.description": "Includes staying with friends or family, living in a motel / hotel, or living in a medical or other facility and those who have received an eviction notice or will imminently lose their current residence", + "application.programs.housingSituation.homeless.label": "I'm homeless", + "application.programs.housingSituation.homeless.description": "Includes living outside, or in your car, or staying at a shelter, or in a motel / hotel paid for with an emergency voucher", + "application.programs.housingSituation.doNotConsider.label": "No", + "application.programs.rentBasedOnIncome.summary": "Flat Rent & Rent Based on Income", + "application.programs.rentBasedOnIncome.flatRent.label": "Affordable apartment with flat rent", + "application.programs.rentBasedOnIncome.flatRent.description": "I would like to apply for an affordable flat rent apartment, which has a set monthly rent amount that is below market rate. Note - applicants with Section 8 Mobile Housing Choice Vouchers (HCV) are welcome to apply.", + "application.programs.rentBasedOnIncome.30Percent.label": "Project based affordable apartments with a rent at 30% of your income", + "application.programs.rentBasedOnIncome.30Percent.description": "I would like to apply for an apartment with Project-Based subsidy. These apartments require initial screening and yearly certification by the Housing Authority. The Housing Authority will calculate my monthly rent payment to approximately 30% of monthly income.", + "application.preferences.title": "Your household may qualify for the following housing preferences.", + "application.preferences.preamble": "If you qualify for this preference, you'll get a higher ranking.", + "application.preferences.selectBelow": "If you have one of these housing preferences, select it below:", + "application.preferences.dontWant": "I don't want these preferences", + "application.preferences.dontWantSingular": "I don't want this preference", + "application.preferences.stillHaveOpportunity": "You'll still have the opportunity to claim other preferences.", + "application.preferences.youHaveClaimed": "You have claimed:", + "application.preferences.displacedHousehold.title": "Displaced Household", + "application.preferences.displacedHousehold.displacedHousehold.label": "Displaced Household Preference", + "application.preferences.displacedHousehold.displacedHousehold.description": "", + "application.preferences.neighborhoodResidence.title": "Neighborhood Residents", + "application.preferences.neighborhoodResidence.neighborhoodResidence.label": "Neighborhood Residents Preference", + "application.preferences.neighborhoodResidence.neighborhoodResidence.description": "", + "application.preferences.liveWork.title": "Live or Work in %{county} County?", + "application.preferences.liveWork.live.label": "Live in %{county} County Preference", + "application.preferences.liveWork.live.description": "Live in %{county} copy goes here…", + "application.preferences.liveWork.live.link": "http://domain.com", + "application.preferences.liveWork.work.label": "Work in %{county} County Preference", + "application.preferences.liveWork.work.description": "Work in %{county} copy goes here…", + "application.preferences.liveWork.work.link": "http://domain.com", + "application.preferences.liveWorkCity.title": "Live or Work in the City of %{county}", + "application.preferences.liveWorkCity.live.label": "Live in the City of %{county} Preference", + "application.preferences.liveWorkCity.live.description": "At least one member of my household lives in the City of %{county}", + "application.preferences.liveWorkCity.work.label": "Work in the City of %{county} Preference", + "application.preferences.liveWorkCity.work.description": "At least one member of my household works in the City of %{county}", + "application.preferences.liveWorkOakland.title": "Live or Work in the City of Oakland", + "application.preferences.liveWorkOakland.live.label": "Live in the City of Oakland Preference", + "application.preferences.liveWorkOakland.live.description": "At least one member of my household lives in the City of Oakland", + "application.preferences.liveWorkOakland.work.label": "Work in the City of Oakland Preference", + "application.preferences.liveWorkOakland.work.description": "At least one member of my household works in the City of Oakland", + "application.preferences.liveWorkLivermore.title": "Live or Work in the City of Livermore", + "application.preferences.liveWorkLivermore.live.label": "Live in the City of Livermore Preference", + "application.preferences.liveWorkLivermore.live.description": "At least one member of my household lives in the City of Livermore", + "application.preferences.liveWorkLivermore.work.label": "Work in the City of Livermore Preference", + "application.preferences.liveWorkLivermore.work.description": "At least one member of my household works at least 20 hours/week in the City of Livermore", + "application.preferences.liveWorkTriValley.title": "Live or Work in the Tri-Valley Area (Dublin, Livermore, Pleasanton)", + "application.preferences.liveWorkTriValley.live.label": "Live in the Tri-Valley Area Preference", + "application.preferences.liveWorkTriValley.live.description": "At least one member of my household lives in the City of Dublin, Livermore or Pleasanton", + "application.preferences.liveWorkTriValley.work.label": "Work in the Tri-Valley Area (Dublin, Livermore, Pleasanton) Preference", + "application.preferences.liveWorkTriValley.work.description": "At least one member of my household works in the City of Dublin, Livermore or Pleasanton", + "application.preferences.PBV.title": "%{county} copy goes here…", + "application.preferences.PBV.residency.label": "Residency", + "application.preferences.PBV.residency.description": "%{county} copy goes here…", + "application.preferences.PBV.generalResidency.label": "Residency", + "application.preferences.PBV.generalResidency.description": "%{county} copy goes here…", + "application.preferences.PBV.residencyNoColiseum.label": "Residency", + "application.preferences.PBV.residencyNoColiseum.description": "%{county} copy goes here…", + "application.preferences.PBV.family.label": "Family", + "application.preferences.PBV.family.description": "%{county} copy goes here…", + "application.preferences.PBV.veteran.label": "Veteran", + "application.preferences.PBV.veteran.description": "%{county} copy goes here…", + "application.preferences.PBV.homeless.label": "Homeless", + "application.preferences.PBV.homeless.description": "%{county} copy goes here…", + "application.preferences.PBV.noneApplyButConsider.label": "None of these preferences apply to me, but I would like to be considered", + "application.preferences.PBV.doNotConsider.label": "I don't want to be considered for [housing authority] project-based voucher units", + "application.preferences.livermorePBV.title": "Livermore Housing Authority Preferences for Project-Based Voucher Units", + "application.preferences.livermorePBV.description": "Housing units for lease in this application process have rental subsidies provided by the Livermore Housing Authority. With that subsidy, tenant households pay 30% of their income as rent. These tenants are required to verify their income annually with the property manager as well as the Livermore Housing Authority.\nIf you would like to also apply for Livermore Housing Authority Project-based Voucher units, please answer the following housing preference questions in the following screens.", + "application.preferences.livermorePBV.insufficientFundingTerminated.label": "Terminated due to Insufficient funding", + "application.preferences.livermorePBV.insufficientFundingTerminated.description": "At least one member of my household was terminated from its Housing Choice Voucher (HCV) program due to insufficient program funding.", + "application.preferences.livermorePBV.insufficientFundingWithdrawn.label": "Withdrawn Voucher due to Insufficient funding", + "application.preferences.livermorePBV.insufficientFundingWithdrawn.description": "At least one member of my household was issued a Livermore Housing Authority voucher and whose voucher was withdrawn due to insufficient program funding.", + "application.preferences.livermorePBV.emergencyTransfer.label": "Existing Participant Emergency Transfer Preference", + "application.preferences.livermorePBV.emergencyTransfer.description": "At least one member of my household was a victim of domestic violence, dating violence, sexual assault, or stalking and is seeking an emergency transfer under the Violence Against Women Act (VAWA) from the Public Housing Authority’s public housing program or other covered housing program operated by the Public Housing Authority. The applicant must certify that the perpetrator will not be permitted to reside, visit or to stay as a guest in the housing choice voucher assisted unit.", + "application.preferences.livermorePBV.homeless.label": "Homeless", + "application.preferences.livermorePBV.homeless.description": "At least one member of my household verifiably lacks housing, including one whose primary residence during the night is a supervised public or private facility that provides temporary living accommodations; an individual who is a resident in transitional housing; or an individual who has a primary residence in a public or private place not designed for, or ordinarily used as, a regular sleeping accommodation for human beings as confirmed by the applicant’s local homeless service organization or consortia of organizations.", + "application.preferences.livermorePBV.displacedFamily.label": "Displaced Family Preference", + "application.preferences.livermorePBV.displacedFamily.description": "At least one member of my household was displaced from their unit due to a nationally declared disaster, or due to a state declared disaster.", + "application.preferences.livermorePBV.residency.label": "Residency Preference", + "application.preferences.livermorePBV.residency.description": "At least one member of my household is a resident family. A resident family is defined as a family who lives, works, or who has been hired to work within the City of Livermore.\nUse of the residency preference will not have the purpose or effect of delaying admission to the program on the basis of race, color, religion, sex, national origin, age, familial status, disability, sexual orientation, gender identity, or marital status.\nHomeless applicants will qualify for the residency preference if homeless within the City of Livermore.\nFor purposes of this preference, the term “homeless” generally means: (1) An individual or family who lacks a fixed, regular, and adequate nighttime residence; (2) An individual or family with a primary nighttime residence that is a public or private place not designed for or ordinarily used as a regular sleeping accommodation for human beings, including a car, park, abandoned building, bus or train station, airport, or camping ground; or (3) An individual or family living in a supervised publicly or privately operated shelter designated to provide temporary living arrangements (including hotels and motels paid for by Federal, State, or local government programs for low income individuals or by charitable organizations, congregate shelters, and transitional housing).", + "application.preferences.livermorePBV.working.label": "Working Preference", + "application.preferences.livermorePBV.working.description": "At least one member of my household is the head, spouse, cohead, or sole member of the household and is working and has worked an average of 20 hours a week for the past 6 months. As required by HUD, families where the head and spouse, or sole member is a person age 62 or older, or is a person with disabilities, will also be given the benefit of the working preference [24 CFR 960.206(b)(2)].\nExample 1: Head of household is elderly, but does not work. There is no spouse or co-head. This family receives benefit of the working preference.\nExample 2: Head of household is 64, spouse is disabled. Neither work. This family receives benefit of the working preference.\nExample 3: Head of household is 63, spouse is neither elderly nor disabled. Neither work. This family does NOT receive benefit of the working preference since both the head of household and spouse (or cohead) must be elderly and/or disabled to receive benefit of the working preference, unless one is working an average of 20 hours a week for the past 6 months.", + "application.preferences.livermorePBV.veteran.label": "Veteran’s Preference", + "application.preferences.livermorePBV.veteran.description": "At least one member of my household has served in the active military, naval, or air service of the United States and received other than a dishonorable discharge. This preference applies to veterans and the unmarried surviving spouses of veterans.", + "application.preferences.livermorePBV.noneApplyButConsider.label": "None of these preferences apply, but I still want to be considered for the PBV units", + "application.preferences.livermorePBV.doNotConsider.label": "Do not consider me for the PBV units", + "application.preferences.livermorePBV.onlyConsider.label": "I only want to be considered for the PBV units", + "application.preferences.developmentalDisability.title": "Do you or anyone in your household have a developmental disability?", + "application.preferences.developmentalDisability.description": "By selecting “yes”, you confirm that you someone in your household meets the definition of developmentally disabled.", + "application.preferences.developmentalDisability.yes.label": "Yes, I or someone in my household has a developmental disability", + "application.preferences.developmentalDisability.yes.description": "A developmental disability is defined in California law as intellectual disability, cerebral palsy, epilepsy, and autism. Other substantially disabling conditions closely related to intellectual disability or which require treatment similar to the treatment required by persons with intellectual disability may be eligible for services. The onset of these conditions had to have been prior to age 18; continues or can be expected to continue indefinitely and constitutes a substantial handicap for the individual.\n\nA Substantial Disability is s a condition which results in major impairment of cognitive and/or social functioning, representing sufficient impairment to require interdisciplinary planning and coordination of special or generic services to assist the individual in achieving maximum potential; and\n\nMust cause significant functional limitations, as determined by the regional center, in three or more of the following areas of major life activity, as appropriate to the person’s age. Since an individual’s cognitive and/or social functioning are many-faceted, the existence of a major impairment shall be determined through assessment(s) in the following areas of daily life activity:\n\n• Receptive and expressive language\n\n• Learning\n\n• Self-care\n\n•Mobility\n\n• Self-direction\n\n• Capacity for independent living\n\n• Economic self-sufficiency\n\nA Developmental Disability shall not include handicapping conditions that are:\n\n• Solely psychiatric disorders where there is impaired intellectual or social functioning which originated as a result of the psychiatric disorder or treatment given for such a disorder. Such psychiatric disorders include psycho-social deprivation and/or psychosis, severe neurosis or personality disorders even where social and intellectual functioning has become seriously impaired as an integral manifestation of the disorder\n\n• Solely learning disabilities. A learning disability is a condition which manifests as a significant discrepancy between estimated cognitive potential and actual level of educational performance, and which is not a result of generalized intellectual disability, educational or psycho-social deprivation, psychiatric disorder, or sensory loss\n\n• Solely physical in nature. These conditions include congenital anomalies or conditions acquired through disease, accident, or faulty development which are not associated with a neurological impairment that results in a need for treatment similar to that required for intellectual disability. Some examples are polio, muscular dystrophy, and arthritis\n\nBy selecting “yes” that you meet the definition for Developmentally Disabled, you give your permission to MidPen Housing to share your information with the Regional Center (or other agency) you have indicated for purposes of confirming your eligibility.", + "application.preferences.developmentalDisability.no.label": "No, no one in my household has a developmental disability", + "application.preferences.developmentalDisability.iDontKnow.label": "I don't know if someone in my household has a developmental disability", + "application.preferences.developmentalDisabilityRegistration.title": "Are you registered with the Regional Center of the East Bay, or another Regional Center that supports people with developmental disabilities?", + "application.preferences.developmentalDisabilityRegistration.description": "If Yes, please provide the name of the organization. You give your permission to share your information with the Regional Center (or other agency) you have indicated for purposes of confirming your eligibility.", + "application.preferences.developmentalDisabilityRegistration.yes.label": "Yes, I am registered with an organization that supports developmental disabilities", + "application.preferences.developmentalDisabilityRegistration.no.label": "No, I am not registered with an organization that supports developmental disabilities", + "application.preferences.developmentalDisabilityRegistration.iDontKnow.label": "I don't know if I am registered with an organization that supports developmental disabilities", + "application.preferences.HOPWA.title": "Housing Opportunities for Persons with AIDS", + "application.preferences.HOPWA.hopwa.label": "Housing Opportunities for Persons with AIDS", + "application.preferences.HOPWA.hopwa.description": "%{county} copy goes here…", + "application.preferences.HOPWA.doNotConsider.label": "I don't want to be considered", + "application.preferences.displacedTenant.title": "Displaced Tenant Housing Preference", + "application.preferences.displacedTenant.whichHouseholdMember": "Which household member is claiming this preference?", + "application.preferences.displacedTenant.whatAddress": "What address was the household member displaced from?", + "application.preferences.displacedTenant.general.label": "Displaced Tenant Housing Preference", + "application.preferences.displacedTenant.general.description": "Displaced Tenant copy goes here…", + "application.preferences.displacedTenant.general.link": "http://domain.com", + "application.preferences.displacedTenant.missionCorridor.label": "Mission Corridor", + "application.preferences.displacedTenant.missionCorridor.description": "Mission Corridor copy goes here…", + "application.preferences.general.title": "Based on the information you have entered, your household has not claimed any housing preferences.", + "application.preferences.general.preamble": "You will be in the general pool of applicants.", + "application.preferences.options.address": "Address", + "application.preferences.options.name": "Name", + "application.preferences.options.organization": "Name of Organization", + "application.preferences.rosefieldLive.title": "Previous Residents of Rosefield Village Relocated Outside of the City of Alameda", + "application.preferences.rosefieldLive.yes.label": "At least one member of my household was a previous resident of Rosefield Village", + "application.preferences.rosefieldLive.yes.description": "At least one member of my household was a previous resident of Rosefield Village", + "application.preferences.rosefieldAUSD.title": "Alameda Unified School District (AUSD) employee", + "application.preferences.rosefieldAUSD.yes.label": "At least one member of my household is an Alameda Unified School District employee", + "application.preferences.rosefieldAUSD.yes.description": "At least one member of my household is an Alameda Unified School District employee.", + "application.preferences.dublinHousing.title": "Alameda Unified School District (AUSD) employee", + "application.preferences.dublinHousing.liveInDublin.label": "At least one member of my household lives in Dublin (3 points)", + "application.preferences.dublinHousing.immediateFamily.label": "At least one member of my household has an immediate family member in Dublin (1 point)", + "application.preferences.dublinHousing.displacedResident.label": "At least one member of my household was required to relocate from current Dublin residence due to demolition of dwelling or conversation of dwelling from rental to for-sale unit (1 point)", + "application.preferences.dublinHousing.worksInDublin.label": "At least one member of my household works full-time in Dublin (3 points)", + "application.preferences.dublinHousing.publicServiceEmployee.label": "At least one member of my household is a public service employee in Dublin (1 additional point)", + "application.preferences.dublinHousing.permanentlyDisabled.label": "At least one member of my household is permanently disabled (1 point)", + "application.preferences.dublinHousing.senior.label": "At least one member of my household is a senior, defined as age 62 and older (1 point)", + "application.preferences.terminationOfAffordability.title": "Persons subject to termination of affordability restrictions", + "application.preferences.terminationOfAffordability.atLeastOne.label": "At least one member of my household is subject to termination of affordability restrictions", + "application.preferences.liveWorkFosterCity.title": "Persons who live and work in Foster City.", + "application.preferences.liveWorkFosterCity.live.label": "At least one member of my household lives in Foster City", + "application.preferences.liveWorkFosterCity.work.label": "At least one member of my household works in Foster City", + "application.preferences.liveFosterCity.title": "Persons who live in Foster City.", + "application.preferences.liveFosterCity.live.label": "At least one member of my household lives in Foster City", + "application.preferences.fosterCityEmployee.title": "Employees of the City of Foster City.", + "application.preferences.fosterCityEmployee.employed.label": "At least one member of my household is an employee of the City of Foster City", + "application.preferences.fosterCitySchoolEmployee.title": "School District Employees", + "application.preferences.fosterCitySchoolEmployee.employed.label": "At least one member of my household is a school district employee", + "application.preferences.workFosterCity.title": "Work in Foster City", + "application.preferences.workFosterCity.work.label": "At least one member of my household works in Foster City", + "application.preferences.liveWorkSanMateo.title": "Live or work in the City of San Mateo", + "application.preferences.liveWorkSanMateo.live.label": "At least one member of my household lives in the City of San Mateo", + "application.preferences.liveWorkSanMateo.work.label": "At least one member of my household works in the City of San Mateo", + "application.preferences.liveWorkEastPaloAlto.title": "Live or work in the City of East Palo Alto", + "application.preferences.liveWorkEastPaloAlto.live.label": "At least one member of my household lives in the City of East Palo Alto", + "application.preferences.liveWorkEastPaloAlto.work.label": "At least one member of my household works 20 hours per week or more in the City of East Palo Alto", + "application.preferences.displaceeEastPaloAlto.title": "Involuntary Displacement from East Palo Alto", + "application.preferences.displaceeEastPaloAlto.naturalDisaster.label": "Natural Disaster declared by Governor", + "application.preferences.displaceeEastPaloAlto.domesticViolence.label": "Domestic Violence", + "application.preferences.displaceeEastPaloAlto.codeEnforcement.label": "City Code Enforcement Activity", + "application.preferences.displaceeEastPaloAlto.noFaultEviction.label": "A \"No Fault\" Eviction from a rental unit in East Palo Alto within the last year of this application", + "application.preferences.displaceeEastPaloAlto.increasedRent.label": "A 10% or higher increase in rent in the last 12 months", + "application.review.takeAMomentToReview": "Take a moment to review your information before submitting your application.", + "application.review.sameAddressAsApplicant": "Same Address as Applicant", + "application.review.noAdditionalMembers": "No additional household members", + "application.review.householdDetails": "Household Details", + "application.review.voucherOrSubsidy": "Housing Voucher or Rental Subsidy", + "application.review.lastChanceToEdit": "This is your last chance to edit before submitting.", + "application.review.terms.title": "Terms", + "application.review.terms.text": "This application must be submitted by %{applicationDueDate}.

Applicants will be contacted by the leasing agent in lottery and preference order or waitlist order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application may be removed from the waitlist if you have made any fraudulent statements and duplicate applications from the same household may be removed as only one application per household is permitted. Should your application be chosen for review, be prepared to fill out a more detailed application and provide required supporting documents. For more information, please contact the developer or leasing agent posted in the listing. Please contact the developer/property manager directly if there are any updates to your application.

If we cannot verify a housing lottery preference that you have claimed, you will not receive the preference but will not be otherwise penalized.

Completing this housing application does not entitle you to housing or indicate you are eligible for housing; all applicants will be screened as outlined in the property’s Resident Selection Criteria. We offer no guarantees about obtaining housing.

You cannot change your online application after you submit.

I declare that the foregoing is true and accurate, and acknowledge that any misstatement fraudulently or negligently made on this application may result in removal from the lottery.

", + "application.review.terms.confirmCheckboxText": "I agree and understand that I cannot change anything after I submit.", + "application.review.demographics.title": "Help us ensure we are meeting our goal to serve all people.", + "application.review.demographics.subTitle": "These questions are optional and won't affect your eligibility for housing. Your answers will be kept private.", + "application.review.demographics.ethnicityLabel": "Which best describes your ethnicity?", + "application.review.demographics.raceLabel": "Which best describes your race?", + "application.review.demographics.genderLabel": "What is your gender?", + "application.review.demographics.genderInfo": "Select one that best describes your current gender identity.", + "application.review.demographics.sexualOrientationLabel": "How do you describe your sexual orientation or sexual identity?", + "application.review.demographics.howDidYouHearLabel": "How did you hear about this listing?", + "application.review.demographics.ethnicityOptions.hispanicLatino": "Hispanic / Latino", + "application.review.demographics.ethnicityOptions.notHispanicLatino": "Not Hispanic / Latino", + "application.review.demographics.raceOptions.americanIndianAlaskanNative": "American Indian / Alaskan Native", + "application.review.demographics.raceOptions.americanIndianAlaskanNativeAndBlackAfricanAmerican": "American Indian / Alaskan Native and Black/African American", + "application.review.demographics.raceOptions.americanIndianAlaskanNativeAndWhite": "American Indian / Alaskan Native and White", + "application.review.demographics.raceOptions.asian": "Asian", + "application.review.demographics.raceOptions.asianAndWhite": "Asian and White", + "application.review.demographics.raceOptions.asian-asianIndian": "Asian Indian", + "application.review.demographics.raceOptions.asian-otherAsian": "Other Asian", + "application.review.demographics.raceOptions.blackAfricanAmerican": "Black / African American", + "application.review.demographics.raceOptions.blackAfricanAmericanAndWhite": "Black / African American and White", + "application.review.demographics.raceOptions.asian-chinese": "Chinese", + "application.review.demographics.raceOptions.declineToRespond": "Decline to Respond", + "application.review.demographics.raceOptions.asian-filipino": "Filipino", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander-guamanianOrChamorro": "Guamanian or Chamorro", + "application.review.demographics.raceOptions.asian-japanese": "Japanese", + "application.review.demographics.raceOptions.asian-korean": "Korean", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander-nativeHawaiian": "Native Hawaiian", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander": "Native Hawaiian / Other Pacific Islander", + "application.review.demographics.raceOptions.otherMultiracial": "Other / Multiracial", + "application.review.demographics.raceOptions.otherMutliracial": "Other / Mutliracial", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander-otherPacificIslander": "Other Pacific Islander", + "application.review.demographics.raceOptions.nativeHawaiianOtherPacificIslander-samoan": "Samoan", + "application.review.demographics.raceOptions.asian-vietnamese": "Vietnamese", + "application.review.demographics.raceOptions.white": "White", + "application.review.demographics.genderOptions.female": "Female", + "application.review.demographics.genderOptions.male": "Male", + "application.review.demographics.genderOptions.genderqueerGenderNon-Binary": "Genderqueer / Gender Non-Binary", + "application.review.demographics.genderOptions.transFemale": "Trans Female", + "application.review.demographics.genderOptions.transMale": "Trans Male", + "application.review.demographics.genderOptions.notListed": "Not Listed", + "application.review.demographics.sexualOrientationOptions.bisexual": "Bisexual", + "application.review.demographics.sexualOrientationOptions.gayLesbianSameGenderLoving": "Gay / Lesbian / Same-Gender Loving", + "application.review.demographics.sexualOrientationOptions.questioningUnsure": "Questioning / Unsure", + "application.review.demographics.sexualOrientationOptions.straightHeterosexual": "Straight / Heterosexual", + "application.review.demographics.sexualOrientationOptions.notListed": "Not Listed", + "application.review.demographics.howDidYouHearOptions.jurisdictionWebsite": "Alameda County HCD Website", + "application.review.demographics.howDidYouHearOptions.developerWebsite": "Developer Website", + "application.review.demographics.howDidYouHearOptions.flyer": "Flyer", + "application.review.demographics.howDidYouHearOptions.emailAlert": "Email Alert", + "application.review.demographics.howDidYouHearOptions.friend": "Friend", + "application.review.demographics.howDidYouHearOptions.housingCounselor": "Housing Counselor", + "application.review.demographics.howDidYouHearOptions.radioAd": "Radio Ad", + "application.review.demographics.howDidYouHearOptions.busAd": "Bus Ad", + "application.review.demographics.howDidYouHearOptions.other": "Other", + "application.review.confirmation.title": "Thanks. We have received your application for ", + "application.review.confirmation.lotteryNumber": "Here's your application confirmation number", + "application.review.confirmation.pleaseWriteNumber": "Please write down your application number and keep it in a safe place. We have also emailed this number to you if you provided an email address.", + "application.review.confirmation.whatExpectTitle": "What to expect next", + "application.review.confirmation.whatExpectFirstParagraph.held": "The lottery will be held on ", + "application.review.confirmation.whatExpectFirstParagraph.attend": " You do not need to attend the housing lottery. Results will be posted ", + "application.review.confirmation.whatExpectFirstParagraph.listing": "on the listing. ", + "application.review.confirmation.whatExpectFirstParagraph.refer": "Please refer to the listing for the lottery results date.", + "application.review.confirmation.whatExpectSecondparagraph": "Applicants will be contacted in order until vacancies are filled. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.", + "application.review.confirmation.doNotSubmitTitle": "Do not submit another application for this listing.", + "application.review.confirmation.needToUpdate": "If you need to update information on your application, do not apply again. Contact the agent if you did not receive an email confirmation.", + "application.review.confirmation.createAccountTitle": "Would you like to create an account?", + "application.review.confirmation.createAccountParagraph": "Creating an account will save your information for future applications, and you can check the status of this application anytime.", + "application.review.confirmation.imdone": "No thanks, I'm done.", + "application.review.confirmation.browseMore": "Browse more listings", + "application.review.confirmation.print": "View submitted application and print a copy.", + "application.confirmation.viewOriginalListing": "View the original listing", + "application.confirmation.informationSubmittedTitle": "Here's the information you submitted.", + "application.confirmation.submitted": "Submitted: ", + "application.confirmation.lotteryNumber": "Your confirmation number", + "application.confirmation.preferences": "Preferences", + "application.confirmation.generalLottery": "Based on information you have entered, your household has not claimed any housing lottery preferences. You will be in the general lottery.", + "application.confirmation.printCopy": "Print a copy for your records", + "application.start.whatToExpect.title": "Here's what to expect from this application.", + "application.start.whatToExpect.info1": "First we'll ask about you and the people you plan to live with. Then, we'll ask about your income. Finally, we'll see if you qualify for any affordable housing lottery preference.", + "application.start.whatToExpect.info2": "Please be aware that each household member can only appear on one application for each listing.", + "application.start.whatToExpect.info3": "Any fraudulent statements will cause your application to be removed.", + "application.timeout.text": "To protect your identity, your session will expire in one minute due to inactivity. You will lose any unsaved information if you choose not to respond.", + "application.timeout.action": "Continue working", + "application.timeout.afterMessage": "We care about your security. We ended your session due to inactivity. Please start a new application to continue.", + "application.continueApplication": "Continue Application", + "application.applicationNeverSubmitted": "Your application was never submitted", + "application.deleteThisApplication": "Delete this application?", + "application.deleteThisMember": "Delete this member?", + "application.deleteMemberDescription": "Do you really want to delete this member?", + "application.deleteApplicationDescription": "Deleting this application means you will lose all the information you've entered.", + "application.edited": "Edited", + "application.status": "Status", + "application.statuses.inProgress": "In Progress", + "application.statuses.neverSubmitted": "Never Submitted", + "application.statuses.submitted": "Submitted", + "application.viewApplication": "View Application", + "application.yourLotteryNumber": "Your confirmation number is", + "users.confirmed": "Confirmed", + "users.unconfirmed": "Unconfirmed", + "users.totalUsers": "total users", + "users.administrator": "Administrator", + "users.partner": "Partner", + "users.addUser": "Add User", + "users.editUser": "Edit User", + "users.userDetails": "User Details", + "users.allListings": "All listings", + "users.inviteSent": "Invite sent", + "users.confirmationSent": "Confirmation sent", + "users.addPassword": "Add a Password", + "users.needUniquePassword": "You'll need to add a unique password in order to confirm your account.", + "users.makeNote": "When creating your password make sure you make note of it so you remember it in the future.", + "users.confirmAccount": "Confirm Account", + "users.accountConfirmed": "Account confirmed", + "users.resendInvite": "Resend Invite", + "users.userUpdated": "User updated", + "users.userDeleted": "User deleted", + "users.doYouWantDeleteUser": "Do you really want to delete this user?", + "users.requestResend": "Request Resend", + "users.requestResendExplanation": "Please enter your email, and we'll send a new confirmation", + "users.requestResendEnterEmail": "Enter a valid email address", + "users.requestResendDescription": "Your token expired, and will need to have a new confirmation email sent to you", + "flags.flaggedSet": "Flagged Set", + "flags.ruleName": "Rule Name", + "flags.pendingReview": "Pending Review", + "flags.totalSets": "Total Sets", + "flags.resolveFlag": "Resolve Flag", + "flags.markedAsDuplicate": "%{quantity} applications marked as duplicate", + "authentication.forgotPassword.changePassword": "Change Password", + "authentication.forgotPassword.sendEmail": "Send email", + "authentication.forgotPassword.message": "If there is an account made with that email, you'll receive an email with a link to reset your password.", + "authentication.forgotPassword.errors.tokenExpired": "Reset password token expired. Please request for a new one.", + "authentication.forgotPassword.errors.tokenMissing": "Token not found. Please request for a new one.", + "authentication.forgotPassword.errors.generic": "There was an error. Please try again, or contact support for help.", + "authentication.forgotPassword.errors.emailNotFound": "Email not found. Please make sure your email has an account with us and is confirmed.", + "authentication.timeout.text": "To protect your identity, your session will expire in one minute due to inactivity. You will lose any unsaved information and be logged out if you choose not to respond.", + "authentication.timeout.action": "Stay logged in", + "authentication.timeout.signOutMessage": "We care about your security. We logged you out due to inactivity. Please sign in to continue.", + "authentication.signIn.loginError": "Please enter a valid email address", + "authentication.signIn.passwordError": "Please enter a valid password", + "authentication.signIn.phoneError": "Please enter a valid US phone number.", + "authentication.signIn.cantFindAccount": "We couldn't find an account with that email address/password.", + "authentication.signIn.changeYourPassword": "You can change your password here", + "authentication.signIn.error": "There was an error signing you in", + "authentication.signIn.errorGenericMessage": "Please try again, or contact support for help.", + "authentication.signIn.forgotPassword": "Forgot password?", + "authentication.signIn.success": "Welcome back, %{name}", + "authentication.signIn.passwordOutdated": "Your password has expired. Please reset your password.", + "authentication.signIn.accountHasBeenLocked": "For security reasons, this account has been locked.", + "authentication.signIn.youHaveToWait": "You’ll have to wait 30 minutes since the last failed attempt before trying again.", + "authentication.signIn.enterValidEmailAndPassword": "Please enter a valid email and password.", + "authentication.signIn.afterFailedAttempts": "For security reasons, after 5 failed attempts, you’ll have to wait 30 minutes before trying again.", + "authentication.signIn.partnersSignIn": "Partners Sign In", + "authentication.signIn.dontHaveAccount": "Don't have an account", + "authentication.signIn.enterLoginPassword": "Please enter your login password", + "authentication.signIn.enterLoginEmail": "Please enter your login email", + "authentication.signIn.yourAccountIsNotConfirmed": "Your account is not confirmed", + "authentication.createAccount.accountConfirmed": "Your account was successfully confirmed.", + "authentication.createAccount.anEmailHasBeenSent": "An email has been sent to %{email}", + "authentication.createAccount.confirmationInstruction": "Please click on the link in the email we sent you in order to complete account creation.", + "authentication.createAccount.confirmationNeeded": "Confirmation needed", + "authentication.createAccount.emailSent": "Confirmation email has been sent. Please check your inbox.", + "authentication.createAccount.yourName": "Your Name", + "authentication.createAccount.firstName": "First Name", + "authentication.createAccount.middleNameOptional": "Middle Name (optional)", + "authentication.createAccount.middleName": "Middle Name", + "authentication.createAccount.noAccount": "Don't have an account?", + "authentication.createAccount.lastName": "Last Name", + "authentication.createAccount.yourDateOfBirth": "Your Date of Birth", + "authentication.createAccount.email": "Email", + "authentication.createAccount.emailPrivacy": "We will only use your email address to contact you about your application.", + "authentication.createAccount.reEnterEmail": "Re-enter email address", + "authentication.createAccount.phonePlaceholder": "(555) 555-5555", + "authentication.createAccount.phone": "Phone", + "authentication.createAccount.reEnterPassword": "Re-enter your password", + "authentication.createAccount.resendTheEmail": "Resend the email", + "authentication.createAccount.emailSubscription": "Email me whenever a listing is posted or updated.", + "authentication.createAccount.smsSubscription": "Text me whenever a listing is posted or updated.", + "authentication.createAccount.linkExpired": "Your link has expired", + "authentication.createAccount.mustBe8Chars": "Must be 8 characters", + "authentication.createAccount.password": "Password", + "authentication.createAccount.passwordInfo": "Must be at least 8 characters and include at least 1 letter and at least one number.", + "authentication.createAccount.resendEmailInfo": "Please click on the link in the email we send you within 24 hours in order to complete account creation.", + "authentication.createAccount.resendAnEmailTo": "Resend an email to", + "authentication.createAccount.errors.accountConfirmed": "Your account is already confirmed.", + "authentication.createAccount.errors.errorSaving": "Oops! Looks like something went wrong. Please try again. \n\nContact your housing department if you're still experiencing issues.", + "authentication.createAccount.errors.emailInUse": "Email is already in use", + "authentication.createAccount.errors.emailMismatch": "The emails do not match", + "authentication.createAccount.errors.emailNotFound": "Email not found. Please register first.", + "authentication.createAccount.errors.passwordMismatch": "The passwords do not match", + "authentication.createAccount.errors.passwordTooWeak": "Password is too weak. Must be at least 8 characters and include at least 1 letter and at least one number.", + "authentication.createAccount.errors.tokenExpired": "Your link has expired.", + "authentication.createAccount.errors.tokenMissing": "Wrong token provided.", + "authentication.createAccount.errors.generic": "Something went wrong while creating your account. Please try again.\n\nContact your housing department if you're still experiencing issues.", + "authentication.signOut.success": "You have successfully logged out of your account.", + "config.routePrefix": "", + "errors.alert.badRequest": "Looks like something went wrong. Please try again. \n\nContact your housing department if you're still experiencing issues.", + "errors.alert.timeoutPleaseTryAgain": "Oops! Looks like something went wrong. Please try again.", + "errors.alert.exportFailed": "Export failed. Please try again later. If the problem persists, please email supportbloom@exygy.com", + "errors.notFound.title": "Page Not Found", + "errors.notFound.message": "Uh oh, we can’t seem to find the page you’re looking for. Try going back to the previous page or click below to browse listings.", + "errors.unauthorized.title": "Unauthorized", + "errors.unauthorized.message": "Uh oh, you are not allowed to access this page.", + "errors.agreeError": "You must agree to the terms in order to continue", + "errors.firstNameError": "Please enter a First Name", + "errors.lastNameError": "Please enter a Last Name", + "errors.dateOfBirthError": "Please enter a valid Date of Birth", + "errors.dateOfBirthErrorAge": "Please enter a valid Date of Birth, must be 18 or older", + "errors.emailAddressError": "Please enter an email address", + "errors.phoneNumberError": "Please enter a phone number", + "errors.phoneNumberTypeError": "Please enter a phone number type", + "errors.streetError": "Please enter an address", + "errors.timeError": "Please enter a valid time", + "errors.cityError": "Please enter a city", + "errors.stateError": "Please enter a state", + "errors.zipCodeError": "Please enter a zipcode", + "errors.multipleZipCodeError": "Please enter one or more comma separated zip codes", + "errors.errorsToResolve": "There are errors you'll need to resolve before moving on.", + "errors.numberError": "Please enter a valid number greater than 0.", + "errors.selectAllThatApply": "Please select all that apply.", + "errors.selectAtLeastOne": "Please select at least one option.", + "errors.selectAnOption": "Please select an option.", + "errors.selectOption": "Please select one of the options above.", + "errors.urlError": "Please enter a valid url", + "errors.householdTooBig": "Your household size is too big.", + "errors.householdTooSmall": "Your household size is too small.", + "errors.dateError": "Please enter a valid date", + "errors.rateLimitExceeded": "Rate limit exceeded, try again later.", + "errors.requiredFieldError": "This field is required.", + "errors.requiredFieldsError": "These fields are required", + "errors.noData": "No data available.", + "errors.noFavorites": "You have not favorited any listings", + "footer.srHeading": "Footer", + "footer.srProjectInformation": "Project information", + "footer.srContactInformation": "Contact information", + "footer.srLegalInformation": "Legal information", + "footer.contact": "Contact", + "footer.terms": "Terms and Conditions", + "footer.forGeneralQuestions": "For general program inquiries, you may call us at 000-000-0000.", + "footer.giveFeedback": "Give Feedback", + "footer.privacyPolicy": "Privacy Policy", + "footer.copyright": "Demonstration Jurisdiction © 2021 • All Rights Reserved", + "housingCounselors.subtitle": "Talk with a local housing counselor specific to your needs.", + "housingCounselors.languageServices": "Language Services: ", + "housingCounselors.call": "Call %{number}", + "housingCounselors.visitWebsite": "Visit %{name}", + "homeType.apartment": "Apartment", + "homeType.duplex": "Duplex", + "homeType.house": "Single-Family House", + "homeType.townhome": "Townhome", + "languages.srHeading": "Languages", + "languages.srNavigation": "Language", + "languages.en": "English", + "languages.es": "Español", + "languages.zh": "中文", + "languages.vi": "Tiếng Việt", + "languages.ar": "عربى", + "languages.bn": "বাংলা", + "languages.tl": "Filipino", + "leasingAgent.contact": "Contact Leasing Agent", + "leasingAgent.dueToHighCallVolume": "Due to high call volume you may hear a message.", + "leasingAgent.name": "Leasing Agent Name", + "leasingAgent.namePlaceholder": "Full Name", + "leasingAgent.title": "Leasing Agent Title", + "leasingAgent.officeHours": "Office Hours", + "leasingAgent.officeHoursPlaceholder": "ex: 9:00am - 5:00pm, Monday to Friday", + "leasingAgent.managementWebsite": "Company Website", + "leasingAgent.managementWebsitePlaceholder": "https://www.google.com", + "listings.error": "There was an issue submitting the form.", + "listings.fieldError": "Please resolve any errors before saving or publishing your listing.", + "listings.closeThisListing": "Do you really want to close this listing?", + "listings.communityProgramsDescription": "This program includes opportunities for members of specific communities", + "listings.publishThisListing": "Publishing will push the listing live on the public site.", + "listings.active": "Accepting Applications", + "listings.pending": "Coming Soon", + "listings.closed": "Closed", + "listings.actions.publish": "Publish", + "listings.actions.draft": "Save as Draft", + "listings.actions.preview": "Preview", + "listings.actions.close": "Close", + "listings.actions.viewListing": "View listing", + "listings.actions.unpublish": "Unpublish", + "listings.actions.postResults": "Post Results", + "listings.actions.resultsPosted": "Results Posted", + "listings.actions.previewLotteryResults": "Preview Lottery Results", + "listings.activePreferences": "Active Preferences", + "listings.addBuildingSelectionCriteria": "Add Building Selection Criteria", + "listings.addListing": "Add Listing", + "listings.addPaperApplication": "Add Paper Application", + "listings.addPhoto": "Add Photo", + "listings.addPreference": "Add Preference", + "listings.addPreferences": "Add Preferences", + "listings.additionalApplicationSubmissionNotes": "Additional Application Submission Notes", + "listings.additionalInformation": "Additional Information", + "listings.allUnits": "All Units", + "listings.allUnitsReservedFor": "All units reserved for %{type}", + "listings.amenities.groceryStores": "Grocery Stores", + "listings.amenities.healthCareResources": "Health Care Resources", + "listings.amenities.parksAndCommunityCenters": "Parks and Community Centers", + "listings.amenities.pharmacies": "Pharmacies", + "listings.amenities.publicTransportation": "Public Transportation", + "listings.amenities.schools": "Schools", + "listings.amiOverrideTitle": "Override for household size of %{householdSize}", + "listings.annualIncome": "%{income} per year", + "listings.annualIncomeRange": "%{from} to %{to} per year", + "listings.applicationTitle": "Application Data", + "listings.applicationAddress": "Address", + "listings.applicationDeadline": "Application Due Date", + "listings.applicationDueTime": "Application Due Time", + "listings.applicationFCFS": "First Come First Serve", + "listings.applicationFee": "Application Fee", + "listings.applicationFeeDueAt": "Due at interview", + "listings.applicationOpenPeriod": "Applications Open", + "listings.applicationPerApplicantAgeDescription": "per applicant age 18 and over", + "listings.applicationPickupQuestion": "Can applications be picked up?", + "listings.applicationsClosed": "Applications Closed", + "listings.applicationDropOffQuestion": "Can applications be dropped off?", + "listings.apply.applicationSeason": "Residents should apply in", + "listings.apply.applicationWillBeAvailableOn": "Application will be available for download and pick up on %{openDate}", + "listings.apply.applyOnline": "Apply Online", + "listings.apply.downloadApplication": "Download Application", + "listings.apply.dropOffApplication": "Drop Off Application", + "listings.apply.dropOffApplicationOrMail": "Drop Off Application or Send by US Mail", + "listings.apply.getAPaperApplication": "Get a Paper Application", + "listings.apply.howToApply": "How to Apply", + "listings.apply.paperApplicationsMustBeMailed": "Paper applications must be sent by US Mail and cannot be submitted in person.", + "listings.apply.pickUpAnApplication": "Pick up an application", + "listings.apply.sendByUsMail": "Send Application by US Mail", + "listings.apply.submitAPaperApplication": "Submit a Paper Application", + "listings.apply.contactManagment": "Contact Management Company", + "listings.apply.submitPaperDueDateNoPostMark": "Applications must be received by the deadline. If sending by U.S. Mail, the application must be postmarked by %{applicationDueDate}. %{developer} is not responsible for lost or delayed mail.", + "listings.apply.submitPaperDueDatePostMark": "Applications must be received by the deadline. If sending by U.S. Mail, the application must be postmarked by %{applicationDueDate} and received by mail no later than %{postmarkReceivedByDate}. Applications received after %{postmarkReceivedByDate} via mail will not be accepted even if they are postmarked by %{applicationDueDate}. %{developer} is not responsible for lost or delayed mail.", + "listings.apply.submitPaperNoDueDateNoPostMark": "%{developer} is not responsible for lost or delayed mail.", + "listings.apply.submitPaperNoDueDatePostMark": "Applications must be received by the deadline. If sending by U.S. Mail, the application must be received by mail no later than %{postmarkReceivedByDate}. Applications received after %{postmarkReceivedByDate} via mail will not be accepted. %{developer} is not responsible for lost or delayed mail.", + "listings.atAnotherAddress": "At another address", + "listings.atLeasingAgentAddress": "At the leasing agent address", + "listings.atMailingAddress": "At the mailing address", + "listings.availableAndWaitlist": "Available Units & Open Waitlist", + "listings.availableUnits": "Available Units", + "listings.availableUnitsAndWaitlist": "Available units and waitlist", + "listings.availableUnitsAndWaitlistDesc": "Once applicants fill all available units, additional applicants will be placed on the waitlist for %{number} units", + "listings.bath": "bath", + "listings.browseListings": "Browse Listings", + "listings.buildingImageAltText": "A picture of the building", + "listings.buildingSelectionCriteria": "Building Criteria Selection", + "listings.closedListings": "Closed Listings", + "listings.confirmedPreferenceList": "Confirmed %{preference} List", + "listings.creditHistory": "Credit History", + "listings.criminalBackground": "Criminal Background", + "listings.customOnlineApplicationUrl": "Custom Online Application URL", + "listings.deleteListingDescription": "Deleting this listing means you will lose all the information you've entered.", + "listings.depositMax": "Deposit Max", + "listings.depositMin": "Deposit Min", + "listings.depositOrMonthsRent": "or one month's rent", + "listings.depositMayBeHigherForLowerCredit": "May be higher for lower credit scores", + "listings.details.listingData": "Listing Data", + "listings.details.createdDate": "Date Created", + "listings.details.updatedDate": "Date Updated", + "listings.details.id": "Listing ID", + "listings.developmentalDisabilities": "Persons with developmental disabilities", + "listings.developmentalDisabilitiesDescription": "A portion number of units in this building are set aside for persons with developmental disabilities. Please visit housingchoices.org for information on eligibility, requirements, how to get an application and for answers to any other questions you may have about the process.", + "listings.dropOffAddress": "Drop Off Address", + "listings.dueDateQuestion": "Is there an application due date?", + "listings.editPreferences": "Edit Preferences", + "listings.enterLotteryForWaitlist": "Submit an application for an open slot on the waitlist for %{units} units.", + "listings.firstComeFirstServe": "First come first serve", + "listings.forIncomeCalculations": "To determine your eligibility for this property, choose your household size (include yourself in that calculation). For each type of unit, your household cannot make more than the income limit shown below.", + "listings.forIncomeCalculationsBMR": "Income calculations are based on unit type", + "listings.hideClosedListings": "Hide Closed Listings", + "listings.householdMaximumIncome": "Household Maximum Income", + "listings.householdSize": "Household Size", + "listings.homeType": "Home Type", + "listings.importantProgramRules": "Important Program Rules", + "listings.includesPriorityUnits": "Includes Priority Units for %{priorities}", + "listings.isDigitalApplication": "Is there a digital application?", + "listings.isPaperApplication": "Is there a paper application?", + "listings.selectJurisdiction": "You must first select a jurisdiction", + "listings.latitude": "Latitude", + "listings.leasingAgentAddress": "Leasing Agent Address", + "listings.listingPreviewOnly": "This is a listing preview only.", + "listings.listingStatus.active": "Open", + "listings.listingStatus.pending": "Draft", + "listings.listingStatus.closed": "Closed", + "listings.listingSubmitted": "Listing Submitted", + "listings.listingUpdated": "Listing Updated", + "listings.longitude": "Longitude", + "listings.lotteryTitle": "Lottery", + "listings.lotteryDateNotes": "Lottery Date Notes", + "listings.lotteryDateQuestion": "When will the lottery be run?", + "listings.lotteryEndTime": "Lottery End Time", + "listings.lotteryResults.completeResultsWillBePosted": "Complete lottery results will be posted soon.", + "listings.lotteryResults.downloadResults": "Download Results", + "listings.lotteryResults.header": "Lottery Results", + "listings.lotteryStartTime": "Lottery Start Time", + "listings.mapPinAutomaticDescription": "Map pin position is based on the address provided", + "listings.mapPinCustomDescription": "Drag the pin to update the marker location", + "listings.mapPinPosition": "Map Pin Position", + "listings.mapPreview": "Map Preview", + "listings.mapPreviewNoAddress": "Enter an address to preview the map", + "listings.marketing": "Marketing", + "listings.marketingSection.status": "Marketing Status", + "listings.marketingSection.date": "Marketing Start Date", + "listings.maxAnnualIncome": "Maximum Annual Income", + "listings.maxIncomeMonth": "Maximum Income / Month", + "listings.maxIncomeYear": "Maximum Income / Year", + "listings.monthlyIncome": "%{income} per month", + "listings.monthlyIncomeRange": "%{from} to %{to} per month", + "listings.moreBuildingSelectionCriteria": "Find out more about Building Selection Criteria", + "listings.moreImagesLabel": "images", + "listings.moreImagesAltDescription": "more images for %{listingName}", + "listings.newListing": "New Listing", + "listings.noAvailableUnits": "There are no available units at this time.", + "listings.noOpenListings": "No listings currently have open applications.", + "listings.occupancyDescriptionNoSro": "Occupancy limits for this building are based on unit type.", + "listings.openHouseEvent.header": "Open Houses", + "listings.openHouseEvent.seeVideo": "See Video", + "listings.paperDifferentAddress": "Paper applications are mailed to another address", + "listings.percentAMIUnit": "%{percent}% AMI Unit", + "listings.pickupAddress": "Pickup Address", + "listings.postmarkByDate": "Postmark by Date", + "listings.postmarkByTime": "Postmark by Time", + "listings.receivedByDate": "Received by Date", + "listings.receivedByTime": "Received by Time", + "listings.postmarksConsideredQuestion": "Are postmarks considered?", + "listings.priorityUnits": "Priority Units", + "listings.priorityUnitsDescription": "This building has units set aside if any of the following apply to you or someone in your household:", + "listings.title": "Property Data", + "listings.developer": "Housing Developer", + "listings.buildingAddress": "Address", + "listings.publicLottery.header": "Public Lottery", + "listings.publicLottery.seeVideo": "See Video", + "listings.recommended": "Recommended", + "listings.referralContactPhone": "Referral Contact Phone", + "listings.referralSummary": "Referral Summary", + "listings.remainingUnitsAfterPreferenceConsideration": "After all preference holders have been considered, any remaining units will be available to other qualified applicants.", + "listings.rentalHistory": "Rental History", + "listings.requiredDocuments": "Required Documents", + "listings.requiredToPublish": "Required to publish", + "listings.reservedCommunityBuilding": "%{type} Building", + "listings.reservedCommunityDescription": "Reserved Community Description", + "listings.reservedCommunitySeniorTitle": "Senior Building", + "listings.reservedCommunityTitleDefault": "Reserved Building", + "listings.reservedCommunityTypes.senior": "Seniors", + "listings.reservedCommunityTypes.senior55": "Seniors 55+", + "listings.reservedCommunityTypes.senior62": "Seniors 62+", + "listings.reservedCommunityTypes.partiallySenior": "Partially Seniors", + "listings.reservedCommunityTypes.specialNeeds": "Accessible", + "listings.reservedCommunityTypes.developmentalDisability": "Developmental Disability", + "listings.reservedFor": "Reserved for %{type}", + "listings.reservedCommunityType": "Reserved Community Type", + "listings.reservedTypePlural.family": "families", + "listings.reservedTypePlural.senior": "seniors", + "listings.reservedTypePlural.veteran": "veterans", + "listings.reservedTypePlural.specialNeeds": "special needs", + "listings.reservedTypePlural.developmentalDisability": "Developmental Disabilities", + "listings.reservedUnits": "Reserved Units", + "listings.reservedUnitsDescription": "In order to qualify for these units one of the following must apply to you or someone in your household:", + "listings.reservedUnitsForWhoAre": "Reserved for %{communityType} who are %{reservedType}", + "listings.reviewOrderQuestion": "How is the application review order determined?", + "listings.sections.additionalDetails": "Additional Details", + "listings.sections.additionalDetailsSubtitle": "Are there any other required documents and selection criteria?", + "listings.sections.additionalEligibilitySubtext": "Let applicants know any other rules of the building.", + "listings.sections.additionalEligibilitySubtitle": "Applicants must also qualify under the rules of the building.", + "listings.sections.additionalEligibilityTitle": "Additional Eligibility Rules", + "listings.sections.additionalFees": "Additional Fees", + "listings.sections.additionalFeesSubtitle": "Tell us about any other fees required by the applicant.", + "listings.sections.additionalInformationSubtitle": "Required documents and selection criteria", + "listings.sections.additionalInformationTitle": "Additional Information", + "listings.sections.applicationAddressSubtitle": "In the event of paper applications, where do you want applications dropped off or mailed?", + "listings.sections.applicationAddressTitle": "Application Address", + "listings.sections.applicationTypesTitle": "Application Types", + "listings.sections.applicationTypesSubtitle": "Configure the online application and upload paper application forms.", + "listings.sections.buildingAddress": "Building Address", + "listings.sections.buildingDetailsSubtitle": "Tell us where the building is located.", + "listings.sections.buildingDetailsTitle": "Building Details", + "listings.sections.buildingFeaturesSubtitle": "Provide details about any amenities and unit details.", + "listings.sections.buildingFeaturesTitle": "Building Features", + "listings.sections.neighborhoodAmenitiesTitle": "Neighborhood Amenities", + "listings.sections.neighborhoodAmenitiesSubtitle": "Provide details about any local amenities including grocery stores, health services and parks within 2 miles of your listings.", + "listings.sections.neighborhoodAmenitiesPublicTitle": "Within 2 miles", + "listings.sections.neighborhoodAmenitiesPublicSubtitle": "The neighborhood around this property has the following resources and services for households.", + "listings.sections.applicationDatesTitle": "Application Dates", + "listings.sections.applicationDatesSubtitle": "Tell us about important dates related to this listing.", + "listings.sections.communityType": "Community Type", + "listings.sections.communityTypeSubtitle": "Are there any requirements that applicants need to meet?", + "listings.sections.costsNotIncluded": "Costs Not Included", + "listings.sections.depositHelperText": "Deposit Helper Text", + "listings.sections.eligibilitySubtitle": "Income, occupancy, preferences, and subsidies", + "listings.sections.eligibilityTitle": "Eligibility", + "listings.sections.featuresSubtitle": "Amenities, unit details and additional fees", + "listings.sections.featuresTitle": "Features", + "listings.sections.housingPreferencesSubtitle": "Preference holders will be given highest ranking.", + "listings.sections.housingPreferencesSubtext": "Tell us about any preferences that will be used to rank qualifying applicants.", + "listings.sections.housingPreferencesTitle": "Housing Preferences", + "listings.sections.introSubtitle": "Let's get started with some basic information about your listing.", + "listings.sections.introTitle": "Listing Intro", + "listings.sections.leasingAgentSubtitle": "Provide details about the leasing agent who will be managing the application process.", + "listings.sections.leasingAgentTitle": "Leasing Agent", + "listings.sections.neighborhoodSubtitle": "Location and transportation", + "listings.sections.neighborhoodTitle": "Neighborhood", + "listings.sections.photoTitle": "Listing Photo", + "listings.sections.photoSubtitle": "Upload an image for the listing that will be used as a preview.", + "listings.sections.processSubtitle": "Important dates and contact information", + "listings.sections.processTitle": "Process", + "listings.sections.rankingsResultsTitle": "Rankings & Results", + "listings.sections.rankingsResultsSubtitle": "Provide details about what happens to applications once they are submitted.", + "listings.sections.rentalAssistanceSubtitle": "Housing Choice Vouchers, Section 8 and other valid rental assistance programs will be considered for this property. In the case of a valid rental subsidy, the required minimum income will be based on the portion of the rent that the tenant pays after use of the subsidy.", + "listings.sections.rentalAssistanceTitle": "Rental Assistance", + "listings.sections.addOpenHouse": "Add Open House", + "listings.sections.openHouse": "Open House", + "listings.sections.publicProgramNote": "Affordable housing properties often receive funding to house specific populations, like seniors, residents with disabilities, etc. Properties can serve more than one population. Contact this property if you are unsure if you qualify.", + "listings.sections.verification": "Verification", + "listings.sections.verificationSubtitle": "Provide verification of listing data accuracy.", + "listings.sections.isVerified": "I verify that this listing data is valid.", + "listings.sections.utilities": "Utilities Included", + "listings.seeMaximumIncomeInformation": "See Maximum Income Information", + "listings.seePreferenceInformation": "See Preference Information", + "listings.seeUnitInformation": "See Unit Information", + "listings.selectionCriteria": "Selection Criteria", + "listings.selectPreferences": "Select Preferences", + "listings.showClosedListings": "Show Closed Listings", + "listings.specialNotes": "Special Notes", + "listings.streetAddressOrPOBox": "Street Address or PO Box", + "listings.totalListings": "Total Listings", + "listings.underConstruction": "Under Construction", + "listings.unitTypes.oneBdrm": "1 BR", + "listings.unitTypes.twoBdrm": "2 BR", + "listings.unitTypes.threeBdrm": "3 BR", + "listings.unitTypes.fourBdrm": "4 BR", + "listings.unitTypes.fiveBdrm": "5 BR", + "listings.unitTypes.studio": "Studio", + "listings.vacantUnit": "Vacant unit", + "listings.vacantUnits": "Vacant units", + "listings.unitSummaryGroupMessage": "For each type of unit, you cannot make more than the income limit associated with your household size, as shown in the Household Maximum Income table below.", + "listings.section8MessageOpening": "If you have a ", + "listings.section8FullName": "Section 8 Housing Choice Voucher", + "listings.section8MessageClosing": ", the income requirements do not apply and you will pay rent based on your income.", + "listings.unitsAreFor": "These units are for %{type}.", + "listings.unitsHaveAccessibilityFeaturesFor": "These units have accessibility features for people with %{type}.", + "listings.upcomingLotteries.hide": "Hide Closed Listings", + "listings.upcomingLotteries.noResults": "There are no closed listings with upcoming lotteries at this time.", + "listings.upcomingLotteries.show": "Show Closed Listings", + "listings.upcomingLotteries.title": "Closed Listings", + "listings.usingCommonDigitalApplication": "Are you using the common digital application?", + "listings.usingCommonPaperApplication": "Are you using the common paper application?", + "listings.verifiedListing": "Confirmed by property", + "listings.verified": "Verified", + "listings.waitlist.closed": "Closed waitlist", + "listings.waitlist.currentSizeQuestion": "How many people are on the current list?", + "listings.waitlist.label": "Waitlist", + "listings.waitlist.isOpen": "Waitlist is open", + "listings.waitlist.currentSize": "Current Waitlist Size", + "listings.waitlist.finalSize": "Final Waitlist Size", + "listings.waitlist.maxSize": "Maximum Waitlist Size", + "listings.waitlist.maxSizeQuestion": "What is the maximum size of the waitlist?", + "listings.waitlist.open": "Open waitlist", + "listings.waitlist.openQuestion": "Do you want to show a waitlist?", + "listings.waitlist.openSize": "Number of Openings", + "listings.waitlist.openSizeQuestion": "How many spots are open on the list?", + "listings.waitlist.openSlots": "Open Waitlist Slots", + "listings.waitlist.sizeQuestion": "Do you want to show a waitlist size?", + "listings.waitlist.submitAnApplication": "Once ranked applicants fill all available units, the remaining ranked applicants will be placed on a waitlist for those same units.", + "listings.waitlist.submitForWaitlist": "Submit an application for an open slot on the waitlist.", + "listings.waitlist.unitsAndWaitlist": "Available Units and Waitlist", + "listings.whatToExpectLabel": "Tell the applicant what to expect from the process", + "listings.whereDropOffQuestion": "Where are applications dropped off?", + "listings.wherePickupQuestion": "Where are applications picked up?", + "listings.yearBuilt": "Year Built", + "listings.whenApplicationsClose": "When applications close to the public", + "listings.cc&r": "Covenants, Conditions and Restrictions (CC&R's)", + "listings.cc&rDescription": "The CC&R's explain the rules of the homeowners' association, and restrict how you can modify the property.", + "listings.downloadPdf": "Download PDF", + "listings.rePricing": "Re-Pricing", + "listings.eligibilityNotebook": "Eligibility Notebook", + "listings.processInfo": "Process Info", + "listings.featuresCards": "Features Cards", + "listings.neighborhoodBuildings": "Neighborhood Buildings", + "listings.additionalInformationEnvelope": "Additional Information Envelope", + "listings.listingName": "Listing Name", + "listings.applicationsSubmitted": "Applications Submitted", + "listings.listingStatusText": "Listing Status", + "listings.applications": "Applications", + "listings.unit.title": "Unit", + "listings.unit.add": "Add Unit", + "listings.unit.number": "Unit #", + "listings.unit.unitNumber": "Unit Number", + "listings.unit.type": "Unit Type", + "listings.unit.ami": "AMI", + "listings.unit.amiChart": "AMI Chart", + "listings.unit.amiPercentage": "Percentage of AMI", + "listings.unit.rent": "Rent", + "listings.unit.sqft": "SQ FT", + "listings.unit.squareFootage": "Square Footage", + "listings.unit.priorityType": "ADA", + "listings.unit.reservedType": "Reserved", + "listings.unit.status": "Status", + "listings.unit.unitCopied": "Unit Copied", + "listings.unit.unitStatus": "Unit Status", + "listings.unit.unitSaved": "Unit Saved", + "listings.unit.details": "Details", + "listings.unit.numBathrooms": "Number of Bathrooms", + "listings.unit.floor": "Unit Floor", + "listings.unit.minOccupancy": "Minimum Occupancy", + "listings.unit.maxOccupancy": "Maximum Occupancy", + "listings.unit.rentType": "How is Rent Determined?", + "listings.unit.fixed": "Fixed amount", + "listings.unit.percentage": "% of income", + "listings.unit.monthlyRent": "Monthly Rent", + "listings.unit.%incomeRent": "Percentage of Income Rent", + "listings.unit.accessibilityPriorityType": "Accessibility Priority Type", + "listings.unit.unitGroups": "Unit Groups", + "listings.unit.unitTypes": "Unit Types", + "listings.unit.individualUnits": "Individual Units", + "listings.unit.delete": "Delete this Unit", + "listings.unit.deleteConf": "Do you really want to delete this unit?", + "listings.unit.eligibility": "Eligibility", + "listings.unit.statusOptions.unknown": "Unknown", + "listings.unit.statusOptions.available": "Available", + "listings.unit.statusOptions.occupied": "Occupied", + "listings.unit.statusOptions.unavailable": "Unavailable", + "listings.unit.typeOptions.studio": "Studio", + "listings.unit.typeOptions.oneBdrm": "One Bedroom", + "listings.unit.typeOptions.twoBdrm": "Two Bedroom", + "listings.unit.typeOptions.threeBdrm": "Three Bedroom", + "listings.unit.typeOptions.fourBdrm": "Four Bedroom", + "listings.unitsSummary.add": "Add unit group", + "listings.unitsSummary.occupancy": "Occupancy", + "listings.unitsSummary.floorMin": "Minimum Floor", + "listings.unitsSummary.floorMax": "Max Floor", + "listings.unitsSummary.monthlyRentMin": "Minimum Monthly Rent", + "listings.unitsSummary.monthlyRentMax": "Max Monthly Rent", + "listings.unitsSummary.sqFeetMin": "Minimum Square Footage", + "listings.unitsSummary.sqFeetMax": "Max Square Footage", + "listings.unitsSummary.availability": "Availability", + "listings.unitsSummary.count": "Affordable Unit Group Quantity", + "listings.unitsSummary.available": "Total Available", + "listings.unitsSummary.notAvailable": "Not available", + "listings.unitsSummary.delete": "Delete this Summary", + "listings.unitsSummary.deleteConf": "Do you really want to delete this summary?", + "listings.unitsSummary.bathroomMin": "Min Number of Bathrooms", + "listings.unitsSummary.bathroomMax": "Max Number of Bathrooms", + "listings.unitsSummary.openWaitlist": "Waitlist Status", + "listings.unitsSummary.open": "Open", + "listings.unitsSummary.closed": "Closed", + "listings.unitsSummary.addAmi": "Add AMI level", + "listings.unitsSummary.amiChart": "AMI Chart", + "listings.unitsSummary.percentageOfAmi": "Percentage of AMI", + "listings.unitsSummary.monthlyRentDeterminationType": "How is rent determined?", + "listings.unitsSummary.flatRentValue": "Monthly Rent", + "listings.unitsSummary.percentageOfIncomeValue": "Percentage of Income", + "listings.unitsSummary.flatRent": "Flat Rent", + "listings.unitsSummary.percentIncome": "% of Income", + "listings.unitsSummary.amiLevel": "AMI Level", + "listings.unitsSummary.rentType": "Rent Type", + "listings.unitsSummary.deleteAmi": "Delete this AMI Level", + "listings.unitsSummary.deleteAmiConf": "Do you really want to delete this AMI Level?", + "listings.unitsSummary.studio": "Studio", + "listings.unitsSummary.oneBdrm": "1 Bedroom", + "listings.unitsSummary.twoBdrm": "2 Bedroom", + "listings.unitsSummary.threeBdrm": "3 Bedroom", + "listings.unitsSummary.fourBdrm": "4+ Bedroom", + "listings.unitsSummary.numUnits": "# of Units", + "listings.unitsSummary.amiRange": "AMI", + "listings.unitsSummary.rentRange": "Rent", + "listings.unitsSummary.occupancyRange": "Occupancy", + "listings.unitsSummary.sqftRange": "SQ Ft", + "listings.unitsSummary.bathRange": "Bath", + "listings.unitsSummary.vacancies": "Unit Group Vacancies", + "listings.events.deleteThisEvent": "Delete this event", + "listings.events.deleteConf": "Do you really want to delete this event?", + "listings.events.openHouseNotes": "Open House Notes", + "listings.units": "Listing Units", + "listings.unitsDescription": "Select the building units that are available through this listing.", + "listings.unitTypesOrIndividual": "Do you want to show unit types or individual units?", + "listings.section8AcceptanceQuestion": "Do you accept Section 8 Housing Choice Vouchers?", + "listings.listingIsAlreadyLive": "This listing is already live. Updates will affect the applicant experience on the housing portal.", + "listings.managerHasConfirmedInformation": "Property management staff or the owner has checked this listing to make sure the information is correct.", + "listings.areaMedianIncome": "Eligibility for regulated affordable housing is based on the area median income, or AMI. To qualify for this housing, you often have to meet certain income requirements. These requirements are based on income levels relative to AMI.", + "listings.utilities.water": "Water", + "listings.utilities.gas": "Gas", + "listings.utilities.trash": "Trash", + "listings.utilities.sewer": "Sewer", + "listings.utilities.electricity": "Electricity", + "listings.utilities.cable": "Cable", + "listings.utilities.phone": "Phone", + "listings.utilities.internet": "Internet", + "listings.additionalResources": "View additional resources", + "lottery.applicationsThatQualifyForPreference": "Applications that qualify for this preference will be given a higher priority.", + "lottery.viewPreferenceList": "View Preference List", + "nav.srHeading": "Navigation Menu", + "nav.srNavigation": "Main", + "nav.accountSettings": "Account Settings", + "nav.browseProperties": "Browse Properties", + "nav.getFeedback": "We'd love to get your feedback!", + "nav.listings": "Listings", + "nav.rentals": "Rentals", + "nav.properties": "Properties", + "nav.applications": "Applications", + "nav.myAccount": "My Account", + "nav.myApplications": "My Applications", + "nav.myDashboard": "My Dashboard", + "nav.mySettings": "My Settings", + "nav.signIn": "Sign in", + "nav.signOut": "Sign out", + "nav.signUp": "Sign up", + "nav.siteTitle": "Housing Portal", + "nav.siteTitlePartners": "Partners Portal", + "nav.skip": "Skip to main content", + "nav.flags": "Flags", + "nav.users": "Users", + "pageTitle.additionalResources": "More Housing Opportunities", + "pageTitle.accessibilityStatement": "Accessibility Statement", + "pageTitle.terms": "Terms and Conditions", + "pageTitle.housingCounselors": "Housing Counselors", + "pageTitle.rentalListings": "See Rentals", + "pageTitle.rent": "Rent affordable housing", + "pageTitle.privacy": "Privacy Policy", + "pageTitle.welcomeEnglish": "Welcome", + "pageTitle.welcomeSpanish": "Bienvenido", + "pageTitle.welcomeVietnamese": "Tiếng Việt", + "pageTitle.about": "About", + "pageTitle.feedback": "Share website feedback", + "pageTitle.resources": "Resources", + "pageTitle.housingBasics": "Affordable Housing Basics", + "pageTitle.getAssistance": "Get Assistance", + "pageDescription.getAssistance": "To help you in your journey to find stable housing, please browse the resources and services below, or learn more about affordable housing.", + "pageDescription.housingBasics": "We know that finding housing can be a difficult and intimidating process. You can find several resources on this page to help you learn about affordable housing and the application process for these properties.", + "pageDescription.welcome": "Search and apply for affordable housing on %{regionName}'s Housing Portal", + "pageDescription.listing": "Apply for affordable housing at %{listingName} in %{regionName}, built in partnership with Exygy.", + "region.name": "Local Region", + "progressNav.completed": "completed", + "progressNav.notCompleted": "not completed", + "progressNav.srHeading": "Progress", + "progressNav.current": "Current step: ", + "seasons.fall": "Fall", + "seasons.spring": "Spring", + "seasons.summer": "Summer", + "seasons.winter": "Winter", + "states.AL": "Alabama", + "states.AK": "Alaska", + "states.AZ": "Arizona", + "states.AR": "Arkansas", + "states.CA": "California", + "states.CO": "Colorado", + "states.CT": "Connecticut", + "states.DE": "Delaware", + "states.DC": "District Of Columbia", + "states.FL": "Florida", + "states.GA": "Georgia", + "states.HI": "Hawaii", + "states.ID": "Idaho", + "states.IL": "Illinois", + "states.IN": "Indiana", + "states.IA": "Iowa", + "states.KS": "Kansas", + "states.KY": "Kentucky", + "states.LA": "Louisiana", + "states.ME": "Maine", + "states.MD": "Maryland", + "states.MA": "Massachusetts", + "states.MI": "Michigan", + "states.MN": "Minnesota", + "states.MS": "Mississippi", + "states.MO": "Missouri", + "states.MT": "Montana", + "states.NE": "Nebraska", + "states.NV": "Nevada", + "states.NH": "New Hampshire", + "states.NJ": "New Jersey", + "states.NM": "New Mexico", + "states.NY": "New York", + "states.NC": "North Carolina", + "states.ND": "North Dakota", + "states.OH": "Ohio", + "states.OK": "Oklahoma", + "states.OR": "Oregon", + "states.PA": "Pennsylvania", + "states.RI": "Rhode Island", + "states.SC": "South Carolina", + "states.SD": "South Dakota", + "states.TN": "Tennessee", + "states.TX": "Texas", + "states.UT": "Utah", + "states.VT": "Vermont", + "states.VA": "Virginia", + "states.WA": "Washington", + "states.WV": "West Virginia", + "states.WI": "Wisconsin", + "states.WY": "Wyoming", + "t.&": "&", + "t.areYouSure": "Are you sure?", + "t.addNotes": "Add notes", + "t.at": "at", + "t.additionalPhone": "Additional Phone", + "t.area": "area", + "t.areYouStillWorking": "Are you still working?", + "t.accessibility": "Accessibility", + "t.additionalAccessibility": "Additional Accessibility Details", + "t.am": "AM", + "t.available": "available", + "t.availability": "Availability", + "t.automatic": "Automatic", + "t.back": "Back", + "t.backToListing": "Back to listing", + "t.built": "Built", + "t.call": "Call", + "t.cancel": "Cancel", + "t.close": "Close", + "t.confirm": "Confirm", + "t.contactPropertyManagement": "Contact property management", + "t.chooseFromFolder": "Choose from folder", + "t.custom": "Custom", + "t.day": "Day", + "t.date": "Date", + "t.delete": "Delete", + "t.deposit": "Deposit", + "t.done": "Done", + "t.descriptionTitle": "Description", + "t.description": "Enter Description", + "t.draft": "Draft", + "t.emailAddressPlaceholder": "you@myemail.com", + "t.end": "End", + "t.dragFilesHere": "Drag files here", + "t.dropFilesHere": "Drop files here…", + "t.export": "Export", + "t.enterAmount": "Enter amount", + "t.fileName": "File Name", + "t.filter": "Filter", + "t.edit": "Edit", + "t.email": "Email", + "t.finish": "Finish", + "t.floor": "floor", + "t.floors": "floors", + "t.getDirections": "Get Directions", + "t.homePage": "Home page", + "t.hour": "Hour", + "t.household": "Household", + "t.income": "Income", + "t.incomeRange": "Income Range", + "t.invite": "Invite", + "t.jumpTo": "Jump to", + "t.jurisdiction": "Jurisdiction", + "t.label": "Label", + "t.language": "Language", + "t.lastUpdated": "Last Updated", + "t.letter": "Letter", + "t.less": "Less", + "t.link": "Link", + "t.listing": "Listings", + "t.loginIsRequired": "Login is required to view this page.", + "t.max": "Max", + "t.menu": "Menu", + "t.min": "Min", + "t.minPeople": "at least %{num} people", + "t.maxPeople": "at most %{num} people", + "t.minPerson": "at least 1 person", + "t.maxPerson": "at most 1 person", + "t.minimumIncome": "Minimum Income", + "t.minutes": "minutes", + "t.month": "Month", + "t.more": "More", + "t.n/a": "n/a", + "t.name": "Name", + "t.neighborhood": "Neighborhood", + "t.newPage": "New Page", + "t.next": "Next", + "t.no": "No", + "t.none": "None", + "t.noneFound": "None found.", + "t.notes": "Notes", + "t.numPeople": "%{num} people", + "t.occupancy": "Occupancy", + "t.ok": "OK", + "t.onePerson": "1 person", + "t.optional": "Optional", + "t.or": "or", + "t.order": "Order", + "t.pageXofY": "Page %{num} of %{total}", + "t.people": "people", + "t.peopleRange": "%{min}-%{max} people", + "t.person": "person", + "t.perMonth": "per month", + "t.perYear": "per year", + "t.petsPolicy": "Pets Policy", + "t.phone": "Phone", + "t.phoneNumberPlaceholder": "(555) 555-5555", + "t.pleaseSelectOne": "Please select one.", + "t.preferNotToSay": "Prefer not to say", + "t.pleaseSelectYesNo": "Please select yes or no.", + "t.pm": "PM", + "t.post": "Post", + "t.preferences": "Preferences", + "t.preview": "Preview", + "t.previous": "Previous", + "t.propertyAmenities": "Property Amenities", + "t.public": "Public", + "t.range": "%{from} to %{to}", + "t.readLess": "read less", + "t.readMore": "read more", + "t.region": "Region", + "t.relationship": "Relationship", + "t.otherRelationShip": "Other Relationship", + "t.rent": "Rent", + "t.review": "Review", + "t.role": "Role", + "t.saved": "Saved", + "t.secondPhone": "Second Phone", + "t.seconds": "seconds", + "t.seeDetails": "See Details", + "t.seeListing": "See Listing", + "t.selectOne": "Select One", + "t.servicesOffered": "Services Offered", + "t.show": "Show", + "t.showLess": "show less", + "t.showMore": "show more", + "t.skipToMainContent": "Skip to main content", + "t.smokingPolicy": "Smoking Policy", + "t.sort": "Sort", + "t.sqFeet": "sqft", + "t.squareFeet": "square feet", + "t.statusHistory": "Status History", + "t.startTime": "Start Time", + "t.endTime": "End Time", + "t.street": "Street", + "t.submit": "Submit", + "t.submitNew": "Submit & New", + "t.copy": "Make a Copy", + "t.copyNew": "Copy & New", + "t.save": "Save", + "t.saveNew": "Save & New", + "t.saveExit": "Save & Exit", + "t.time": "time", + "t.text": "Text", + "t.to": "to", + "t.totalCount": "Total Count", + "t.unit": "unit", + "t.units": "units", + "t.unitAmenities": "Unit Amenities", + "t.unitFeatures": "Unit Features", + "t.unitInformation": "Unit Information", + "t.unitType": "Unit Type", + "t.url": "URL", + "t.view": "View", + "t.viewListings": "View Listings", + "t.viewMap": "View Map", + "t.viewOnMap": "View on Map", + "t.website": "Website", + "t.year": "Year", + "t.yes": "Yes", + "t.you": "You", + "t.favorite": "Favorite", + "t.favorited": "Favorited", + "t.unfavorite": "unfavorite", + "t.unfavorited": "unfavorited", + "t.addFavorite": "Added to favorites", + "t.removeFavorite": "Removed from favorites", + "welcome.allApplicationClosed": "All applications are currently closed, but you can view closed listings.", + "welcome.seeRentalListings": "See all rentals", + "welcome.title": "Apply for affordable housing in", + "welcome.seeMoreOpportunities": "See more rental and ownership housing opportunities", + "welcome.bedrooms.studios": "(%{smart_count}) studio available |||| (%{smart_count}) studios available", + "welcome.bedrooms.numBed": "(%{smart_count}) %{num_bed} bedroom available |||| (%{smart_count}) %{num_bed} bedrooms available", + "welcome.bedrooms.fourPlusBed": "(%{smart_count}) 4+ bedroom available |||| (%{smart_count}) 4+ bedrooms available", + "welcome.checkEligibility": "Do I Qualify?", + "welcome.checkEligibilityDescription": "Check in Minutes if You Qualify!", + "welcome.findRentalsForMe": "Find rentals for me", + "welcome.latestListings": "Latest listings", + "welcome.lastUpdated": "Last updated %{date}", + "welcome.cityRegions": "City Regions", + "welcome.subTitle": "Click the button below to find rental housing based on your income and household needs", + "welcome.seeMoreOpportunitiesTruncated": "See more housing opportunities and resources", + "welcome.signUp": "Get alerts whenever a new listing is posted", + "welcome.learnHousingBasics": "Learn how you can qualify and apply for affordable housing", + "welcome.learnMore": "Learn more", + "welcome.signUpToday": "Sign up today", + "welcome.viewAdditionalHousing": "View additional housing opportunities and resources", + "welcome.viewAdditionalHousingTruncated": "View opportunities and resources", + "welcome.underConstructionButton": "See all under construction", + "whatToExpect.label": "What to Expect", + "whatToExpect.default": "Applicants will be contacted by the property agent in rank order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application will be removed from the waitlist if you have made any fraudulent statements. If we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.", + "listingFilters.allRentals": "All rentals", + "listingFilters.buttonTitle": "Filter", + "listingFilters.buttonTitleWithNumber": "Filter - %{number} Filters Applied", + "listingFilters.buttonTitleExtended": "Find rentals for you", + "listingFilters.loading": "Loading...", + "listingFilters.rentalsFound": "%{smart_count} rental found |||| %{smart_count} rentals found", + "listingFilters.resetButton": "Reset", + "listingFilters.modalTitle": "Filter", + "listingFilters.modalHeader": "Use these options to refine your list of properties.", + "listingFilters.neighborhood": "Neighborhood", + "listingFilters.bedrooms": "Bedrooms", + "listingFilters.bedroomsOptions.studioPlus": "Studio", + "listingFilters.bedroomsOptions.onePlus": "1 Bedroom", + "listingFilters.bedroomsOptions.twoPlus": "2 Bedroom", + "listingFilters.bedroomsOptions.threePlus": "3 Bedroom", + "listingFilters.bedroomsOptions.fourPlus": "4 plus Bedroom", + "listingFilters.zipCode": "Zip code", + "listingFilters.zipCodeDescription": "Enter zip code", + "listingFilters.region": "Region", + "listingFilters.region.GreaterDowntown": "Greater Downtown", + "listingFilters.region.Eastside": "Eastside", + "listingFilters.region.Westside": "Westside", + "listingFilters.region.Southwest": "Southwest", + "listingFilters.adaCompliant": "Accessible Features for Residents with Disabilities?", + "listingFilters.rentRange": "Rent range", + "listingFilters.availability": "Unit availability", + "listingFilters.hasAvailability": "Has availability", + "listingFilters.noAvailability": "No availability", + "listingFilters.waitlist": "Waitlist", + "listingFilters.applyFilter": "Apply filter", + "listingFilters.noResults": "No results", + "listingFilters.noResultsSubtitle": "Try updating your filters or see other affordable housing resources.", + "listingFilters.includeUnknowns": "Show homes with missing information", + "listingFilters.senior": "Senior housing (62+)", + "listingFilters.independentLivingHousing": "Independent living community", + "listingFilters.minAmiPercentageLabel": "Units set aside for people with incomes of", + "listingFilters.minAmiPercentageOptions.amiOption20": "20% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption25": "25% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption30": "30% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption35": "35% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption40": "40% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption45": "45% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption50": "50% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption55": "55% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption60": "60% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption70": "70% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption80": "80% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption100": "100% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption120": "120% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption125": "125% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption140": "140% of AMI or more", + "listingFilters.minAmiPercentageOptions.amiOption150": "150% of AMI or more", + "listingFilters.program.Seniors 55+": "Seniors 55+", + "listingFilters.program.Seniors 62+": "Seniors 62+", + "listingFilters.program.Residents with Disabilities": "Residents with Disabilities", + "listingFilters.program.Families": "Families", + "listingFilters.program.Supportive Housing for the Homeless": "Supportive Housing for the Homeless", + "listingFilters.program.Veterans": "Veterans", + "listingFilters.clear": "Clear", + "listingFilters.section8": "Accepts Section 8 Housing Choice Vouchers", + "eligibility.progress.header": "Find rentals for me", + "eligibility.progress.sections.welcome": "Welcome", + "eligibility.progress.sections.household": "Household", + "eligibility.progress.sections.age": "Age", + "eligibility.progress.sections.disability": "Disability", + "eligibility.progress.sections.accessibility": "Accessibility", + "eligibility.progress.sections.income": "Income", + "eligibility.progress.sections.disclaimer": "Disclaimer", + "eligibility.welcome.header": "Welcome", + "eligibility.welcome.description": "Welcome to Detroit Home Connect! To see rentals that you may qualify for, just answer four questions.", + "eligibility.household.prompt": "How many people will live in your next rental, including yourself?", + "eligibility.household.srCountLabel": "Household Size", + "eligibility.household.ranges.one": "1 Member", + "eligibility.household.ranges.two": "2 Members", + "eligibility.household.ranges.three": "3 Members", + "eligibility.household.ranges.four": "4 Members", + "eligibility.household.ranges.five": "5 Members", + "eligibility.household.ranges.six": "6 Members", + "eligibility.household.ranges.seven": "7 Members", + "eligibility.household.ranges.eight": "8+ Members", + "eligibility.age.prompt": "How old are you?", + "eligibility.age.description": "You can choose multiple age ranges. Some rentals have minimum age requirements.", + "eligibility.age.lessThan55": "< 55", + "eligibility.age.55to61": "55 - 61", + "eligibility.age.62plus": "62+", + "eligibility.disability.prompt": "Does anyone in your household have a disability?", + "eligibility.disability.description": "You might need to provide proof of disability when you apply for some homes.", + "eligibility.income.prompt": "What is the estimated total annual income for everyone who will live with you, including yourself?", + "eligibility.income.description": "Include income from the last 12 months for yourself and everyone who will live with you. When you apply for a rental, properties will also consider other factors to determine if your estimated annual income meets eligibility requirements. Examples of income:", + "eligibility.income.examples.wages": "Wages and tips", + "eligibility.income.examples.socialSecurity": "Social Security", + "eligibility.income.examples.retirement": "Retirement income", + "eligibility.income.examples.unemployment": "Unemployment compensation", + "eligibility.income.label": "Income range", + "eligibility.income.ranges.below10k": "$0 - $9,999", + "eligibility.income.ranges.10kTo20k": "$10,000 - $19,999", + "eligibility.income.ranges.20kTo30k": "$20,000 - $29,999", + "eligibility.income.ranges.30kTo40k": "$30,000 - $39,999", + "eligibility.income.ranges.40kTo50k": "$40,000 - $49,999", + "eligibility.income.ranges.over50k": "$50,000 or more", + "eligibility.accessibility.acInUnit": "AC in Unit", + "eligibility.accessibility.accessibleParking": "Accessible Parking Spots", + "eligibility.accessibility.barrierFreeBathroom": "Barrier-free bathrooms", + "eligibility.accessibility.barrierFreeEntrance": "Barrier-free (no-step) property entrance", + "eligibility.accessibility.barrierFreeUnitEntrance": "Barrier-free (no-step) unit entrances", + "eligibility.accessibility.description": "Some properties have accessibility features that others may not have.", + "eligibility.accessibility.elevator": "Elevator", + "eligibility.accessibility.grabBars": "Grab bars in bathrooms", + "eligibility.accessibility.hearing": "Units for those with hearing disabilities", + "eligibility.accessibility.heatingInUnit": "Heating in Unit", + "eligibility.accessibility.inUnitWasherDryer": "In-unit washer/dryer", + "eligibility.accessibility.laundryInBuilding": "Laundry in Building", + "eligibility.accessibility.loweredCabinets": "Lowered cabinets and countertops", + "eligibility.accessibility.loweredLightSwitch": "Lowered light switches", + "eligibility.accessibility.mobility": "Units for those with mobility disabilities", + "eligibility.accessibility.parkingOnSite": "Parking On Site", + "eligibility.accessibility.prompt": "Do you require additional accessibility features?", + "eligibility.accessibility.rollInShower": "Roll-in showers", + "eligibility.accessibility.serviceAnimalsAllowed": "Service Animals Allowed", + "eligibility.accessibility.title": "Accessibility Features", + "eligibility.accessibility.visual": "Units for those with visual disabilities", + "eligibility.accessibility.wheelchairRamp": "Wheelchair Ramp", + "eligibility.accessibility.wideDoorways": "Wide unit doorways for wheelchairs", + "eligibility.disclaimer.description": "Thank you for answering these questions. When you click or tap \"See results now\", you'll see rentals that might fit your needs based on your answers. Multiple factors can affect eligibility for specific rentals, so if you see a rental you're interested in, contact the Property Agent on the listing. They can help you determine if you're eligible for that rental.", + "eligibility.preferNotToSay": "Prefer not to say", + "eligibility.seeResultsNow": "See results now", + "resources.body1": "You may need additional assistance in your search for housing. HRD has compiled a list of resources to help you find and maintain your housing.", + "resources.evictionAssistance": "Eviction assistance", + "resources.detroitHousingNetwork": "Detroit Housing Network", + "resources.detroitHousingNetworkBody": "The Detroit Housing Network is a collection of Detroit community organizations that provide a variety of services for homeowners and renters, including utility assistance, tenant and landlord counseling, eviction counseling, and property tax solutions. For more information, visit ", + "resources.utilityAssistance": "Utility assistance", + "resources.homelessnessServices": "Homelessness services", + "resources.detroitLandBankAuthority": "Detroit Land Bank Authority", + "resources.homeRepairResources": "Home repair resources", + "resources.affordableHousingTitle": "Housing Basics", + "resources.affordableHousingSubtitle": "Learn more about how to qualify and apply for affordable housing", + "resources.affordableHousingLinkLabel": "Read more to learn how it works", + "resources.housingResourcesTitle": "Additional housing resources", + "resources.housingResourcesSubtitle": "Browse local resources and services in your search for housing", + "resources.housingResourcesLinkLabel": "View community resources", + "publicFilter.confirmedListings": "Confirmed Listings", + "publicFilter.confirmedListingsFieldLabel": "Only show listings confirmed by property", + "publicFilter.bedRoomSize": "Bedroom Size", + "publicFilter.rentRange": "Monthly Rent Range", + "publicFilter.rentRangeMin": "No Min Rent", + "publicFilter.rentRangeMinReader": "Enter the dollar amount of your minimum rent", + "publicFilter.rentRangeMax": "No Max Rent", + "publicFilter.rentRangeMaxReader": "Enter the dollar amount of your maximum rent", + "publicFilter.communityTypes": "Community Types", + "publicFilter.waitlist.open": "Open Waitlist", + "publicFilter.waitlist.closed": "Closed Waitlist", + "account.noFavorites": "It looks like you haven't favorited any listings yet.", + "sr.pageTitle": "Page Title:" +} diff --git a/shared-helpers/src/minMaxFinder.ts b/shared-helpers/src/minMaxFinder.ts new file mode 100644 index 0000000000..b8e839c558 --- /dev/null +++ b/shared-helpers/src/minMaxFinder.ts @@ -0,0 +1,15 @@ +import { MinMax } from "@bloom-housing/backend-core/types" + +export function minMaxFinder(range: MinMax, value: number): MinMax { + if (range === undefined) { + return { + min: value, + max: value, + } + } else { + range.min = Math.min(range.min, value) + range.max = Math.max(range.max, value) + + return range + } +} diff --git a/ui-components/src/helpers/nextjs.ts b/shared-helpers/src/nextjs.ts similarity index 100% rename from ui-components/src/helpers/nextjs.ts rename to shared-helpers/src/nextjs.ts diff --git a/shared-helpers/src/occupancyFormatting.tsx b/shared-helpers/src/occupancyFormatting.tsx new file mode 100644 index 0000000000..ee0185f98c --- /dev/null +++ b/shared-helpers/src/occupancyFormatting.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { t, StandardTableData } from "@bloom-housing/ui-components" +import { Listing, UnitType } from "@bloom-housing/backend-core/types" + +// Differs from core due to unit groups +export const occupancyTable = (listing: Listing) => { + const getOccupancyString = (min?: number, max?: number) => { + if (!max && min) return min === 1 ? t("t.minPerson") : t("t.minPeople", { num: min }) + if (!min && max) return max === 1 ? t("t.maxPerson") : t("t.maxPeople", { num: max }) + if (min === max) return max === 1 ? t("t.onePerson") : t("t.numPeople", { num: max }) + return t("t.peopleRange", { min, max }) + } + + const getUnitTypeNameString = (unitType: UnitType) => { + return t("listings.unitTypes." + unitType.name) + } + + const getUnitTypeString = (unitTypes: UnitType[]) => { + const unitTypesString = unitTypes.reduce((acc, curr, index) => { + return index > 0 ? `${acc}, ${getUnitTypeNameString(curr)}` : getUnitTypeNameString(curr) + }, "") + + return {unitTypesString} + } + + const sortedUnitGroups = listing.unitGroups + ?.sort( + (a, b) => + a.unitType.sort((c, d) => c.numBedrooms - d.numBedrooms)[0].numBedrooms - + b.unitType.sort((e, f) => e.numBedrooms - f.numBedrooms)[0].numBedrooms + ) + .filter((unitGroup) => unitGroup.maxOccupancy || unitGroup.minOccupancy) + + const tableRows = sortedUnitGroups?.reduce((acc, curr) => { + const unitTypeString = getUnitTypeString(curr.unitType) + const occupancyString = getOccupancyString(curr.minOccupancy, curr.maxOccupancy) + if (occupancyString) { + acc.push({ + unitType: { content: unitTypeString }, + occupancy: { content: occupancyString }, + }) + } + return acc + }, []) + return tableRows +} diff --git a/ui-components/src/helpers/pdfs.ts b/shared-helpers/src/pdfs.ts similarity index 77% rename from ui-components/src/helpers/pdfs.ts rename to shared-helpers/src/pdfs.ts index 9edc962bc1..8280db830e 100644 --- a/ui-components/src/helpers/pdfs.ts +++ b/shared-helpers/src/pdfs.ts @@ -6,14 +6,14 @@ export const cloudinaryPdfFromId = (publicId: string, cloudName: string) => { export const pdfUrlFromListingEvents = ( events: ListingEvent[], - listingType: ListingEventType, + listingEventType: ListingEventType, cloudName: string ) => { - const event = events.find((event) => event.type === listingType) + const event = events.find((event) => event?.type === listingEventType) if (event) { return event.file?.label == "cloudinaryPDF" ? cloudinaryPdfFromId(event.file.fileId, cloudName) - : event.url + : event.url ?? null } return null } diff --git a/shared-helpers/src/photos.ts b/shared-helpers/src/photos.ts new file mode 100644 index 0000000000..fa23ec1c8b --- /dev/null +++ b/shared-helpers/src/photos.ts @@ -0,0 +1,39 @@ +import { Asset, Listing } from "@bloom-housing/backend-core/types" + +export const CLOUDINARY_BUILDING_LABEL = "cloudinaryBuilding" + +export const cloudinaryUrlFromId = (publicId: string, size = 400) => { + const cloudName = process.env.cloudinaryCloudName || process.env.CLOUDINARY_CLOUD_NAME + return `https://res.cloudinary.com/${cloudName}/image/upload/w_${size},c_limit,q_65/${publicId}.jpg` +} + +export const getUrlForListingImage = (image: Asset, size = 400) => { + if (!image) return null + + if (image.label == CLOUDINARY_BUILDING_LABEL) { + return cloudinaryUrlFromId(image.fileId, size) + } else { + return image.fileId + } +} + +export const imageUrlFromListing = (listing: Listing, size = 400): string[] => { + const imageAssets = + listing?.images?.length && listing.images[0].image + ? listing.images + .sort((imageA, imageB) => (imageA.ordinal ?? 10) - (imageB?.ordinal ?? 10)) + .map((imageObj) => imageObj.image) + : listing?.assets + + const imageUrls = imageAssets + ?.filter( + (asset: Asset) => asset.label === CLOUDINARY_BUILDING_LABEL || asset.label === "building" + ) + ?.map((asset: Asset) => { + return asset.label === CLOUDINARY_BUILDING_LABEL + ? cloudinaryUrlFromId(asset.fileId, size) + : asset.fileId + }) + + return imageUrls?.length > 0 ? imageUrls : ["/images/detroitDefault.png"] +} diff --git a/shared-helpers/src/postmarkString.ts b/shared-helpers/src/postmarkString.ts new file mode 100644 index 0000000000..326f9bdd63 --- /dev/null +++ b/shared-helpers/src/postmarkString.ts @@ -0,0 +1,28 @@ +import { t } from "@bloom-housing/ui-components" + +export const getPostmarkString = ( + applicationDueDate: string | null, + postmarkReceivedByDate: string | null, + developer: string | null +) => { + if (applicationDueDate) { + return postmarkReceivedByDate + ? t("listings.apply.submitPaperDueDatePostMark", { + applicationDueDate, + postmarkReceivedByDate, + developer, + }) + : t("listings.apply.submitPaperDueDateNoPostMark", { + applicationDueDate, + developer, + }) + } else { + if (postmarkReceivedByDate) { + return t("listings.apply.submitPaperNoDueDatePostMark", { postmarkReceivedByDate, developer }) + } + if (developer) { + return t("listings.apply.submitPaperNoDueDateNoPostMark", { developer }) + } + return "" + } +} diff --git a/ui-components/src/helpers/preferences.tsx b/shared-helpers/src/preferences.tsx similarity index 93% rename from ui-components/src/helpers/preferences.tsx rename to shared-helpers/src/preferences.tsx index 53202cd463..17c0ceba5d 100644 --- a/ui-components/src/helpers/preferences.tsx +++ b/shared-helpers/src/preferences.tsx @@ -4,6 +4,7 @@ import { ApplicationPreference, FormMetadataOptions, Preference, + ListingPreference, } from "@bloom-housing/backend-core/types" import { UseFormMethods } from "react-hook-form" import { @@ -16,7 +17,6 @@ import { SelectOption, resolveObject, } from "@bloom-housing/ui-components" -import { stateKeys } from "@bloom-housing/shared-helpers" type ExtraFieldProps = { metaKey: string @@ -27,6 +27,7 @@ type ExtraFieldProps = { errors?: UseFormMethods["errors"] // eslint-disable-next-line @typescript-eslint/no-explicit-any hhMembersOptions?: SelectOption[] + stateKeys: string[] } type FormAddressProps = { @@ -36,6 +37,7 @@ type FormAddressProps = { register: UseFormMethods["register"] errors?: UseFormMethods["errors"] required?: boolean + stateKeys: string[] } type AddressType = @@ -62,6 +64,7 @@ export const ExtraField = ({ register, errors, hhMembersOptions, + stateKeys, }: ExtraFieldProps) => { const FIELD_NAME = `${PREFERENCES_FORM_PATH}.${metaKey}.${optionKey}.${extraKey}` @@ -91,6 +94,7 @@ export const ExtraField = ({ register={register} errors={errors} required={true} + stateKeys={stateKeys} />
) @@ -140,6 +144,7 @@ export const FormAddress = ({ register, errors, required, + stateKeys, }: FormAddressProps) => { return ( <> @@ -369,20 +374,25 @@ export type ExclusiveKey = { /* Create an array of all exclusive keys from a preference set */ -export const getExclusiveKeys = (preferences: Preference[]) => { +export const getExclusiveKeys = (preferences: ListingPreference[]) => { const exclusive: ExclusiveKey[] = [] - preferences?.forEach((preference) => { - preference?.formMetadata?.options.forEach((option: FormMetadataOptions) => { + preferences?.forEach((listingPreference) => { + listingPreference.preference?.formMetadata?.options.forEach((option: FormMetadataOptions) => { if (option.exclusive) exclusive.push({ - optionKey: getPreferenceOptionName(option.key, preference?.formMetadata?.key ?? ""), - preferenceKey: preference?.formMetadata?.key, + optionKey: getPreferenceOptionName( + option.key, + listingPreference.preference?.formMetadata?.key ?? "" + ), + preferenceKey: listingPreference.preference?.formMetadata?.key, }) }) - if (!preference?.formMetadata?.hideGenericDecline) + if (!listingPreference.preference?.formMetadata?.hideGenericDecline) exclusive.push({ - optionKey: getExclusivePreferenceOptionName(preference?.formMetadata?.key), - preferenceKey: preference?.formMetadata?.key, + optionKey: getExclusivePreferenceOptionName( + listingPreference.preference?.formMetadata?.key + ), + preferenceKey: listingPreference.preference?.formMetadata?.key, }) }) return exclusive diff --git a/shared-helpers/src/programHelpers.ts b/shared-helpers/src/programHelpers.ts new file mode 100644 index 0000000000..44facdca84 --- /dev/null +++ b/shared-helpers/src/programHelpers.ts @@ -0,0 +1,96 @@ +import { ApplicationProgram, Program, FormMetaDataType } from "@bloom-housing/backend-core/types" + +export const PROGRAMS_FORM_PATH = "application.programs" + +export const mapProgramToApi = (program: Program, data: Record) => { + if (Object.keys(data).length === 0) { + return { + key: "", + claimed: false, + options: [], + } + } + + const [key, value] = Object.entries(data)[0] + const options = [] + + if (program?.formMetadata?.type === FormMetaDataType.checkbox) { + value.forEach((option: string) => { + options.push({ + key: option, + checked: true, + extraData: [], + }) + }) + } else { + options.push({ + key: value, + checked: true, + extraData: [], + }) + program?.formMetadata?.options.forEach((option) => { + if (option.key !== value) { + options.push({ + key: option.key, + checked: false, + extraData: [], + }) + } + }) + } + + return { + key, + claimed: true, + options, + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const mapProgramsToApi = (programs: Program[], data: Record) => { + if (Object.keys(data).length === 0) return [] + const savedPrograms = [] as ApplicationProgram[] + Object.entries(data).forEach(([key, value]) => { + const program = programs.find((item) => item.formMetadata?.key === key) + if (!program) return + + const mappedProgram = mapProgramToApi(program, { [key]: value }) + if (mappedProgram.key) { + savedPrograms.push(mappedProgram) + } + }) + return savedPrograms +} + +// used in the paper apps only +export const mapApiToProgramsPaperForm = (programs: ApplicationProgram[]) => { + const result = {} + + programs?.forEach((program) => { + const key = program.key + + const selectedOption = program.options.reduce((accum, item) => { + if (item.checked) { + return [...accum, item.key] + } + return accum + }, []) + if (program.claimed) { + Object.assign(result, { + [key]: selectedOption.length === 1 ? selectedOption[0] : selectedOption, + }) + } + }) + + return result +} + +export const getProgramOptionName = (key: string, metaKey: string) => { + return key === "preferNotToSay" + ? "t.preferNotToSay" + : `${PROGRAMS_FORM_PATH}.${metaKey}.${key}.label` +} + +export const getProgramOptionDescription = (key: string, metaKey: string) => { + return `${PROGRAMS_FORM_PATH}.${metaKey}.${key}.description` +} diff --git a/shared-helpers/src/regions.ts b/shared-helpers/src/regions.ts new file mode 100644 index 0000000000..09c1367578 --- /dev/null +++ b/shared-helpers/src/regions.ts @@ -0,0 +1,54 @@ +export enum Region { + GreaterDowntown = "Greater Downtown", + Eastside = "Eastside", + Southwest = "Southwest", + Westside = "Westside", +} + +// TODO(#674): Get official hosted images +export const regionImageUrls: Map = new Map([ + [Region.GreaterDowntown, "https://pbs.twimg.com/media/DSzZwQKVAAASkw_?format=jpg&name=large"], + [ + Region.Eastside, + "https://res.cloudinary.com/exygy/image/upload/v1740007497/detroit/Detroit-Eastside_qzxx6n.jpg", + ], + [ + Region.Southwest, + "https://res.cloudinary.com/exygy/image/upload/v1740007258/detroit/Detroit-Southwest_wbx3nu.jpg", + ], + [ + Region.Westside, + "https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Atkinson_avenue_historic_district.JPG/1920px-Atkinson_avenue_historic_district.JPG", + ], +]) + +export interface Neighborhood { + name: string + region: Region +} + +export const neighborhoodRegions: Neighborhood[] = [ + { name: "Airport Sub area", region: Region.Eastside }, + { name: "Barton McFarland area", region: Region.Westside }, + { name: "Boston-Edison / North End area", region: Region.Westside }, + { name: "Boynton", region: Region.Southwest }, + { name: "Campau / Banglatown", region: Region.Eastside }, + { name: "Dexter Linwood", region: Region.Westside }, + { name: "Farwell area", region: Region.Eastside }, + { name: "Gratiot Town / Kettering area", region: Region.Eastside }, + { name: "Gratiot / 7 Mile area", region: Region.Eastside }, + { name: "Greater Corktown area", region: Region.GreaterDowntown }, + { name: "Greater Downtown area", region: Region.GreaterDowntown }, + { name: "Islandview / Greater Villages area", region: Region.Eastside }, + { name: "Jefferson Chalmers area", region: Region.Eastside }, + { name: "Livernois / McNichols area", region: Region.Westside }, + { name: "Morningside area", region: Region.Eastside }, + { name: "North Campau area", region: Region.Eastside }, + { name: "Northwest Grand River area", region: Region.Westside }, + { name: "Northwest University District area", region: Region.Westside }, + { name: "Palmer Park area", region: Region.Westside }, + { name: "Russell Woods / Nardin Park area", region: Region.Westside }, + { name: "Southwest / Vernor area", region: Region.Southwest }, + { name: "Warrendale / Cody Rouge", region: Region.Westside }, + { name: "West End area", region: Region.Eastside }, +] diff --git a/shared-helpers/src/stringFormatting.ts b/shared-helpers/src/stringFormatting.ts new file mode 100644 index 0000000000..e89cdb8abd --- /dev/null +++ b/shared-helpers/src/stringFormatting.ts @@ -0,0 +1,14 @@ +import dayjs from "dayjs" + +export const getTimeRangeString = (start: Date, end: Date) => { + const startTime = dayjs(start).format("hh:mma") + const endTime = dayjs(end).format("hh:mma") + return startTime === endTime ? startTime : `${startTime} - ${endTime}` +} + +export const getCurrencyRange = (min: number | null, max: number | null) => { + if (min && max && min !== max) { + return `$${min} – $${max}` + } + return min || max ? `$${min ?? max}` : "" +} diff --git a/ui-components/src/global/vendor/ag_grid.scss b/shared-helpers/src/styles/ag_grid.scss similarity index 75% rename from ui-components/src/global/vendor/ag_grid.scss rename to shared-helpers/src/styles/ag_grid.scss index 3f8d93c84f..823ebffe9a 100644 --- a/ui-components/src/global/vendor/ag_grid.scss +++ b/shared-helpers/src/styles/ag_grid.scss @@ -11,6 +11,12 @@ ) ); + --ag-selected-row-background-color: var(--bloom-color-primary-light); + + a { + color: var(--bloom-color-primary-dark); + } + .ag-row { height: ag-param(row-height); } @@ -72,6 +78,15 @@ @apply border-b-0; } + .ag-pinned-right-header, + .ag-cell.ag-cell-first-right-pinned:not(.ag-cell-range-left):not(.ag-cell-range-single-cell) { + @apply border-gray-450; + @apply border-r-0; + @apply border-t-0; + @apply border-l-4; + @apply border-b-0; + } + .ag-row { @apply border-t-0; @apply border-l-0; @@ -108,6 +123,28 @@ @apply rounded-b-none; overflow: visible; } + + .ag-layout-auto-height { + .ag-center-cols-container, + .ag-center-cols-clipper { + --table-min-height: 124px; + min-height: var(--table-min-height); + } + } + + .ag-ltr { + .ag-selection-checkbox { + margin-right: var(--bloom-s6); + } + } + + .ag-horizontal-right-spacer:not(.ag-scroller-corner) { + border: none; + } + + .ag-horizontal-left-spacer:not(.ag-scroller-corner) { + border: none; + } } .data-pager { diff --git a/shared-helpers/src/styles/app-css.scss b/shared-helpers/src/styles/app-css.scss new file mode 100644 index 0000000000..1b7b8aed90 --- /dev/null +++ b/shared-helpers/src/styles/app-css.scss @@ -0,0 +1,75 @@ +html { + @apply antialiased; +} + +body { + @apply font-sans; + @apply bg-white; + @apply text-gray-900; +} + +.-top { + top: -0.8rem; +} + +main { +} + +.site-wrapper { + display: flex; + min-height: 100vh; + flex-direction: column; +} + +.site-content { + flex: 1; + display: flex; + flex-direction: column; + + main { + flex: 1; + display: flex; + flex-direction: column; + + > section { + flex: 1; + } + } +} + +.site-content--wide-content { + .navbar, + .page-header__group { + @apply max-w-screen-xl; + } +} + +.site-banner-container { + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.site-alert-banner-content { + padding-block: 1rem; + width: 100%; + + .alert-box__icon { + margin-inline-end: 0.75rem; + } + + a { + color: black; + text-decoration: underline; + } +} + +@import "mixins.scss"; +@import "headers.scss"; +@import "custom_counter.scss"; +@import "forms.scss"; +@import "tables.scss"; +@import "homepage.scss"; +@import "print.scss"; +@import "ag_grid.scss"; diff --git a/shared-helpers/src/styles/blocks.scss b/shared-helpers/src/styles/blocks.scss new file mode 100644 index 0000000000..1419a97695 --- /dev/null +++ b/shared-helpers/src/styles/blocks.scss @@ -0,0 +1,141 @@ +// Box +.box, +.block { + @apply text-sm; + @apply text-gray-700; + @apply rounded; +} + +details.disclosure { + @apply relative; + min-height: 3em; + + &[open] { + @apply mb-16; + } + + & > summary { + @apply text-primary; + @apply absolute; + @apply list-none; + @apply outline-none; + @apply cursor-pointer; + @apply bottom-0; + @apply pr-1; + @apply pb-2; + @include has-toggle; + } + + &[open] > summary { + bottom: -3em; + + &:after { + transform: rotate(0deg) translateY(2px); + } + } + + & > summary::-webkit-details-marker { + @apply hidden; + } + + p { + @apply my-4; + } +} + +.listing-detail-panel { + @screen md { + @apply ml-16; + @apply pr-2; + @apply pt-0; + @apply pl-0; + } +} + +.aside-block { + @apply border-gray-400; + @apply p-5; + @apply pb-2; + @apply -mx-4; + + &.is-tinted { + @apply bg-primary-lighter; + @apply border-t; + } + &:not(.is-tinted) + &.is-tinted { + @apply mt-3; + } + + &.is-tinted, + &:last-of-type { + @apply pb-5; + } + + @screen md { + @apply border-b; + @apply pb-5; + @apply mx-0; + + &.is-tinted { + @apply mt-0; + } + + &:not(.is-tinted) + &.is-tinted { + @apply border-t-0; + @apply mt-0; + } + } +} + +.aside-block__divider { + @apply -mx-5; + @apply mt-6; + @apply mb-2; + @apply border-t; + @apply border-gray-400; + @apply text-center; +} + +.aside-block__conjunction { + @apply relative; + @apply -top; + @apply px-1; + @apply uppercase; + @apply text-primary-dark; + @apply font-semibold; +} + +.notice-block { + @apply flex; + @apply flex-row; + @apply flex-wrap; + @apply max-w-5xl; + @apply m-auto; + @apply mt-5; + @apply mb-12; + @apply text-center; + @apply p-4; + @apply bg-primary-lighter; +} + +$shadow-left-slight: -3px 0px 3px -1px rgba(0, 0, 0, 0.1); + +.shadow-left { + box-shadow: $shadow-left-slight; +} +.md\:shadow-left { + @screen md { + box-shadow: $shadow-left-slight; + } +} + +// temp global style until status bar component +.status-bar__status { + .tag.is-pill { + @apply block; + } +} + +.is-tinted { + @apply bg-primary-lighter; +} diff --git a/shared-helpers/src/styles/css-imports.scss b/shared-helpers/src/styles/css-imports.scss new file mode 100644 index 0000000000..76fcadcc01 --- /dev/null +++ b/shared-helpers/src/styles/css-imports.scss @@ -0,0 +1,3 @@ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; diff --git a/ui-components/src/global/custom_counter.scss b/shared-helpers/src/styles/custom_counter.scss similarity index 97% rename from ui-components/src/global/custom_counter.scss rename to shared-helpers/src/styles/custom_counter.scss index 5e06cb649e..019bc7ee01 100644 --- a/ui-components/src/global/custom_counter.scss +++ b/shared-helpers/src/styles/custom_counter.scss @@ -46,5 +46,5 @@ .custom-counter__subtitle { @apply text-gray-700; - @apply text-tiny; + @apply text-sm; } diff --git a/shared-helpers/src/styles/forms.scss b/shared-helpers/src/styles/forms.scss new file mode 100644 index 0000000000..bc279ad66a --- /dev/null +++ b/shared-helpers/src/styles/forms.scss @@ -0,0 +1,428 @@ +.field { + --bordered-border-width: 1px; + --bordered-border-radius: var(--bloom-s1_5); + --bordered-vertical-padding: var(--bloom-s4); + --bordered-leftward-padding: 2.875rem; + --bordered-rightward-padding: 2.875rem; + --bordered-checked-bg-color: var(--bloom-color-gray-100); + --leftward-margin: 2rem; + + margin-bottom: 1.25rem; + + * > &:last-child { + margin-bottom: 0; + } + + label, + .label { + @apply pb-2; + @apply text-sm; + @apply text-gray-800; + } + + label + .control { + @apply mt-2; + } + + label.sr-only + .control, + label.text__caps-spaced + .control { + @apply mt-0; + } + + label.sr-only + .control, + label.field-label--caps + .control { + @apply mt-0; + } + + .label + .field-note { + @apply mt-2; + } + + .field-label--caps.label + .field-note { + @apply mt-0; + } + + .field-border { + label { + background-color: var(--bloom-color-white); + padding-top: var(--bordered-vertical-padding); + padding-bottom: var(--bordered-vertical-padding); + padding-left: var(--bordered-leftward-padding); + padding-right: var(--bordered-rightward-padding); + border-width: var(--bordered-border-width); + border-radius: var(--bordered-border-radius); + border-color: var(--bloom-color-gray-450); + } + input[type="checkbox"] + label::before, + input[type="radio"] + label::before, + input[type="checkbox"]:focus + label::before, + input[type="radio"]:focus + label::before { + background-color: none; + box-shadow: 0 0 0 1px white, 0 0 0 2px var(--bloom-color-gray-450); + } + input[type="checkbox"]:focus + label, + input[type="radio"]:focus + label { + box-shadow: 0 0 0 1px white, 0 0 0 2px var(--bloom-color-accent-cool), + 0 0 3px 4px var(--bloom-color-accent-cool); + } + input[type="checkbox"]:checked + label, + input[type="radio"]:checked + label { + background-color: var(--bordered-checked-bg-color); + } + } + + .control { + position: relative; + + .input { + @apply border; + @apply border-gray-500; + @apply bg-gray-200; + @apply rounded; + @apply w-full; + @apply py-3; + @apply text-gray-900; + font-family: inherit; + font-size: 1rem; + line-height: normal; + } + + select.input { + @apply pl-3; + @apply pr-8; + } + + input.input { + @apply px-3; + } + + .prepend { + @apply absolute; + @apply px-3; + @apply py-2; + @apply items-center; + @apply text-lg; + background: transparent; + } + + .prepend + input[aria-invalid="false"] { + padding-left: var(--bloom-s8); + padding-right: 0; + [dir="rtl"] & { + padding-right: var(--bloom-s8); + padding-left: 0; + } + } + + .prepend + input[aria-invalid="true"] { + @apply pl-12; + } + + &.control-narrower { + max-width: 8rem; + } + } + + input[type="checkbox"], + input[type="radio"] { + opacity: 0; + position: absolute; + margin-left: -20px; + + &:focus { + text-decoration: none; + // Appears to be for the case of no label + outline: none; + box-shadow: 0 0 0 2px #ffffff, 0 0 3px 4px $tailwind-primary; + } + } + + input[type="checkbox"] + label, + input[type="radio"] + label { + cursor: pointer; + margin-inline-start: var(--leftward-margin); + display: block; + text-indent: -2rem; + } + + input[type="checkbox"] + label::before, + input[type="radio"] + label::before { + background: white; + border-radius: 4px; + box-shadow: 0 0 0 1px white, 0 0 0 2px map-get($tailwind-gray, 550); + content: "\a0"; + display: inline-block; + height: 1.25rem; + line-height: 0.8; + margin-inline-end: 0.8em; + text-indent: 0.15em; + vertical-align: 0.2em; + width: 1.25rem; + } + + input[type="radio"] + label::before { + height: 1.2rem; + width: 1.2rem; + border-radius: 100%; + } + + input[type="checkbox"]:checked + label::before, + input[type="radio"]:checked + label::before { + background-color: $tailwind-primary; + box-shadow: 0 0 0 1px $tailwind-primary; + } + + input[type="radio"]:checked + label::before { + box-shadow: 0 0 0 1px white, 0 0 0 2px $tailwind-primary; + } + + input[type="checkbox"]:checked + label::before { + background-image: url("/images/check.png"); + background-image: url("/images/check.svg"); + background-position: 50%; + background-repeat: no-repeat; + } + + input[type="radio"]:focus + label::before, + input[type="checkbox"]:focus + label::before { + box-shadow: 0 0 0 1px white, 0 0 0 2px $tailwind-accent-cool, 0 0 3px 4px $tailwind-accent-cool; + } + + input[type="checkbox"]:disabled + label { + color: map-get($tailwind-gray, 450) !important; + } + + input[type="checkbox"]:disabled + label::before, + input[type="radio"]:disabled + label::before { + background: map-get($tailwind-gray, 400); + box-shadow: 0 0 0 1px map-get($tailwind-gray, 450); + cursor: not-allowed; + } + + select { + @apply text-gray-900; + @apply rounded; + @apply border; + @apply border-gray-500; + @apply bg-gray-200; + @apply text-gray-900; + @apply leading-tight; + @apply py-2; + @apply px-3; + height: 3em; + font-family: inherit; + font-size: 1rem; + line-height: normal; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + background-image: url("/images/arrow-down.png"); + background-image: url("/images/arrow-down.svg"); + background-position: right 0.75rem center; + background-repeat: no-repeat; + background-size: 0.75rem; + padding-right: 2.25rem; + } + + input:disabled, + select:disabled, + textarea:disabled, + .control input:disabled, + .control select:disabled, + .control textarea:disabled { + @apply bg-gray-400; + cursor: not-allowed; + @apply text-gray-700; + @apply border-gray-500; + } + + select:disabled, + .control select:disabled { + opacity: 1; + } + + &.error { + label { + @apply text-red-700; + } + + .control { + .input, + .prepend, + select { + @apply border-red-700; + @apply border-2; + } + + .prepend { + @apply text-red-700; + } + } + + input[type="radio"] + label::before { + box-shadow: 0 0 0 2px #fff, 0 0 0 4px $tailwind-alert; + } + + input[type="checkbox"] + label::before { + box-shadow: 0 0 0 1px #fff, 0 0 0 3px $tailwind-alert; + } + } +} + +/* Restore previous Tailwind v1 color */ +input::placeholder, +textarea::placeholder { + color: #a0aec0; +} + +input[type], +textarea, +select { + @apply rounded; + + &:focus { + outline: none; + box-shadow: 0 0 0 2px #fff, 0 0 3px 4px $tailwind-accent-cool; + } +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +.field-group--inline { + @apply flex; + + .field { + @apply mr-5; + @apply mb-0; + } +} + +.field-label { + @apply pb-2; + @apply text-sm; + @apply text-gray-800; +} + +.field-label--caps { + @apply font-sans; + @apply uppercase; + @apply text-gray-750; + @apply tracking-widest; + @apply font-semibold; + @apply text-sm; + @apply mb-3; + @apply inline-block; +} + +.field--inline { + display: inline-block; +} + +.field--inline + .field--inline { + margin-left: 1.5rem; +} + +.flex > .field, +.field-group--date .field { + @apply flex-auto; + margin-left: 0.325rem; + margin-right: 0.325rem; +} + +.flex > .field, +.field-group--date .field { + &:first-of-type { + margin-left: 0; + } + &:last-of-type { + margin-right: 0; + } +} + +.field-group--date { + @apply flex; + + .field { + max-width: 3.5rem; + margin-bottom: 0; + + select { + padding-right: 1.75rem; + background-position: right 0.625rem center; + } + + &:last-child { + max-width: 4.5rem; + } + } +} + +.field-note { + @apply text-sm; + @apply text-gray-700; + @apply font-semibold; + white-space: pre-line; +} + +.error-message { + display: inline-block; + @apply text-sm; + @apply text-red-700; + @apply tracking-wide; + @apply leading-5; + @apply mt-2; +} + +.field-sub-note { + @apply mt-2; + @apply text-gray-750; + @apply text-sm; + @apply tracking-wide; + @apply font-sans; + font-weight: normal; +} + +.form-section__title { + @apply font-alt-sans; + @apply text-xl; + @apply text-gray-900; + @apply font-medium; + @apply block; + @apply mb-1; +} + +.form-section__description { + @apply font-sans; + @apply text-base; + @apply text-gray-750; + @apply block; + @apply mb-8; +} + +progress, +::-webkit-progress-bar { + appearance: none; + width: 100%; + @apply bg-gray-400; + border: 0; + height: 12px; + border-radius: 6px; +} +::-webkit-progress-value { + border-radius: 6px; + @apply bg-primary; + transition: width 0.25s; +} +::-moz-progress-bar { + border-radius: 6px; + @apply bg-primary; + transition: width 0.25s; +} diff --git a/shared-helpers/src/styles/headers.scss b/shared-helpers/src/styles/headers.scss new file mode 100644 index 0000000000..d1943d591b --- /dev/null +++ b/shared-helpers/src/styles/headers.scss @@ -0,0 +1,86 @@ +// Listing Detail +.detail-header { + @apply relative; + @apply pe-4; + @apply pb-6; + @apply ps-4; + @apply pt-4; + @apply border-b; + @apply border-gray-400; + @apply text-primary-darker; + + @screen md { + @apply pt-0; + @apply pb-8; + @apply text-sm; + @apply border-none; + @apply text-gray-800; + } +} + +.detail-header__image { + @apply me-2; + @apply ms-2; + @apply w-12; +} + +.detail-header__hgroup { + @apply ps-4; + + @screen md { + @apply ml-12; + @apply border-l-2; + @apply border-primary; + @apply w-full; + } + + .ui-icon { + @screen md { + @apply hidden; + } + + svg { + @apply w-3; + @apply h-3; + } + } +} + +.detail-header__title { + @apply font-alt-sans; + @apply uppercase; + @apply text-sm; + @apply tracking-widest; + @apply text-primary-darker; + + @screen md { + @apply text-black; + @apply font-serif; + @apply text-2xl; + @apply normal-case; + @apply tracking-normal; + @apply text-gray-900; + } +} + +.detail-header__subtitle { + @apply text-sm; + + @screen md { + @apply text-gray-700; + } +} + +.toggle-header { + @apply bg-primary-light; + @apply p-4; + @apply border-b; + @apply border-primary; + display: flex; + justify-content: space-between; +} +.toggle-header-content { + @apply font-sans; + @apply text-sm; + @apply text-gray-800; +} diff --git a/ui-components/src/global/homepage.scss b/shared-helpers/src/styles/homepage.scss similarity index 90% rename from ui-components/src/global/homepage.scss rename to shared-helpers/src/styles/homepage.scss index 72b9a631c8..a018a4839d 100644 --- a/ui-components/src/global/homepage.scss +++ b/shared-helpers/src/styles/homepage.scss @@ -24,4 +24,9 @@ } } } + + h2 { + @apply text-base; + @apply mb-4; + } } diff --git a/shared-helpers/src/styles/mixins.scss b/shared-helpers/src/styles/mixins.scss new file mode 100644 index 0000000000..57894f63d0 --- /dev/null +++ b/shared-helpers/src/styles/mixins.scss @@ -0,0 +1,202 @@ +@use "sass:math"; + +// Using due to errors from compass mixin +@mixin custom-linear-gradient($top, $bottom) { + background: $top; /* Old browsers */ + background: -moz-linear-gradient(top, $top 0%, $bottom 100%); /* FF3.6+ */ + background: -webkit-linear-gradient(top, $top 0%, $bottom 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, $top 0%, $bottom 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, $top 0%, $bottom 100%); /* IE10+ */ + background: linear-gradient(to bottom, $top 0%, $bottom 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#000000',GradientType=0 ); /* IE6-9 */ +} + +// Adds full screen image to pseudo element +@mixin overlay-image() { + display: block; + position: absolute; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; + content: ""; + z-index: 2; +} + +@mixin clearfix() { + &::after { + display: table; + clear: both; + content: ""; + } +} + +@mixin has-toggle() { + &:after { + font-weight: bold; + display: inline-block; + margin-left: var(--bloom-s2); + margin-right: var(--bloom-s2); + font-size: var(--bloom-font-size-xs); + content: "⌃"; + transform: rotate(180deg) translateY(2px); + } + &[aria-expanded="true"]:after { + margin-right: var(--bloom-s5); + transform: rotate(0deg) translateY(2px); + } +} + +@mixin has-image-skeleton() { + background-color: var(--bloom-color-gray-500); + + &::before { + float: left; + content: ""; + width: 1px; + margin-left: -1px; + height: 0; + padding-top: math.div(591.44px, 1127.34px) * 100%; + } + + &::after { + /* to clear float */ + display: table; + clear: both; + content: ""; + } +} + +@mixin filled-appearances() { + &.is-primary { + // @apply bg-primary; + // @apply border-primary; + // @apply text-white; + + // &:hover { + // @apply bg-primary-dark; + // @apply border-primary-dark; + background-color: var(--primary-appearance-background-color, var(--bloom-color-primary)); + border-color: var(--primary-appearance-border-color, var(--bloom-color-primary)); + color: var(--primary-appearance-label-color, var(--bloom-color-white)); + + &:hover { + background-color: var( + --primary-appearance-hover-background-color, + var(--bloom-color-primary-dark) + ); + border-color: var(--primary-appearance-hover-border-color, var(--bloom-color-primary-dark)); + color: var(--primary-appearance-hover-label-color, var(--bloom-color-white)); + } + } + + &.is-success { + background-color: var(--success-appearance-background-color, var(--bloom-color-success)); + border-color: var(--success-appearance-border-color, var(--bloom-color-success)); + color: var(--success-appearance-label-color, var(--bloom-color-white)); + + &:hover { + background-color: var( + --success-appearance-hover-background-color, + var(--bloom-color-success-dark) + ); + border-color: var(--success-appearance-hover-border-color, var(--bloom-color-success)); + color: var(--success-appearance-hover-label-color, var(--bloom-color-white)); + } + } + + &.is-alert { + background-color: var(--bloom-color-alert); + border-color: var(--bloom-color-alert); + color: var(--bloom-color-white); + + &:hover { + background-color: var(--bloom-color-alert-dark); + border-color: var(--bloom-color-alert-dark); + } + } + + &.is-warning { + background-color: var(--bloom-color-warn); + border-color: var(--bloom-color-warn); + color: var(--bloom-color-gray-800); + + &:hover { + color: var(--bloom-color-white); + background-color: var(--bloom-color-warn-dark); + border-color: var(--bloom-color-warn-dark); + } + } + + &.is-borderless { + color: var(--bloom-color-primary-darker); + border-color: transparent; + background: transparent; + + &:hover { + background: transparent; + border-color: transparent; + color: var(--bloom-color-primary-darker); + } + } +} + +// this seems to be causing a lot of issues +@mixin outlined-appearances() { + background-color: var(--bloom-color-white); + color: var(--bloom-color-primary); + border-width: var(--bloom-border-2); + border-color: var(--bloom-color-primary); //hm + + &:hover { + background-color: var( + --outlined-appearance-hover-background-color, + var(--bloom-color-primary-dark) + ); + border-color: var(--outlined-appearance-hover-border-color, var(--bloom-color-primary)); + color: var(--outlined-appearance-hover-label-color, var(--bloom-color-white)); + } + + &.is-outlined { + background: transparent; + + &.is-success { + color: var(--bloom-color-success); + + &:hover { + color: var(--bloom-color-white); + } + } + + &.is-alert { + color: var(--bloom-color-alert); + + &:hover { + color: var(--bloom-color-white); + } + } + + &.is-warning { + color: var(--bloom-color-gray-800); + + &:hover { + color: var(--bloom-color-white); + } + } + } +} + +@mixin transition-timing { + transition-duration: 0.25s; + transition-timing-function: cubic-bezier(0.475, 0.335, 0.43, 0.94); + + @media (prefers-reduced-motion: reduce) { + transition-duration: 0s; + } +} + +@mixin ellipsis() { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/ui-components/src/global/print.scss b/shared-helpers/src/styles/print.scss similarity index 100% rename from ui-components/src/global/print.scss rename to shared-helpers/src/styles/print.scss diff --git a/shared-helpers/src/styles/tables.scss b/shared-helpers/src/styles/tables.scss new file mode 100644 index 0000000000..1c89b4a7d3 --- /dev/null +++ b/shared-helpers/src/styles/tables.scss @@ -0,0 +1,444 @@ +/* Hat tip to this CSS solution: +https://www.cssscript.com/pure-html5-css3-responsive-table-solution/ */ + +//@screen md +@media screen and (max-width: 767px) { + table.responsive-collapse { + thead { + @apply hidden; + } + + tr { + @apply block; + @apply mb-6; + } + + tr:nth-of-type(even) { + background: inherit; + } + + td:nth-of-type(even) { + @apply bg-primary-lighter; + } + + td { + @apply block; + @apply text-right; + border: 0px; + display: flex; + @apply py-2; + } + + td:before { + @apply font-bold; + @apply uppercase; + content: attr(data-label); + text-align: left; + [dir="rtl"] & { + text-align: right; + } + width: 50%; + } + + td:last-child { + @apply border-b-0; + } + } + table.base { + td:nth-of-type(even) { + background: transparent; + } + tbody td { + @apply border-0; + } + } + + tr.group-reserved td { + @apply border-l-8; + @apply border-solid; + @apply border-accent-warm; + } +} + +table { + @apply text-gray-750; + + strong { + @apply font-semibold; + } + + tr { + @apply bg-white; + } + + thead tr th { + @apply text-left; + @apply uppercase; + @apply bg-primary-lighter; + @apply p-5; + @apply font-semibold; + @apply tracking-wider; + @apply border-0; + @apply border-b; + @apply border-primary; + } + + &.th-plain { + thead tr { + background: transparent; + } + + thead tr th { + @apply border-0; + @apply py-2; + background: transparent; + } + } + + &.td-plain { + tr, + tr:nth-of-type(even) { + background: transparent; + } + + tr td { + @apply border-b; + background: transparent; + } + } + &.is-flush-left { + thead tr th:first-of-type { + padding-left: 0 !important; + } + tr td:first-of-type { + padding-left: 0 !important; + } + } + &.is-flush-right { + thead tr th:last-of-type { + padding-right: 0 !important; + } + tr td:last-of-type { + padding-right: 0 !important; + } + } + + &.base { + thead tr { + background: transparent; + } + + tr:nth-of-type(even) { + background: transparent; + } + + thead tr th { + background: transparent; + @apply border-gray-450; + } + + tbody td { + @apply border-b; + @apply border-gray-450; + } + + td:last-child, + th:last-child { + text-align: right; + } + } + + &.stacked-table { + .stacked-table-cell-container { + td:before { + @apply text-base; + @apply text-gray-700; + } + + @screen md { + @apply flex; + @apply flex-col; + @apply px-0; + @apply text-left; + @apply w-full; + } + width: 50%; + @apply text-right; + @apply pl-2; + + .stacked-table-cell { + @apply font-semibold; + @apply text-gray-750; + @apply text-base; + } + + .stacked-table-subtext { + @apply text-sm; + @apply text-gray-700; + @apply pl-1; + @apply font-normal; + @screen md { + @apply pl-0; + } + } + + @apply pr-0; + @screen md { + @apply pr-3; + } + } + + .stacked-table-header { + @apply px-0; + @apply text-base; + @apply text-gray-700; + @screen md { + @apply py-3; + } + @apply py-2; + } + + thead { + @apply border-b; + } + + td:last-child, + th:last-child { + .stacked-table-cell-container { + @apply text-right; + @apply pr-0; + @screen md { + width: auto; + } + width: 100%; + } + } + } + + &.category-table { + thead { + height: 2rem; + @apply border-0; + } + + .stacked-table-header { + @apply align-baseline; + @apply text-black; + @apply normal-case; + @apply px-0; + @apply pb-0; + @apply text-sm; + @screen md { + @apply py-3; + } + @apply pt-0; + @apply pb-2; + } + + td:before { + @apply align-baseline; + @apply text-black; + @apply normal-case; + @apply px-0; + @apply pb-0; + @apply text-sm; + @apply pl-2; + @apply font-normal; + } + + tr { + @apply mb-1; + td:first-child:before { + @apply pl-0; + @apply pr-2; + @apply font-semibold; + width: 50%; + } + } + + td { + @md { + @apply py-3; + } + @apply py-0; + } + + tbody td { + @apply border-0; + } + + td:last-child, + th:last-child { + text-align: inherit; + } + + .stacked-table-cell-container { + width: 50%; + @apply flex; + @apply flex-col; + @apply items-start; + @apply pl-2; + @apply text-left; + @screen md { + @apply pl-0; + @apply py-0; + } + .stacked-table-cell { + @screen md { + @apply text-base; + } + @apply text-sm; + @apply text-black; + @apply font-normal; + } + .stacked-table-subtext { + @apply pl-0; + @apply text-left; + } + } + + td:last-child, + th:last-child { + .stacked-table-cell-container { + width: 50%; + } + } + + tr { + td:first-child { + .stacked-table-cell:first-child { + @screen md { + display: block; + } + display: none; + } + } + } + + @media screen and (max-width: 767px) { + td { + @apply pb-3; + } + td:first-child:before { + content: attr(data-cell); + } + } + } +} + +table tr:nth-of-type(even) { + @apply bg-primary-lighter; +} + +td.reserved { + @apply font-sans; + @apply font-bold; + @apply uppercase; + @apply text-2xs; + @apply leading-tight; + @apply py-2; + @apply text-gray-750; + @apply bg-accent-warm-lighter; + @apply tracking-widest; + @apply pl-2; + @apply border-l-8; + @apply border-b; + @apply border-t; + @apply border-solid; + @apply border-accent-warm; + + .reserved-icon { + @apply text-accent-warm; + @apply text-sm; + } +} + +tr.group-reserved td:first-of-type { + @apply border-l-8; + @apply border-solid; + @apply border-accent-warm; +} + +/* Hat tip to this CSS solution: +https://www.cssscript.com/pure-html5-css3-responsive-table-solution/ */ + +//@screen md +@media screen and (max-width: 767px) { + table.responsive-collapse { + thead { + @apply hidden; + } + + tr { + @apply block; + @apply mb-6; + } + + tr:nth-of-type(even) { + background: inherit; + } + + td:nth-of-type(even) { + @apply bg-primary-lighter; + } + + td { + @apply block; + @apply text-right; + border: 0px; + display: flex; + } + + td:before { + @apply font-bold; + @apply uppercase; + content: attr(data-label); + text-align: left; + width: 50%; + } + + td:last-child { + @apply border-b-0; + } + } + table.base { + td:nth-of-type(even) { + background: transparent; + } + tbody td { + @apply border-0; + } + } + + tr.group-reserved td { + @apply border-l-8; + @apply border-solid; + @apply border-accent-warm; + } +} + +.table__thumbnail img { + max-height: 80px; + max-width: 142px; + @apply -my-3; + @apply inline-block; +} + +table.td-plain { + .table__thumbnail img { + @apply -my-1; + } +} + +.table__draggable-cell { + width: 60px; + padding: 0px; + + .ui-icon svg { + color: var(--bloom-color-gray-750); + } +} + +.table__is-dragging { + display: table; +} diff --git a/ui-components/src/authentication/timeout.tsx b/shared-helpers/src/timeout.tsx similarity index 89% rename from ui-components/src/authentication/timeout.tsx rename to shared-helpers/src/timeout.tsx index e9cbc06140..4154b9ab60 100644 --- a/ui-components/src/authentication/timeout.tsx +++ b/shared-helpers/src/timeout.tsx @@ -1,12 +1,15 @@ import React, { createElement, FunctionComponent, useContext, useEffect, useState } from "react" import { AuthContext } from "./AuthContext" -import { ConfigContext, NavigationContext } from "../config" -import { Button } from "../actions/Button" -import { Modal } from "../overlays/Modal" -import { setSiteAlertMessage } from "../notifications/SiteAlert" -import { AlertTypes } from "../notifications/alertTypes" -import { t } from "../helpers/translator" -import { AppearanceStyleType } from "../global/AppearanceTypes" +import { ConfigContext } from "./ConfigContext" +import { + Button, + setSiteAlertMessage, + AlertTypes, + t, + AppearanceStyleType, + NavigationContext, + Modal, +} from "@bloom-housing/ui-components" const PROMPT_TIMEOUT = 60000 const events = ["mousemove", "keypress", "scroll"] @@ -98,6 +101,7 @@ export const IdleTimeout: FunctionComponent = ({ ariaDescription={promptText} actions={modalActions} hideCloseIcon + role="alertdialog" > {promptText} diff --git a/ui-components/src/authentication/token.ts b/shared-helpers/src/token.ts similarity index 100% rename from ui-components/src/authentication/token.ts rename to shared-helpers/src/token.ts diff --git a/ui-components/src/helpers/unitTypes.ts b/shared-helpers/src/unitTypes.ts similarity index 85% rename from ui-components/src/helpers/unitTypes.ts rename to shared-helpers/src/unitTypes.ts index aaef1c2305..6dae932335 100644 --- a/ui-components/src/helpers/unitTypes.ts +++ b/shared-helpers/src/unitTypes.ts @@ -5,7 +5,7 @@ type GetUnitTypeNamesReturn = { name: string } -export const UnitTypeSort = ["studio", "oneBdrm", "twoBdrm", "threeBdrm", "fourBdrm"] +export const UnitTypeSort = ["SRO", "studio", "oneBdrm", "twoBdrm", "threeBdrm", "fourBdrm"] export const sortUnitTypes = (units: UnitType[] | GetUnitTypeNamesReturn[]) => { if (!units) return [] @@ -21,7 +21,7 @@ export const getUniqueUnitTypes = (units: Unit[]): GetUnitTypeNamesReturn[] => { if (!id || !name) return acc - const unitTypeExists = acc.find((item) => item.id === id) + const unitTypeExists = acc.some((item) => item.id === id) if (!unitTypeExists) { acc.push({ diff --git a/ui-components/src/helpers/useKeyPress.ts b/shared-helpers/src/useKeyPress.ts similarity index 75% rename from ui-components/src/helpers/useKeyPress.ts rename to shared-helpers/src/useKeyPress.ts index 840e374fd3..7ab7726333 100644 --- a/ui-components/src/helpers/useKeyPress.ts +++ b/shared-helpers/src/useKeyPress.ts @@ -1,6 +1,6 @@ import { useEffect } from "react" -function useKeyPress(targetKey: string, callback: () => unknown) { +export function useKeyPress(targetKey: string, callback: () => unknown) { useEffect(() => { function keyUp(e: KeyboardEvent) { if (e.key === targetKey) callback() @@ -13,5 +13,3 @@ function useKeyPress(targetKey: string, callback: () => unknown) { } }, [targetKey, callback]) } - -export default useKeyPress diff --git a/ui-components/src/authentication/useRequireLoggedInUser.ts b/shared-helpers/src/useRequireLoggedInUser.ts similarity index 89% rename from ui-components/src/authentication/useRequireLoggedInUser.ts rename to shared-helpers/src/useRequireLoggedInUser.ts index 4b8bd112a5..835e959813 100644 --- a/ui-components/src/authentication/useRequireLoggedInUser.ts +++ b/shared-helpers/src/useRequireLoggedInUser.ts @@ -1,6 +1,6 @@ import { useContext } from "react" import { AuthContext } from "./AuthContext" -import { NavigationContext } from "../config/NavigationContext" +import { NavigationContext } from "@bloom-housing/ui-components" /** * Require a logged in user. Waits on initial load, then initiates a redirect to `redirectPath` if user is not diff --git a/sites/partners/.env.template b/sites/partners/.env.template index 58c62ab0c7..414040d1fb 100644 --- a/sites/partners/.env.template +++ b/sites/partners/.env.template @@ -1,9 +1,9 @@ ## == IMPORTANT REMINDER: ANY EDITS HERE MUST BE UPDATED IN ALL ENVIRONMENTS (incl. CI) == ## BACKEND_API_BASE=http://localhost:3100 +BACKEND_PROXY_BASE= LISTINGS_QUERY=/listings NEXTJS_PORT=3001 SHOW_DUPLICATES=FALSE -PUBLIC_BASE_URL=http://localhost:3000 SHOW_LM_LINKS=TRUE CLOUDINARY_CLOUD_NAME=exygy CLOUDINARY_KEY='abcxyz' diff --git a/sites/partners/.jest/setup-tests.js b/sites/partners/.jest/setup-tests.js new file mode 100644 index 0000000000..8429f96b28 --- /dev/null +++ b/sites/partners/.jest/setup-tests.js @@ -0,0 +1,23 @@ +// Future home of additional Jest config +import { addTranslation } from "@bloom-housing/ui-components" +import generalTranslations from "../../../shared-helpers/src/locales/general.json" +import { serviceOptions } from "@bloom-housing/backend-core" +import axios from "axios" +import "@testing-library/jest-dom/extend-expect" +import general from "../src/page_content/locale_overrides/general.json" +addTranslation({ ...generalTranslations, ...general }) + +process.env.cloudinaryCloudName = "exygy" +process.env.cloudinarySignedPreset = "test123" +process.env.backendApiBase = "http://localhost:3100" + +global.beforeEach(() => { + serviceOptions.axios = axios.create({ + baseURL: "http://localhost:3100", + }) +}) + +// Need to set __next on base div to handle the overlay +const portalRoot = document.createElement("div") +portalRoot.setAttribute("id", "__next") +document.body.appendChild(portalRoot) diff --git a/sites/partners/CHANGELOG.md b/sites/partners/CHANGELOG.md new file mode 100644 index 0000000000..aa22864d23 --- /dev/null +++ b/sites/partners/CHANGELOG.md @@ -0,0 +1,3595 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.4.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.2.3...@bloom-housing/partners@4.4.0) (2022-05-24) + + +* 2022-05-24 release (#2753) ([3beb6b7](https://github.com/seanmalbert/bloom/commit/3beb6b77f74e51ec37457d4676a1fd01d1304a65)), closes [#2753](https://github.com/seanmalbert/bloom/issues/2753) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.3.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.3.1-alpha.2...@bloom-housing/partners@4.3.1-alpha.3) (2022-05-24) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.3.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.3.1-alpha.1...@bloom-housing/partners@4.3.1-alpha.2) (2022-05-24) + + +### Bug Fixes + +* check for empty url before rendering ([#2749](https://github.com/bloom-housing/bloom/issues/2749)) ([dc384de](https://github.com/bloom-housing/bloom/commit/dc384deaee2db64a857fbb257924a40be90266d4)) + + + + + +## [4.3.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.3.1-alpha.0...@bloom-housing/partners@4.3.1-alpha.1) (2022-05-24) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.3.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.34...@bloom-housing/partners@4.3.1-alpha.0) (2022-05-16) + + +### Bug Fixes + +* adds jurisdictionId to useSWR path ([c7d6adb](https://github.com/bloom-housing/bloom/commit/c7d6adba109aa50f3c1556c89c0ec714fd4c6e50)) +* ami charts without all households ([#2430](https://github.com/bloom-housing/bloom/issues/2430)) ([5e18eba](https://github.com/bloom-housing/bloom/commit/5e18eba1d24bff038b192477b72d9d3f1f05a39d)) +* cannot save custom mailing, dropoff, or pickup address ([edcb068](https://github.com/bloom-housing/bloom/commit/edcb068ca23411e0a34f1dc2ff4c77ab489ac0fc)) +* listings management keep empty strings, remove empty objects ([3aba274](https://github.com/bloom-housing/bloom/commit/3aba274a751cdb2db55b65ade1cda5d1689ca681)) +* listings pending translation for ag grid ([5b42ab0](https://github.com/bloom-housing/bloom/commit/5b42ab006abddc19a0fcc260fbf519c3903e44df)) +* lottery results uploads now save ([8c9dd0f](https://github.com/bloom-housing/bloom/commit/8c9dd0f043dd3835f12bc8f087b9a5519cbfd4f8)) +* paper application submission ([384b86b](https://github.com/bloom-housing/bloom/commit/384b86b624392012b56039dc4a289393f24653f5)) +* remove description for the partners programs ([d4478b8](https://github.com/bloom-housing/bloom/commit/d4478b8eaf68efdf4b23c55f15656e82a907dbc4)) +* removes Duplicate identifier fieldGroupObjectToArray ([a3a2f43](https://github.com/bloom-housing/bloom/commit/a3a2f434606628e4ad141250c401405ced10cdf4)) +* retnal assistance eror message ([09f583b](https://github.com/bloom-housing/bloom/commit/09f583be137336c92f7077beb1f1fbab2b82aefb)) +* updates lastName on application save ([a977ffd](https://github.com/bloom-housing/bloom/commit/a977ffd4b81fbf09122c51ccf066d0a3f3f6544c)) +* versioning issues ([#2311](https://github.com/bloom-housing/bloom/issues/2311)) ([c274a29](https://github.com/bloom-housing/bloom/commit/c274a2985061b389c2cae6386137a4caacd7f7c0)) + + +* 2022-04-08 release (#2646) ([aa9de52](https://github.com/bloom-housing/bloom/commit/aa9de524d5e849ffded475070abf529de77c9a92)), closes [#2646](https://github.com/bloom-housing/bloom/issues/2646) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-04-05 release (#2627) ([485fb48](https://github.com/bloom-housing/bloom/commit/485fb48cfbad48bcabfef5e2e704025f608aee89)), closes [#2627](https://github.com/bloom-housing/bloom/issues/2627) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-04-04 release (#2614) ([fecab85](https://github.com/bloom-housing/bloom/commit/fecab85c748a55ab4aff5d591c8e0ac702254559)), closes [#2614](https://github.com/bloom-housing/bloom/issues/2614) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-03-01 release (#2550) ([2f2264c](https://github.com/bloom-housing/bloom/commit/2f2264cffe41d0cc1ebb79ef5c894458694d9340)), closes [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) +* 2022-01-27 release (#2439) ([860f6af](https://github.com/bloom-housing/bloom/commit/860f6af6204903e4dcddf671d7ba54f3ec04f121)), closes [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) +* Release 11 11 21 (#2162) ([4847469](https://github.com/bloom-housing/bloom/commit/484746982e440c1c1c87c85089d86cd5968f1cae)), closes [#2162](https://github.com/bloom-housing/bloom/issues/2162) + + +### Features + +* adds listing management cypress tests to partner portal ([2e37eec](https://github.com/bloom-housing/bloom/commit/2e37eecf6344f6e25422a24ad7f4563fee4564de)) +* adds updating open listing modal ([#2288](https://github.com/bloom-housing/bloom/issues/2288)) ([d184326](https://github.com/bloom-housing/bloom/commit/d18432610a55a5e54f567ff6157bb863ed61cb21)) +* ami chart jurisdictionalized ([b2e2537](https://github.com/bloom-housing/bloom/commit/b2e2537818d92ff41ea51fbbeb23d9d7e8c1cf52)) +* filter partner users ([3dd8f9b](https://github.com/bloom-housing/bloom/commit/3dd8f9b3cc1f9f90916d49b7136d5f1f73df5291)) +* new demographics sub-race questions ([910df6a](https://github.com/bloom-housing/bloom/commit/910df6ad3985980becdc2798076ed5dfeeb310b5)) +* one month rent ([319743d](https://github.com/bloom-housing/bloom/commit/319743d23268f5b55e129c0878510edb4204b668)) +* overrides fallback to english, tagalog support ([b79fd10](https://github.com/bloom-housing/bloom/commit/b79fd1018619f618bd9be8e870d35c1180b81dfb)) +* overrides partner app website trans ([#2534](https://github.com/bloom-housing/bloom/issues/2534)) ([16c7a4e](https://github.com/bloom-housing/bloom/commit/16c7a4eb8f5ae05dbea9380702c2150a922ca3f0)) +* postmark date time fields partners ([#2239](https://github.com/bloom-housing/bloom/issues/2239)) ([cf20b88](https://github.com/bloom-housing/bloom/commit/cf20b88cb613b815c641cad34a38908e22722a4a)) +* simplify Waitlist component and use more flexible schema ([aa8e006](https://github.com/bloom-housing/bloom/commit/aa8e00616d886e8d57316b2362d35c0c550007c6)) +* temp disable terms and set mfa enabled to false ([#2595](https://github.com/bloom-housing/bloom/issues/2595)) ([6de2dcd](https://github.com/bloom-housing/bloom/commit/6de2dcd8baeb28166d7a6c383846a7ab9a84b0e2)) + + +### Reverts + +* Revert "chore(release): version" ([47a2c67](https://github.com/bloom-housing/bloom/commit/47a2c67af5c7c41f360fafc6c5386476866ea403)) +* Revert "chore: removes application program partners" ([91e22d8](https://github.com/bloom-housing/bloom/commit/91e22d891104e8d4fc024d709a6a14cec1400733)) +* Revert "chore: removes application program display" ([740cf00](https://github.com/bloom-housing/bloom/commit/740cf00dc3a729eed037d56a8dfc5988decd2651)) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + + + + + +## [4.2.2-alpha.34](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.33...@bloom-housing/partners@4.2.2-alpha.34) (2022-05-13) +## [4.2.3](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.2.2...@bloom-housing/partners@4.2.3) (2022-04-28) +## [4.2.2-alpha.33](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.32...@bloom-housing/partners@4.2.2-alpha.33) (2022-05-11) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.33](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.32...@bloom-housing/partners@4.2.2-alpha.33) (2022-05-11) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.2.1...@bloom-housing/partners@4.2.2) (2022-04-19) + +### Bug Fixes + +* listings pending translation for ag grid ([5b42ab0](https://github.com/seanmalbert/bloom/commit/5b42ab006abddc19a0fcc260fbf519c3903e44df)) +## [4.2.2-alpha.32](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.31...@bloom-housing/partners@4.2.2-alpha.32) (2022-05-11) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.31](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.30...@bloom-housing/partners@4.2.2-alpha.31) (2022-05-10) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.30](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.29...@bloom-housing/partners@4.2.2-alpha.30) (2022-05-05) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.29](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.28...@bloom-housing/partners@4.2.2-alpha.29) (2022-05-04) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.28](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.27...@bloom-housing/partners@4.2.2-alpha.28) (2022-05-04) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.27](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.26...@bloom-housing/partners@4.2.2-alpha.27) (2022-05-04) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.26](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.25...@bloom-housing/partners@4.2.2-alpha.26) (2022-05-04) + + +### Bug Fixes + +* max update depth unit form issue ([#2682](https://github.com/bloom-housing/bloom/issues/2682)) ([57200b6](https://github.com/bloom-housing/bloom/commit/57200b69f3cb0b26965a1735196cb126a2754570)) + + + + + +## [4.2.2-alpha.25](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.24...@bloom-housing/partners@4.2.2-alpha.25) (2022-05-03) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.24](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.23...@bloom-housing/partners@4.2.2-alpha.24) (2022-04-29) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.23](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.22...@bloom-housing/partners@4.2.2-alpha.23) (2022-04-29) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.22](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.21...@bloom-housing/partners@4.2.2-alpha.22) (2022-04-28) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.21](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.20...@bloom-housing/partners@4.2.2-alpha.21) (2022-04-28) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.20](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.19...@bloom-housing/partners@4.2.2-alpha.20) (2022-04-28) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.19](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.18...@bloom-housing/partners@4.2.2-alpha.19) (2022-04-27) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.18](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.17...@bloom-housing/partners@4.2.2-alpha.18) (2022-04-26) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.17](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.16...@bloom-housing/partners@4.2.2-alpha.17) (2022-04-25) + + +### Bug Fixes + +* Add admins into users table ([#2683](https://github.com/bloom-housing/bloom/issues/2683)) ([f1e0972](https://github.com/bloom-housing/bloom/commit/f1e0972838116ed5e76814dff002556de625e2e7)), closes [#2657](https://github.com/bloom-housing/bloom/issues/2657) + + + + + +## [4.2.2-alpha.16](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.15...@bloom-housing/partners@4.2.2-alpha.16) (2022-04-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.15](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.14...@bloom-housing/partners@4.2.2-alpha.15) (2022-04-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.14](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.13...@bloom-housing/partners@4.2.2-alpha.14) (2022-04-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.13](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.12...@bloom-housing/partners@4.2.2-alpha.13) (2022-04-21) + + +### Features + +* **backend:** improve user queries ([#2676](https://github.com/bloom-housing/bloom/issues/2676)) ([4733e8a](https://github.com/bloom-housing/bloom/commit/4733e8a9909e47bb2522f9b319f45fe25923cdb5)) + + + + + +## [4.2.2-alpha.12](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.11...@bloom-housing/partners@4.2.2-alpha.12) (2022-04-21) + + +### Features + +* min max occupancy validation in Add Unit ([#2661](https://github.com/bloom-housing/bloom/issues/2661)) ([67a6723](https://github.com/bloom-housing/bloom/commit/67a67231e26ef407808f1e6f9137d60dbb442002)) + + + + + +## [4.2.2-alpha.11](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.10...@bloom-housing/partners@4.2.2-alpha.11) (2022-04-21) + + +### Features + +* new category table component ([#2648](https://github.com/bloom-housing/bloom/issues/2648)) ([3b3fe46](https://github.com/bloom-housing/bloom/commit/3b3fe46dda3d0e553664c10cea46849551ce064c)) + + +### BREAKING CHANGES + +* There is a new prop interface for the StandardTable component and all components that use it, which includes passing cell content within a new object, allowing us to support new cell options - all tables will need to pass data with the new format. + + + + + +## [4.2.2-alpha.10](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.9...@bloom-housing/partners@4.2.2-alpha.10) (2022-04-20) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.8...@bloom-housing/partners@4.2.2-alpha.9) (2022-04-20) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.7...@bloom-housing/partners@4.2.2-alpha.8) (2022-04-20) + + +### Features + +* **backend:** add jurisdiction default rental assistance text ([#2604](https://github.com/bloom-housing/bloom/issues/2604)) ([00b684c](https://github.com/bloom-housing/bloom/commit/00b684cd8b8b1f9ef201b8aec78c13572a4125a5)) + + + + + +## [4.2.2-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.6...@bloom-housing/partners@4.2.2-alpha.7) (2022-04-20) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.5...@bloom-housing/partners@4.2.2-alpha.6) (2022-04-19) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.4...@bloom-housing/partners@4.2.2-alpha.5) (2022-04-18) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.3...@bloom-housing/partners@4.2.2-alpha.4) (2022-04-18) + + +### Features + +* refactor ada form fields ([#2612](https://github.com/bloom-housing/bloom/issues/2612)) ([f516f21](https://github.com/bloom-housing/bloom/commit/f516f2164249cea5b622b6bb5cd6efb5455003ca)) + + + + + +## [4.2.2-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.2...@bloom-housing/partners@4.2.2-alpha.3) (2022-04-14) + + +### Bug Fixes + +* listings pending translation for ag grid ([4016b8f](https://github.com/bloom-housing/bloom/commit/4016b8fcbfb4f6646ef8c76f38a22f916b1b981f)) + + + + + +## [4.2.2-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.1...@bloom-housing/partners@4.2.2-alpha.2) (2022-04-14) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.2-alpha.0...@bloom-housing/partners@4.2.2-alpha.1) (2022-04-13) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.2-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.1-alpha.5...@bloom-housing/partners@4.2.2-alpha.0) (2022-04-13) + + +* 2022-04-11 sync master (#2649) ([9d30acf](https://github.com/bloom-housing/bloom/commit/9d30acf7b53fca50a87fc8bd2658c11d3ed37427)), closes [#2649](https://github.com/bloom-housing/bloom/issues/2649) [#2037](https://github.com/bloom-housing/bloom/issues/2037) [#2095](https://github.com/bloom-housing/bloom/issues/2095) [#2162](https://github.com/bloom-housing/bloom/issues/2162) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2519](https://github.com/bloom-housing/bloom/issues/2519) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2534](https://github.com/bloom-housing/bloom/issues/2534) [#2544](https://github.com/bloom-housing/bloom/issues/2544) [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) + + +### BREAKING CHANGES + +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + +* fix: adds jurisdictionId to useSWR path + +* fix: recalculate units available on listing update + +picked form dev f1a3dbce6478b16542ed61ab20de5dfb9b797262 + +* feat: feat(backend): make use of new application confirmation codes + +picked from dev 3c45c2904818200eed4568931d4cc352fd2f449e + +* revert: revert "chore(deps): bump axios from 0.21.1 to 0.21.2 + +picked from dev 2b83bc0393afc42eed542e326d5ef75502ce119c + +* fix: app submission w/ no due date + +picked from dev 4af1f5a8448f16d347b4a65ecb85fda4d6ed71fc + +* feat: adds new preferences, reserved community type + +* feat: adds bottom border to preferences + +* feat: updates preference string + +* fix: preference cleanup for avance + +* refactor: remove applicationAddress + +picked from dev bf10632a62bf2f14922948c046ea3352ed010f4f + +* feat: refactor and add public site application flow cypress tests + +picked from dev 9ec0e8d05f9570773110754e7fdaf49254d1eab8 + +* feat: better seed data for ami-charts + +picked from dev d8b1d4d185731a589c563a32bd592d01537785f3 + +* feat: adds listing management cypress tests to partner portal + +* fix: listings management keep empty strings, remove empty objects + +picked from dev c4b1e833ec128f457015ac7ffa421ee6047083d9 + +* feat: one month rent + +picked from dev 883b0d53030e1c4d54f2f75bd5e188bb1d255f64 + +* test: view.spec.ts test + +picked from dev 324446c90138d8fac50aba445f515009b5a58bfb + +* refactor: removes jsonpath + +picked from dev deb39acc005607ce3076942b1f49590d08afc10c + +* feat: adds jurisdictions to pref seeds + +picked from dev 9e47cec3b1acfe769207ccbb33c07019cd742e33 + +* feat: new demographics sub-race questions + +picked from dev 9ab892694c1ad2fa8890b411b3b32af68ade1fc3 + +* feat: updates email confirmation for lottery + +picked from dev 1a5e824c96d8e23674c32ea92688b9f7255528d3 + +* fix: add ariaHidden to Icon component + +picked from dev c7bb86aec6fd5ad386c7ca50087d0113b14503be + +* fix: add ariaLabel prop to Button component + +picked from dev 509ddc898ba44c05e26f8ed8c777f1ba456eeee5 + +* fix: change the yes/no radio text to be more descriptive + +picked from dev 0c46054574535523d6f217bb0677bbe732b8945f + +* fix: remove alameda reference in demographics + +picked from dev 7d5991cbf6dbe0b61f2b14d265e87ce3687f743d + +* chore: release version + +picked from dev fe82f25dc349877d974ae62d228fea0354978fb7 + +* feat: ami chart jurisdictionalized + +picked from dev 0a5cbc88a9d9e3c2ff716fe0f44ca6c48f5dcc50 + +* refactor: make backend a peer dependency in ui-components + +picked from dev 952aaa14a77e0960312ff0eeee51399d1d6af9f3 + +* feat: add a phone number column to the user_accounts table + +picked from dev 2647df9ab9888a525cc8a164d091dda6482c502a + +* chore: removes application program partners + +* chore: removes application program display + +* Revert "chore: removes application program display" + +This reverts commit 14825b4a6c9cd1a7235e32074e32af18a71b5c26. + +* Revert "chore: removes application program partners" + +This reverts commit d7aa38c777972a2e21d9f816441caa27f98d3f86. + +* chore: yarn.lock and backend-swagger + +* fix: removes Duplicate identifier fieldGroupObjectToArray + +* feat: skip preferences if not on listing + +* chore(release): version + +* fix: cannot save custom mailing, dropoff, or pickup address + +* chore(release): version + +* chore: converge on one axios version, remove peer dependency + +* chore(release): version + +* feat: simplify Waitlist component and use more flexible schema + +* chore(release): version + +* fix: lottery results uploads now save + +* chore(release): version + +* feat: add SRO unit type + +* chore(release): version + +* fix: paper application submission + +* chore(release): version + +* fix: choose-language context + +* chore(release): version + +* fix: applications/view hide prefs + +* chore(release): version + +* feat: overrides fallback to english, tagalog support + +* chore(release): version + +* fix: account translations + +* chore(release): version + +* fix: units with invalid ami chart + +* chore(release): version + +* fix: remove description for the partners programs + +* fix: fix modal styles on mobile + +* fix: visual improvement to programs form display + +* fix: submission tests not running +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.2.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.2.0...@bloom-housing/partners@4.2.1) (2022-04-11) + + +* 2022-04-08 release (#2646) ([aa9de52](https://github.com/seanmalbert/bloom/commit/aa9de524d5e849ffded475070abf529de77c9a92)), closes [#2646](https://github.com/seanmalbert/bloom/issues/2646) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.2.1-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.1-alpha.4...@bloom-housing/partners@4.2.1-alpha.5) (2022-04-13) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.1-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.1-alpha.3...@bloom-housing/partners@4.2.1-alpha.4) (2022-04-08) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.1-alpha.2...@bloom-housing/partners@4.2.1-alpha.3) (2022-04-07) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.1-alpha.1...@bloom-housing/partners@4.2.1-alpha.2) (2022-04-07) + + +### Bug Fixes + +* sets agreedToTermsOfService properly ([#2635](https://github.com/bloom-housing/bloom/issues/2635)) ([4d405ce](https://github.com/bloom-housing/bloom/commit/4d405ce96fcbc2ffad77277ed0d60a1356630f4d)) + + + + + +## [4.2.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.2.1-alpha.0...@bloom-housing/partners@4.2.1-alpha.1) (2022-04-07) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.2.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.3-alpha.7...@bloom-housing/partners@4.2.1-alpha.0) (2022-04-06) + + +* 2022-04-06 sync master (#2628) ([bc31833](https://github.com/bloom-housing/bloom/commit/bc31833f7ea5720a242d93a01bb1b539181fbad4)), closes [#2628](https://github.com/bloom-housing/bloom/issues/2628) [#2037](https://github.com/bloom-housing/bloom/issues/2037) [#2095](https://github.com/bloom-housing/bloom/issues/2095) [#2162](https://github.com/bloom-housing/bloom/issues/2162) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2519](https://github.com/bloom-housing/bloom/issues/2519) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2534](https://github.com/bloom-housing/bloom/issues/2534) [#2544](https://github.com/bloom-housing/bloom/issues/2544) [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) + + +### BREAKING CHANGES + +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + +* fix: adds jurisdictionId to useSWR path + +* fix: recalculate units available on listing update + +picked form dev f1a3dbce6478b16542ed61ab20de5dfb9b797262 + +* feat: feat(backend): make use of new application confirmation codes + +picked from dev 3c45c2904818200eed4568931d4cc352fd2f449e + +* revert: revert "chore(deps): bump axios from 0.21.1 to 0.21.2 + +picked from dev 2b83bc0393afc42eed542e326d5ef75502ce119c + +* fix: app submission w/ no due date + +picked from dev 4af1f5a8448f16d347b4a65ecb85fda4d6ed71fc + +* feat: adds new preferences, reserved community type + +* feat: adds bottom border to preferences + +* feat: updates preference string + +* fix: preference cleanup for avance + +* refactor: remove applicationAddress + +picked from dev bf10632a62bf2f14922948c046ea3352ed010f4f + +* feat: refactor and add public site application flow cypress tests + +picked from dev 9ec0e8d05f9570773110754e7fdaf49254d1eab8 + +* feat: better seed data for ami-charts + +picked from dev d8b1d4d185731a589c563a32bd592d01537785f3 + +* feat: adds listing management cypress tests to partner portal + +* fix: listings management keep empty strings, remove empty objects + +picked from dev c4b1e833ec128f457015ac7ffa421ee6047083d9 + +* feat: one month rent + +picked from dev 883b0d53030e1c4d54f2f75bd5e188bb1d255f64 + +* test: view.spec.ts test + +picked from dev 324446c90138d8fac50aba445f515009b5a58bfb + +* refactor: removes jsonpath + +picked from dev deb39acc005607ce3076942b1f49590d08afc10c + +* feat: adds jurisdictions to pref seeds + +picked from dev 9e47cec3b1acfe769207ccbb33c07019cd742e33 + +* feat: new demographics sub-race questions + +picked from dev 9ab892694c1ad2fa8890b411b3b32af68ade1fc3 + +* feat: updates email confirmation for lottery + +picked from dev 1a5e824c96d8e23674c32ea92688b9f7255528d3 + +* fix: add ariaHidden to Icon component + +picked from dev c7bb86aec6fd5ad386c7ca50087d0113b14503be + +* fix: add ariaLabel prop to Button component + +picked from dev 509ddc898ba44c05e26f8ed8c777f1ba456eeee5 + +* fix: change the yes/no radio text to be more descriptive + +picked from dev 0c46054574535523d6f217bb0677bbe732b8945f + +* fix: remove alameda reference in demographics + +picked from dev 7d5991cbf6dbe0b61f2b14d265e87ce3687f743d + +* chore: release version + +picked from dev fe82f25dc349877d974ae62d228fea0354978fb7 + +* feat: ami chart jurisdictionalized + +picked from dev 0a5cbc88a9d9e3c2ff716fe0f44ca6c48f5dcc50 + +* refactor: make backend a peer dependency in ui-components + +picked from dev 952aaa14a77e0960312ff0eeee51399d1d6af9f3 + +* feat: add a phone number column to the user_accounts table + +picked from dev 2647df9ab9888a525cc8a164d091dda6482c502a + +* chore: removes application program partners + +* chore: removes application program display + +* Revert "chore: removes application program display" + +This reverts commit 14825b4a6c9cd1a7235e32074e32af18a71b5c26. + +* Revert "chore: removes application program partners" + +This reverts commit d7aa38c777972a2e21d9f816441caa27f98d3f86. + +* chore: yarn.lock and backend-swagger + +* fix: removes Duplicate identifier fieldGroupObjectToArray + +* feat: skip preferences if not on listing + +* chore(release): version + +* fix: cannot save custom mailing, dropoff, or pickup address + +* chore(release): version + +* chore: converge on one axios version, remove peer dependency + +* chore(release): version + +* feat: simplify Waitlist component and use more flexible schema + +* chore(release): version + +* fix: lottery results uploads now save + +* chore(release): version + +* feat: add SRO unit type + +* chore(release): version + +* fix: paper application submission + +* chore(release): version + +* fix: choose-language context + +* chore(release): version + +* fix: applications/view hide prefs + +* chore(release): version + +* feat: overrides fallback to english, tagalog support + +* chore(release): version + +* fix: account translations + +* chore(release): version + +* fix: units with invalid ami chart + +* chore(release): version + +* fix: remove description for the partners programs + +* fix: fix modal styles on mobile + +* fix: visual improvement to programs form display + +* fix: submission tests not running +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +# [4.2.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.1.2...@bloom-housing/partners@4.2.0) (2022-04-06) + + +* 2022-04-05 release (#2627) ([485fb48](https://github.com/seanmalbert/bloom/commit/485fb48cfbad48bcabfef5e2e704025f608aee89)), closes [#2627](https://github.com/seanmalbert/bloom/issues/2627) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) +* 2022-04-04 release (#2614) ([fecab85](https://github.com/seanmalbert/bloom/commit/fecab85c748a55ab4aff5d591c8e0ac702254559)), closes [#2614](https://github.com/seanmalbert/bloom/issues/2614) [#2349](https://github.com/seanmalbert/bloom/issues/2349) [#2350](https://github.com/seanmalbert/bloom/issues/2350) [#2351](https://github.com/seanmalbert/bloom/issues/2351) [#2348](https://github.com/seanmalbert/bloom/issues/2348) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.1.3-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.3-alpha.6...@bloom-housing/partners@4.1.3-alpha.7) (2022-04-05) + + +### Bug Fixes + +* remove shared-helpers dependency from ui-components ([#2620](https://github.com/bloom-housing/bloom/issues/2620)) ([cd6ea54](https://github.com/bloom-housing/bloom/commit/cd6ea5450402a9b5d2a8681c403cbfcff6b6b1c9)) + + + + + +## [4.1.3-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.3-alpha.5...@bloom-housing/partners@4.1.3-alpha.6) (2022-04-05) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.3-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.3-alpha.4...@bloom-housing/partners@4.1.3-alpha.5) (2022-04-04) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.3-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.3-alpha.3...@bloom-housing/partners@4.1.3-alpha.4) (2022-04-04) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.3-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.3-alpha.2...@bloom-housing/partners@4.1.3-alpha.3) (2022-04-04) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.3-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.3-alpha.1...@bloom-housing/partners@4.1.3-alpha.2) (2022-03-31) + + +### Bug Fixes + +* select programs working with single jurisdiction ([#2598](https://github.com/bloom-housing/bloom/issues/2598)) ([7fec414](https://github.com/bloom-housing/bloom/commit/7fec414c8ede55f16679f2e099f58965773cf5a3)) + + + + + +## [4.1.3-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.3-alpha.0...@bloom-housing/partners@4.1.3-alpha.1) (2022-03-30) + + +### Bug Fixes + +* added margin bottom to button on mobile in unit modal ([e26b763](https://github.com/bloom-housing/bloom/commit/e26b763b4ec4024f1e90131ee30d5ecbbb8a9daf)) +* added margin bottom to second button in unit modal, only on mobile ([a94f5f2](https://github.com/bloom-housing/bloom/commit/a94f5f2ca5b6687baff5f29dc7294e150cc106f3)) + + + + + +## [4.1.3-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.2-alpha.3...@bloom-housing/partners@4.1.3-alpha.0) (2022-03-30) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.2](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.1.1...@bloom-housing/partners@4.1.2) (2022-03-29) + + + + + +## [4.1.2-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.2-alpha.2...@bloom-housing/partners@4.1.2-alpha.3) (2022-03-29) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.2-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.2-alpha.1...@bloom-housing/partners@4.1.2-alpha.2) (2022-03-29) + + +### Bug Fixes + +* partners user lisitngs all checkbox ([#2592](https://github.com/bloom-housing/bloom/issues/2592)) ([47fd4b3](https://github.com/bloom-housing/bloom/commit/47fd4b31dc710ef2ccc28473faefe5f047d614b4)) +* removed unused partner footer links ([#2590](https://github.com/bloom-housing/bloom/issues/2590)) ([318d42e](https://github.com/bloom-housing/bloom/commit/318d42e01f5374c7cd2d3e7b35a4bb44e3659c94)) + + + + + +## [4.1.2-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.2-alpha.0...@bloom-housing/partners@4.1.2-alpha.1) (2022-03-28) + + +### Features + +* adds partners re-request confirmation ([#2574](https://github.com/bloom-housing/bloom/issues/2574)) ([235af78](https://github.com/bloom-housing/bloom/commit/235af781914e5c36104bb3862dd55152a16e6750)), closes [#2577](https://github.com/bloom-housing/bloom/issues/2577) + + + + + +## [4.1.2-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.1-alpha.5...@bloom-housing/partners@4.1.2-alpha.0) (2022-03-28) + + +* 2022 03 28 sync master (#2593) ([580283d](https://github.com/bloom-housing/bloom/commit/580283da22246b7d39978e7dfa08016b2c0c3757)), closes [#2593](https://github.com/bloom-housing/bloom/issues/2593) [#2037](https://github.com/bloom-housing/bloom/issues/2037) [#2095](https://github.com/bloom-housing/bloom/issues/2095) [#2162](https://github.com/bloom-housing/bloom/issues/2162) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2439](https://github.com/bloom-housing/bloom/issues/2439) [#2196](https://github.com/bloom-housing/bloom/issues/2196) [#2238](https://github.com/bloom-housing/bloom/issues/2238) [#2226](https://github.com/bloom-housing/bloom/issues/2226) [#2230](https://github.com/bloom-housing/bloom/issues/2230) [#2243](https://github.com/bloom-housing/bloom/issues/2243) [#2195](https://github.com/bloom-housing/bloom/issues/2195) [#2215](https://github.com/bloom-housing/bloom/issues/2215) [#2266](https://github.com/bloom-housing/bloom/issues/2266) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2270](https://github.com/bloom-housing/bloom/issues/2270) [#2188](https://github.com/bloom-housing/bloom/issues/2188) [#2213](https://github.com/bloom-housing/bloom/issues/2213) [#2234](https://github.com/bloom-housing/bloom/issues/2234) [#1901](https://github.com/bloom-housing/bloom/issues/1901) [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2280](https://github.com/bloom-housing/bloom/issues/2280) [#2253](https://github.com/bloom-housing/bloom/issues/2253) [#2276](https://github.com/bloom-housing/bloom/issues/2276) [#2282](https://github.com/bloom-housing/bloom/issues/2282) [#2262](https://github.com/bloom-housing/bloom/issues/2262) [#2278](https://github.com/bloom-housing/bloom/issues/2278) [#2293](https://github.com/bloom-housing/bloom/issues/2293) [#2295](https://github.com/bloom-housing/bloom/issues/2295) [#2296](https://github.com/bloom-housing/bloom/issues/2296) [#2294](https://github.com/bloom-housing/bloom/issues/2294) [#2277](https://github.com/bloom-housing/bloom/issues/2277) [#2290](https://github.com/bloom-housing/bloom/issues/2290) [#2299](https://github.com/bloom-housing/bloom/issues/2299) [#2292](https://github.com/bloom-housing/bloom/issues/2292) [#2303](https://github.com/bloom-housing/bloom/issues/2303) [#2305](https://github.com/bloom-housing/bloom/issues/2305) [#2306](https://github.com/bloom-housing/bloom/issues/2306) [#2308](https://github.com/bloom-housing/bloom/issues/2308) [#2190](https://github.com/bloom-housing/bloom/issues/2190) [#2239](https://github.com/bloom-housing/bloom/issues/2239) [#2311](https://github.com/bloom-housing/bloom/issues/2311) [#2302](https://github.com/bloom-housing/bloom/issues/2302) [#2301](https://github.com/bloom-housing/bloom/issues/2301) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#2313](https://github.com/bloom-housing/bloom/issues/2313) [#2289](https://github.com/bloom-housing/bloom/issues/2289) [#2279](https://github.com/bloom-housing/bloom/issues/2279) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2519](https://github.com/bloom-housing/bloom/issues/2519) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2534](https://github.com/bloom-housing/bloom/issues/2534) [#2544](https://github.com/bloom-housing/bloom/issues/2544) [#2550](https://github.com/bloom-housing/bloom/issues/2550) [#2288](https://github.com/bloom-housing/bloom/issues/2288) [#2317](https://github.com/bloom-housing/bloom/issues/2317) [#2319](https://github.com/bloom-housing/bloom/issues/2319) [#2108](https://github.com/bloom-housing/bloom/issues/2108) [#2326](https://github.com/bloom-housing/bloom/issues/2326) [#2349](https://github.com/bloom-housing/bloom/issues/2349) [#2350](https://github.com/bloom-housing/bloom/issues/2350) [#2351](https://github.com/bloom-housing/bloom/issues/2351) [#2348](https://github.com/bloom-housing/bloom/issues/2348) [#2352](https://github.com/bloom-housing/bloom/issues/2352) [#2316](https://github.com/bloom-housing/bloom/issues/2316) [#2356](https://github.com/bloom-housing/bloom/issues/2356) [#2353](https://github.com/bloom-housing/bloom/issues/2353) [#2338](https://github.com/bloom-housing/bloom/issues/2338) [#2377](https://github.com/bloom-housing/bloom/issues/2377) [#2320](https://github.com/bloom-housing/bloom/issues/2320) [#2386](https://github.com/bloom-housing/bloom/issues/2386) [#2362](https://github.com/bloom-housing/bloom/issues/2362) [#2395](https://github.com/bloom-housing/bloom/issues/2395) [#2410](https://github.com/bloom-housing/bloom/issues/2410) [#2407](https://github.com/bloom-housing/bloom/issues/2407) [#2430](https://github.com/bloom-housing/bloom/issues/2430) [#2418](https://github.com/bloom-housing/bloom/issues/2418) [#2434](https://github.com/bloom-housing/bloom/issues/2434) [#2374](https://github.com/bloom-housing/bloom/issues/2374) [#2435](https://github.com/bloom-housing/bloom/issues/2435) [#2420](https://github.com/bloom-housing/bloom/issues/2420) [#2412](https://github.com/bloom-housing/bloom/issues/2412) [#2438](https://github.com/bloom-housing/bloom/issues/2438) [#2429](https://github.com/bloom-housing/bloom/issues/2429) [#2452](https://github.com/bloom-housing/bloom/issues/2452) [#2458](https://github.com/bloom-housing/bloom/issues/2458) [#2423](https://github.com/bloom-housing/bloom/issues/2423) [#2432](https://github.com/bloom-housing/bloom/issues/2432) [#2437](https://github.com/bloom-housing/bloom/issues/2437) [#2440](https://github.com/bloom-housing/bloom/issues/2440) [#2441](https://github.com/bloom-housing/bloom/issues/2441) [#2460](https://github.com/bloom-housing/bloom/issues/2460) [#2459](https://github.com/bloom-housing/bloom/issues/2459) [#2464](https://github.com/bloom-housing/bloom/issues/2464) [#2465](https://github.com/bloom-housing/bloom/issues/2465) [#2466](https://github.com/bloom-housing/bloom/issues/2466) [#2436](https://github.com/bloom-housing/bloom/issues/2436) [#2451](https://github.com/bloom-housing/bloom/issues/2451) [#2415](https://github.com/bloom-housing/bloom/issues/2415) [#2354](https://github.com/bloom-housing/bloom/issues/2354) [#2455](https://github.com/bloom-housing/bloom/issues/2455) [#2484](https://github.com/bloom-housing/bloom/issues/2484) [#2482](https://github.com/bloom-housing/bloom/issues/2482) [#2483](https://github.com/bloom-housing/bloom/issues/2483) [#2476](https://github.com/bloom-housing/bloom/issues/2476) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2470](https://github.com/bloom-housing/bloom/issues/2470) [#2488](https://github.com/bloom-housing/bloom/issues/2488) [#2487](https://github.com/bloom-housing/bloom/issues/2487) [#2496](https://github.com/bloom-housing/bloom/issues/2496) [#2498](https://github.com/bloom-housing/bloom/issues/2498) [#2499](https://github.com/bloom-housing/bloom/issues/2499) [#2291](https://github.com/bloom-housing/bloom/issues/2291) [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) [#2494](https://github.com/bloom-housing/bloom/issues/2494) [#2503](https://github.com/bloom-housing/bloom/issues/2503) [#2495](https://github.com/bloom-housing/bloom/issues/2495) [#2477](https://github.com/bloom-housing/bloom/issues/2477) [#2505](https://github.com/bloom-housing/bloom/issues/2505) [#2372](https://github.com/bloom-housing/bloom/issues/2372) [#2489](https://github.com/bloom-housing/bloom/issues/2489) [#2497](https://github.com/bloom-housing/bloom/issues/2497) [#2506](https://github.com/bloom-housing/bloom/issues/2506) [#2486](https://github.com/bloom-housing/bloom/issues/2486) + + +### BREAKING CHANGES + +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + +* fix: adds jurisdictionId to useSWR path + +* fix: recalculate units available on listing update + +picked form dev f1a3dbce6478b16542ed61ab20de5dfb9b797262 + +* feat: feat(backend): make use of new application confirmation codes + +picked from dev 3c45c2904818200eed4568931d4cc352fd2f449e + +* revert: revert "chore(deps): bump axios from 0.21.1 to 0.21.2 + +picked from dev 2b83bc0393afc42eed542e326d5ef75502ce119c + +* fix: app submission w/ no due date + +picked from dev 4af1f5a8448f16d347b4a65ecb85fda4d6ed71fc + +* feat: adds new preferences, reserved community type + +* feat: adds bottom border to preferences + +* feat: updates preference string + +* fix: preference cleanup for avance + +* refactor: remove applicationAddress + +picked from dev bf10632a62bf2f14922948c046ea3352ed010f4f + +* feat: refactor and add public site application flow cypress tests + +picked from dev 9ec0e8d05f9570773110754e7fdaf49254d1eab8 + +* feat: better seed data for ami-charts + +picked from dev d8b1d4d185731a589c563a32bd592d01537785f3 + +* feat: adds listing management cypress tests to partner portal + +* fix: listings management keep empty strings, remove empty objects + +picked from dev c4b1e833ec128f457015ac7ffa421ee6047083d9 + +* feat: one month rent + +picked from dev 883b0d53030e1c4d54f2f75bd5e188bb1d255f64 + +* test: view.spec.ts test + +picked from dev 324446c90138d8fac50aba445f515009b5a58bfb + +* refactor: removes jsonpath + +picked from dev deb39acc005607ce3076942b1f49590d08afc10c + +* feat: adds jurisdictions to pref seeds + +picked from dev 9e47cec3b1acfe769207ccbb33c07019cd742e33 + +* feat: new demographics sub-race questions + +picked from dev 9ab892694c1ad2fa8890b411b3b32af68ade1fc3 + +* feat: updates email confirmation for lottery + +picked from dev 1a5e824c96d8e23674c32ea92688b9f7255528d3 + +* fix: add ariaHidden to Icon component + +picked from dev c7bb86aec6fd5ad386c7ca50087d0113b14503be + +* fix: add ariaLabel prop to Button component + +picked from dev 509ddc898ba44c05e26f8ed8c777f1ba456eeee5 + +* fix: change the yes/no radio text to be more descriptive + +picked from dev 0c46054574535523d6f217bb0677bbe732b8945f + +* fix: remove alameda reference in demographics + +picked from dev 7d5991cbf6dbe0b61f2b14d265e87ce3687f743d + +* chore: release version + +picked from dev fe82f25dc349877d974ae62d228fea0354978fb7 + +* feat: ami chart jurisdictionalized + +picked from dev 0a5cbc88a9d9e3c2ff716fe0f44ca6c48f5dcc50 + +* refactor: make backend a peer dependency in ui-components + +picked from dev 952aaa14a77e0960312ff0eeee51399d1d6af9f3 + +* feat: add a phone number column to the user_accounts table + +picked from dev 2647df9ab9888a525cc8a164d091dda6482c502a + +* chore: removes application program partners + +* chore: removes application program display + +* Revert "chore: removes application program display" + +This reverts commit 14825b4a6c9cd1a7235e32074e32af18a71b5c26. + +* Revert "chore: removes application program partners" + +This reverts commit d7aa38c777972a2e21d9f816441caa27f98d3f86. + +* chore: yarn.lock and backend-swagger + +* fix: removes Duplicate identifier fieldGroupObjectToArray + +* feat: skip preferences if not on listing + +* chore(release): version + +* fix: cannot save custom mailing, dropoff, or pickup address + +* chore(release): version + +* chore: converge on one axios version, remove peer dependency + +* chore(release): version + +* feat: simplify Waitlist component and use more flexible schema + +* chore(release): version + +* fix: lottery results uploads now save + +* chore(release): version + +* feat: add SRO unit type + +* chore(release): version + +* fix: paper application submission + +* chore(release): version + +* fix: choose-language context + +* chore(release): version + +* fix: applications/view hide prefs + +* chore(release): version + +* feat: overrides fallback to english, tagalog support + +* chore(release): version + +* fix: account translations + +* chore(release): version + +* fix: units with invalid ami chart + +* chore(release): version + +* fix: remove description for the partners programs + +* fix: fix modal styles on mobile + +* fix: visual improvement to programs form display + +* fix: submission tests not running +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.1.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.1.0...@bloom-housing/partners@4.1.1) (2022-03-28) + + + + + +## [4.1.1-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.1-alpha.4...@bloom-housing/partners@4.1.1-alpha.5) (2022-03-28) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.1-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.1-alpha.3...@bloom-housing/partners@4.1.1-alpha.4) (2022-03-25) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.1-alpha.2...@bloom-housing/partners@4.1.1-alpha.3) (2022-03-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.1-alpha.1...@bloom-housing/partners@4.1.1-alpha.2) (2022-03-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.1.1-alpha.0...@bloom-housing/partners@4.1.1-alpha.1) (2022-03-10) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.1.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.87...@bloom-housing/partners@4.1.1-alpha.0) (2022-03-02) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.0.1-alpha.87...@bloom-housing/partners@4.0.1) (2022-03-02) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [4.1.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.0.3...@bloom-housing/partners@4.1.0) (2022-03-02) + + +* 2022-03-01 release (#2550) ([2f2264c](https://github.com/seanmalbert/bloom/commit/2f2264cffe41d0cc1ebb79ef5c894458694d9340)), closes [#2550](https://github.com/seanmalbert/bloom/issues/2550) [#2288](https://github.com/seanmalbert/bloom/issues/2288) [#2317](https://github.com/seanmalbert/bloom/issues/2317) [#2319](https://github.com/seanmalbert/bloom/issues/2319) [#2108](https://github.com/seanmalbert/bloom/issues/2108) [#2326](https://github.com/seanmalbert/bloom/issues/2326) [#2349](https://github.com/seanmalbert/bloom/issues/2349) [#2350](https://github.com/seanmalbert/bloom/issues/2350) [#2351](https://github.com/seanmalbert/bloom/issues/2351) [#2348](https://github.com/seanmalbert/bloom/issues/2348) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2438](https://github.com/seanmalbert/bloom/issues/2438) [#2429](https://github.com/seanmalbert/bloom/issues/2429) [#2452](https://github.com/seanmalbert/bloom/issues/2452) [#2458](https://github.com/seanmalbert/bloom/issues/2458) [#2423](https://github.com/seanmalbert/bloom/issues/2423) [#2432](https://github.com/seanmalbert/bloom/issues/2432) [#2437](https://github.com/seanmalbert/bloom/issues/2437) [#2440](https://github.com/seanmalbert/bloom/issues/2440) [#2441](https://github.com/seanmalbert/bloom/issues/2441) [#2460](https://github.com/seanmalbert/bloom/issues/2460) [#2459](https://github.com/seanmalbert/bloom/issues/2459) [#2464](https://github.com/seanmalbert/bloom/issues/2464) [#2465](https://github.com/seanmalbert/bloom/issues/2465) [#2466](https://github.com/seanmalbert/bloom/issues/2466) [#2436](https://github.com/seanmalbert/bloom/issues/2436) [#2451](https://github.com/seanmalbert/bloom/issues/2451) [#2415](https://github.com/seanmalbert/bloom/issues/2415) [#2354](https://github.com/seanmalbert/bloom/issues/2354) [#2455](https://github.com/seanmalbert/bloom/issues/2455) [#2484](https://github.com/seanmalbert/bloom/issues/2484) [#2482](https://github.com/seanmalbert/bloom/issues/2482) [#2483](https://github.com/seanmalbert/bloom/issues/2483) [#2476](https://github.com/seanmalbert/bloom/issues/2476) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2470](https://github.com/seanmalbert/bloom/issues/2470) [#2488](https://github.com/seanmalbert/bloom/issues/2488) [#2487](https://github.com/seanmalbert/bloom/issues/2487) [#2496](https://github.com/seanmalbert/bloom/issues/2496) [#2498](https://github.com/seanmalbert/bloom/issues/2498) [#2499](https://github.com/seanmalbert/bloom/issues/2499) [#2291](https://github.com/seanmalbert/bloom/issues/2291) [#2461](https://github.com/seanmalbert/bloom/issues/2461) [#2485](https://github.com/seanmalbert/bloom/issues/2485) [#2494](https://github.com/seanmalbert/bloom/issues/2494) [#2503](https://github.com/seanmalbert/bloom/issues/2503) [#2495](https://github.com/seanmalbert/bloom/issues/2495) [#2477](https://github.com/seanmalbert/bloom/issues/2477) [#2505](https://github.com/seanmalbert/bloom/issues/2505) [#2372](https://github.com/seanmalbert/bloom/issues/2372) [#2489](https://github.com/seanmalbert/bloom/issues/2489) [#2497](https://github.com/seanmalbert/bloom/issues/2497) [#2506](https://github.com/seanmalbert/bloom/issues/2506) [#2486](https://github.com/seanmalbert/bloom/issues/2486) + + +### BREAKING CHANGES + +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 +* consolidated all event section components in one new component, uptake will require removing the deprecated components and uptaking EventSection + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.38 + - @bloom-housing/shared-helpers@4.0.1-alpha.63 + - @bloom-housing/partners@4.0.1-alpha.67 + - @bloom-housing/public@4.0.1-alpha.66 + - @bloom-housing/ui-components@4.0.1-alpha.62 + + + + + +## [4.0.1-alpha.87](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.86...@bloom-housing/partners@4.0.1-alpha.87) (2022-02-28) + + +### Features + +* updates to mfa styling ([#2532](https://github.com/bloom-housing/bloom/issues/2532)) ([7654efc](https://github.com/bloom-housing/bloom/commit/7654efc8a7c5cba0f7436fda62b886f646fe8a03)) + + + + + +## [4.0.1-alpha.86](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.85...@bloom-housing/partners@4.0.1-alpha.86) (2022-02-28) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.85](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.84...@bloom-housing/partners@4.0.1-alpha.85) (2022-02-26) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.3](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.0.2...@bloom-housing/partners@4.0.3) (2022-02-25) + +## Features + +* overrides partner app website trans ([#2534](https://github.com/seanmalbert/bloom/issues/2534)) ([16c7a4e](https://github.com/seanmalbert/bloom/commit/16c7a4eb8f5ae05dbea9380702c2150a922ca3f0)) + + + + + +## [4.0.1-alpha.84](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.83...@bloom-housing/partners@4.0.1-alpha.84) (2022-02-25) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.83](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.82...@bloom-housing/partners@4.0.1-alpha.83) (2022-02-25) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.82](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.81...@bloom-housing/partners@4.0.1-alpha.82) (2022-02-24) + + +### Features + +* overrides partner app website trans ([#2534](https://github.com/bloom-housing/bloom/issues/2534)) ([9e09b0b](https://github.com/bloom-housing/bloom/commit/9e09b0bbb3e394c92dcce18bb0cba74db03c49fa)) + + + + + +## [4.0.1-alpha.81](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.80...@bloom-housing/partners@4.0.1-alpha.81) (2022-02-22) + + +### Bug Fixes + +* purge listing detail with wildcard ([4fd2137](https://github.com/bloom-housing/bloom/commit/4fd21374c2dc213dfe1b8dde004d41895126c1d6)) + + + + + +## [4.0.1-alpha.80](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.79...@bloom-housing/partners@4.0.1-alpha.80) (2022-02-22) + + +### Features + +* updates cache clear to separate individual and lists ([#2529](https://github.com/bloom-housing/bloom/issues/2529)) ([1521191](https://github.com/bloom-housing/bloom/commit/15211918b8bf0741ff6a25265b1bf3a60d5678b2)) + + + + + +## [4.0.1-alpha.79](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.78...@bloom-housing/partners@4.0.1-alpha.79) (2022-02-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.78](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.77...@bloom-housing/partners@4.0.1-alpha.78) (2022-02-18) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.77](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.76...@bloom-housing/partners@4.0.1-alpha.77) (2022-02-17) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.76](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.75...@bloom-housing/partners@4.0.1-alpha.76) (2022-02-17) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.75](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.74...@bloom-housing/partners@4.0.1-alpha.75) (2022-02-17) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.74](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.73...@bloom-housing/partners@4.0.1-alpha.74) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.73](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.72...@bloom-housing/partners@4.0.1-alpha.73) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.72](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.71...@bloom-housing/partners@4.0.1-alpha.72) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.71](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.70...@bloom-housing/partners@4.0.1-alpha.71) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.70](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.69...@bloom-housing/partners@4.0.1-alpha.70) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.69](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.68...@bloom-housing/partners@4.0.1-alpha.69) (2022-02-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.68](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.67...@bloom-housing/partners@4.0.1-alpha.68) (2022-02-15) + + +### Features + +* **backend:** make listing image an array ([#2477](https://github.com/bloom-housing/bloom/issues/2477)) ([cab9800](https://github.com/bloom-housing/bloom/commit/cab98003e640c880be2218fa42321eadeec35e9c)) + + + + + +## [4.0.1-alpha.67](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.66...@bloom-housing/partners@4.0.1-alpha.67) (2022-02-15) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.66](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.65...@bloom-housing/partners@4.0.1-alpha.66) (2022-02-15) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.65](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.64...@bloom-housing/partners@4.0.1-alpha.65) (2022-02-15) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.64](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.63...@bloom-housing/partners@4.0.1-alpha.64) (2022-02-15) + + +### Features + +* **backend:** add partners portal users multi factor authentication ([#2291](https://github.com/bloom-housing/bloom/issues/2291)) ([5b10098](https://github.com/bloom-housing/bloom/commit/5b10098d8668f9f42c60e90236db16d6cc517793)), closes [#2461](https://github.com/bloom-housing/bloom/issues/2461) [#2485](https://github.com/bloom-housing/bloom/issues/2485) + + + + + +## [4.0.1-alpha.63](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.62...@bloom-housing/partners@4.0.1-alpha.63) (2022-02-14) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.62](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.61...@bloom-housing/partners@4.0.1-alpha.62) (2022-02-14) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.61](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.60...@bloom-housing/partners@4.0.1-alpha.61) (2022-02-12) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.60](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.59...@bloom-housing/partners@4.0.1-alpha.60) (2022-02-10) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.59](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.58...@bloom-housing/partners@4.0.1-alpha.59) (2022-02-10) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.58](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.57...@bloom-housing/partners@4.0.1-alpha.58) (2022-02-10) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.57](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.56...@bloom-housing/partners@4.0.1-alpha.57) (2022-02-10) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.2](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.0.1...@bloom-housing/partners@4.0.2) (2022-02-09) + + + + + +## [4.0.1-alpha.56](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.55...@bloom-housing/partners@4.0.1-alpha.56) (2022-02-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.55](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.54...@bloom-housing/partners@4.0.1-alpha.55) (2022-02-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.54](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.53...@bloom-housing/partners@4.0.1-alpha.54) (2022-02-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.53](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.52...@bloom-housing/partners@4.0.1-alpha.53) (2022-02-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.52](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.51...@bloom-housing/partners@4.0.1-alpha.52) (2022-02-09) + + +### Bug Fixes + +* cannot remove some fields in listings management ([#2455](https://github.com/bloom-housing/bloom/issues/2455)) ([acd9b51](https://github.com/bloom-housing/bloom/commit/acd9b51bb49581b4728b445d56c5c0a3c43e2777)) + + + + + +## [4.0.1-alpha.51](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.50...@bloom-housing/partners@4.0.1-alpha.51) (2022-02-08) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.50](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.49...@bloom-housing/partners@4.0.1-alpha.50) (2022-02-07) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@3.0.1...@bloom-housing/partners@4.0.1) (2022-02-03) + +## Bug Fixes + +* ami charts without all households ([#2430](https://github.com/seanmalbert/bloom/issues/2430)) ([5e18eba](https://github.com/seanmalbert/bloom/commit/5e18eba1d24bff038b192477b72d9d3f1f05a39d)) + + +* 2022-01-27 release (#2439) ([860f6af](https://github.com/seanmalbert/bloom/commit/860f6af6204903e4dcddf671d7ba54f3ec04f121)), closes [#2439](https://github.com/seanmalbert/bloom/issues/2439) [#2196](https://github.com/seanmalbert/bloom/issues/2196) [#2238](https://github.com/seanmalbert/bloom/issues/2238) [#2226](https://github.com/seanmalbert/bloom/issues/2226) [#2230](https://github.com/seanmalbert/bloom/issues/2230) [#2243](https://github.com/seanmalbert/bloom/issues/2243) [#2195](https://github.com/seanmalbert/bloom/issues/2195) [#2215](https://github.com/seanmalbert/bloom/issues/2215) [#2266](https://github.com/seanmalbert/bloom/issues/2266) [#2188](https://github.com/seanmalbert/bloom/issues/2188) [#2270](https://github.com/seanmalbert/bloom/issues/2270) [#2188](https://github.com/seanmalbert/bloom/issues/2188) [#2213](https://github.com/seanmalbert/bloom/issues/2213) [#2234](https://github.com/seanmalbert/bloom/issues/2234) [#1901](https://github.com/seanmalbert/bloom/issues/1901) [#2260](https://github.com/seanmalbert/bloom/issues/2260) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#2280](https://github.com/seanmalbert/bloom/issues/2280) [#2253](https://github.com/seanmalbert/bloom/issues/2253) [#2276](https://github.com/seanmalbert/bloom/issues/2276) [#2282](https://github.com/seanmalbert/bloom/issues/2282) [#2262](https://github.com/seanmalbert/bloom/issues/2262) [#2278](https://github.com/seanmalbert/bloom/issues/2278) [#2293](https://github.com/seanmalbert/bloom/issues/2293) [#2295](https://github.com/seanmalbert/bloom/issues/2295) [#2296](https://github.com/seanmalbert/bloom/issues/2296) [#2294](https://github.com/seanmalbert/bloom/issues/2294) [#2277](https://github.com/seanmalbert/bloom/issues/2277) [#2290](https://github.com/seanmalbert/bloom/issues/2290) [#2299](https://github.com/seanmalbert/bloom/issues/2299) [#2292](https://github.com/seanmalbert/bloom/issues/2292) [#2303](https://github.com/seanmalbert/bloom/issues/2303) [#2305](https://github.com/seanmalbert/bloom/issues/2305) [#2306](https://github.com/seanmalbert/bloom/issues/2306) [#2308](https://github.com/seanmalbert/bloom/issues/2308) [#2190](https://github.com/seanmalbert/bloom/issues/2190) [#2239](https://github.com/seanmalbert/bloom/issues/2239) [#2311](https://github.com/seanmalbert/bloom/issues/2311) [#2302](https://github.com/seanmalbert/bloom/issues/2302) [#2301](https://github.com/seanmalbert/bloom/issues/2301) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#2313](https://github.com/seanmalbert/bloom/issues/2313) [#2289](https://github.com/seanmalbert/bloom/issues/2289) [#2279](https://github.com/seanmalbert/bloom/issues/2279) [#2288](https://github.com/seanmalbert/bloom/issues/2288) [#2317](https://github.com/seanmalbert/bloom/issues/2317) [#2319](https://github.com/seanmalbert/bloom/issues/2319) [#2108](https://github.com/seanmalbert/bloom/issues/2108) [#2326](https://github.com/seanmalbert/bloom/issues/2326) [#2349](https://github.com/seanmalbert/bloom/issues/2349) [#2350](https://github.com/seanmalbert/bloom/issues/2350) [#2351](https://github.com/seanmalbert/bloom/issues/2351) [#2348](https://github.com/seanmalbert/bloom/issues/2348) [#2352](https://github.com/seanmalbert/bloom/issues/2352) [#2316](https://github.com/seanmalbert/bloom/issues/2316) [#2356](https://github.com/seanmalbert/bloom/issues/2356) [#2353](https://github.com/seanmalbert/bloom/issues/2353) [#2338](https://github.com/seanmalbert/bloom/issues/2338) [#2377](https://github.com/seanmalbert/bloom/issues/2377) [#2320](https://github.com/seanmalbert/bloom/issues/2320) [#2386](https://github.com/seanmalbert/bloom/issues/2386) [#2362](https://github.com/seanmalbert/bloom/issues/2362) [#2395](https://github.com/seanmalbert/bloom/issues/2395) [#2410](https://github.com/seanmalbert/bloom/issues/2410) [#2407](https://github.com/seanmalbert/bloom/issues/2407) [#2430](https://github.com/seanmalbert/bloom/issues/2430) [#2418](https://github.com/seanmalbert/bloom/issues/2418) [#2434](https://github.com/seanmalbert/bloom/issues/2434) [#2374](https://github.com/seanmalbert/bloom/issues/2374) [#2435](https://github.com/seanmalbert/bloom/issues/2435) [#2420](https://github.com/seanmalbert/bloom/issues/2420) [#2412](https://github.com/seanmalbert/bloom/issues/2412) [#2434](https://github.com/seanmalbert/bloom/issues/2434) + + +### BREAKING CHANGES + +* sign-in pages have been updated +* moved some helpers from ui-components to shared-helpers +* remove applicationDueTime field and consolidated into applicationDueDate + +* chore(release): version + + - @bloom-housing/backend-core@3.0.2-alpha.13 + - @bloom-housing/shared-helpers@4.0.1-alpha.21 + - @bloom-housing/partners@4.0.1-alpha.23 + - @bloom-housing/public@4.0.1-alpha.22 + - @bloom-housing/ui-components@4.0.1-alpha.21 + + + + + +## [4.0.1-alpha.49](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.48...@bloom-housing/partners@4.0.1-alpha.49) (2022-02-02) + + +### Bug Fixes + +* unit accordion radio button not showing default value ([#2451](https://github.com/bloom-housing/bloom/issues/2451)) ([4ed8103](https://github.com/bloom-housing/bloom/commit/4ed81039b9130d0433b11df2bdabc495ce2b9f24)) + + + + + +## [4.0.1-alpha.48](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.47...@bloom-housing/partners@4.0.1-alpha.48) (2022-02-02) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.47](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.46...@bloom-housing/partners@4.0.1-alpha.47) (2022-02-02) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.46](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.45...@bloom-housing/partners@4.0.1-alpha.46) (2022-02-02) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.45](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.44...@bloom-housing/partners@4.0.1-alpha.45) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.44](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.43...@bloom-housing/partners@4.0.1-alpha.44) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.43](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.42...@bloom-housing/partners@4.0.1-alpha.43) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.42](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.41...@bloom-housing/partners@4.0.1-alpha.42) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.41](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.40...@bloom-housing/partners@4.0.1-alpha.41) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.40](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.39...@bloom-housing/partners@4.0.1-alpha.40) (2022-02-01) + + +### Features + +* partners terms page ([#2440](https://github.com/bloom-housing/bloom/issues/2440)) ([63105bc](https://github.com/bloom-housing/bloom/commit/63105bcedfe371a4a9995e25b1e5aba67d06ad0c)) + + + + + +## [4.0.1-alpha.39](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.38...@bloom-housing/partners@4.0.1-alpha.39) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.38](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.37...@bloom-housing/partners@4.0.1-alpha.38) (2022-02-01) + + +### Features + +* **backend:** add publishedAt and closedAt to listing entity ([#2432](https://github.com/bloom-housing/bloom/issues/2432)) ([f3b0f86](https://github.com/bloom-housing/bloom/commit/f3b0f864a6d5d2ad3d886e828743454c3e8fca71)) + + + + + +## [4.0.1-alpha.37](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.36...@bloom-housing/partners@4.0.1-alpha.37) (2022-02-01) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.36](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.35...@bloom-housing/partners@4.0.1-alpha.36) (2022-01-31) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.35](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.34...@bloom-housing/partners@4.0.1-alpha.35) (2022-01-31) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.34](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.33...@bloom-housing/partners@4.0.1-alpha.34) (2022-01-27) + + +### Features + +* outdated password messaging updates ([b14e19d](https://github.com/bloom-housing/bloom/commit/b14e19d43099af2ba721d8aaaeeb2be886d05111)) + + + + + +## [4.0.1-alpha.33](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.32...@bloom-housing/partners@4.0.1-alpha.33) (2022-01-26) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.32](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.31...@bloom-housing/partners@4.0.1-alpha.32) (2022-01-26) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.31](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.30...@bloom-housing/partners@4.0.1-alpha.31) (2022-01-26) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.30](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.29...@bloom-housing/partners@4.0.1-alpha.30) (2022-01-24) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.29](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.28...@bloom-housing/partners@4.0.1-alpha.29) (2022-01-24) + + +### Bug Fixes + +* ami charts without all households ([#2430](https://github.com/bloom-housing/bloom/issues/2430)) ([92dfbad](https://github.com/bloom-housing/bloom/commit/92dfbad32c90d84ee1ec3a3468c084cb110aa8be)) + + + + + +## [4.0.1-alpha.28](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.27...@bloom-housing/partners@4.0.1-alpha.28) (2022-01-21) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.27](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.26...@bloom-housing/partners@4.0.1-alpha.27) (2022-01-21) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.26](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.25...@bloom-housing/partners@4.0.1-alpha.26) (2022-01-20) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.25](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.24...@bloom-housing/partners@4.0.1-alpha.25) (2022-01-14) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@3.0.1-alpha.34...@bloom-housing/partners@3.0.1) (2022-01-13) + + +### Bug Fixes + +* adds jurisdictionId to useSWR path ([c7d6adb](https://github.com/seanmalbert/bloom/commit/c7d6adba109aa50f3c1556c89c0ec714fd4c6e50)) +* cannot save custom mailing, dropoff, or pickup address ([edcb068](https://github.com/seanmalbert/bloom/commit/edcb068ca23411e0a34f1dc2ff4c77ab489ac0fc)) +* listings management keep empty strings, remove empty objects ([3aba274](https://github.com/seanmalbert/bloom/commit/3aba274a751cdb2db55b65ade1cda5d1689ca681)) +* lottery results uploads now save ([8c9dd0f](https://github.com/seanmalbert/bloom/commit/8c9dd0f043dd3835f12bc8f087b9a5519cbfd4f8)) +* paper application submission ([384b86b](https://github.com/seanmalbert/bloom/commit/384b86b624392012b56039dc4a289393f24653f5)) +* remove description for the partners programs ([d4478b8](https://github.com/seanmalbert/bloom/commit/d4478b8eaf68efdf4b23c55f15656e82a907dbc4)) +* removes Duplicate identifier fieldGroupObjectToArray ([a3a2f43](https://github.com/seanmalbert/bloom/commit/a3a2f434606628e4ad141250c401405ced10cdf4)) +* retnal assistance eror message ([09f583b](https://github.com/seanmalbert/bloom/commit/09f583be137336c92f7077beb1f1fbab2b82aefb)) +* updates lastName on application save ([a977ffd](https://github.com/seanmalbert/bloom/commit/a977ffd4b81fbf09122c51ccf066d0a3f3f6544c)) +* versioning issues ([#2311](https://github.com/seanmalbert/bloom/issues/2311)) ([c274a29](https://github.com/seanmalbert/bloom/commit/c274a2985061b389c2cae6386137a4caacd7f7c0)) + + +* Release 11 11 21 (#2162) ([4847469](https://github.com/seanmalbert/bloom/commit/484746982e440c1c1c87c85089d86cd5968f1cae)), closes [#2162](https://github.com/seanmalbert/bloom/issues/2162) + + + +### Features + +* adds listing management cypress tests to partner portal ([2e37eec](https://github.com/seanmalbert/bloom/commit/2e37eecf6344f6e25422a24ad7f4563fee4564de)) +* adds updating open listing modal ([#2288](https://github.com/seanmalbert/bloom/issues/2288)) ([d184326](https://github.com/seanmalbert/bloom/commit/d18432610a55a5e54f567ff6157bb863ed61cb21)) +* ami chart jurisdictionalized ([b2e2537](https://github.com/seanmalbert/bloom/commit/b2e2537818d92ff41ea51fbbeb23d9d7e8c1cf52)) +* filter partner users ([3dd8f9b](https://github.com/seanmalbert/bloom/commit/3dd8f9b3cc1f9f90916d49b7136d5f1f73df5291)) +* new demographics sub-race questions ([910df6a](https://github.com/seanmalbert/bloom/commit/910df6ad3985980becdc2798076ed5dfeeb310b5)) +* one month rent ([319743d](https://github.com/seanmalbert/bloom/commit/319743d23268f5b55e129c0878510edb4204b668)) +* overrides fallback to english, tagalog support ([b79fd10](https://github.com/seanmalbert/bloom/commit/b79fd1018619f618bd9be8e870d35c1180b81dfb)) +* postmark date time fields partners ([#2239](https://github.com/seanmalbert/bloom/issues/2239)) ([cf20b88](https://github.com/seanmalbert/bloom/commit/cf20b88cb613b815c641cad34a38908e22722a4a)) +* simplify Waitlist component and use more flexible schema ([aa8e006](https://github.com/seanmalbert/bloom/commit/aa8e00616d886e8d57316b2362d35c0c550007c6)) + + +### Reverts + +* Revert "chore(release): version" ([47a2c67](https://github.com/seanmalbert/bloom/commit/47a2c67af5c7c41f360fafc6c5386476866ea403)) +* Revert "chore: removes application program partners" ([91e22d8](https://github.com/seanmalbert/bloom/commit/91e22d891104e8d4fc024d709a6a14cec1400733)) +* Revert "chore: removes application program display" ([740cf00](https://github.com/seanmalbert/bloom/commit/740cf00dc3a729eed037d56a8dfc5988decd2651)) + + + +### BREAKING CHANGES + +* preferences model and relationships changed + +* feat: feat(backend): extend UserUpdateDto to support email change + +picked from dev 3e1fdbd0ea91d4773973d5c485a5ba61303db90a + +* fix: 2056/user account edit fix + +picked from dev a15618c0cb548ff5b2ae913b802c9e08bb673f30 + +* refactor: 2085/adds top level catchAll exception filter + +picked from dev aeaa63d1af1fa3d11671e169cb3bd23d356fface + +* feat: feat: Change unit number field type to text + +picked from dev f54be7c7ba6aac8e00fee610dc86584b60cc212d + +* feat(backend): improve application flagged set saving efficiency + +* fix: fix: updates address order + +picked from dev 252e014dcbd2e4c305384ed552135f5a8e4e4767 + +* fix: sets programs to optoinal and updates versions + +* chore: chore(deps): bump electron from 13.1.7 to 13.3.0 + +* chore: chore(deps): bump axios from 0.21.1 to 0.21.2 + +* fix: adds programs service + +* fix: fix lisitng e2e tests + +* fix: fix member tests + + + + + +## [4.0.1-alpha.24](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.23...@bloom-housing/partners@4.0.1-alpha.24) (2022-01-13) + + +### Bug Fixes + +* partners render issue ([#2395](https://github.com/bloom-housing/bloom/issues/2395)) ([7fb108d](https://github.com/bloom-housing/bloom/commit/7fb108d744fcafd6b9df42706d2a2f58fbc30f0a)) + + + + + +## [4.0.1-alpha.23](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.22...@bloom-housing/partners@4.0.1-alpha.23) (2022-01-13) + + +### Bug Fixes + +* dates showing as invalid in send by mail section ([#2362](https://github.com/bloom-housing/bloom/issues/2362)) ([3567388](https://github.com/bloom-housing/bloom/commit/35673882d87e2b524b2c94d1fb7b40c9d777f0a3)) + + +### BREAKING CHANGES + +* remove applicationDueTime field and consolidated into applicationDueDate + + + + + +## [4.0.1-alpha.22](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.21...@bloom-housing/partners@4.0.1-alpha.22) (2022-01-13) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.21](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.20...@bloom-housing/partners@4.0.1-alpha.21) (2022-01-11) + + +### Bug Fixes + +* open house events can now be edited and work cross-browser ([#2320](https://github.com/bloom-housing/bloom/issues/2320)) ([4af6efd](https://github.com/bloom-housing/bloom/commit/4af6efdd29787a93faf1a314073e2e201584214f)) + + + + + +## [4.0.1-alpha.20](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.19...@bloom-housing/partners@4.0.1-alpha.20) (2022-01-11) + + +### Bug Fixes + +* add test id ([d4b0ed2](https://github.com/bloom-housing/bloom/commit/d4b0ed2426b7f8aced3b2dd44baf2a438410838d)) +* update naming ([b9c645c](https://github.com/bloom-housing/bloom/commit/b9c645cd1460567f9c35a54c7fd93bc5957d593e)) +* use drag n drop ([a354904](https://github.com/bloom-housing/bloom/commit/a3549045d4f0da64692318f84f0336f1287ad48a)) + + + + + +## [4.0.1-alpha.19](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.18...@bloom-housing/partners@4.0.1-alpha.19) (2022-01-08) + + +### Bug Fixes + +* ensure dayjs parsing strings will work as expected ([eb44939](https://github.com/bloom-housing/bloom/commit/eb449395ebea3a3b4b58eb217df1e1313c722a0d)) + + + + + +## [4.0.1-alpha.18](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.17...@bloom-housing/partners@4.0.1-alpha.18) (2022-01-07) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.17](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.16...@bloom-housing/partners@4.0.1-alpha.17) (2022-01-07) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.16](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.15...@bloom-housing/partners@4.0.1-alpha.16) (2022-01-07) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.15](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.14...@bloom-housing/partners@4.0.1-alpha.15) (2022-01-04) + + +### Bug Fixes + +* applications drop off address ([d73cdf6](https://github.com/bloom-housing/bloom/commit/d73cdf69fa550bf178a7f433ca9a1bbe2ce678a2)) +* helper imports of form types ([e58ed71](https://github.com/bloom-housing/bloom/commit/e58ed71c703a53a7ab4284e6d7e2c1857cb8ed7b)) +* jest tests in partners now allows jsx ([3fef534](https://github.com/bloom-housing/bloom/commit/3fef534925f8fee6a63a2a99def692b59df83bdd)) + + + + + +## [4.0.1-alpha.14](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.13...@bloom-housing/partners@4.0.1-alpha.14) (2022-01-04) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.13](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.12...@bloom-housing/partners@4.0.1-alpha.13) (2022-01-04) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.12](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.11...@bloom-housing/partners@4.0.1-alpha.12) (2022-01-04) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.11](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.10...@bloom-housing/partners@4.0.1-alpha.11) (2022-01-03) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.10](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.9...@bloom-housing/partners@4.0.1-alpha.10) (2022-01-03) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.8...@bloom-housing/partners@4.0.1-alpha.9) (2022-01-03) + + +### Bug Fixes + +* cypress coverage configs ([eec74ee](https://github.com/bloom-housing/bloom/commit/eec74eef138f6af275ae3cfe16262ed215b16907)) + + + + + +## [4.0.1-alpha.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.1-alpha.7...@bloom-housing/partners@4.0.1-alpha.8) (2022-01-03) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [4.0.1-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.0...@bloom-housing/partners@4.0.1-alpha.7) (2022-01-03) + + +### Bug Fixes + +* bump version ([#2349](https://github.com/bloom-housing/bloom/issues/2349)) ([b9e3ba1](https://github.com/bloom-housing/bloom/commit/b9e3ba10aebd6534090f8be231a9ea77b3c929b6)) +* bump version ([#2350](https://github.com/bloom-housing/bloom/issues/2350)) ([05863f5](https://github.com/bloom-housing/bloom/commit/05863f55f3939bea4387bd7cf4eb1f34df106124)) + + +* 2227/lock login attempts frontend (#2260) ([281ea43](https://github.com/bloom-housing/bloom/commit/281ea435e618a73a73f233a7a494f961fbac8fa2)), closes [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) + + +### Features + +* **backend:** add user password expiration ([107c2f0](https://github.com/bloom-housing/bloom/commit/107c2f06e2f8367b52cb7cc8f00e6d9aef751fe0)) +* password reset message ([0cba6e6](https://github.com/bloom-housing/bloom/commit/0cba6e62b45622a430612672daef5c97c1e6b140)) + + +### BREAKING CHANGES + +* sign-in pages have been updated + + + + + +## [4.0.1-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@4.0.0...@bloom-housing/partners@4.0.1-alpha.6) (2022-01-03) + + +### Bug Fixes + +* bump version ([#2349](https://github.com/bloom-housing/bloom/issues/2349)) ([b9e3ba1](https://github.com/bloom-housing/bloom/commit/b9e3ba10aebd6534090f8be231a9ea77b3c929b6)) +* bump version ([#2350](https://github.com/bloom-housing/bloom/issues/2350)) ([05863f5](https://github.com/bloom-housing/bloom/commit/05863f55f3939bea4387bd7cf4eb1f34df106124)) + + +* 2227/lock login attempts frontend (#2260) ([281ea43](https://github.com/bloom-housing/bloom/commit/281ea435e618a73a73f233a7a494f961fbac8fa2)), closes [#2260](https://github.com/bloom-housing/bloom/issues/2260) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) [#1927](https://github.com/bloom-housing/bloom/issues/1927) + + +### Features + +* **backend:** add user password expiration ([107c2f0](https://github.com/bloom-housing/bloom/commit/107c2f06e2f8367b52cb7cc8f00e6d9aef751fe0)) +* password reset message ([0cba6e6](https://github.com/bloom-housing/bloom/commit/0cba6e62b45622a430612672daef5c97c1e6b140)) + + +### BREAKING CHANGES + +* sign-in pages have been updated + + + + + +## [4.0.1-alpha.1](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.0.0...@bloom-housing/partners@4.0.1-alpha.1) (2021-12-23) + + +* 2227/lock login attempts frontend (#2260) ([281ea43](https://github.com/seanmalbert/bloom/commit/281ea435e618a73a73f233a7a494f961fbac8fa2)), closes [#2260](https://github.com/seanmalbert/bloom/issues/2260) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) + + +### BREAKING CHANGES + +* sign-in pages have been updated + + + + + +## [4.0.1-alpha.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@4.0.0...@bloom-housing/partners@4.0.1-alpha.0) (2021-12-23) + + +* 2227/lock login attempts frontend (#2260) ([281ea43](https://github.com/seanmalbert/bloom/commit/281ea435e618a73a73f233a7a494f961fbac8fa2)), closes [#2260](https://github.com/seanmalbert/bloom/issues/2260) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) [#1927](https://github.com/seanmalbert/bloom/issues/1927) + + +### BREAKING CHANGES + +* sign-in pages have been updated + + + + + +# [4.0.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@3.0.1-alpha.65...@bloom-housing/partners@4.0.0) (2021-12-22) + + +### Bug Fixes + +* partners shared-helpers version ([50f4a06](https://github.com/seanmalbert/bloom/commit/50f4a0658761b8675064a98f316b2dd35b0d3fe0)) + + +### Code Refactoring + +* removing helpers from ui-components that are backend dependent ([#2108](https://github.com/seanmalbert/bloom/issues/2108)) ([1d0c1f3](https://github.com/seanmalbert/bloom/commit/1d0c1f340781a3ba76c89462d8bee954dd40b889)) + + +### Features + +* adds updating open listing modal ([#2288](https://github.com/seanmalbert/bloom/issues/2288)) ([4f6945f](https://github.com/seanmalbert/bloom/commit/4f6945f04d797fad1b3140bcdc74b134ea42810a)) + + +### BREAKING CHANGES + +* moved some helpers from ui-components to shared-helpers + + + + + +## [3.0.1-alpha.65](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.64...@bloom-housing/partners@3.0.1-alpha.65) (2021-12-15) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.64](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.63...@bloom-housing/partners@3.0.1-alpha.64) (2021-12-15) + + +### Features + +* filter partner users ([63566f2](https://github.com/bloom-housing/bloom/commit/63566f206b154031a143b649b986aaecd5181313)) + + + + + +## [3.0.1-alpha.63](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.62...@bloom-housing/partners@3.0.1-alpha.63) (2021-12-15) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.62](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.61...@bloom-housing/partners@3.0.1-alpha.62) (2021-12-15) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.61](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.60...@bloom-housing/partners@3.0.1-alpha.61) (2021-12-14) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.60](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.59...@bloom-housing/partners@3.0.1-alpha.60) (2021-12-14) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.59](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.58...@bloom-housing/partners@3.0.1-alpha.59) (2021-12-13) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.58](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.56...@bloom-housing/partners@3.0.1-alpha.58) (2021-12-13) + + +### Bug Fixes + +* versioning issues ([#2311](https://github.com/bloom-housing/bloom/issues/2311)) ([0b1d143](https://github.com/bloom-housing/bloom/commit/0b1d143ab8b17add9d52533560f28d7a1f6dfd3d)) + + + + + +## [3.0.1-alpha.56](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.55...@bloom-housing/partners@3.0.1-alpha.56) (2021-12-10) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.55](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.54...@bloom-housing/partners@3.0.1-alpha.55) (2021-12-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.54](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.53...@bloom-housing/partners@3.0.1-alpha.54) (2021-12-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.53](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.52...@bloom-housing/partners@3.0.1-alpha.53) (2021-12-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.52](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.51...@bloom-housing/partners@3.0.1-alpha.52) (2021-12-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.51](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.50...@bloom-housing/partners@3.0.1-alpha.51) (2021-12-08) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.50](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.49...@bloom-housing/partners@3.0.1-alpha.50) (2021-12-07) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.49](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.48...@bloom-housing/partners@3.0.1-alpha.49) (2021-12-07) + + +### Features + +* overrides fallback to english, tagalog support ([#2262](https://github.com/bloom-housing/bloom/issues/2262)) ([679ab9b](https://github.com/bloom-housing/bloom/commit/679ab9b1816d5934f48f02ca5f5696952ef88ae7)) + + + + + +## [3.0.1-alpha.48](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.47...@bloom-housing/partners@3.0.1-alpha.48) (2021-12-07) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.47](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.46...@bloom-housing/partners@3.0.1-alpha.47) (2021-12-06) + + +### Bug Fixes + +* Remove description for the partners programs ([#2234](https://github.com/bloom-housing/bloom/issues/2234)) ([2bbbeb5](https://github.com/bloom-housing/bloom/commit/2bbbeb52868d8f4b5ee6723018fa34619073017b)), closes [#1901](https://github.com/bloom-housing/bloom/issues/1901) + + + + + +## [3.0.1-alpha.46](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.45...@bloom-housing/partners@3.0.1-alpha.46) (2021-12-06) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.45](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.44...@bloom-housing/partners@3.0.1-alpha.45) (2021-12-03) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.44](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.43...@bloom-housing/partners@3.0.1-alpha.44) (2021-12-03) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.43](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.42...@bloom-housing/partners@3.0.1-alpha.43) (2021-12-03) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.42](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.41...@bloom-housing/partners@3.0.1-alpha.42) (2021-12-01) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.41](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.40...@bloom-housing/partners@3.0.1-alpha.41) (2021-12-01) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.40](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.39...@bloom-housing/partners@3.0.1-alpha.40) (2021-12-01) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.39](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.38...@bloom-housing/partners@3.0.1-alpha.39) (2021-11-30) + + +### Bug Fixes + +* lottery results uploads now save ([#2226](https://github.com/bloom-housing/bloom/issues/2226)) ([8964bba](https://github.com/bloom-housing/bloom/commit/8964bba2deddbd077a049649c26f6fe8b576ed2f)) + + + + + +## [3.0.1-alpha.38](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.37...@bloom-housing/partners@3.0.1-alpha.38) (2021-11-30) + + +### Bug Fixes + +* **backend:** nginx with heroku configuration ([#2196](https://github.com/bloom-housing/bloom/issues/2196)) ([a1e2630](https://github.com/bloom-housing/bloom/commit/a1e26303bdd660b9ac267da55dc8d09661216f1c)) + + + + + +## [3.0.1-alpha.37](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.36...@bloom-housing/partners@3.0.1-alpha.37) (2021-11-29) + + +### Bug Fixes + +* feedback on the waitlist data and display ([9432542](https://github.com/bloom-housing/bloom/commit/9432542efd9ba2e4bf8dd7195895e75f5d2e0623)) + + +### Features + +* simplify Waitlist component and use more flexible schema ([96df149](https://github.com/bloom-housing/bloom/commit/96df1496f377ddfa6f0e6c016c84954b6a43ff4a)) + + + + + +## [3.0.1-alpha.36](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.35...@bloom-housing/partners@3.0.1-alpha.36) (2021-11-29) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.35](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.34...@bloom-housing/partners@3.0.1-alpha.35) (2021-11-29) + + +### Bug Fixes + +* cannot save custom mailing, dropoff, or pickup address ([#2207](https://github.com/bloom-housing/bloom/issues/2207)) ([96484b5](https://github.com/bloom-housing/bloom/commit/96484b5676ecb000e492851ee12766ba9e6cd86f)) + + + + + +## [3.0.1-alpha.34](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.33...@bloom-housing/partners@3.0.1-alpha.34) (2021-11-23) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.33](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.32...@bloom-housing/partners@3.0.1-alpha.33) (2021-11-23) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.32](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.31...@bloom-housing/partners@3.0.1-alpha.32) (2021-11-23) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.31](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.30...@bloom-housing/partners@3.0.1-alpha.31) (2021-11-23) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.30](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.29...@bloom-housing/partners@3.0.1-alpha.30) (2021-11-23) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.29](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.28...@bloom-housing/partners@3.0.1-alpha.29) (2021-11-23) + + +### Features + +* new demographics sub-race questions ([#2109](https://github.com/bloom-housing/bloom/issues/2109)) ([9ab8926](https://github.com/bloom-housing/bloom/commit/9ab892694c1ad2fa8890b411b3b32af68ade1fc3)) + + + + + +## [3.0.1-alpha.28](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.27...@bloom-housing/partners@3.0.1-alpha.28) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.27](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.26...@bloom-housing/partners@3.0.1-alpha.27) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.26](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.25...@bloom-housing/partners@3.0.1-alpha.26) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.25](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.24...@bloom-housing/partners@3.0.1-alpha.25) (2021-11-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.24](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.23...@bloom-housing/partners@3.0.1-alpha.24) (2021-11-17) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.23](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.22...@bloom-housing/partners@3.0.1-alpha.23) (2021-11-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.22](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.21...@bloom-housing/partners@3.0.1-alpha.22) (2021-11-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.21](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.20...@bloom-housing/partners@3.0.1-alpha.21) (2021-11-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.20](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.19...@bloom-housing/partners@3.0.1-alpha.20) (2021-11-16) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.19](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.18...@bloom-housing/partners@3.0.1-alpha.19) (2021-11-15) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.18](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.17...@bloom-housing/partners@3.0.1-alpha.18) (2021-11-15) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.17](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.16...@bloom-housing/partners@3.0.1-alpha.17) (2021-11-15) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.16](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.15...@bloom-housing/partners@3.0.1-alpha.16) (2021-11-12) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.15](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.14...@bloom-housing/partners@3.0.1-alpha.15) (2021-11-12) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.14](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.13...@bloom-housing/partners@3.0.1-alpha.14) (2021-11-12) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.13](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.12...@bloom-housing/partners@3.0.1-alpha.13) (2021-11-11) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.12](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.11...@bloom-housing/partners@3.0.1-alpha.12) (2021-11-11) + + +### Bug Fixes + +* fixes program, preference, ami-chart de-dupe ([#2169](https://github.com/bloom-housing/bloom/issues/2169)) ([3530757](https://github.com/bloom-housing/bloom/commit/35307575bd78f4a0ceee03ae21f07a61e9018bba)) + + + + + +## [3.0.1-alpha.11](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.10...@bloom-housing/partners@3.0.1-alpha.11) (2021-11-10) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.10](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.9...@bloom-housing/partners@3.0.1-alpha.10) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.8...@bloom-housing/partners@3.0.1-alpha.9) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.7...@bloom-housing/partners@3.0.1-alpha.8) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.6...@bloom-housing/partners@3.0.1-alpha.7) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.5...@bloom-housing/partners@3.0.1-alpha.6) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.4...@bloom-housing/partners@3.0.1-alpha.5) (2021-11-09) + + +### Features + +* Change unit number field type to text ([#2136](https://github.com/bloom-housing/bloom/issues/2136)) ([f54be7c](https://github.com/bloom-housing/bloom/commit/f54be7c7ba6aac8e00fee610dc86584b60cc212d)) + + + + + +## [3.0.1-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.3...@bloom-housing/partners@3.0.1-alpha.4) (2021-11-09) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.2...@bloom-housing/partners@3.0.1-alpha.3) (2021-11-08) + + +### Features + +* add Programs section to listings management ([#2093](https://github.com/bloom-housing/bloom/issues/2093)) ([9bd1fe1](https://github.com/bloom-housing/bloom/commit/9bd1fe1033dee0fb7e73756254474471bc304f5e)) + + + + + +## [3.0.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.1...@bloom-housing/partners@3.0.1-alpha.2) (2021-11-08) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.1-alpha.0...@bloom-housing/partners@3.0.1-alpha.1) (2021-11-08) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [3.0.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@3.0.0...@bloom-housing/partners@3.0.1-alpha.0) (2021-11-05) + + +* 1837/preferences cleanup 3 (#2144) ([3ce6d5e](https://github.com/bloom-housing/bloom/commit/3ce6d5eb5aac49431ec5bf4912dbfcbe9077d84e)), closes [#2144](https://github.com/bloom-housing/bloom/issues/2144) + + +### BREAKING CHANGES + +* Preferences are now M-N relation with a listing and have an intermediate table with ordinal number + +* refactor(backend): preferences deduplication + +So far each listing referenced it's own unique Preferences. This change introduces Many to Many +relationship between Preference and Listing entity and forces sharing Preferences between listings. + +* feat(backend): extend preferences migration with moving existing relations to a new intermediate tab + +* feat(backend): add Preference - Jurisdiction ManyToMany relation + +* feat: adapt frontend to backend changes + +* fix(backend): typeORM preferences select statement + +* fix(backend): connect preferences with jurisdictions in seeds, fix pref filter validator + +* fix(backend): fix missing import in preferences-filter-params.ts + +* refactor: rebase issue + +* feat: uptake jurisdictional preferences + +* fix: fixup tests + +* fix: application preferences ignore page, always separate + +* Remove page from src/migration/1633359409242-add-listing-preferences-intermediate-relation.ts + +* fix: preference fetching and ordering/pages + +* Fix code style issues with Prettier + +* fix(backend): query User__leasingAgentInListings__jurisdiction_User__leasingAgentIn specified more + +* fix: perferences cypress tests + +Co-authored-by: Michal Plebanski +Co-authored-by: Emily Jablonski +Co-authored-by: Lint Action + + + + + +# [3.0.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@2.0.1-alpha.9...@bloom-housing/partners@3.0.0) (2021-11-05) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [2.0.1-alpha.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.1-alpha.8...@bloom-housing/partners@2.0.1-alpha.9) (2021-11-05) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [2.0.1-alpha.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.1-alpha.7...@bloom-housing/partners@2.0.1-alpha.8) (2021-11-04) + + +### Reverts + +* Revert "refactor: listing preferences and adds jurisdictional filtering" ([41f72c0](https://github.com/bloom-housing/bloom/commit/41f72c0db49cf94d7930f5cfc88f6ee9d6040986)) + + + + + +## [2.0.1-alpha.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.1-alpha.6...@bloom-housing/partners@2.0.1-alpha.7) (2021-11-04) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [2.0.1-alpha.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.1-alpha.5...@bloom-housing/partners@2.0.1-alpha.6) (2021-11-04) + + +### Features + +* Updates application confirmation numbers ([#2072](https://github.com/bloom-housing/bloom/issues/2072)) ([75cd67b](https://github.com/bloom-housing/bloom/commit/75cd67bcb62280936bdeeaee8c9b7b2583a1339d)) + + + + + +## [2.0.1-alpha.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.1-alpha.4...@bloom-housing/partners@2.0.1-alpha.5) (2021-11-03) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [2.0.1-alpha.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.1-alpha.3...@bloom-housing/partners@2.0.1-alpha.4) (2021-11-03) + + +### Bug Fixes + +* don't send email confirmation on paper app submission ([#2110](https://github.com/bloom-housing/bloom/issues/2110)) ([7f83b70](https://github.com/bloom-housing/bloom/commit/7f83b70327049245ecfba04ae3aea4e967929b2a)) + + + + + +## [2.0.1-alpha.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.1-alpha.2...@bloom-housing/partners@2.0.1-alpha.3) (2021-11-03) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [2.0.1-alpha.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.1-alpha.1...@bloom-housing/partners@2.0.1-alpha.2) (2021-11-02) + + +### Features + +* two new common app questions - Household Changes and Household Student ([#2070](https://github.com/bloom-housing/bloom/issues/2070)) ([42a752e](https://github.com/bloom-housing/bloom/commit/42a752ec073c0f5b65374c7a68da1e34b0b1c949)) + + + + + +## [2.0.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.1-alpha.0...@bloom-housing/partners@2.0.1-alpha.1) (2021-11-02) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +## [2.0.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0...@bloom-housing/partners@2.0.1-alpha.0) (2021-11-02) + + +### Bug Fixes + +* Updates lastName on application save ([aff87ec](https://github.com/bloom-housing/bloom/commit/aff87ec99ad2fbd4a1f9a6853157ea7770f85a56)) + + + + + +# [2.0.0](https://github.com/seanmalbert/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.26...@bloom-housing/partners@2.0.0) (2021-11-02) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.26](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.25...@bloom-housing/partners@2.0.0-pre-tailwind.26) (2021-11-02) + + +### Code Refactoring + +* listing preferences and adds jurisdictional filtering ([9f661b4](https://github.com/bloom-housing/bloom/commit/9f661b43921ec939bd1bf5709c934ad6f56dd859)) + + +### BREAKING CHANGES + +* updates preference relationship with listings + + + + + +# [2.0.0-pre-tailwind.25](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.24...@bloom-housing/partners@2.0.0-pre-tailwind.25) (2021-11-01) + + +### Bug Fixes + +* reverts preferences to re-add as breaking/major bump ([4f7d893](https://github.com/bloom-housing/bloom/commit/4f7d89327361b3b28b368c23cfd24e6e8123a0a8)) + + + + + +# [2.0.0-pre-tailwind.24](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.23...@bloom-housing/partners@2.0.0-pre-tailwind.24) (2021-10-30) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.23](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.22...@bloom-housing/partners@2.0.0-pre-tailwind.23) (2021-10-30) + + +* Preferences cleanup (#1947) ([7329a58](https://github.com/bloom-housing/bloom/commit/7329a58cc9242faf647459e46de1e3cff3fe9c9d)), closes [#1947](https://github.com/bloom-housing/bloom/issues/1947) + + +### BREAKING CHANGES + +* Preferences are now M-N relation with a listing and have an intermediate table with ordinal number + +* refactor(backend): preferences deduplication + +So far each listing referenced it's own unique Preferences. This change introduces Many to Many +relationship between Preference and Listing entity and forces sharing Preferences between listings. + +* feat(backend): extend preferences migration with moving existing relations to a new intermediate tab + +* feat(backend): add Preference - Jurisdiction ManyToMany relation + +* feat: adapt frontend to backend changes + +* fix(backend): typeORM preferences select statement + +* fix(backend): connect preferences with jurisdictions in seeds, fix pref filter validator + +* fix(backend): fix missing import in preferences-filter-params.ts + +* refactor: rebase issue + +* feat: uptake jurisdictional preferences + +* fix: fixup tests + +* fix: application preferences ignore page, always separate + +* Remove page from src/migration/1633359409242-add-listing-preferences-intermediate-relation.ts + +* fix: preference fetching and ordering/pages + +* Fix code style issues with Prettier + +* fix(backend): query User__leasingAgentInListings__jurisdiction_User__leasingAgentIn specified more + +* fix: perferences cypress tests + +Co-authored-by: Emily Jablonski +Co-authored-by: Sean Albert +Co-authored-by: Lint Action + + + + + +# [2.0.0-pre-tailwind.22](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.21...@bloom-housing/partners@2.0.0-pre-tailwind.22) (2021-10-29) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.21](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.20...@bloom-housing/partners@2.0.0-pre-tailwind.21) (2021-10-29) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.20](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.19...@bloom-housing/partners@2.0.0-pre-tailwind.20) (2021-10-29) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.19](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.18...@bloom-housing/partners@2.0.0-pre-tailwind.19) (2021-10-29) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.18](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.17...@bloom-housing/partners@2.0.0-pre-tailwind.18) (2021-10-29) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.17](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.16...@bloom-housing/partners@2.0.0-pre-tailwind.17) (2021-10-29) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.16](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.15...@bloom-housing/partners@2.0.0-pre-tailwind.16) (2021-10-28) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.15](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.14...@bloom-housing/partners@2.0.0-pre-tailwind.15) (2021-10-28) + + +### Bug Fixes + +* in listings management keep empty strings, remove empty objects ([#2064](https://github.com/bloom-housing/bloom/issues/2064)) ([c4b1e83](https://github.com/bloom-housing/bloom/commit/c4b1e833ec128f457015ac7ffa421ee6047083d9)) + + + + + +# [2.0.0-pre-tailwind.14](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.13...@bloom-housing/partners@2.0.0-pre-tailwind.14) (2021-10-27) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.13](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.12...@bloom-housing/partners@2.0.0-pre-tailwind.13) (2021-10-26) + + +### Bug Fixes + +* Incorrect listing status ([#2015](https://github.com/bloom-housing/bloom/issues/2015)) ([48aa14e](https://github.com/bloom-housing/bloom/commit/48aa14eb522cb8e4d0a25fdeadcc392b30d7f1a9)) + + + + + +# [2.0.0-pre-tailwind.12](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.11...@bloom-housing/partners@2.0.0-pre-tailwind.12) (2021-10-25) + + +### Bug Fixes + +* duplicate unit during Copy & New and Save & New ([#1963](https://github.com/bloom-housing/bloom/issues/1963)) ([d597a3f](https://github.com/bloom-housing/bloom/commit/d597a3f57ed4c489804e10e3b6bac99e5f9bedcc)) + + + + + +# [2.0.0-pre-tailwind.11](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.10...@bloom-housing/partners@2.0.0-pre-tailwind.11) (2021-10-25) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.10](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.9...@bloom-housing/partners@2.0.0-pre-tailwind.10) (2021-10-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.9](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.8...@bloom-housing/partners@2.0.0-pre-tailwind.9) (2021-10-22) + + +### Bug Fixes + +* makes listing programs optional ([fbe7134](https://github.com/bloom-housing/bloom/commit/fbe7134348e59e3fdb86663cfdca7648655e7b4b)) + + + + + +# [2.0.0-pre-tailwind.8](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.7...@bloom-housing/partners@2.0.0-pre-tailwind.8) (2021-10-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.7](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.6...@bloom-housing/partners@2.0.0-pre-tailwind.7) (2021-10-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.6](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.5...@bloom-housing/partners@2.0.0-pre-tailwind.6) (2021-10-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.5](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.4...@bloom-housing/partners@2.0.0-pre-tailwind.5) (2021-10-22) + + +### Bug Fixes + +* do not show login required on forgot password page ([6578dda](https://github.com/bloom-housing/bloom/commit/6578dda1db68b9d63058900ae7e847f7b7021912)) + + + + + +# [2.0.0-pre-tailwind.4](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.3...@bloom-housing/partners@2.0.0-pre-tailwind.4) (2021-10-22) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.3](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.2...@bloom-housing/partners@2.0.0-pre-tailwind.3) (2021-10-21) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.2](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.1...@bloom-housing/partners@2.0.0-pre-tailwind.2) (2021-10-21) + +**Note:** Version bump only for package @bloom-housing/partners + + + + + +# [2.0.0-pre-tailwind.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/partners@2.0.0-pre-tailwind.0...@bloom-housing/partners@2.0.0-pre-tailwind.1) (2021-10-19) + +### Bug Fixes + +- Modals no longer prevent scroll after being closed ([#1962](https://github.com/bloom-housing/bloom/issues/1962)) ([667d5d3](https://github.com/bloom-housing/bloom/commit/667d5d3234c9a463c947c99d8c47acb9ac963e95)) +- Remove shared-helpers dependency from ui-components ([#2032](https://github.com/bloom-housing/bloom/issues/2032)) ([dba201f](https://github.com/bloom-housing/bloom/commit/dba201fa62523c59fc160addab793a7eac20609f)) + +# 2.0.0-pre-tailwind.0 (2021-10-19) + +### Bug Fixes + +- Adds applicationDueDate and Time to removeEmptyFields keysToIgnore ([f024427](https://github.com/bloom-housing/bloom/commit/f024427d4cd2039429e0d9e5db67a50011b5356e)) +- Ami chart values were not appearing after save and new ([#1952](https://github.com/bloom-housing/bloom/issues/1952)) ([cb65340](https://github.com/bloom-housing/bloom/commit/cb653409c8d2c403e1fbfa6ea71415b98af21455)) +- **backend:** unitCreate and UnitUpdateDto now require only IdDto for… ([#1956](https://github.com/bloom-housing/bloom/issues/1956)) ([43dcfbe](https://github.com/bloom-housing/bloom/commit/43dcfbe7493bdd654d7b898ed9650804a016065c)), closes [#1897](https://github.com/bloom-housing/bloom/issues/1897) +- Aan remove application pick up and drop off addresses ([#1954](https://github.com/bloom-housing/bloom/issues/1954)) ([68238ce](https://github.com/bloom-housing/bloom/commit/68238ce87968345d4a8b1a0308a1a70295174675)) +- Improved UX for the Building Selection Criteria drawer ([#1994](https://github.com/bloom-housing/bloom/issues/1994)) ([4bd8b09](https://github.com/bloom-housing/bloom/commit/4bd8b09456b54584c3731bcca64019dc231d0c55)) +- Removes 150 char limit on textarea fields ([6eb7036](https://github.com/bloom-housing/bloom/commit/6eb70364409c5910aa9b8277b37a8214c2a94358)) +- Removes duplicate unitStatusOptions from UnitForm ([d3e71c5](https://github.com/bloom-housing/bloom/commit/d3e71c5dcc40b154f211b16ad3a1d1abac05ebae)) +- Reponsive TW grid classes, nested overlays ([#1881](https://github.com/bloom-housing/bloom/issues/1881)) ([620ed1f](https://github.com/bloom-housing/bloom/commit/620ed1fbbf0466336a53ea233cdb0c3984eeda15)) +- Typo in the paper applications table ([#1965](https://github.com/bloom-housing/bloom/issues/1965)) ([a342772](https://github.com/bloom-housing/bloom/commit/a3427723cbaeb3282abbaa78ae61a69262b5d71c)) +- Visual QA on SiteHeader ([#2010](https://github.com/bloom-housing/bloom/issues/2010)) ([ce86277](https://github.com/bloom-housing/bloom/commit/ce86277d451d83630ba79e89dfb8ad9c4b69bdae)) + +### chore + +- Add new `shared-helpers` package ([#1911](https://github.com/bloom-housing/bloom/issues/1911)) ([6e5d91b](https://github.com/bloom-housing/bloom/commit/6e5d91be5ccafd3d4b5bc1a578f2246a5e7f905b)) + +### Code Refactoring + +- Update textarea character limits ([#1906](https://github.com/bloom-housing/bloom/issues/1906)) ([96d362f](https://github.com/bloom-housing/bloom/commit/96d362f0e8740d255f298ef7505f4933982e270d)), closes [#1890](https://github.com/bloom-housing/bloom/issues/1890) + +### Features + +- Listings management draft and publish validation backend & frontend ([#1850](https://github.com/bloom-housing/bloom/issues/1850)) ([ef67997](https://github.com/bloom-housing/bloom/commit/ef67997a056c6f1f758d2fa67bf877d4a3d897ab)) +- Required labels on listings management fields ([#1924](https://github.com/bloom-housing/bloom/issues/1924)) ([0a2e2da](https://github.com/bloom-housing/bloom/commit/0a2e2da473938c510afbb7cd1ddcd2287813a972)) +- Show confirmation modal when publishing listings ([#1847](https://github.com/bloom-housing/bloom/issues/1847)) ([2de8062](https://github.com/bloom-housing/bloom/commit/2de80625ee9569f41f57debf04e2030829b6c969)), closes [#1772](https://github.com/bloom-housing/bloom/issues/1772) [#1772](https://github.com/bloom-housing/bloom/issues/1772) +- Support PDF uploads or webpage links for building selection criteria ([#1893](https://github.com/bloom-housing/bloom/issues/1893)) ([8514b43](https://github.com/bloom-housing/bloom/commit/8514b43ba337d33cb877ff468bf780ff47fdc772)) + +### Performance Improvements + +- Separates css imports and disables local purge ([#1883](https://github.com/bloom-housing/bloom/issues/1883)) ([668968e](https://github.com/bloom-housing/bloom/commit/668968e45072e9a5121af3cf32d0d8307c671907)), closes [#1882](https://github.com/bloom-housing/bloom/issues/1882) + +### Reverts + +- Revert "latest dev (#1999)" ([73a2789](https://github.com/bloom-housing/bloom/commit/73a2789d8f133f2d788e2399faa42b374d74ab15)), closes [#1999](https://github.com/bloom-housing/bloom/issues/1999) + +### BREAKING CHANGES + +- **backend:** POST/PUT /listings interface change +- Manually add totalFlagged until fixed +- Moves form keys out of ui-components +- Default limit on Textarea is 1000 now diff --git a/sites/partners/__tests__/components/listings/PaperListingForm/sections/DetailUnits.test.tsx b/sites/partners/__tests__/components/listings/PaperListingForm/sections/DetailUnits.test.tsx new file mode 100644 index 0000000000..ff8a16e919 --- /dev/null +++ b/sites/partners/__tests__/components/listings/PaperListingForm/sections/DetailUnits.test.tsx @@ -0,0 +1,72 @@ +import React from "react" +import { render, within } from "@testing-library/react" +import { DetailUnits } from "../../../../../src/components/listings/PaperListingDetails/sections/DetailUnits" +import { ListingContext } from "../../../../../src/components/listings/ListingContext" +import { listing } from "../../../../testHelpers" +import { ListingReviewOrder } from "@bloom-housing/backend-core" + +describe("DetailUnits", () => { + it("should render the detail units when no units exist", () => { + const results = render( + + + + ) + + // Above the table + expect(results.getByText("Listing Units")).toBeInTheDocument() + expect(results.getByText("Home Type")).toBeInTheDocument() + expect(results.getByText("Apartment")).toBeInTheDocument() + + // Table + expect(results.getByText("Unit Groups")).toBeInTheDocument() + expect(results.getByText("None")).toBeInTheDocument() + }) + + it("should render the detail units", () => { + const callUnitDrawer = jest.fn() + const results = render( + + + + ) + + // Above the table + expect(results.getByText("Listing Units")).toBeInTheDocument() + expect(results.getByText("Home Type")).toBeInTheDocument() + expect(results.getByText("Apartment")).toBeInTheDocument() + + // Table + const table = results.getByRole("table") + const headAndBody = within(table).getAllByRole("rowgroup") + expect(headAndBody).toHaveLength(2) + const [head, body] = headAndBody + expect(within(head).getAllByRole("columnheader")).toHaveLength(8) + const rows = within(body).getAllByRole("row") + expect(rows).toHaveLength(1) + // Validate first row + const [type, unitNumber, ami, rent, occupancy, sqft, bath, actions] = within( + rows[0] + ).getAllByRole("cell") + expect(type).toHaveTextContent("1 Bedroom") + expect(unitNumber).toBeEmptyDOMElement() + expect(ami).toBeEmptyDOMElement() + expect(rent).toBeEmptyDOMElement() + expect(occupancy).toHaveTextContent("1 - 3") + expect(sqft).toBeEmptyDOMElement() + expect(bath).toHaveTextContent("1 - 2") + expect(actions).toBeEmptyDOMElement() + }) +}) diff --git a/ui-components/__tests__/notifications/StatusAside.test.tsx b/sites/partners/__tests__/components/shared/StatusAside.test.tsx similarity index 89% rename from ui-components/__tests__/notifications/StatusAside.test.tsx rename to sites/partners/__tests__/components/shared/StatusAside.test.tsx index 152a8cc975..1d09f7ab24 100644 --- a/ui-components/__tests__/notifications/StatusAside.test.tsx +++ b/sites/partners/__tests__/components/shared/StatusAside.test.tsx @@ -1,6 +1,6 @@ import React from "react" import { render, cleanup } from "@testing-library/react" -import { StatusAside } from "../../src/notifications/StatusAside" +import { StatusAside } from "../../../src/components/shared/StatusAside" afterEach(cleanup) diff --git a/ui-components/__tests__/blocks/StatusBar.test.tsx b/sites/partners/__tests__/components/shared/StatusBar.test.tsx similarity index 84% rename from ui-components/__tests__/blocks/StatusBar.test.tsx rename to sites/partners/__tests__/components/shared/StatusBar.test.tsx index fb7081d18b..bdaa53d561 100644 --- a/ui-components/__tests__/blocks/StatusBar.test.tsx +++ b/sites/partners/__tests__/components/shared/StatusBar.test.tsx @@ -1,8 +1,7 @@ import React from "react" import { render, cleanup, fireEvent } from "@testing-library/react" -import { StatusBar } from "../../src/blocks/StatusBar" -import { Button } from "../../src/actions/Button" -import { AppearanceStyleType } from "../../src/global/AppearanceTypes" +import { Button, AppearanceStyleType } from "@bloom-housing/ui-components" +import { StatusBar } from "../../../src/components/shared/StatusBar" afterEach(cleanup) diff --git a/sites/partners/__tests__/lib/listings/AdditionalMetadataFormatter.test.ts b/sites/partners/__tests__/lib/listings/AdditionalMetadataFormatter.test.ts new file mode 100644 index 0000000000..d493ee42cd --- /dev/null +++ b/sites/partners/__tests__/lib/listings/AdditionalMetadataFormatter.test.ts @@ -0,0 +1,34 @@ +import { LatitudeLongitude } from "@bloom-housing/ui-components" +import AdditionalMetadataFormatter from "../../../src/lib/listings/AdditionalMetadataFormatter" +import { FormListing } from "../../../src/lib/listings/formTypes" + +const latLong: LatitudeLongitude = { + latitude: 37.36537, + longitude: -121.91071, +} + +const formatData = (data, metadata) => { + return new AdditionalMetadataFormatter({ ...data }, metadata).format().data +} + +const fixtureData = { + reservedCommunityType: { id: "12345" }, + neighborhoodAmenities: {}, +} as FormListing + +describe("AdditionalMetadataFormatter", () => { + it("should format buildingAddress", () => { + const address = { street: "123 Anywhere St.", city: "Anytown", state: "CA" } + const data = { + ...fixtureData, + buildingAddress: address, + } + const metadata = { + preferences: [], + programs: [], + latLong, + } + + expect(formatData(data, metadata).buildingAddress).toEqual({ ...address, ...latLong }) + }) +}) diff --git a/sites/partners/__tests__/lib/listings/BooleansFormatter.test.ts b/sites/partners/__tests__/lib/listings/BooleansFormatter.test.ts new file mode 100644 index 0000000000..aaa5b85313 --- /dev/null +++ b/sites/partners/__tests__/lib/listings/BooleansFormatter.test.ts @@ -0,0 +1,39 @@ +import { ListingApplicationAddressType } from "@bloom-housing/backend-core/types" +import { YesNoAnswer } from "../../../src/lib/helpers" +import BooleansFormatter from "../../../src/lib/listings/BooleansFormatter" +import { AnotherAddressEnum, FormListing, FormMetadata } from "../../../src/lib/listings/formTypes" + +// test helpers +const metadata = {} as FormMetadata +const formatData = (data) => { + return new BooleansFormatter({ ...data }, metadata).format().data +} + +describe("BooleansFormatter", () => { + it("should format applicationDropOffAddressType", () => { + const data = {} as FormListing + + expect(formatData(data).applicationDropOffAddressType).toBeNull() + + data.canApplicationsBeDroppedOff = YesNoAnswer.Yes + data.whereApplicationsDroppedOff = ListingApplicationAddressType.leasingAgent + expect(formatData(data).applicationDropOffAddressType).toEqual( + ListingApplicationAddressType.leasingAgent + ) + + data.whereApplicationsDroppedOff = AnotherAddressEnum.anotherAddress + expect(formatData(data).applicationDropOffAddressType).toBeNull() + }) + + it("should format digitalApplication", () => { + const data = {} as FormListing + + expect(formatData(data).digitalApplication).toBeNull() + + data.digitalApplicationChoice = YesNoAnswer.Yes + expect(formatData(data).digitalApplication).toBe(true) + + data.digitalApplicationChoice = YesNoAnswer.No + expect(formatData(data).digitalApplication).toBe(false) + }) +}) diff --git a/sites/partners/__tests__/lib/listings/DatesFormatter.test.ts b/sites/partners/__tests__/lib/listings/DatesFormatter.test.ts new file mode 100644 index 0000000000..a8971a81ca --- /dev/null +++ b/sites/partners/__tests__/lib/listings/DatesFormatter.test.ts @@ -0,0 +1,48 @@ +import { TimeFieldPeriod } from "@bloom-housing/ui-components" +import { YesNoAnswer, createTime } from "../../../src/lib/helpers" +import DatesFormatter from "../../../src/lib/listings/DatesFormatter" +import { FormMetadata } from "../../../src/lib/listings/formTypes" + +// test helpers +const metadata = {} as FormMetadata +const formatData = (data) => { + return new DatesFormatter({ ...data }, metadata).format().data +} +const dueDate = { + year: "2021", + month: "10", + day: "20", +} +const dueTime = { + hours: "10", + minutes: "30", + period: "am" as TimeFieldPeriod, +} + +describe("DatesFormatter", () => { + it("should format applicationDueDate and Time", () => { + const data = { + applicationDueDateField: dueDate, + applicationDueTimeField: dueTime, + } + const applicationDueDate = formatData(data).applicationDueDate + expect(applicationDueDate).toEqual(createTime(applicationDueDate, dueTime)) + expect(formatData(data).applicationDueDate).toEqual(createTime(applicationDueDate, dueTime)) + }) + + it("should format postmarkedApplicationsReceivedByDate", () => { + let data = {} + + expect(formatData(data).postmarkedApplicationsReceivedByDate).toBeNull() + + data = { + postmarkByDateDateField: dueDate, + postmarkByDateTimeField: dueTime, + arePostmarksConsidered: YesNoAnswer.Yes, + } + + expect(formatData(data).postmarkedApplicationsReceivedByDate.toISOString()).toEqual( + "2021-10-20T10:30:00.000Z" + ) + }) +}) diff --git a/sites/partners/__tests__/lib/listings/JurisdictionFormatter.test.ts b/sites/partners/__tests__/lib/listings/JurisdictionFormatter.test.ts new file mode 100644 index 0000000000..89634e336e --- /dev/null +++ b/sites/partners/__tests__/lib/listings/JurisdictionFormatter.test.ts @@ -0,0 +1,24 @@ +import JurisdictionFormatter from "../../../src/lib/listings/JurisdictionFormatter" +import { FormListing, FormMetadata } from "../../../src/lib/listings/formTypes" +const metadata = { + profile: { + jurisdictions: [{ name: "Alameda" }], + }, +} as FormMetadata + +describe("JurisdictionFormatter", () => { + it("should pull from profile when data is blank", () => { + const data = {} as FormListing + + const formatter = new JurisdictionFormatter(data, metadata).format() + expect(formatter.data.jurisdiction).toEqual({ name: "Alameda" }) + }) + it("should use data when present and ignore profile", () => { + const data = { + jurisdiction: { name: "San Jose" }, + } as FormListing + + const formatter = new JurisdictionFormatter(data, metadata).format() + expect(formatter.data.jurisdiction).toEqual({ name: "San Jose" }) + }) +}) diff --git a/sites/partners/__tests__/lib/listings/WaitlistFormatter.test.ts b/sites/partners/__tests__/lib/listings/WaitlistFormatter.test.ts new file mode 100644 index 0000000000..b699e27900 --- /dev/null +++ b/sites/partners/__tests__/lib/listings/WaitlistFormatter.test.ts @@ -0,0 +1,63 @@ +import { YesNoAnswer } from "../../../src/lib/helpers" +import WaitlistFormatter from "../../../src/lib/listings/WaitlistFormatter" +import { FormListing, FormMetadata } from "../../../src/lib/listings/formTypes" + +const metadata = {} as FormMetadata +const formatData = (data) => { + return new WaitlistFormatter({ ...data }, metadata).format().data +} + +describe("WaitlistFormatter", () => { + it("should format waitlistCurrentSize", () => { + const data = {} as FormListing + expect(formatData(data).waitlistCurrentSize).toBeNull() + + data.waitlistOpenQuestion = YesNoAnswer.Yes + expect(formatData(data).waitlistCurrentSize).toBeNull() + + data.waitlistCurrentSize = 10 + expect(formatData(data).waitlistCurrentSize).toEqual(10) + + data.waitlistOpenQuestion = YesNoAnswer.No + expect(formatData(data).waitlistCurrentSize).toBeNull() + }) + + it("should format waitlistMaxSize", () => { + const data = {} as FormListing + expect(formatData(data).waitlistMaxSize).toBeNull() + + data.waitlistOpenQuestion = YesNoAnswer.Yes + expect(formatData(data).waitlistMaxSize).toBeNull() + + data.waitlistMaxSize = 20 + expect(formatData(data).waitlistMaxSize).toEqual(20) + + data.waitlistOpenQuestion = YesNoAnswer.No + expect(formatData(data).waitlistMaxSize).toBeNull() + }) + + it("should format waitlistOpenSpots", () => { + const data = {} as FormListing + expect(formatData(data).waitlistOpenSpots).toBeNull() + + data.waitlistOpenQuestion = YesNoAnswer.Yes + expect(formatData(data).waitlistOpenSpots).toBeNull() + + data.waitlistOpenSpots = 15 + expect(formatData(data).waitlistOpenSpots).toEqual(15) + + data.waitlistOpenQuestion = YesNoAnswer.No + expect(formatData(data).waitlistOpenSpots).toBeNull() + }) + + it("should format isWaitlistOpen", () => { + const data = {} as FormListing + expect(formatData(data).isWaitlistOpen).toBeNull() + + data.waitlistOpenQuestion = YesNoAnswer.Yes + expect(formatData(data).isWaitlistOpen).toEqual(true) + + data.waitlistOpenQuestion = YesNoAnswer.No + expect(formatData(data).isWaitlistOpen).toEqual(false) + }) +}) diff --git a/sites/partners/__tests__/pages/listings/index.test.tsx b/sites/partners/__tests__/pages/listings/index.test.tsx new file mode 100644 index 0000000000..d15f4392fc --- /dev/null +++ b/sites/partners/__tests__/pages/listings/index.test.tsx @@ -0,0 +1,160 @@ +import { + ACCESS_TOKEN_LOCAL_STORAGE_KEY, + AuthProvider, + ConfigProvider, +} from "@bloom-housing/shared-helpers" + +import { fireEvent, render } from "@testing-library/react" +import { rest } from "msw" +import { setupServer } from "msw/node" +import ListingsList from "../../../src/pages/index" +import React from "react" +import { listing } from "../../testHelpers" +import { mockNextRouter } from "../../testUtils" + +//Mock the jszip package used for Export +const mockFile = jest.fn() +let mockFolder: jest.Mock +function mockJszip() { + mockFolder = jest.fn(mockJszip) + return { + folder: mockFolder, + file: mockFile, + generateAsync: jest.fn().mockImplementation(() => { + const blob = {} + const response = { blob } + return Promise.resolve(response) + }), + } +} +jest.mock("jszip", () => { + return { + __esModule: true, + default: mockJszip, + } +}) + +const server = setupServer() +beforeAll(() => { + server.listen() + mockNextRouter() +}) + +afterEach(() => { + server.resetHandlers() + window.sessionStorage.clear() +}) + +afterAll(() => server.close()) + +describe("listings", () => { + it("should not render Export to CSV when user is not admin", async () => { + const fakeToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ZTMxODNhOC0yMGFiLTRiMDYtYTg4MC0xMmE5NjYwNmYwOWMiLCJpYXQiOjE2Nzc2MDAxNDIsImV4cCI6MjM5NzkwMDc0Mn0.ve1U5tAardpFjNyJ_b85QZLtu12MoMTa2aM25E8D1BQ" + window.sessionStorage.setItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY, fakeToken) + server.use( + rest.get("http://localhost:3100/listings", (_req, res, ctx) => { + return res(ctx.json({ items: [listing], meta: { totalItems: 1, totalPages: 1 } })) + }), + rest.get("http://localhost:3100/user", (_req, res, ctx) => { + return res( + ctx.json({ id: "user1", roles: { id: "user1", isAdmin: false, isPartner: true } }) + ) + }), + rest.post("http://localhost:3100/auth/token", (_req, res, ctx) => { + return res(ctx.json("")) + }) + ) + + const { findByText, queryByText } = render( + + + + + + ) + const header = await findByText("Detroit Partner Portal") + expect(header).toBeInTheDocument() + const exportButton = queryByText("Export to CSV") + expect(exportButton).not.toBeInTheDocument() + }) + + it("should render the error text when listings csv api call fails", async () => { + const fakeToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ZTMxODNhOC0yMGFiLTRiMDYtYTg4MC0xMmE5NjYwNmYwOWMiLCJpYXQiOjE2Nzc2MDAxNDIsImV4cCI6MjM5NzkwMDc0Mn0.ve1U5tAardpFjNyJ_b85QZLtu12MoMTa2aM25E8D1BQ" + window.sessionStorage.setItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY, fakeToken) + server.use( + rest.get("http://localhost:3100/listings", (_req, res, ctx) => { + return res(ctx.json({ items: [listing], meta: { totalItems: 1, totalPages: 1 } })) + }), + rest.get("http://localhost:3100/listings/csv", (_req, res, ctx) => { + return res(ctx.status(500), ctx.json("")) + }), + rest.get("http://localhost:3100/user", (_req, res, ctx) => { + return res(ctx.json({ id: "user1", roles: { id: "user1", isAdmin: true } })) + }), + rest.post("http://localhost:3100/auth/token", (_req, res, ctx) => { + return res(ctx.json("")) + }) + ) + + const { findByText, getByText } = render( + + + + + + ) + const header = await findByText("Detroit Partner Portal") + expect(header).toBeInTheDocument() + const exportButton = getByText("Export to CSV") + expect(exportButton).toBeInTheDocument() + fireEvent.click(exportButton) + const error = await findByText( + "Export failed. Please try again later. If the problem persists, please email supportbloom@exygy.com", + { + exact: false, + } + ) + expect(error).toBeInTheDocument() + }) + + it("should render Export to CSV when user is admin and success message when clicked", async () => { + window.URL.createObjectURL = jest.fn() + //Prevent error from clicking anchor tag within test + HTMLAnchorElement.prototype.click = jest.fn() + const fakeToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ZTMxODNhOC0yMGFiLTRiMDYtYTg4MC0xMmE5NjYwNmYwOWMiLCJpYXQiOjE2Nzc2MDAxNDIsImV4cCI6MjM5NzkwMDc0Mn0.ve1U5tAardpFjNyJ_b85QZLtu12MoMTa2aM25E8D1BQ" + window.sessionStorage.setItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY, fakeToken) + server.use( + rest.get("http://localhost:3100/listings", (_req, res, ctx) => { + return res(ctx.json({ items: [listing], meta: { totalItems: 1, totalPages: 1 } })) + }), + + rest.get("http://localhost:3100/listings/csv", (_req, res, ctx) => { + return res(ctx.json({ listingCSV: "", unitCSV: "" })) + }), + rest.get("http://localhost:3100/user", (_req, res, ctx) => { + return res(ctx.json({ id: "user1", roles: { id: "user1", isAdmin: true } })) + }), + rest.post("http://localhost:3100/auth/token", (_req, res, ctx) => { + return res(ctx.json("")) + }) + ) + + const { findByText, getByText } = render( + + + + + + ) + const header = await findByText("Detroit Partner Portal") + expect(header).toBeInTheDocument() + const exportButton = getByText("Export to CSV") + expect(exportButton).toBeInTheDocument() + fireEvent.click(exportButton) + const success = await findByText("The file has been exported") + expect(success).toBeInTheDocument() + }) +}) diff --git a/sites/partners/__tests__/pages/users/index.test.tsx b/sites/partners/__tests__/pages/users/index.test.tsx new file mode 100644 index 0000000000..1b4b85da97 --- /dev/null +++ b/sites/partners/__tests__/pages/users/index.test.tsx @@ -0,0 +1,166 @@ +import { + ACCESS_TOKEN_LOCAL_STORAGE_KEY, + AuthProvider, + ConfigProvider, +} from "@bloom-housing/shared-helpers" +import { fireEvent, render } from "@testing-library/react" +import { rest } from "msw" +import { setupServer } from "msw/node" +import React from "react" +import Users from "../../../src/pages/users" +import { user } from "../../testHelpers" +import { mockNextRouter } from "../../testUtils" + +const server = setupServer() + +beforeAll(() => { + server.listen() + mockNextRouter() +}) + +afterEach(() => { + server.resetHandlers() + window.sessionStorage.clear() +}) + +afterAll(() => server.close()) + +describe("users", () => { + it("should render the error text when api call fails", async () => { + server.use( + rest.get("http://localhost:3100/listings", (_req, res, ctx) => { + return res(ctx.json([])) + }), + rest.get("http://localhost:3100/user/list", (_req, res, ctx) => { + return res(ctx.status(500), ctx.json("")) + }) + ) + const { findByText } = render( + + + + + + ) + + const error = await findByText("An error has occurred.") + expect(error).toBeInTheDocument() + }) + + it("should render user table when data is returned", async () => { + server.use( + rest.get("http://localhost:3100/listings", (_req, res, ctx) => { + return res(ctx.json([])) + }), + rest.get("http://localhost:3100/user/list", (_req, res, ctx) => { + return res(ctx.json({ items: [user], meta: { totalItems: 1, totalPages: 1 } })) + }) + ) + const { findByText, getByText, queryAllByText } = render( + + + + + + ) + + const header = await findByText("Detroit Partner Portal") + expect(header).toBeInTheDocument() + expect(getByText("Users")).toBeInTheDocument() + expect(getByText("Filter")).toBeInTheDocument() + expect(getByText("Add User")).toBeInTheDocument() + expect(queryAllByText("Export to CSV")).toHaveLength(0) + + const name = await findByText("First Last") + expect(name).toBeInTheDocument() + expect(getByText("first.last@bloom.com")).toBeInTheDocument() + expect(getByText("Administrator")).toBeInTheDocument() + expect(getByText("09/04/2022")).toBeInTheDocument() + expect(getByText("Confirmed")).toBeInTheDocument() + }) + + it("should render Export to CSV when user is admin and success when clicked", async () => { + window.URL.createObjectURL = jest.fn() + // set a logged in token + const fakeToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ZTMxODNhOC0yMGFiLTRiMDYtYTg4MC0xMmE5NjYwNmYwOWMiLCJpYXQiOjE2Nzc2MDAxNDIsImV4cCI6MjM5NzkwMDc0Mn0.ve1U5tAardpFjNyJ_b85QZLtu12MoMTa2aM25E8D1BQ" + window.sessionStorage.setItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY, fakeToken) + server.use( + rest.get("http://localhost:3100/listings", (_req, res, ctx) => { + return res(ctx.json([])) + }), + rest.get("http://localhost:3100/user/list", (_req, res, ctx) => { + return res(ctx.json({ items: [user], meta: { totalItems: 1, totalPages: 1 } })) + }), + // set logged in user as admin + rest.get("http://localhost:3100/user", (_req, res, ctx) => { + return res(ctx.json({ id: "user1", roles: { id: "user1", isAdmin: true } })) + }), + rest.get("http://localhost:3100/user/csv", (_req, res, ctx) => { + return res(ctx.json("")) + }), + rest.post("http://localhost:3100/auth/token", (_req, res, ctx) => { + return res(ctx.json("")) + }) + ) + const { findByText, getByText } = render( + + + + + + ) + + const header = await findByText("Detroit Partner Portal") + expect(header).toBeInTheDocument() + expect(getByText("Add User")).toBeInTheDocument() + const exportButton = await findByText("Export to CSV") + expect(exportButton).toBeInTheDocument() + fireEvent.click(exportButton) + const successMessage = await findByText("The file has been exported") + expect(successMessage).toBeInTheDocument() + }) + + it("should render error message csv fails", async () => { + jest.spyOn(console, "log").mockImplementation(jest.fn()) + // set a logged in token + const fakeToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ZTMxODNhOC0yMGFiLTRiMDYtYTg4MC0xMmE5NjYwNmYwOWMiLCJpYXQiOjE2Nzc2MDAxNDIsImV4cCI6MjM5NzkwMDc0Mn0.ve1U5tAardpFjNyJ_b85QZLtu12MoMTa2aM25E8D1BQ" + window.sessionStorage.setItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY, fakeToken) + server.use( + rest.get("http://localhost:3100/listings", (_req, res, ctx) => { + return res(ctx.json([])) + }), + rest.get("http://localhost:3100/user/list", (_req, res, ctx) => { + return res(ctx.json({ items: [user], meta: { totalItems: 1, totalPages: 1 } })) + }), + // set logged in user as admin + rest.get("http://localhost:3100/user", (_req, res, ctx) => { + return res(ctx.json({ id: "user1", roles: { id: "user1", isAdmin: true } })) + }), + rest.get("http://localhost:3100/user/csv", (_req, res, ctx) => { + return res(ctx.status(500), ctx.json("")) + }), + rest.post("http://localhost:3100/auth/token", (_req, res, ctx) => { + return res(ctx.json("")) + }) + ) + const { findByText } = render( + + + + + + ) + + const header = await findByText("Detroit Partner Portal") + expect(header).toBeInTheDocument() + const exportButton = await findByText("Export to CSV") + expect(exportButton).toBeInTheDocument() + fireEvent.click(exportButton) + const errorMessage = await findByText("Export failed. Please try again later.", { + exact: false, + }) + expect(errorMessage).toBeInTheDocument() + }) +}) diff --git a/sites/partners/__tests__/testHelpers.ts b/sites/partners/__tests__/testHelpers.ts new file mode 100644 index 0000000000..4bf821347e --- /dev/null +++ b/sites/partners/__tests__/testHelpers.ts @@ -0,0 +1,190 @@ +import { + HomeTypeEnum, + Listing, + ListingMarketingTypeEnum, + ListingReviewOrder, + ListingStatus, + Unit, + UnitGroup, + UnitStatus, +} from "@bloom-housing/backend-core" + +export const user = { + agreedToTermsOfService: false, + confirmedAt: new Date(), + createdAt: new Date("2022-09-04T17:13:31.513Z"), + dob: new Date(), + email: "first.last@bloom.com", + failedLoginAttemptsCount: 0, + firstName: "First", + hitConfirmationURL: null, + id: "user_1", + jurisdictions: [ + { id: "e50e64bc-4bc8-4cef-a4d1-1812add9981b" }, + { id: "d6b652a0-9947-418a-b69b-cd72028ed913" }, + ], + language: null, + lastLoginAt: new Date(), + lastName: "Last", + leasingAgentInListings: [], + mfaEnabled: true, + middleName: "Middle", + passwordUpdatedAt: new Date(), + passwordValidForDays: 180, + phoneNumber: null, + phoneNumberVerified: false, + roles: { user: { id: "user_1" }, isAdmin: true, isJurisdictionalAdmin: false, isPartner: false }, + updatedAt: new Date(), +} + +export const unit: Unit = { + status: UnitStatus.available, + id: "sQ19KuyILEo0uuNqti2fl", + amiPercentage: "45.0", + annualIncomeMin: "26496.0", + monthlyIncomeMin: "2208.0", + floor: 2, + annualIncomeMax: "46125.0", + maxOccupancy: 2, + minOccupancy: 1, + monthlyRent: "1104.0", + numBathrooms: null, + numBedrooms: null, + number: null, + priorityType: null, + sqFeet: "285", + + unitType: { + id: "random_id_35edf", + createdAt: new Date(), + updatedAt: new Date(), + name: "studio", + numBedrooms: 0, + }, + createdAt: new Date("2019-07-09T21:20:05.783Z"), + updatedAt: new Date("2019-08-14T23:05:43.913Z"), + monthlyRentAsPercentOfIncome: null, +} + +const unitGroup: UnitGroup = { + unitType: [ + { + id: "unitType1", + createdAt: new Date(), + updatedAt: new Date(), + name: "oneBdrm", + numBedrooms: 1, + }, + ], + amiLevels: [], + id: "unitGroup1", + listingId: "listing1", + openWaitlist: true, + maxOccupancy: 3, + minOccupancy: 1, + bathroomMin: 1, + bathroomMax: 2, +} + +const address = { + id: "id", + createdAt: new Date(), + updatedAt: new Date(), + city: "San Francisco", + street: "548 Market St.", + zipCode: "94104", + state: "CA", + latitude: 37.36537, + longitude: -121.91071, +} + +export const listing: Listing = { + id: "Uvbk5qurpB2WI9V6WnNdH", + marketingType: ListingMarketingTypeEnum.marketing, + homeType: HomeTypeEnum.apartment, + applicationConfig: undefined, + applicationOpenDate: new Date("2019-12-31T15:22:57.000-07:00"), + applicationPickUpAddress: undefined, + applicationPickUpAddressOfficeHours: "", + applicationDropOffAddress: null, + applicationDropOffAddressOfficeHours: null, + applicationMailingAddress: null, + countyCode: "Detroit", + jurisdiction: { + id: "id", + name: "Detroit", + publicUrl: "", + }, + depositMax: "", + disableUnitsAccordion: false, + events: [], + showWaitlist: false, + reviewOrderType: ListingReviewOrder.firstComeFirstServe, + urlSlug: "listing-slug-abcdef", + whatToExpect: "Applicant will be contacted. All info will be verified. Be prepared if chosen.", + status: ListingStatus.active, + postmarkedApplicationsReceivedByDate: new Date("2019-12-05"), + applicationDueDate: new Date("2019-12-31T15:22:57.000-07:00"), + applicationMethods: [], + applicationOrganization: "98 Archer Street", + assets: [ + { + label: "building", + fileId: + "https://regional-dahlia-staging.s3-us-west-1.amazonaws.com/listings/archer/archer-studios.jpg", + }, + ], + buildingSelectionCriteria: + "Tenant Selection Criteria will be available to all applicants upon request.", + costsNotIncluded: + "Resident responsible for PG&E, internet and phone. Owner pays for water, trash, and sewage. Residents encouraged to obtain renter's insurance but this is not a requirement. Rent is due by the 5th of each month. Late fee $35 and returned check fee is $35 additional.", + creditHistory: + "Applications will be rated on a score system for housing. An applicant's score may be impacted by negative tenant peformance information provided to the credit reporting agency. All applicants are expected have a passing acore of 70 points out of 100 to be considered for housing. Applicants with no credit history will receive a maximum of 80 points to fairly outweigh positive and/or negative trades as would an applicant with established credit history. Refer to Tenant Selection Criteria or Qualification Criteria for details related to the qualification process. ", + depositMin: "1140.0", + programRules: + "Applicants must adhere to minimum & maximum income limits. Tenant Selection Criteria applies.", + waitlistMaxSize: 300, + name: "Archer Studios", + waitlistCurrentSize: 300, + waitlistOpenSpots: 0, + isWaitlistOpen: true, + displayWaitlistSize: false, + requiredDocuments: "Completed application and government issued IDs", + createdAt: new Date("2019-07-08T15:37:19.565-07:00"), + updatedAt: new Date("2019-07-09T14:35:11.142-07:00"), + applicationFee: "30.0", + criminalBackground: + "A criminal background investigation will be obtained on each applicant. As criminal background checks are done county by county and will be ran for all counties in which the applicant lived, Applicants will be disqualified for tenancy if they have been convicted of a felony or misdemeanor. Refer to Tenant Selection Criteria or Qualification Criteria for details related to the qualification process. ", + leasingAgentAddress: address, + leasingAgentEmail: "admin@example.com", + leasingAgentName: "First Last", + leasingAgentOfficeHours: "Monday, Tuesday & Friday, 9:00AM - 5:00PM", + leasingAgentPhone: "(408) 217-8562", + leasingAgentTitle: "", + rentalAssistance: "Custom rental assistance", + rentalHistory: + "Two years of rental history will be verified with all applicable landlords. Household family members and/or personal friends are not acceptable landlord references. Two professional character references may be used in lieu of rental history for applicants with no prior rental history. An unlawful detainer report will be processed thourhg the U.D. Registry, Inc. Applicants will be disqualified if they have any evictions filing within the last 7 years. Refer to Tenant Selection Criteria or Qualification Criteria for details related to the qualification process.", + householdSizeMin: 2, + householdSizeMax: 3, + smokingPolicy: "Non-smoking building", + unitsAvailable: 0, + unitAmenities: "Dishwasher", + developer: "Charities Housing ", + yearBuilt: 2012, + accessibility: + "There is a total of 5 ADA units in the complex, all others are adaptable. Exterior Wheelchair ramp (front entry)", + amenities: + "Community Room, Laundry Room, Assigned Parking, Bike Storage, Roof Top Garden, Part-time Resident Service Coordinator", + buildingTotalUnits: 35, + buildingAddress: address, + neighborhood: "Rosemary Gardens Park", + petPolicy: + "No pets allowed. Accommodation animals may be granted to persons with disabilities via a reasonable accommodation request.", + listingPreferences: [], + unitGroups: [unitGroup], + unitSummaries: { + unitGroupSummary: [], + householdMaxIncomeSummary: { columns: { householdSize: "3" }, rows: [] }, + }, + units: [unit], +} diff --git a/sites/partners/__tests__/testUtils.tsx b/sites/partners/__tests__/testUtils.tsx new file mode 100644 index 0000000000..e3c7f63663 --- /dev/null +++ b/sites/partners/__tests__/testUtils.tsx @@ -0,0 +1,31 @@ +import { AuthProvider, ConfigProvider } from "@bloom-housing/shared-helpers" +import { render, RenderOptions } from "@testing-library/react" +import React, { FC, ReactElement } from "react" +import { SWRConfig } from "swr" + +const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + new Map(), dedupingInterval: 0 }}> + + {children} + + + ) +} + +const customRender = (ui: ReactElement, options?: Omit) => + render(ui, { wrapper: AllTheProviders, ...options }) + +// re-export everything +export * from "@testing-library/react" + +// override render method +export { customRender as render } + +export const mockNextRouter = () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const useRouter = jest.spyOn(require("next/router"), "useRouter") + useRouter.mockImplementation(() => ({ + pathname: "/", + })) +} diff --git a/sites/partners/cypress.config.ts b/sites/partners/cypress.config.ts new file mode 100644 index 0000000000..08e5d023e4 --- /dev/null +++ b/sites/partners/cypress.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "cypress" + +export default defineConfig({ + defaultCommandTimeout: 10000, + projectId: "bloom-partners-reference", + numTestsKeptInMemory: 0, + trashAssetsBeforeRuns: true, + env: { + codeCoverage: { + url: "/api/__coverage__", + }, + }, + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require("./cypress/plugins/index.js")(on, config) + }, + baseUrl: "http://localhost:3001", + specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}", + }, +}) diff --git a/sites/partners/cypress.json b/sites/partners/cypress.json deleted file mode 100644 index c8deb906b1..0000000000 --- a/sites/partners/cypress.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "baseUrl": "http://localhost:3000", - "defaultCommandTimeout": 10000, - "projectId": "bloom-partners-reference" -} diff --git a/sites/partners/cypress/e2e/admin-user-management.spec.ts b/sites/partners/cypress/e2e/admin-user-management.spec.ts new file mode 100644 index 0000000000..33775200b3 --- /dev/null +++ b/sites/partners/cypress/e2e/admin-user-management.spec.ts @@ -0,0 +1,38 @@ +describe("Admin User Mangement Tests", () => { + beforeEach(() => { + cy.login() + }) + + after(() => { + cy.signOut() + }) + + it("as admin user, should show all users", () => { + cy.visit("/") + cy.getByTestId("Users-1").click() + const rolesArray = ["Partner", "Administrator"] + cy.getByTestId("ag-page-size").select("100", { force: true }) + + const regex = new RegExp(`${rolesArray.join("|")}`, "g") + + cy.get(`.ag-center-cols-container [col-id="roles"]`).each((role) => { + cy.wrap(role).contains(regex) + }) + }) + + it("as admin user, should be able to download export", () => { + cy.visit("/") + cy.getByTestId("Users-1").click() + cy.getByTestId("export-users").click() + const convertToString = (value: number) => { + return value < 10 ? `0${value}` : `${value}` + } + const now = new Date() + const month = now.getMonth() + 1 + cy.readFile( + `cypress/downloads/users-${now.getFullYear()}-${convertToString(month)}-${convertToString( + now.getDate() + )}_${convertToString(now.getHours())}_${convertToString(now.getMinutes())}.csv` + ) + }) +}) diff --git a/sites/partners/cypress/e2e/listing.spec.ts b/sites/partners/cypress/e2e/listing.spec.ts new file mode 100644 index 0000000000..3f5f9ae5a9 --- /dev/null +++ b/sites/partners/cypress/e2e/listing.spec.ts @@ -0,0 +1,284 @@ +describe("Listing Management Tests", () => { + beforeEach(() => { + cy.login() + }) + + after(() => { + cy.signOut() + }) + + const listingDetailsFieldsToType = [ + { fieldID: "name" }, + { fieldID: "developer" }, + { fieldID: "buildingAddress.street" }, + { fieldID: "buildingAddress.city" }, + { fieldID: "buildingAddress.zipCode" }, + { fieldID: "yearBuilt" }, + { fieldID: "applicationFee" }, + { fieldID: "depositMin", hardcodedValue: "02" }, + { fieldID: "depositMax", hardcodedValue: "0100" }, + { fieldID: "costsNotIncluded" }, + { fieldID: "amenities" }, + { fieldID: "neighborhoodAmenities.groceryStores" }, + { fieldID: "neighborhoodAmenities.publicTransportation" }, + { fieldID: "neighborhoodAmenities.schools" }, + { fieldID: "neighborhoodAmenities.parksAndCommunityCenters" }, + { fieldID: "neighborhoodAmenities.pharmacies" }, + { fieldID: "neighborhoodAmenities.healthCareResources" }, + { fieldID: "accessibility" }, + { fieldID: "unitAmenities" }, + { fieldID: "smokingPolicy" }, + { fieldID: "petPolicy" }, + { fieldID: "servicesOffered" }, + { fieldID: "creditHistory" }, + { fieldID: "rentalHistory" }, + { fieldID: "criminalBackground" }, + { fieldID: "requiredDocuments" }, + { fieldID: "programRules" }, + { fieldID: "specialNotes" }, + ] + + const listingDetailsFieldsToSelect = [ + { fieldID: "jurisdiction.id" }, + { fieldID: "neighborhood" }, + { fieldID: "buildingAddress.state", hardcodedValue: "CA" }, + ] + + const unitFormFieldsToType = [ + { fieldID: "totalCount", byTestID: true }, + { fieldID: "sqFeetMin", byTestID: true }, + { fieldID: "sqFeetMax", byTestID: true }, + { fieldID: "totalAvailable", byTestID: true }, + ] + + const unitFormFieldsToSelect = [ + { fieldID: "minOccupancy", byTestID: true }, + { fieldID: "maxOccupancy", byTestID: true }, + { fieldID: "floorMin", byTestID: true }, + { fieldID: "floorMax", byTestID: true }, + { fieldID: "bathroomMin", byTestID: true }, + { fieldID: "bathroomMax", byTestID: true }, + ] + + const amiFormFieldsToType = [{ fieldID: "flatRentValue", byTestID: true }] + + const amiFormFieldsToSelect = [ + { fieldID: "amiChartId", byTestID: true }, + { fieldID: "amiPercentage", byTestID: true }, + ] + + const applicationDetailsFieldsToType = [ + { fieldID: "leasingAgentName", byTestID: true }, + { fieldID: "leasingAgentEmail", byTestID: true, hardcodedValue: "basicagent@email.com" }, + { fieldID: "leasingAgentPhone", byTestID: true, hardcodedValue: "(520) 245-8811" }, + { fieldID: "leasingAgentTitle", byTestID: true }, + { fieldID: "leasingAgentOfficeHours", byTestID: true }, + { fieldID: "leasingAgentAddress.street", byTestID: true }, + { fieldID: "leasingAgentAddress.street2", byTestID: true }, + { fieldID: "leasingAgentAddress.city", byTestID: true }, + { fieldID: "leasingAgentAddress.zipCode", byTestID: true }, + { fieldID: "mailing-address-street", byTestID: true, fixtureID: "leasingAgentAddress.street" }, + { + fieldID: "mailing-address-street2", + byTestID: true, + fixtureID: "leasingAgentAddress.street2", + }, + { fieldID: "mailing-address-city", byTestID: true, fixtureID: "leasingAgentAddress.city" }, + { fieldID: "mailing-address-zip", byTestID: true, fixtureID: "leasingAgentAddress.zipCode" }, + { fieldID: "postmark-date-field-month", byTestID: true }, + { fieldID: "postmark-date-field-day", byTestID: true }, + { fieldID: "postmark-date-field-year", byTestID: true }, + { fieldID: "postmark-time-field-hours", byTestID: true }, + { fieldID: "postmark-time-field-minutes", byTestID: true }, + ] + + const applicationDetailsFieldsToSelect = [ + { fieldID: "leasingAgentAddress.state", byTestID: true, hardcodedValue: "CA" }, + { + fieldID: "mailing-address-state", + byTestID: true, + fixtureID: "leasingAgentAddress.state", + hardcodedValue: "CA", + }, + { fieldID: "postmark-time-field-period", byTestID: true }, + ] + + it("full listing publish", () => { + cy.visit("/") + cy.get("a > .button").contains("Add Listing").click() + cy.contains("New Listing") + + // Test photo upload + cy.getByTestId("add-photos-button").contains("Add Photo").click() + cy.getByTestId("dropzone-input").attachFile( + "cypress-automated-image-upload-071e2ab9-5a52-4f34-85f0-e41f696f4b96.jpeg", + { + subjectType: "drag-n-drop", + } + ) + cy.getByTestId("drawer-photos-table").contains( + "cypress-automated-image-upload-071e2ab9-5a52-4f34-85f0-e41f696f4b96" + ) + cy.getByTestId("listing-photo-uploaded").contains("Save").click() + cy.getByTestId("photos-table").contains( + "cypress-automated-image-upload-071e2ab9-5a52-4f34-85f0-e41f696f4b96" + ) + + cy.getByTestId("add-photos-button").contains("Edit Photos").click() + cy.getByTestId("dropzone-input").attachFile( + "cypress-automated-image-upload-46806882-b98d-49d7-ac83-8016ab4b2f08.jpg", + { + subjectType: "drag-n-drop", + } + ) + cy.getByTestId("drawer-photos-table").contains( + "cypress-automated-image-upload-46806882-b98d-49d7-ac83-8016ab4b2f08" + ) + cy.getByTestId("listing-photo-uploaded").contains("Save").click() + cy.getByTestId("photos-table").contains( + "cypress-automated-image-upload-46806882-b98d-49d7-ac83-8016ab4b2f08" + ) + cy.getByTestId("photos-table").get("tbody > tr").should("have.length", 2) + cy.getByTestId("photos-table") + .get("tbody > tr:nth-of-type(2)") + .should("not.contain", "Primary photo") + + // Fill out a bunch of fields + cy.fillFormFields("listing", listingDetailsFieldsToType, listingDetailsFieldsToSelect) + + // Add units + cy.getByTestId("addUnitsButton").contains("Add unit group").click() + cy.get(`[data-testid="unitTypeCheckBox"]`).first().click() + cy.get(`[data-testid="openWaitListQuestion"]`).last().click() + cy.fillFormFields("listing", unitFormFieldsToType, unitFormFieldsToSelect) + + // Add AMI data + cy.getByTestId("openAmiDrawer").contains("Add AMI level").click() + cy.fillFormFields("listing", amiFormFieldsToType, amiFormFieldsToSelect) + cy.getByTestId("saveAmi").click() + cy.getByTestId("saveUnit").click() + + cy.fixture("listing").then((listing) => { + cy.get(".addressPopup").contains(listing["buildingAddress.street"]) + cy.get("#addBuildingSelectionCriteriaButton") + .contains("Add Building Selection Criteria") + .click() + cy.get("#criteriaAttachTypeURL").check() + cy.getByID("buildingSelectionCriteriaURL").type(listing["buildingSelectionCriteriaURL"]) + }) + + cy.get(".p-4 > .is-primary").contains("Save").click() + cy.get(".text-right > .button").contains("Application Process").click() + cy.get("#reviewOrderFCFS").check() + cy.get("#waitlistOpenNo").check() + cy.get("#digitalApplicationChoiceYes").check() + cy.get("#paperApplicationNo").check() + cy.get("#applicationsMailedInYes").check() + cy.get("#mailInAnotherAddress").check() + cy.get("#applicationsPickedUpNo").check() + cy.get("#applicationsDroppedOffNo").check() + cy.get("#postmarksConsideredYes").check() + cy.fillFormFields("listing", applicationDetailsFieldsToType, applicationDetailsFieldsToSelect) + + cy.get("#publishButton").contains("Publish").click() + cy.get("#publishButtonConfirm").contains("Publish").click() + cy.fixture("listing").then((listing) => { + cy.get(".page-header__title > .font-semibold").contains(listing["name"]) + }) + + //verify the details section is correct + const fieldsToVerify = [ + ...listingDetailsFieldsToType, + ...listingDetailsFieldsToSelect, + ...applicationDetailsFieldsToType, + ...applicationDetailsFieldsToSelect, + ] + + cy.verifyFormFields( + "listing", + fieldsToVerify.reduce((accum, elem) => { + if (elem.fieldID === "jurisdiction.id") { + accum.push({ fieldID: "jurisdiction.name", fixtureID: "jurisdiction.id" }) + } else if (elem.fieldID === "mailing-address-street") { + accum.push({ ...elem, fieldID: "applicationMailingAddress.street" }) + } else if (elem.fieldID === "mailing-address-street2") { + accum.push({ ...elem, fieldID: "applicationMailingAddress.street2" }) + } else if (elem.fieldID === "mailing-address-city") { + accum.push({ ...elem, fieldID: "applicationMailingAddress.city" }) + } else if (elem.fieldID === "mailing-address-zip") { + accum.push({ ...elem, fieldID: "applicationMailingAddress.zipCode" }) + } else if (elem.fieldID === "mailing-address-state") { + accum.push({ ...elem, fieldID: "applicationMailingAddress.state" }) + } else if (elem.fieldID === "postmark-date-field-month") { + // skip this one + } else if (elem.fieldID === "postmark-date-field-day") { + // skip this one + } else if (elem.fieldID === "postmark-date-field-year") { + // skip this one + } else if (elem.fieldID === "postmark-time-field-hours") { + // skip this one + } else if (elem.fieldID === "postmark-time-field-minutes") { + // skip this one + } else if (elem.fieldID === "postmark-time-field-period") { + // skip this one + } else { + accum.push(elem) + } + return accum + }, [] as fillFromFieldOption[]) + ) + + cy.get("#longitude").contains("-122") + cy.get("#latitude").contains("37") + cy.get("#unitTable").contains("Unit Type") + cy.getByID("waitlist.openQuestion").contains("No") + cy.get("#digitalApplication").contains("Yes") + cy.getByID("digitalMethod.type").contains("No") + cy.get("#paperApplication").contains("No") + cy.getByID("applicationPickupQuestion").contains("No") + cy.getByID("applicationMailingSection").contains("Yes") + cy.get("#applicationDropOffQuestion").contains("No") + cy.get("#postmarksConsideredQuestion").contains("Yes") + + cy.fixture("listing").then((listing) => { + cy.get("#unitTable").contains(listing["totalCount"]) + cy.get("#unitTable").contains(`${listing["amiPercentage"]}%`) + cy.get("#unitTable").contains(`$${listing["flatRentValue"]}`) + cy.get("#unitTable").contains(`${listing["minOccupancy"]} - ${listing["maxOccupancy"]}`) + cy.get("#unitTable").contains(`${listing["sqFeetMin"]} - ${listing["sqFeetMax"]}`) + cy.get("#unitTable").contains(`${listing["bathroomMin"]} - ${listing["bathroomMax"]}`) + cy.get("#postmarkedApplicationsReceivedByDate").contains( + `${listing["postmark-date-field-month"]}/${listing["postmark-date-field-day"]}/${listing["postmark-date-field-year"]}` + ) + cy.get("#postmarkedApplicationsReceivedByDateTime").contains( + `${listing["postmark-time-field-hours"]}:${listing["postmark-time-field-minutes"]} ${listing["postmark-time-field-period"]}` + ) + }) + + // try editing the listing + cy.fixture("listing").then((listing) => { + cy.getByTestId("listingEditButton").contains("Edit").click() + cy.getByTestId("nameField").type(" (Edited)") + cy.getByTestId("saveAndExitButton").contains("Save & Exit").click() + cy.getByTestId("listingIsAlreadyLiveButton").contains("Save").click() + cy.getByTestId("page-header-text").should("have.text", `${listing["name"]} (Edited)`) + }) + }) + + it("as admin user, should be able to download listings export zip", () => { + const convertToString = (value: number) => { + return value < 10 ? `0${value}` : `${value}` + } + cy.visit("/") + cy.getByTestId("export-listings").click() + const now = new Date() + const dateString = `${now.getFullYear()}-${convertToString( + now.getMonth() + 1 + )}-${convertToString(now.getDate())}` + const timeString = `${convertToString(now.getHours())}-${convertToString(now.getMinutes())}` + const zipName = `${dateString}_${timeString}-complete-listing-data.zip` + const downloadFolder = Cypress.config("downloadsFolder") + const completeZipPath = `${downloadFolder}/${zipName}` + cy.readFile(completeZipPath) + }) +}) diff --git a/sites/partners/cypress/fixtures/alternateContactOnlyData.json b/sites/partners/cypress/fixtures/alternateContactOnlyData.json new file mode 100644 index 0000000000..0e61eb93c7 --- /dev/null +++ b/sites/partners/cypress/fixtures/alternateContactOnlyData.json @@ -0,0 +1,76 @@ +{ + "language": "n/a", + "applicant.firstName": "n/a", + "applicant.middleName": "n/a", + "applicant.lastName": "n/a", + "dateOfBirth.birthMonth": "n/a", + "dateOfBirth.birthDay": "n/a", + "dateOfBirth.birthYear": "n/a", + "applicant.emailAddress": "n/a", + "phoneNumber": "n/a", + "applicant.phoneNumberType": "n/a", + "additionalPhoneNumber": "n/a", + "additionalPhoneNumberType": "n/a", + "applicant.workInRegion": "n/a", + "applicant.address.street": "n/a", + "applicant.address.street2": "n/a", + "applicant.address.city": "n/a", + "applicant.address.state": "n/a", + "applicant.address.stateCode": "n/a", + "applicant.address.zipCode": "n/a", + "alternateContact.firstName": "Alternate Contact", + "alternateContact.lastName": "Test", + "alternateContact.agency": "alt contact agency", + "alternateContact.emailAddress": "alt@Contact.com", + "alternateContact.phoneNumber": "5202457893", + "alternateContact.type": "Friend", + "alternateContact.mailingAddress.street": "245 e west street", + "alternateContact.mailingAddress.street2": "n/a", + "alternateContact.mailingAddress.city": "blorg", + "alternateContact.mailingAddress.state": "Nevada", + "alternateContact.mailingAddress.stateCode": "NV", + "alternateContact.mailingAddress.zipCode": "85748", + "householdExpectingChanges": "No", + "householdStudent": "No", + "incomePeriod": "n/a", + "incomeMonth": "n/a", + "incomeVouchers": "No", + "demographics.ethnicity": "n/a", + "acceptedTerms": "Yes", + "firstName": "n/a", + "middleName": "n/a", + "lastName": "n/a", + "dob-field-month": "n/a", + "dob-field-day": "n/a", + "dob-field-year": "n/a", + "relationship": "n/a", + "sameAddress": "n/a", + "workInRegion": "n/a", + "submittedBy": "n/a", + "dateOfBirth": "n/a", + "formattedPhoneNumber": "n/a", + "formattedAdditionalPhoneNumber": "n/a", + "alternateContact.formattedPhoneNumber": "(520) 245-7893", + "householdMemberDoB": "n/a", + "householdMemberName": "n/a", + "preferredUnitSize": "n/a", + "formattedMonthlyIncome": "n/a", + "preferredContact": "n/a", + "preferredUnit": "oneBdrm", + "submittedDate": "n/a", + "timeDate": "n/a", + "workAddress.streetAddress": "n/a", + "workAddress.street2": "n/a", + "workAddress.city": "n/a", + "workAddress.state": "n/a", + "workAddress.zipCode": "n/a", + "adaPriorities": "No", + "annualIncome": "n/a", + "applicationType": "Paper", + "mailingAddress.street": "n/a", + "mailingAddress.street2": "n/a", + "mailingAddress.city": "n/a", + "mailingAddress.zipCode": "n/a", + "mailingAddress.state": "n/a", + "mailingAddress.stateCode": "n/a" +} diff --git a/sites/partners/cypress/fixtures/applicantOnlyData.json b/sites/partners/cypress/fixtures/applicantOnlyData.json new file mode 100644 index 0000000000..9ed74656b7 --- /dev/null +++ b/sites/partners/cypress/fixtures/applicantOnlyData.json @@ -0,0 +1,76 @@ +{ + "language": "English", + "applicant.firstName": "Applicant", + "applicant.middleName": "Only", + "applicant.lastName": "Test", + "dateOfBirth.birthMonth": "10", + "dateOfBirth.birthDay": "5", + "dateOfBirth.birthYear": "2000", + "applicant.emailAddress": "Applicant@only.test", + "phoneNumber": "5202588811", + "applicant.phoneNumberType": "Home", + "additionalPhoneNumber": "n/a", + "additionalPhoneNumberType": "n/a", + "applicant.workInRegion": "Yes", + "applicant.address.street": "123 e street avenue", + "applicant.address.street2": "n/a", + "applicant.address.city": "pheonix", + "applicant.address.state": "Arizona", + "applicant.address.stateCode": "AZ", + "applicant.address.zipCode": "85745", + "alternateContact.firstName": "n/a", + "alternateContact.lastName": "n/a", + "alternateContact.agency": "n/a", + "alternateContact.emailAddress": "n/a", + "alternateContact.phoneNumber": "n/a", + "alternateContact.type": "n/a", + "alternateContact.mailingAddress.street": "n/a", + "alternateContact.mailingAddress.street2": "n/a", + "alternateContact.mailingAddress.city": "n/a", + "alternateContact.mailingAddress.state": "n/a", + "alternateContact.mailingAddress.stateCode": "n/a", + "alternateContact.mailingAddress.zipCode": "n/a", + "householdExpectingChanges": "No", + "householdStudent": "No", + "incomePeriod": "n/a", + "incomeMonth": "n/a", + "incomeVouchers": "No", + "demographics.ethnicity": "n/a", + "acceptedTerms": "Yes", + "firstName": "n/a", + "middleName": "n/a", + "lastName": "n/a", + "dob-field-month": "n/a", + "dob-field-day": "n/a", + "dob-field-year": "n/a", + "relationship": "n/a", + "sameAddress": "n/a", + "workInRegion": "n/a", + "submittedBy": "Applicant Test", + "dateOfBirth": "10/5/2000", + "formattedPhoneNumber": "(520) 258-8811", + "formattedAdditionalPhoneNumber": "n/a", + "alternateContact.formattedPhoneNumber": "n/a", + "householdMemberDoB": "n/a", + "householdMemberName": "n/a", + "preferredUnitSize": "n/a", + "formattedMonthlyIncome": "n/a", + "preferredContact": "Email", + "preferredUnit": "oneBdrm", + "submittedDate": "n/a", + "timeDate": "n/a", + "workAddress.streetAddress": "n/a", + "workAddress.street2": "n/a", + "workAddress.city": "n/a", + "workAddress.state": "n/a", + "workAddress.zipCode": "n/a", + "adaPriorities": "No", + "annualIncome": "n/a", + "applicationType": "Paper", + "mailingAddress.street": "123 e street avenue", + "mailingAddress.street2": "n/a", + "mailingAddress.city": "pheonix", + "mailingAddress.zipCode": "85745", + "mailingAddress.state": "Arizona", + "mailingAddress.stateCode": "AZ" +} diff --git a/sites/partners/cypress/fixtures/application.json b/sites/partners/cypress/fixtures/application.json new file mode 100644 index 0000000000..bf5bd9a356 --- /dev/null +++ b/sites/partners/cypress/fixtures/application.json @@ -0,0 +1,76 @@ +{ + "language": "English", + "applicant.firstName": "First Name", + "applicant.middleName": "Middle Name", + "applicant.lastName": "Last Name", + "dateOfBirth.birthMonth": 12, + "dateOfBirth.birthDay": 17, + "dateOfBirth.birthYear": 1993, + "applicant.emailAddress": "testEmail@exygy.com", + "phoneNumber": "5202458811", + "applicant.phoneNumberType": "Cell", + "additionalPhoneNumber": "5202587847", + "additionalPhoneNumberType": "Home", + "applicant.workInRegion": "No", + "applicant.address.street": "2325 w example trail", + "applicant.address.street2": "test", + "applicant.address.city": "San Francisco", + "applicant.address.state": "California", + "applicant.address.stateCode": "CA", + "applicant.address.zipCode": "85755", + "alternateContact.firstName": "Alt First Name", + "alternateContact.lastName": "Alt Last Name", + "alternateContact.agency": "Last Name", + "alternateContact.emailAddress": "altEmail@exygy.com", + "alternateContact.phoneNumber": "5202458811", + "alternateContact.type": "Friend", + "alternateContact.mailingAddress.street": "198 e solution drive", + "alternateContact.mailingAddress.street2": "Test 2", + "alternateContact.mailingAddress.city": "San Francisco 2", + "alternateContact.mailingAddress.state": "California", + "alternateContact.mailingAddress.stateCode": "CA", + "alternateContact.mailingAddress.zipCode": "85748", + "householdExpectingChanges": "No", + "householdStudent": "No", + "incomePeriod": "Month", + "incomeMonth": "5000", + "incomeVouchers": "No", + "demographics.ethnicity": "Not Hispanic / Latino", + "acceptedTerms": "Yes", + "firstName": "HouseHold First Name", + "middleName": "HouseHold Middle Name", + "lastName": "HouseHold Last Name", + "dob-field-month": "1", + "dob-field-day": "2", + "dob-field-year": "2000", + "relationship": "Friend", + "sameAddress": "Yes", + "workInRegion": "No", + "submittedBy": "First Name Last Name", + "dateOfBirth": "12/17/1993", + "formattedPhoneNumber": "(520) 245-8811", + "formattedAdditionalPhoneNumber": "(520) 258-7847", + "alternateContact.formattedPhoneNumber": "(520) 245-8811", + "householdMemberDoB": "1/2/2000", + "householdMemberName": "HouseHold First Name HouseHold Middle Name HouseHold Last Name", + "preferredUnitSize": "1 Bedroom", + "formattedMonthlyIncome": "$5,000", + "preferredContact": "Email", + "preferredUnit": "oneBdrm", + "submittedDate": "n/a", + "timeDate": "n/a", + "workAddress.streetAddress": "n/a", + "workAddress.street2": "n/a", + "workAddress.city": "n/a", + "workAddress.state": "n/a", + "workAddress.zipCode": "n/a", + "adaPriorities": "No", + "annualIncome": "n/a", + "applicationType": "Paper", + "mailingAddress.street": "2325 w example trail", + "mailingAddress.street2": "test", + "mailingAddress.city": "San Francisco", + "mailingAddress.state": "California", + "mailingAddress.stateCode": "CA", + "mailingAddress.zipCode": "85755" +} diff --git a/sites/partners/cypress/fixtures/cypress-automated-image-upload-071e2ab9-5a52-4f34-85f0-e41f696f4b96.jpeg b/sites/partners/cypress/fixtures/cypress-automated-image-upload-071e2ab9-5a52-4f34-85f0-e41f696f4b96.jpeg new file mode 100644 index 0000000000..8e36785ad8 Binary files /dev/null and b/sites/partners/cypress/fixtures/cypress-automated-image-upload-071e2ab9-5a52-4f34-85f0-e41f696f4b96.jpeg differ diff --git a/ui-components/public/images/listing.jpg b/sites/partners/cypress/fixtures/cypress-automated-image-upload-46806882-b98d-49d7-ac83-8016ab4b2f08.jpg similarity index 100% rename from ui-components/public/images/listing.jpg rename to sites/partners/cypress/fixtures/cypress-automated-image-upload-46806882-b98d-49d7-ac83-8016ab4b2f08.jpg diff --git a/sites/partners/cypress/fixtures/demographicsOnlyData.json b/sites/partners/cypress/fixtures/demographicsOnlyData.json new file mode 100644 index 0000000000..5fdee71374 --- /dev/null +++ b/sites/partners/cypress/fixtures/demographicsOnlyData.json @@ -0,0 +1,76 @@ +{ + "language": "n/a", + "applicant.firstName": "n/a", + "applicant.middleName": "n/a", + "applicant.lastName": "n/a", + "dateOfBirth.birthMonth": "n/a", + "dateOfBirth.birthDay": "n/a", + "dateOfBirth.birthYear": "n/a", + "applicant.emailAddress": "n/a", + "phoneNumber": "n/a", + "applicant.phoneNumberType": "n/a", + "additionalPhoneNumber": "n/a", + "additionalPhoneNumberType": "n/a", + "applicant.workInRegion": "n/a", + "applicant.address.street": "n/a", + "applicant.address.street2": "n/a", + "applicant.address.city": "n/a", + "applicant.address.state": "n/a", + "applicant.address.stateCode": "n/a", + "applicant.address.zipCode": "n/a", + "alternateContact.firstName": "n/a", + "alternateContact.lastName": "n/a", + "alternateContact.agency": "n/a", + "alternateContact.emailAddress": "n/a", + "alternateContact.phoneNumber": "n/a", + "alternateContact.type": "n/a", + "alternateContact.mailingAddress.street": "n/a", + "alternateContact.mailingAddress.street2": "n/a", + "alternateContact.mailingAddress.city": "n/a", + "alternateContact.mailingAddress.state": "n/a", + "alternateContact.mailingAddress.stateCode": "n/a", + "alternateContact.mailingAddress.zipCode": "n/a", + "householdExpectingChanges": "No", + "householdStudent": "No", + "incomePeriod": "n/a", + "incomeMonth": "n/a", + "incomeVouchers": "No", + "demographics.ethnicity": "Not Hispanic / Latino", + "acceptedTerms": "Yes", + "firstName": "n/a", + "middleName": "n/a", + "lastName": "n/a", + "dob-field-month": "n/a", + "dob-field-day": "n/a", + "dob-field-year": "n/a", + "relationship": "n/a", + "sameAddress": "n/a", + "workInRegion": "n/a", + "submittedBy": "n/a", + "dateOfBirth": "n/a", + "formattedPhoneNumber": "n/a", + "formattedAdditionalPhoneNumber": "n/a", + "alternateContact.formattedPhoneNumber": "n/a", + "householdMemberDoB": "n/a", + "householdMemberName": "n/a", + "preferredUnitSize": "n/a", + "formattedMonthlyIncome": "n/a", + "preferredContact": "n/a", + "preferredUnit": "oneBdrm", + "submittedDate": "n/a", + "timeDate": "n/a", + "workAddress.streetAddress": "n/a", + "workAddress.street2": "n/a", + "workAddress.city": "n/a", + "workAddress.state": "n/a", + "workAddress.zipCode": "n/a", + "adaPriorities": "No", + "annualIncome": "n/a", + "applicationType": "Paper", + "mailingAddress.street": "n/a", + "mailingAddress.street2": "n/a", + "mailingAddress.city": "n/a", + "mailingAddress.zipCode": "n/a", + "mailingAddress.state": "n/a", + "mailingAddress.stateCode": "n/a" +} diff --git a/sites/partners/cypress/fixtures/emptyApplication.json b/sites/partners/cypress/fixtures/emptyApplication.json new file mode 100644 index 0000000000..825f431a7c --- /dev/null +++ b/sites/partners/cypress/fixtures/emptyApplication.json @@ -0,0 +1,76 @@ +{ + "language": "n/a", + "applicant.firstName": "n/a", + "applicant.middleName": "n/a", + "applicant.lastName": "n/a", + "dateOfBirth.birthMonth": "n/a", + "dateOfBirth.birthDay": "n/a", + "dateOfBirth.birthYear": "n/a", + "applicant.emailAddress": "n/a", + "phoneNumber": "n/a", + "applicant.phoneNumberType": "n/a", + "additionalPhoneNumber": "n/a", + "additionalPhoneNumberType": "n/a", + "applicant.workInRegion": "n/a", + "applicant.address.street": "n/a", + "applicant.address.street2": "n/a", + "applicant.address.city": "n/a", + "applicant.address.state": "n/a", + "applicant.address.stateCode": "n/a", + "applicant.address.zipCode": "n/a", + "alternateContact.firstName": "n/a", + "alternateContact.lastName": "n/a", + "alternateContact.agency": "n/a", + "alternateContact.emailAddress": "n/a", + "alternateContact.phoneNumber": "n/a", + "alternateContact.type": "n/a", + "alternateContact.mailingAddress.street": "n/a", + "alternateContact.mailingAddress.street2": "n/a", + "alternateContact.mailingAddress.city": "n/a", + "alternateContact.mailingAddress.state": "n/a", + "alternateContact.mailingAddress.stateCode": "n/a", + "alternateContact.mailingAddress.zipCode": "n/a", + "householdExpectingChanges": "No", + "householdStudent": "No", + "incomePeriod": "n/a", + "incomeMonth": "n/a", + "incomeVouchers": "No", + "demographics.ethnicity": "n/a", + "acceptedTerms": "Yes", + "firstName": "n/a", + "middleName": "n/a", + "lastName": "n/a", + "dob-field-month": "n/a", + "dob-field-day": "n/a", + "dob-field-year": "n/a", + "relationship": "n/a", + "sameAddress": "n/a", + "workInRegion": "n/a", + "submittedBy": "n/a", + "dateOfBirth": "n/a", + "formattedPhoneNumber": "n/a", + "formattedAdditionalPhoneNumber": "n/a", + "alternateContact.formattedPhoneNumber": "n/a", + "householdMemberDoB": "n/a", + "householdMemberName": "n/a", + "preferredUnitSize": "n/a", + "formattedMonthlyIncome": "n/a", + "preferredContact": "n/a", + "preferredUnit": "oneBdrm", + "submittedDate": "n/a", + "timeDate": "n/a", + "workAddress.streetAddress": "n/a", + "workAddress.street2": "n/a", + "workAddress.city": "n/a", + "workAddress.state": "n/a", + "workAddress.zipCode": "n/a", + "adaPriorities": "No", + "annualIncome": "n/a", + "applicationType": "Paper", + "mailingAddress.street": "n/a", + "mailingAddress.street2": "n/a", + "mailingAddress.city": "n/a", + "mailingAddress.zipCode": "n/a", + "mailingAddress.state": "n/a", + "mailingAddress.stateCode": "n/a" +} diff --git a/sites/partners/cypress/fixtures/householdDetailsOnlyData.json b/sites/partners/cypress/fixtures/householdDetailsOnlyData.json new file mode 100644 index 0000000000..fae1b742ab --- /dev/null +++ b/sites/partners/cypress/fixtures/householdDetailsOnlyData.json @@ -0,0 +1,76 @@ +{ + "language": "n/a", + "applicant.firstName": "n/a", + "applicant.middleName": "n/a", + "applicant.lastName": "n/a", + "dateOfBirth.birthMonth": "n/a", + "dateOfBirth.birthDay": "n/a", + "dateOfBirth.birthYear": "n/a", + "applicant.emailAddress": "n/a", + "phoneNumber": "n/a", + "applicant.phoneNumberType": "n/a", + "additionalPhoneNumber": "n/a", + "additionalPhoneNumberType": "n/a", + "applicant.workInRegion": "n/a", + "applicant.address.street": "n/a", + "applicant.address.street2": "n/a", + "applicant.address.city": "n/a", + "applicant.address.state": "n/a", + "applicant.address.stateCode": "n/a", + "applicant.address.zipCode": "n/a", + "alternateContact.firstName": "n/a", + "alternateContact.lastName": "n/a", + "alternateContact.agency": "n/a", + "alternateContact.emailAddress": "n/a", + "alternateContact.phoneNumber": "n/a", + "alternateContact.type": "n/a", + "alternateContact.mailingAddress.street": "n/a", + "alternateContact.mailingAddress.street2": "n/a", + "alternateContact.mailingAddress.city": "n/a", + "alternateContact.mailingAddress.state": "n/a", + "alternateContact.mailingAddress.stateCode": "n/a", + "alternateContact.mailingAddress.zipCode": "n/a", + "householdExpectingChanges": "Yes", + "householdStudent": "Yes", + "incomePeriod": "n/a", + "incomeMonth": "n/a", + "incomeVouchers": "No", + "demographics.ethnicity": "n/a", + "acceptedTerms": "Yes", + "firstName": "n/a", + "middleName": "n/a", + "lastName": "n/a", + "dob-field-month": "n/a", + "dob-field-day": "n/a", + "dob-field-year": "n/a", + "relationship": "n/a", + "sameAddress": "n/a", + "workInRegion": "n/a", + "submittedBy": "n/a", + "dateOfBirth": "n/a", + "formattedPhoneNumber": "n/a", + "formattedAdditionalPhoneNumber": "n/a", + "alternateContact.formattedPhoneNumber": "n/a", + "householdMemberDoB": "n/a", + "householdMemberName": "n/a", + "preferredUnitSize": "1 Bedroom", + "formattedMonthlyIncome": "n/a", + "preferredContact": "n/a", + "preferredUnit": "oneBdrm", + "submittedDate": "n/a", + "timeDate": "n/a", + "workAddress.streetAddress": "n/a", + "workAddress.street2": "n/a", + "workAddress.city": "n/a", + "workAddress.state": "n/a", + "workAddress.zipCode": "n/a", + "adaPriorities": "No", + "annualIncome": "n/a", + "applicationType": "Paper", + "mailingAddress.street": "n/a", + "mailingAddress.street2": "n/a", + "mailingAddress.city": "n/a", + "mailingAddress.zipCode": "n/a", + "mailingAddress.state": "n/a", + "mailingAddress.stateCode": "n/a" +} diff --git a/sites/partners/cypress/fixtures/householdIncomeOnlyData.json b/sites/partners/cypress/fixtures/householdIncomeOnlyData.json new file mode 100644 index 0000000000..6068fcee77 --- /dev/null +++ b/sites/partners/cypress/fixtures/householdIncomeOnlyData.json @@ -0,0 +1,76 @@ +{ + "language": "n/a", + "applicant.firstName": "n/a", + "applicant.middleName": "n/a", + "applicant.lastName": "n/a", + "dateOfBirth.birthMonth": "n/a", + "dateOfBirth.birthDay": "n/a", + "dateOfBirth.birthYear": "n/a", + "applicant.emailAddress": "n/a", + "phoneNumber": "n/a", + "applicant.phoneNumberType": "n/a", + "additionalPhoneNumber": "n/a", + "additionalPhoneNumberType": "n/a", + "applicant.workInRegion": "n/a", + "applicant.address.street": "n/a", + "applicant.address.street2": "n/a", + "applicant.address.city": "n/a", + "applicant.address.state": "n/a", + "applicant.address.stateCode": "n/a", + "applicant.address.zipCode": "n/a", + "alternateContact.firstName": "n/a", + "alternateContact.lastName": "n/a", + "alternateContact.agency": "n/a", + "alternateContact.emailAddress": "n/a", + "alternateContact.phoneNumber": "n/a", + "alternateContact.type": "n/a", + "alternateContact.mailingAddress.street": "n/a", + "alternateContact.mailingAddress.street2": "n/a", + "alternateContact.mailingAddress.city": "n/a", + "alternateContact.mailingAddress.state": "n/a", + "alternateContact.mailingAddress.stateCode": "n/a", + "alternateContact.mailingAddress.zipCode": "n/a", + "householdExpectingChanges": "No", + "householdStudent": "No", + "incomePeriod": "Month", + "incomeMonth": "6000", + "incomeVouchers": "Yes", + "demographics.ethnicity": "n/a", + "acceptedTerms": "Yes", + "firstName": "n/a", + "middleName": "n/a", + "lastName": "n/a", + "dob-field-month": "n/a", + "dob-field-day": "n/a", + "dob-field-year": "n/a", + "relationship": "n/a", + "sameAddress": "n/a", + "workInRegion": "n/a", + "submittedBy": "n/a", + "dateOfBirth": "n/a", + "formattedPhoneNumber": "n/a", + "formattedAdditionalPhoneNumber": "n/a", + "alternateContact.formattedPhoneNumber": "n/a", + "householdMemberDoB": "n/a", + "householdMemberName": "n/a", + "preferredUnitSize": "n/a", + "formattedMonthlyIncome": "$6,000", + "preferredContact": "n/a", + "preferredUnit": "oneBdrm", + "submittedDate": "n/a", + "timeDate": "n/a", + "workAddress.streetAddress": "n/a", + "workAddress.street2": "n/a", + "workAddress.city": "n/a", + "workAddress.state": "n/a", + "workAddress.zipCode": "n/a", + "adaPriorities": "No", + "annualIncome": "n/a", + "applicationType": "Paper", + "mailingAddress.street": "n/a", + "mailingAddress.street2": "n/a", + "mailingAddress.city": "n/a", + "mailingAddress.zipCode": "n/a", + "mailingAddress.state": "n/a", + "mailingAddress.stateCode": "n/a" +} diff --git a/sites/partners/cypress/fixtures/householdMemberOnlyData.json b/sites/partners/cypress/fixtures/householdMemberOnlyData.json new file mode 100644 index 0000000000..818e388730 --- /dev/null +++ b/sites/partners/cypress/fixtures/householdMemberOnlyData.json @@ -0,0 +1,76 @@ +{ + "language": "n/a", + "applicant.firstName": "n/a", + "applicant.middleName": "n/a", + "applicant.lastName": "n/a", + "dateOfBirth.birthMonth": "n/a", + "dateOfBirth.birthDay": "n/a", + "dateOfBirth.birthYear": "n/a", + "applicant.emailAddress": "n/a", + "phoneNumber": "n/a", + "applicant.phoneNumberType": "n/a", + "additionalPhoneNumber": "n/a", + "additionalPhoneNumberType": "n/a", + "applicant.workInRegion": "n/a", + "applicant.address.street": "n/a", + "applicant.address.street2": "n/a", + "applicant.address.city": "n/a", + "applicant.address.state": "n/a", + "applicant.address.stateCode": "n/a", + "applicant.address.zipCode": "n/a", + "alternateContact.firstName": "n/a", + "alternateContact.lastName": "n/a", + "alternateContact.agency": "n/a", + "alternateContact.emailAddress": "n/a", + "alternateContact.phoneNumber": "n/a", + "alternateContact.type": "n/a", + "alternateContact.mailingAddress.street": "n/a", + "alternateContact.mailingAddress.street2": "n/a", + "alternateContact.mailingAddress.city": "n/a", + "alternateContact.mailingAddress.state": "n/a", + "alternateContact.mailingAddress.stateCode": "n/a", + "alternateContact.mailingAddress.zipCode": "n/a", + "householdExpectingChanges": "No", + "householdStudent": "No", + "incomePeriod": "n/a", + "incomeMonth": "n/a", + "incomeVouchers": "No", + "demographics.ethnicity": "n/a", + "acceptedTerms": "Yes", + "firstName": "household", + "middleName": "member", + "lastName": "test", + "dob-field-month": "4", + "dob-field-day": "18", + "dob-field-year": "1999", + "relationship": "Friend", + "sameAddress": "Yes", + "workInRegion": "No", + "submittedBy": "n/a", + "dateOfBirth": "n/a", + "formattedPhoneNumber": "n/a", + "formattedAdditionalPhoneNumber": "n/a", + "alternateContact.formattedPhoneNumber": "n/a", + "householdMemberDoB": "4/18/1999", + "householdMemberName": "household member test", + "preferredUnitSize": "n/a", + "formattedMonthlyIncome": "n/a", + "preferredContact": "n/a", + "preferredUnit": "oneBdrm", + "submittedDate": "n/a", + "timeDate": "n/a", + "workAddress.streetAddress": "n/a", + "workAddress.street2": "n/a", + "workAddress.city": "n/a", + "workAddress.state": "n/a", + "workAddress.zipCode": "n/a", + "adaPriorities": "No", + "annualIncome": "n/a", + "applicationType": "Paper", + "mailingAddress.street": "n/a", + "mailingAddress.street2": "n/a", + "mailingAddress.city": "n/a", + "mailingAddress.zipCode": "n/a", + "mailingAddress.state": "n/a", + "mailingAddress.stateCode": "n/a" +} diff --git a/sites/partners/cypress/fixtures/listing.json b/sites/partners/cypress/fixtures/listing.json new file mode 100644 index 0000000000..41ddd4bee2 --- /dev/null +++ b/sites/partners/cypress/fixtures/listing.json @@ -0,0 +1,83 @@ +{ + "jurisdiction.id": "Detroit", + "name": "Basic Test Listing", + "developer": "Basic Test Developer", + "buildingAddress.street": "548 Market St. #59930", + "region": "Basic Test Region", + "neighborhood": "Airport Sub area", + "buildingAddress.city": "San Francisco", + "buildingAddress.state": "California", + "buildingAddress.zipCode": "94104", + "yearBuilt": "2021", + "number": "2", + "unitType.id": "One Bedroom", + "numBathrooms": "2", + "floor": "2", + "sqFeet": "300", + "monthlyIncomeMin": "900", + "monthlyRent": "1000", + "priorityType.id": "Visual", + "applicationFee": "4", + "depositMin": "2", + "depositMax": "100", + "costsNotIncluded": "Internet", + "amenities": "Basic Amenity Info", + "neighborhoodAmenities.groceryStores": "Basic Neighborhood Amenities Grocery Stores", + "neighborhoodAmenities.pharmacies": "Basic Neighborhood Amenities Pharmacies", + "neighborhoodAmenities.publicTransportation": "Basic Neighborhood Amenities Public Transportation", + "neighborhoodAmenities.schools": "Basic Neighborhood Amenities Schools", + "neighborhoodAmenities.parksAndCommunityCenters": "Basic Neighborhood Amenities Parks and Community Centers", + "neighborhoodAmenities.healthCareResources": "Basic Neighborhood Amenities Health Care Resources", + "accessibility": "Basic Accessibility Info", + "unitAmenities": "Basic Unit Amenity Info", + "smokingPolicy": "No Thanks", + "petPolicy": "Pets welcome. Please send in pictures, they aren't required, we just like pictures", + "servicesOffered": "Basic Services", + "creditHistory": "Basic Credit History", + "rentalHistory": "Basic Rental History", + "criminalBackground": "Basic Criminal Background", + "buildingSelectionCriteriaURL": "https://www.exygy.com", + "requiredDocuments": "Basic Required Documents", + "programRules": "Basic program rules", + "specialNotes": "basic special notes", + "leasingAgentName": "Basic Agent Name", + "leasingAgentEmail": "basicAgent@email.com", + "leasingAgentPhone": "520-245-8811", + "leasingAgentTitle": "Basic Agent Title", + "leasingAgentOfficeHours": "Basic Agent Office Hours", + "leasingAgentAddress.street": "548 Market St.", + "leasingAgentAddress.street2": "#59930", + "leasingAgentAddress.city": "San Francisco", + "leasingAgentAddress.zipCode": "94104", + "leasingAgentAddress.state": "California", + "additionalApplicationSubmissionNotes": "Basic Additional Application Submission Notes", + "date.month": "10", + "date.day": "04", + "date.year": "2022", + "label": "Basic Label", + "url": "https://www.exygy.com", + "startTime.hours": "10", + "startTime.minutes": "04", + "endTime.hours": "11", + "endTime.minutes": "05", + "note": "Basic Note", + "totalCount": "13", + "sqFeetMin": "11", + "sqFeetMax": "12", + "totalAvailable": "10", + "minOccupancy": "1", + "maxOccupancy": "2", + "floorMin": "3", + "floorMax": "4", + "bathroomMin": "4", + "bathroomMax": "5", + "amiChartId": "HUD 2021", + "amiPercentage": "30", + "flatRentValue": "200", + "postmark-date-field-month": "12", + "postmark-date-field-day": "17", + "postmark-date-field-year": "2022", + "postmark-time-field-hours": "5", + "postmark-time-field-minutes": "45", + "postmark-time-field-period": "PM" +} diff --git a/sites/partners/cypress/fixtures/listingImage.json b/sites/partners/cypress/fixtures/listingImage.json new file mode 100644 index 0000000000..6d04fb471f --- /dev/null +++ b/sites/partners/cypress/fixtures/listingImage.json @@ -0,0 +1,3 @@ +{ + "filePath": "cypress/fixtures/cypress-automated-image-upload-071e2ab9-5a52-4f34-85f0-e41f696f4b96.jpeg" +} diff --git a/sites/partners/cypress/fixtures/mfaUser.json b/sites/partners/cypress/fixtures/mfaUser.json new file mode 100644 index 0000000000..759500ac45 --- /dev/null +++ b/sites/partners/cypress/fixtures/mfaUser.json @@ -0,0 +1,5 @@ +{ + "email": "mfaUser@bloom.com", + "password": "abcdef12", + "mfaCode": "123456" +} diff --git a/sites/partners/cypress/fixtures/partialApplicationA.json b/sites/partners/cypress/fixtures/partialApplicationA.json new file mode 100644 index 0000000000..2570aa2203 --- /dev/null +++ b/sites/partners/cypress/fixtures/partialApplicationA.json @@ -0,0 +1,77 @@ +{ + "language": "n/a", + "applicant.firstName": "n/a", + "applicant.middleName": "n/a", + "applicant.lastName": "n/a", + "dateOfBirth.birthMonth": "n/a", + "dateOfBirth.birthDay": "n/a", + "dateOfBirth.birthYear": "n/a", + "applicant.emailAddress": "n/a", + "phoneNumber": "n/a", + "applicant.phoneNumberType": "n/a", + "additionalPhoneNumber": "n/a", + "additionalPhoneNumberType": "n/a", + "applicant.workInRegion": "n/a", + "applicant.address.street": "n/a", + "applicant.address.street2": "n/a", + "applicant.address.city": "n/a", + "applicant.address.state": "n/a", + "applicant.address.stateCode": "n/a", + "applicant.address.zipCode": "n/a", + "alternateContact.firstName": "n/a", + "alternateContact.lastName": "n/a", + "alternateContact.agency": "n/a", + "alternateContact.emailAddress": "n/a", + "alternateContact.phoneNumber": "n/a", + "alternateContact.type": "n/a", + "alternateContact.mailingAddress.street": "n/a", + "alternateContact.mailingAddress.street2": "n/a", + "alternateContact.mailingAddress.city": "n/a", + "alternateContact.mailingAddress.state": "n/a", + "alternateContact.mailingAddress.stateCode": "n/a", + "alternateContact.mailingAddress.zipCode": "n/a", + "householdExpectingChanges": "No", + "householdStudent": "No", + "incomePeriod": "Year", + "incomeMonth": "n/a", + "incomeVouchers": "No", + "demographics.ethnicity": "n/a", + "acceptedTerms": "Yes", + "firstName": "n/a", + "middleName": "n/a", + "lastName": "n/a", + "dob-field-month": "n/a", + "dob-field-day": "n/a", + "dob-field-year": "n/a", + "relationship": "n/a", + "sameAddress": "n/a", + "workInRegion": "n/a", + "submittedBy": "n/a", + "dateOfBirth": "n/a", + "formattedPhoneNumber": "n/a", + "formattedAdditionalPhoneNumber": "n/a", + "alternateContact.formattedPhoneNumber": "n/a", + "householdMemberDoB": "n/a", + "householdMemberName": "n/a", + "preferredUnitSize": "n/a", + "formattedMonthlyIncome": "n/a", + "preferredContact": "n/a", + "preferredUnit": "oneBdrm", + "submittedDate": "n/a", + "timeDate": "n/a", + "workAddress.streetAddress": "n/a", + "workAddress.street2": "n/a", + "workAddress.city": "n/a", + "workAddress.state": "n/a", + "workAddress.zipCode": "n/a", + "adaPriorities": "No", + "annualIncome": "$60,000", + "applicationType": "Paper", + "incomeYear": "60000", + "mailingAddress.street": "123 e delivery street", + "mailingAddress.street2": "23", + "mailingAddress.city": "Tucson", + "mailingAddress.zipCode": "85748", + "mailingAddress.state": "Arizona", + "mailingAddress.stateCode": "AZ" +} diff --git a/sites/partners/cypress/fixtures/user.json b/sites/partners/cypress/fixtures/user.json index eda513e2cd..7908ad362a 100644 --- a/sites/partners/cypress/fixtures/user.json +++ b/sites/partners/cypress/fixtures/user.json @@ -1,6 +1,8 @@ { - "email": "test@example.com", + "email": "admin@example.com", + "emailConfirmation": "admin@example.com", "password": "abcdef", + "passwordConfirmation": "abcdef", "firstName": "Test", "lastName": "User", "dob": "1980-01-01" diff --git a/sites/partners/cypress/integration/navigation.ts b/sites/partners/cypress/integration/navigation.ts deleted file mode 100644 index bdee0a73e9..0000000000 --- a/sites/partners/cypress/integration/navigation.ts +++ /dev/null @@ -1,41 +0,0 @@ -/// - -describe("Navigating around the site", () => { - before(() => { - cy.fixture("user").then((user) => cy.createUser(user)) - }) - - describe("with a logged out user", () => { - beforeEach(() => { - cy.logout() - }) - - it("visiting the homepage redirects to sign in page", () => { - cy.visit("/") - // Check if sign in prompt is displayed - cy.contains("Sign In") - }) - - it("Signs in", () => { - cy.visit("/") - cy.fixture("user").then((user) => { - cy.get("input#email").type(user.email) - cy.get("input#password").type(user.password) - cy.get(".button.is-filled").contains("Sign In").click() - cy.contains("This will be the home page") - }) - }) - }) - - describe("with a logged in user", () => { - beforeEach(() => { - cy.fixture("user").then(({ email, password }) => cy.login(email, password)) - }) - - it("Visits the applications page using the home page link", () => { - cy.visit("/") - cy.contains("View Submitted Applications").click() - cy.contains("List of Applications will go here.") - }) - }) -}) diff --git a/sites/partners/cypress/plugins/index.js b/sites/partners/cypress/plugins/index.js index 5311609525..12e655088d 100644 --- a/sites/partners/cypress/plugins/index.js +++ b/sites/partners/cypress/plugins/index.js @@ -1,4 +1,5 @@ -/// +/*eslint-env node*/ + // *********************************************************** // This example plugins/index.js can be used to load plugins // @@ -9,39 +10,14 @@ // https://on.cypress.io/plugins-guide // *********************************************************** -// Use Webpack to compile Typescript files so that we can use TypeScript in tests -// eslint-disable-next-line @typescript-eslint/no-var-requires,no-undef -const webpack = require("@cypress/webpack-preprocessor") - -/** - * @type {Cypress.PluginConfig} - */ -export default (on) => { - const options = { - webpackOptions: { - resolve: { - extensions: [".js", ".jsx", ".ts", ".tsx"], - }, - module: { - rules: [ - { - test: /\.tsx?$/, - use: "ts-loader", - exclude: /node_modules/, - }, - // Ignore imported css files when building cypress tests (this doesn't affect the actual website build, - // just the tests & utilities themselves) - { - test: /\.(sa|sc|c)ss$/, - loader: "ignore-loader", - }, - ], - }, - }, - watchOptions: {}, - } +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) +// eslint-disable-next-line @typescript-eslint/no-unused-vars +module.exports = (on, config) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + require("@cypress/code-coverage/task")(on, config) // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config - on("file:preprocessor", webpack(options)) + return config } diff --git a/sites/partners/cypress/support/commands.js b/sites/partners/cypress/support/commands.js new file mode 100644 index 0000000000..0acc40630f --- /dev/null +++ b/sites/partners/cypress/support/commands.js @@ -0,0 +1,107 @@ +/* eslint-disable no-undef */ + +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +import "cypress-file-upload" + +Cypress.Commands.add("getByID", (id, ...args) => { + return cy.get(`#${CSS.escape(id)}`, ...args) +}) + +Cypress.Commands.add("getByTestId", (testId) => { + return cy.get(`[data-testid="${testId}"]`) +}) + +Cypress.Commands.add("login", () => { + cy.visit("/") + cy.fixture("user").then((user) => { + cy.get("input#email").type(user.email) + cy.get("input#password").type(user.password) + cy.get(".button").contains("Sign in").click() + cy.contains("Listings") + }) +}) + +Cypress.Commands.add("signOut", () => { + cy.get("button").contains("Sign out").click() + cy.get("input#email") +}) + +const fillFormFieldHelper = (byTestID, fieldKey, fixtureKey, fixture, shouldSelect) => { + let elem + if (byTestID) { + elem = cy.getByTestId(fieldKey) + } else { + elem = cy.getByID(fieldKey) + } + + if (shouldSelect) { + elem.select(fixture[fixtureKey]) + } else { + elem.type(fixture[fixtureKey]) + } +} + +Cypress.Commands.add("fillFormFields", (fixture, fieldsToType, fieldsToSelect) => { + cy.fixture(fixture).then((obj) => { + fieldsToType.forEach(({ byTestID = false, fieldID, fixtureID = fieldID }) => { + fillFormFieldHelper(byTestID, fieldID, fixtureID, obj, false) + }) + + fieldsToSelect.forEach(({ byTestID = false, fieldID, fixtureID = fieldID }) => { + fillFormFieldHelper(byTestID, fieldID, fixtureID, obj, true) + }) + }) +}) + +const verifyHelper = (byTestID, fieldKey, fixtureKey, fixture, hardcodedValue) => { + let elem + if (byTestID) { + elem = cy.getByTestId(fieldKey) + } else { + elem = cy.getByID(fieldKey) + } + + const val = hardcodedValue ?? fixture[fixtureKey] + elem.contains(val).should("have.text", val) +} + +Cypress.Commands.add("verifyFormFields", (fixture, fieldsToVerify) => { + cy.fixture(fixture).then((obj) => { + fieldsToVerify.forEach(({ byTestID = false, fieldID, fixtureID = fieldID, hardcodedValue }) => { + verifyHelper(byTestID, fieldID, fixtureID, obj, hardcodedValue) + }) + }) +}) + +// see: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded +const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/ +Cypress.on("uncaught:exception", (err) => { + /* returning false here prevents Cypress from failing the test */ + if (resizeObserverLoopErrRe.test(err.message)) { + return false + } +}) diff --git a/sites/partners/cypress/support/e2e.js b/sites/partners/cypress/support/e2e.js new file mode 100644 index 0000000000..5b1090d1ed --- /dev/null +++ b/sites/partners/cypress/support/e2e.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** +import "@cypress/code-coverage/support" +// Import commands.js using ES2015 syntax: +import "./commands" + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/sites/partners/cypress/support/helpers.ts b/sites/partners/cypress/support/helpers.ts new file mode 100644 index 0000000000..c8305a8daa --- /dev/null +++ b/sites/partners/cypress/support/helpers.ts @@ -0,0 +1,2 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const listingsUrl = "http://localhost:3100/listings?limit=all" diff --git a/sites/partners/cypress/support/index.d.ts b/sites/partners/cypress/support/index.d.ts new file mode 100644 index 0000000000..21821d99da --- /dev/null +++ b/sites/partners/cypress/support/index.d.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +type attachFileSubjectArgs = { + subjectType: string +} + +interface fillFromFieldOption { + byTestID?: boolean + fieldID: string + fixtureID?: string + hardcodedValue?: string +} + +declare namespace Cypress { + interface Chainable { + /** + * Custom command to select DOM element by data-cy attribute. + * @example cy.dataCy('greeting') + */ + getByID(value: string): Chainable + getByTestId(value: string): Chainable + login(): Chainable + verifyAlertBox(): Chainable + signOut(): Chainable + fillFormFields( + fixture: string, + fieldsToType: fillFromFieldOption[], + fieldsToSelect: fillFromFieldOption[] + ): Chainable + verifyFormFields(fixture: string, verifyFormFields: fillFromFieldOption[]): Chainable + } +} +/* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/sites/partners/cypress/support/index.ts b/sites/partners/cypress/support/index.ts deleted file mode 100644 index cc5b61d8f3..0000000000 --- a/sites/partners/cypress/support/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.ts using ES2015 syntax: -import { ACCESS_TOKEN_LOCAL_STORAGE_KEY } from "@bloom-housing/ui-components" - -// TODO: Replace this with a DTO to avoid duplication -type UserFields = { - email: string - firstName: string - middleName?: string - lastName: string - dob: Date - password: string -} - -// If TypeScript considers this file a module (as opposed to a script), then we need to define this as a global for -// it to register. See: -// https://github.com/cypress-io/add-cypress-custom-command-in-typescript/issues/2#issuecomment-389870033 -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Cypress { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Chainable { - /** - * Login to the app using the specified email/password - * @example cy.login('email@example.com", "123abc").then(...) - */ - login(email: string, password: string, options?: { apiBase: string }): Chainable - - /** - * Logout from the app (clears session storage's saved version of the key.) - */ - logout(): Chainable - - /** - * Create a new user with the specified options. - */ - createUser(user: UserFields, options?: { apiBase: string }): Chainable - } - } -} - -Cypress.Commands.add( - "login", - (email: string, password: string, { apiBase = "http://localhost:3100" } = {}) => { - return cy - .request({ - url: `${apiBase}/auth/login`, - method: "POST", - body: { email, password }, - }) - .its("body") - .then(({ accessToken }) => { - cy.window().then((window) => - window.sessionStorage.setItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY, accessToken) - ) - }) - } -) - -Cypress.Commands.add("logout", () => - cy.window().then((window) => window.sessionStorage.removeItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY)) -) - -Cypress.Commands.add("createUser", (user: UserFields, { apiBase = "http://localhost:3100" } = {}) => - cy - .request({ - url: `${apiBase}/user`, - method: "POST", - body: user, - }) - .its("body") - .then(({ accessToken }) => accessToken) -) diff --git a/sites/partners/cypress/tsconfig.json b/sites/partners/cypress/tsconfig.json index 5c9a5d0285..1492c473ce 100644 --- a/sites/partners/cypress/tsconfig.json +++ b/sites/partners/cypress/tsconfig.json @@ -1,12 +1,9 @@ { - // "extends": "../tsconfig.json", "compilerOptions": { - "types": ["cypress"], - "isolatedModules": false, - "noEmit": false, - "jsx": "react", - "experimentalDecorators": true, - "esModuleInterop": true + "strict": true, + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "cypress-file-upload"] }, - "include": ["./**/*.ts", "../../../node_modules/cypress/types/cypress-global-vars.d.ts"] + "include": ["**/*.ts"] } diff --git a/sites/partners/jest.config.js b/sites/partners/jest.config.js new file mode 100644 index 0000000000..31f6b0b171 --- /dev/null +++ b/sites/partners/jest.config.js @@ -0,0 +1,36 @@ +/*eslint no-undef: "error"*/ +/*eslint-env node*/ + +process.env.TZ = "UTC" + +module.exports = { + testRegex: ["/*.test.tsx$", "/*.test.ts$"], + collectCoverageFrom: ["**/*.ts", "!**/*.tsx"], + coverageReporters: ["lcov", "text"], + coverageDirectory: "test-coverage", + coverageThreshold: { + global: { + branches: 0, + functions: 0, + lines: 0, + statements: 0, + }, + }, + preset: "ts-jest", + globals: { + "ts-jest": { + tsconfig: "tsconfig.test.json", + }, + }, + rootDir: "../..", + roots: ["/sites/partners"], + transform: { + "^.+\\.[t|j]sx?$": "ts-jest", + }, + setupFiles: ["dotenv/config"], + setupFilesAfterEnv: ["/sites/partners/.jest/setup-tests.js"], + transformIgnorePatterns: ["node_modules/?!(@bloom-housing/ui-components)"], + moduleNameMapper: { + "\\.(scss|css|less)$": "identity-obj-proxy", + }, +} diff --git a/sites/partners/layouts/forms.tsx b/sites/partners/layouts/forms.tsx deleted file mode 100644 index bb7c3b17e6..0000000000 --- a/sites/partners/layouts/forms.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Layout from "." - -const Forms = (props) => { - return ( - -
-
- {props.children} -
-
-
- ) -} - -export default Forms diff --git a/sites/partners/layouts/index.tsx b/sites/partners/layouts/index.tsx deleted file mode 100644 index 51e901d5bb..0000000000 --- a/sites/partners/layouts/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useContext } from "react" -import Head from "next/head" -import { useRouter } from "next/router" -import { - LocalizedLink, - SiteHeader, - SiteFooter, - FooterNav, - FooterSection, - ExygyFooter, - t, - AuthContext, - MenuLink, - setSiteAlertMessage, -} from "@bloom-housing/ui-components" - -const Layout = (props) => { - const { profile, signOut } = useContext(AuthContext) - const router = useRouter() - - const menuLinks: MenuLink[] = [] - if (profile) { - menuLinks.push({ - title: t("nav.listings"), - href: "/", - }) - } - if (profile?.roles?.isAdmin) { - menuLinks.push({ - title: t("nav.users"), - href: "/users", - }) - } - if (profile) { - menuLinks.push({ - title: t("nav.signOut"), - onClick: async () => { - setSiteAlertMessage(t(`authentication.signOut.success`), "notice") - await router.push("/sign-in") - signOut() - }, - }) - } - return ( -
-
- - {t("nav.siteTitlePartners")} - - - - -
{props.children}
- - - - {t("pageTitle.privacy")} - {t("pageTitle.disclaimer")} - - - - - -
-
- ) -} - -export default Layout diff --git a/sites/partners/lib/formatApplicationData.ts b/sites/partners/lib/formatApplicationData.ts deleted file mode 100644 index cdde1f6214..0000000000 --- a/sites/partners/lib/formatApplicationData.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { - ApplicationUpdate, - ApplicantUpdate, - Language, - IncomePeriod, - ApplicationSubmissionType, - ApplicationStatus, - AddressUpdate, - HouseholdMember, -} from "@bloom-housing/backend-core/types" - -import { - TimeFieldPeriod, - mapPreferencesToApi, - mapApiToPreferencesForm, -} from "@bloom-housing/ui-components" -import { - FormTypes, - YesNoAnswer, - ApplicationTypes, - Address, -} from "../src/applications/PaperApplicationForm/FormTypes" -import moment from "moment" -/* - Some of fields are optional, not active, so it occurs 'undefined' as value. - This function eliminates those fields and parse to a proper format. -*/ - -const getAddress = (condition: boolean, addressData?: Address): AddressUpdate => { - const blankAddress: AddressUpdate = { - street: "", - street2: "", - city: "", - state: "", - zipCode: "", - } - - return condition ? (addressData as AddressUpdate) : blankAddress -} - -const mapEmptyStringToNull = (value: string) => (value === "" ? null : value) - -interface FormData extends FormTypes { - householdMembers: HouseholdMember[] - submissionType: ApplicationSubmissionType -} - -/* - Format data which comes from react-hook-form into correct API format. -*/ - -export const mapFormToApi = (data: FormData, listingId: string, editMode: boolean) => { - const language: Language | null = data.application?.language ? data.application?.language : null - - const submissionDate: Date | null = (() => { - const TIME_24H_FORMAT = "MM/DD/YYYY HH:mm:ss" - - // rename default (wrong property names) - const { day: submissionDay, month: submissionMonth, year: submissionYear } = - data.dateSubmitted || {} - const { hours, minutes = 0, seconds = 0, period } = data?.timeSubmitted || {} - - if (!submissionDay || !submissionMonth || !submissionYear) return null - - const dateString = moment( - `${submissionMonth}/${submissionDay}/${submissionYear} ${hours}:${minutes}:${seconds} ${period}`, - "MM/DD/YYYY hh:mm:ss A" - ).format(TIME_24H_FORMAT) - - const formattedDate = moment(dateString, TIME_24H_FORMAT).utc(true).toDate() - - return formattedDate - })() - - // create applicant - const applicant = ((): ApplicantUpdate => { - const phoneNumber: string | null = data?.phoneNumber || null - const { applicant: applicantData } = data.application - const phoneNumberType: string | null = applicantData.phoneNumberType || null - const noEmail = !applicantData.emailAddress - const noPhone = !phoneNumber - const workInRegion: string | null = applicantData?.workInRegion || null - const emailAddress: string | null = applicantData?.emailAddress || null - - applicantData.firstName = mapEmptyStringToNull(applicantData.firstName) - applicantData.lastName = mapEmptyStringToNull(applicantData.firstName) - - const workAddress = getAddress( - applicantData?.workInRegion === YesNoAnswer.Yes, - applicantData?.workAddress - ) - - return { - ...applicantData, - ...data.dateOfBirth, - emailAddress, - workInRegion, - workAddress, - phoneNumber, - phoneNumberType, - noEmail, - noPhone, - } - })() - - const preferences = mapPreferencesToApi(data) - - // additional phone - const { - additionalPhoneNumber: additionalPhoneNumberData, - additionalPhoneNumberType: additionalPhoneNumberTypeData, - mailingAddress: mailingAddressData, - additionalPhoneNumber, - contactPreferences, - sendMailToMailingAddress, - accessibility, - demographics, - } = data.application - - const additionalPhone = !additionalPhoneNumberData - const additionalPhoneNumberType = additionalPhoneNumberTypeData - ? additionalPhoneNumberTypeData - : null - - const mailingAddress = getAddress(sendMailToMailingAddress, mailingAddressData) - - const alternateContact = data.application.alternateContact - - // send null instead of empty string - alternateContact.emailAddress = alternateContact.emailAddress || null - - // pass blank address, not used for now everywhere - const alternateAddress = getAddress(false, null) - - const { incomeMonth, incomeYear, householdMembers } = data - - const incomePeriod: IncomePeriod | null = data.application?.incomePeriod || null - - const income = incomePeriod === IncomePeriod.perMonth ? incomeMonth : incomeYear || null - const incomeVouchers = - data.application.incomeVouchers === YesNoAnswer.Yes - ? true - : data.application.incomeVouchers === YesNoAnswer.No - ? false - : null - - const acceptedTerms = - data.application.acceptedTerms === YesNoAnswer.Yes - ? true - : data.application.acceptedTerms === YesNoAnswer.No - ? false - : null - - const submissionType = editMode ? data.submissionType : ApplicationSubmissionType.paper - const status = ApplicationStatus.submitted - - const listing = { - id: listingId, - } - - // we need to add primary applicant - const householdSize = householdMembers.length + 1 || 1 - - const preferredUnit = data.application?.preferredUnit - ? data.application.preferredUnit?.map((id) => ({ id })) - : [] - - const result: ApplicationUpdate = { - submissionDate, - language, - applicant, - additionalPhone, - additionalPhoneNumber, - additionalPhoneNumberType, - contactPreferences, - sendMailToMailingAddress, - mailingAddress, - alternateContact, - accessibility, - preferences, - income, - incomePeriod, - incomeVouchers, - demographics, - acceptedTerms, - submissionType, - status, - listing, - preferredUnit, - alternateAddress, - householdMembers, - householdSize, - } - - return result -} - -/* - Format data which comes from the API into correct react-hook-form format. -*/ - -export const mapApiToForm = (applicationData: ApplicationUpdate) => { - const submissionDate = applicationData.submissionDate - ? moment(new Date(applicationData.submissionDate)).utc() - : null - - const dateOfBirth = (() => { - const { birthDay, birthMonth, birthYear } = applicationData.applicant - - return { - birthDay, - birthMonth, - birthYear, - } - })() - - const incomePeriod = applicationData.incomePeriod - const incomeMonth = incomePeriod === "perMonth" ? applicationData.income : null - const incomeYear = incomePeriod === "perYear" ? applicationData.income : null - - const timeSubmitted = (() => { - if (!submissionDate) return - - const hours = submissionDate.format("hh") - const minutes = submissionDate.format("mm") - const seconds = submissionDate.format("ss") - const period = submissionDate.format("A").toLowerCase() as TimeFieldPeriod - - return { - hours, - minutes, - seconds, - period, - } - })() - - const dateSubmitted = (() => { - if (!submissionDate) return null - - const month = submissionDate.format("MM") - const day = submissionDate.format("DD") - const year = submissionDate.format("YYYY") - - return { - month, - day, - year, - } - })() - - const phoneNumber = applicationData.applicant.phoneNumber - - const preferences = mapApiToPreferencesForm(applicationData.preferences) - - const application: ApplicationTypes = (() => { - const { - language, - contactPreferences, - sendMailToMailingAddress, - mailingAddress, - accessibility, - incomePeriod, - demographics, - additionalPhoneNumber, - additionalPhoneNumberType, - alternateContact, - } = applicationData - - const incomeVouchers: YesNoAnswer = - applicationData.incomeVouchers === null - ? null - : applicationData.incomeVouchers - ? YesNoAnswer.Yes - : YesNoAnswer.No - - const acceptedTerms: YesNoAnswer = - applicationData.acceptedTerms === null - ? null - : applicationData.acceptedTerms - ? YesNoAnswer.Yes - : YesNoAnswer.No - const workInRegion = applicationData.applicant.workInRegion as YesNoAnswer - - const applicant = { - ...applicationData.applicant, - workInRegion, - } - - const preferredUnit = applicationData?.preferredUnit?.map((unit) => unit.id) - - const result = { - applicant, - language, - phoneNumber, - additionalPhoneNumber, - additionalPhoneNumberType, - preferences, - contactPreferences, - sendMailToMailingAddress, - mailingAddress, - preferredUnit, - accessibility, - incomePeriod, - incomeVouchers, - demographics, - acceptedTerms, - alternateContact, - } - - return result - })() - - const values: FormTypes = { - dateOfBirth, - dateSubmitted, - timeSubmitted, - phoneNumber, - incomeMonth, - incomeYear, - application, - } - - return values -} diff --git a/sites/partners/lib/helpers.ts b/sites/partners/lib/helpers.ts deleted file mode 100644 index 80e0c1de6b..0000000000 --- a/sites/partners/lib/helpers.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { SetStateAction } from "react" -import { - t, - cloudinaryUrlFromId, - CloudinaryUpload, - TimeFieldPeriod, -} from "@bloom-housing/ui-components" -import moment from "moment" -import { - ApplicationSubmissionType, - AssetsService, - ListingEventType, - ListingEvent, - IncomePeriod, -} from "@bloom-housing/backend-core/types" -import { TempUnit, FormListing } from "../src/listings/PaperListingForm" -import { FieldError } from "react-hook-form" - -type DateTimePST = { - hour: string - minute: string - second: string - dayPeriod: string - year: string - day: string - month: string -} - -interface FormOption { - label: string - value: string -} - -export interface FormOptions { - [key: string]: FormOption[] -} - -export const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - -export const convertDataToPst = (dateObj: Date, type: ApplicationSubmissionType) => { - if (!dateObj) { - return { - date: t("t.n/a"), - time: t("t.n/a"), - } - } - - if (type === ApplicationSubmissionType.electronical) { - // convert date and time to PST (electronical applications) - const ptFormat = new Intl.DateTimeFormat("en-US", { - timeZone: "America/Los_Angeles", - hour: "numeric", - minute: "numeric", - second: "numeric", - year: "numeric", - day: "numeric", - month: "numeric", - }) - - const originalDate = new Date(dateObj) - const ptDateParts = ptFormat.formatToParts(originalDate) - const timeValues = ptDateParts.reduce((acc, curr) => { - Object.assign(acc, { - [curr.type]: curr.value, - }) - return acc - }, {} as DateTimePST) - - const { month, day, year, hour, minute, second, dayPeriod } = timeValues - - const date = `${month}/${day}/${year}` - const time = `${hour}:${minute}:${second} ${dayPeriod} PT` - - return { - date, - time, - } - } - - if (type === ApplicationSubmissionType.paper) { - const momentDate = moment(dateObj) - - const date = momentDate.utc().format("MM/DD/YYYY") - const time = momentDate.utc().format("hh:mm:ss A") - - return { - date, - time, - } - } -} - -export const stringToNumber = (str: string | number | undefined): number => { - return str ? Number(str) : 1 -} - -export const stringToBoolean = (str: string | boolean | undefined): boolean => { - return str === true || str === "true" || str === "yes" -} - -export const booleanToString = (bool: boolean): string => { - return bool === true ? "true" : "false" -} - -export const getRentType = (unit: TempUnit): string | null => { - return unit?.monthlyIncomeMin && unit?.monthlyRent - ? "fixed" - : unit?.monthlyRentAsPercentOfIncome - ? "percentage" - : null -} - -export const isNullOrUndefined = (value: unknown): boolean => { - return value === null || value === undefined -} - -export const getLotteryEvent = (listing: FormListing): ListingEvent | undefined => { - const lotteryEvents = listing?.events.filter( - (event) => event.type === ListingEventType.publicLottery - ) - return lotteryEvents && lotteryEvents.length && lotteryEvents[0].startTime - ? lotteryEvents[0] - : null -} - -export function arrayToFormOptions( - arr: T[], - label: string, - value: string, - translateLabel?: string -): FormOption[] { - return arr.map((val: T) => ({ - label: translateLabel ? t(`${translateLabel}.${val[label]}`) : val[label], - value: val[value], - })) -} - -/** - * Create Date object with date and time which comes from the TimeField component - */ -export const createTime = ( - date: Date, - formTime: { hours: string; minutes: string; period: TimeFieldPeriod } -) => { - // date should be cloned, operations in the reference directly can occur unexpected changes - const dateClone = new Date(date.getTime()) - if (!dateClone || (!formTime.hours && !formTime.minutes)) return null - let formattedHours = parseInt(formTime.hours) - if (formTime.period === "am" && formattedHours === 12) { - formattedHours = 0 - } - if (formTime.period === "pm" && formattedHours !== 12) { - formattedHours = formattedHours + 12 - } - dateClone.setHours(formattedHours, parseInt(formTime.minutes), 0) - return dateClone -} - -/** - * Create Date object depending on DateField component - */ -export const createDate = (formDate: { year: string; month: string; day: string }) => { - if (!formDate) return null - return new Date(`${formDate.month}-${formDate.day}-${formDate.year}`) -} - -interface FileUploaderParams { - file: File - setCloudinaryData: (data: SetStateAction<{ id: string; url: string }>) => void - setProgressValue: (value: SetStateAction) => void -} - -/** - * Accept a file from the Dropzone component along with data and progress state - * setters. It will then handle obtaining a signature from the backend and - * uploading the file to Cloudinary, setting progress along the way and the - * id/url of the file when the upload is complete. - */ -export const cloudinaryFileUploader = async ({ - file, - setCloudinaryData, - setProgressValue, -}: FileUploaderParams) => { - const cloudName = process.env.cloudinaryCloudName - const uploadPreset = process.env.cloudinarySignedPreset - - setProgressValue(1) - - const timestamp = Math.round(new Date().getTime() / 1000) - const tag = "browser_upload" - - const assetsService = new AssetsService() - const params = { - timestamp, - tags: tag, - upload_preset: uploadPreset, - } - - const resp = await assetsService.createPresignedUploadMetadata({ - body: { parametersToSign: params }, - }) - const signature = resp.signature - - setProgressValue(3) - - void CloudinaryUpload({ - signature, - apiKey: process.env.cloudinaryKey, - timestamp, - file, - onUploadProgress: (progress) => { - setProgressValue(progress) - }, - cloudName, - uploadPreset, - tag, - }).then((response) => { - setProgressValue(100) - setCloudinaryData({ - id: response.data.public_id, - url: cloudinaryUrlFromId(response.data.public_id), - }) - }) -} - -export function formatIncome(value: number, currentType: IncomePeriod, returnType: IncomePeriod) { - const usd = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }) - - if (returnType === "perMonth") { - const monthIncomeNumber = currentType === "perYear" ? value / 12 : value - return usd.format(monthIncomeNumber) - } else { - const yearIncomeNumber = currentType === "perMonth" ? value * 12 : value - return usd.format(yearIncomeNumber) - } -} - -export const removeEmptyFields = (obj, keysToIgnore?: string[]) => { - Object.keys(obj).forEach(function (key) { - if (!keysToIgnore.includes(key)) { - if (obj[key] && typeof obj[key] === "object") { - removeEmptyFields(obj[key], keysToIgnore) - } - if (obj[key] === null || obj[key] === undefined || obj[key] === "") { - delete obj[key] - } - if ( - typeof obj[key] === "object" && - !Array.isArray(obj[key]) && - Object.keys(obj[key]).length === 0 - ) { - delete obj[key] - } - } - }) -} - -export const fieldHasError = (errorObj: FieldError) => { - return errorObj !== undefined -} - -export const fieldMessage = (errorObj: FieldError) => { - return errorObj?.message -} diff --git a/sites/partners/lib/hooks.ts b/sites/partners/lib/hooks.ts deleted file mode 100644 index 0d2259c24f..0000000000 --- a/sites/partners/lib/hooks.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { useContext } from "react" -import useSWR, { mutate } from "swr" -import qs from "qs" - -import { AuthContext } from "@bloom-housing/ui-components" -import { - EnumApplicationsApiExtraModelOrder, - EnumApplicationsApiExtraModelOrderBy, - EnumListingFilterParamsComparison, - EnumUserFilterParamsComparison, -} from "@bloom-housing/backend-core/types" - -interface PaginationProps { - page?: number - limit: number | "all" -} - -interface UseSingleApplicationDataProps extends PaginationProps { - listingId: string -} - -type UseUserListProps = PaginationProps - -type UseListingsDataProps = PaginationProps & { - userId?: string -} - -export function useSingleListingData(listingId: string) { - const { listingsService } = useContext(AuthContext) - const fetcher = () => listingsService.retrieve({ listingId }) - - const { data, error } = useSWR(`${process.env.backendApiBase}/listings/${listingId}`, fetcher) - - return { - listingDto: data, - listingLoading: !error && !data, - listingError: error, - } -} - -export function useListingsData({ page, limit, userId }: UseListingsDataProps) { - const params = { - page, - limit, - } - - // filter if logged user is an agent - if (typeof userId !== undefined) { - Object.assign(params, { - filter: [ - { - $comparison: EnumListingFilterParamsComparison["="], - leasingAgents: userId, - }, - ], - view: "base", - }) - } - - const { listingsService } = useContext(AuthContext) - const fetcher = () => listingsService.list(params) - - const paramsString = qs.stringify(params) - const { data, error } = useSWR(`${process.env.backendApiBase}/listings?${paramsString}`, fetcher) - - return { - listingDtos: data, - listingsLoading: !error && !data, - listingsError: error, - } -} - -export function useApplicationsData( - pageIndex: number, - limit = 10, - listingId: string, - search: string, - orderBy?: EnumApplicationsApiExtraModelOrderBy, - order?: EnumApplicationsApiExtraModelOrder -) { - const { applicationsService } = useContext(AuthContext) - - const queryParams = new URLSearchParams() - queryParams.append("listingId", listingId) - queryParams.append("page", pageIndex.toString()) - queryParams.append("limit", limit.toString()) - - if (search) { - queryParams.append("search", search) - } - - if (orderBy) { - queryParams.append("orderBy", search) - queryParams.append("order", order ?? EnumApplicationsApiExtraModelOrder.ASC) - } - - const endpoint = `${process.env.backendApiBase}/applications?${queryParams.toString()}` - - const params = { - listingId, - page: pageIndex, - limit, - } - - if (search) { - Object.assign(params, { search }) - } - - if (orderBy) { - Object.assign(params, { orderBy, order: order ?? "ASC" }) - } - - const fetcher = () => applicationsService.list(params) - const { data, error } = useSWR(endpoint, fetcher) - - return { - appsData: data, - appsLoading: !error && !data, - appsError: error, - } -} - -export function useSingleApplicationData(applicationId: string) { - const { applicationsService } = useContext(AuthContext) - const backendSingleApplicationsEndpointUrl = `${process.env.backendApiBase}/applications/${applicationId}` - - const fetcher = () => applicationsService.retrieve({ applicationId }) - const { data, error } = useSWR(backendSingleApplicationsEndpointUrl, fetcher) - - return { - application: data, - applicationLoading: !error && !data, - applicationError: error, - } -} - -export function useFlaggedApplicationsList({ - listingId, - page, - limit, -}: UseSingleApplicationDataProps) { - const { applicationFlaggedSetsService } = useContext(AuthContext) - - const params = { - listingId, - page, - } - - const queryParams = new URLSearchParams() - queryParams.append("listingId", listingId) - queryParams.append("page", page.toString()) - - if (typeof limit === "number") { - queryParams.append("limit", limit.toString()) - Object.assign(params, limit) - } - - const endpoint = `${process.env.backendApiBase}/applicationFlaggedSets?${queryParams.toString()}` - - const fetcher = () => applicationFlaggedSetsService.list(params) - - const { data, error } = useSWR(endpoint, fetcher) - - return { - data, - error, - } -} - -export function useSingleFlaggedApplication(afsId: string) { - const { applicationFlaggedSetsService } = useContext(AuthContext) - - const endpoint = `${process.env.backendApiBase}/applicationFlaggedSets/${afsId}` - const fetcher = () => - applicationFlaggedSetsService.retrieve({ - afsId, - }) - - const { data, error } = useSWR(endpoint, fetcher) - - const revalidate = () => mutate(endpoint) - - return { - revalidate, - data, - error, - } -} - -export function useSingleAmiChartData(amiChartId: string) { - const { amiChartsService } = useContext(AuthContext) - const fetcher = () => amiChartsService.retrieve({ amiChartId }) - - const { data, error } = useSWR(`${process.env.backendApiBase}/amiCharts/${amiChartId}`, fetcher) - - return { - data, - error, - } -} - -export function useAmiChartList() { - const { amiChartsService } = useContext(AuthContext) - const fetcher = () => amiChartsService.list() - - const { data, error } = useSWR(`${process.env.backendApiBase}/amiCharts`, fetcher) - - return { - data, - loading: !error && !data, - error, - } -} - -export function useSingleAmiChart(amiChartId: string) { - const { amiChartsService } = useContext(AuthContext) - const fetcher = () => amiChartsService.retrieve({ amiChartId }) - - const { data, error } = useSWR(`${process.env.backendApiBase}/amiCharts/${amiChartId}`, fetcher) - - return { - data, - loading: !error && !data, - error, - } -} - -export function useUnitPriorityList() { - const { unitPriorityService } = useContext(AuthContext) - const fetcher = () => unitPriorityService.list() - - const { data, error } = useSWR( - `${process.env.backendApiBase}/unitAccessibilityPriorityTypes`, - fetcher - ) - - return { - data, - loading: !error && !data, - error, - } -} - -export function useUnitTypeList() { - const { unitTypesService } = useContext(AuthContext) - const fetcher = () => unitTypesService.list() - - const { data, error } = useSWR(`${process.env.backendApiBase}/unitTypes`, fetcher) - - return { - data, - loading: !error && !data, - error, - } -} - -export function usePreferenceList() { - const { preferencesService } = useContext(AuthContext) - const fetcher = () => preferencesService.list() - - const { data, error } = useSWR(`${process.env.backendApiBase}/preferences`, fetcher) - - return { - data, - loading: !error && !data, - error, - } -} - -export function useReservedCommunityTypeList() { - const { reservedCommunityTypeService } = useContext(AuthContext) - const fetcher = () => reservedCommunityTypeService.list() - - const { data, error } = useSWR(`${process.env.backendApiBase}/reservedCommunityTypes`, fetcher) - - return { - data, - loading: !error && !data, - error, - } -} - -export function useUserList({ page, limit }: UseUserListProps) { - const queryParams = new URLSearchParams() - queryParams.append("page", page.toString()) - queryParams.append("limit", limit.toString()) - - const { userService } = useContext(AuthContext) - - const fetcher = () => - userService.list({ - page, - limit, - filter: [ - { - isPartner: true, - $comparison: EnumUserFilterParamsComparison["="], - }, - ], - }) - - const { data, error } = useSWR( - `${process.env.backendApiBase}/user/list?${queryParams.toString()}`, - fetcher - ) - - return { - data, - loading: !error && !data, - error, - } -} diff --git a/sites/partners/netlify.toml b/sites/partners/netlify.toml index 48d26efb59..2c6f363d26 100644 --- a/sites/partners/netlify.toml +++ b/sites/partners/netlify.toml @@ -3,11 +3,21 @@ command = "yarn run build" ignore = "/bin/false" +[[plugins]] +package = "@netlify/plugin-nextjs" + [build.environment] -NODE_VERSION = "14.17.6" +NODE_VERSION = "18.14.2" YARN_VERSION = "1.22.4" NEXT_TELEMETRY_DISABLED = "1" +NODE_OPTIONS = "--max_old_space_size=4096" # reminder: URLs and fragments should *not* have a trailing / -LISTINGS_QUERY = "/listings" \ No newline at end of file +LISTINGS_QUERY = "/listings" + +[[redirects]] +from = "https://detroit-partners-prod.netlify.app/*" +to = "https://partners.homeconnect.detroitmi.gov/:splat" +status = 301 +force = true diff --git a/sites/partners/next-env.d.ts b/sites/partners/next-env.d.ts index 9bc3dd46b9..4f11a03dc6 100644 --- a/sites/partners/next-env.d.ts +++ b/sites/partners/next-env.d.ts @@ -1,5 +1,4 @@ /// -/// /// // NOTE: This file should not be edited diff --git a/sites/partners/next.config.js b/sites/partners/next.config.js index 9c22c1f7d9..31cc5bf660 100644 --- a/sites/partners/next.config.js +++ b/sites/partners/next.config.js @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const withTM = require("next-transpile-modules")([ + "@bloom-housing/shared-helpers", "@bloom-housing/ui-components", "@bloom-housing/backend-core", ]) @@ -24,22 +25,22 @@ if (process.env.INCOMING_HOOK_BODY && process.env.INCOMING_HOOK_BODY.startsWith( const LISTINGS_QUERY = process.env.LISTINGS_QUERY || "/listings" console.log(`Using ${BACKEND_API_BASE}${LISTINGS_QUERY} for the listing service.`) +const BACKEND_PROXY_BASE = process.env.BACKEND_PROXY_BASE + const MAPBOX_TOKEN = process.env.MAPBOX_TOKEN // Load the Tailwind theme and set up SASS vars const bloomTheme = require("./tailwind.config.js") const tailwindVars = require("@bloom-housing/ui-components/tailwind.tosass.js")(bloomTheme) - // Tell webpack to compile the ui components package // https://www.npmjs.com/package/next-transpile-modules module.exports = withBundleAnalyzer( withTM({ - target: "serverless", env: { backendApiBase: BACKEND_API_BASE, + backendProxyBase: BACKEND_PROXY_BASE, listingServiceUrl: BACKEND_API_BASE + LISTINGS_QUERY, idleTimeout: process.env.IDLE_TIMEOUT, showDuplicates: process.env.SHOW_DUPLICATES === "TRUE", - publicBaseUrl: process.env.PUBLIC_BASE_URL, cloudinaryCloudName: process.env.CLOUDINARY_CLOUD_NAME, cloudinaryKey: process.env.CLOUDINARY_KEY, cloudinarySignedPreset: process.env.CLOUDINARY_SIGNED_PRESET, diff --git a/sites/partners/package.json b/sites/partners/package.json index 79844ec54f..31e36bbb74 100644 --- a/sites/partners/package.json +++ b/sites/partners/package.json @@ -1,58 +1,81 @@ { "name": "@bloom-housing/partners", - "version": "1.0.5", + "version": "4.4.0", + "author": "Sean Albert ", "description": "Partners app reference implementation for the Bloom affordable housing system", "main": "index.js", "license": "Apache-2.0", "private": true, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "scripts": { "dev": "NODE_OPTIONS='--inspect=9231' next -p ${NEXTJS_PORT:-3001}", "build": "next build", "test": "concurrently \"yarn dev\" \"cypress open\"", + "test:unit": "jest -w 1", "test:headless": "concurrently \"yarn dev\" \"cypress run\"", + "test:coverage": "yarn nyc report --reporter=text-summary --check-coverage", "export": "next export", - "start": "next start", + "start": "next start -p ${NEXTJS_PORT:-3001}", "dev:listings": "cd ../../backend/core && yarn dev", "dev:server-wait": "wait-on \"http-get://localhost:${PORT:-3100}/listings\" && yarn dev", - "dev:all": "concurrently \"yarn dev:listings\" \"yarn dev:server-wait\"" + "dev:all": "concurrently \"yarn dev:listings\" \"yarn dev:server-wait\"", + "dev:all-cypress": "concurrently \"yarn dev:listings\" \"yarn dev:server-wait-cypress\"", + "dev:server-wait-cypress": "wait-on \"http-get://localhost:${PORT:-3100}/listings\" --httpTimeout 60000 --tcpTimeout 1500 -v --interval 15000 && yarn build && yarn start" }, "dependencies": { - "@bloom-housing/ui-components": "^2.0.0-alpha.0", + "@bloom-housing/backend-core": "^4.4.0", + "@bloom-housing/shared-helpers": "^4.4.0", + "@bloom-housing/ui-components": "^12.0.11", "@mapbox/mapbox-sdk": "^0.13.0", + "@zeit/next-sass": "^1.0.1", "ag-grid-community": "^26.0.0", "ag-grid-react": "^26.0.0", - "axios": "^0.21.1", + "@mdx-js/loader": "1.6.18", + "@next/mdx": "^10.1.0", + "axios": "^0.21.2", + "dayjs": "^1.10.7", "dotenv": "^8.2.0", "electron": "^13.1.7", - "moment": "^2.29.1", "nanoid": "^3.1.12", - "next": "^11.1.1", + "next": "^13.2.4", "next-plugin-custom-babel-config": "^1.0.2", "node-polyglot": "^2.4.0", - "node-sass": "^4.14.1", + "node-sass": "^7.0.0", "qs": "^6.10.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "18.2.0", + "react-dom": "18.2.0", "react-hook-form": "^6.15.5", - "swr": "^1.0.1", - "tailwindcss": "2.2.10" + "swr": "^2.1.2", + "tailwindcss": "npm:@tailwindcss/postcss7-compat@2.2.10" }, "devDependencies": { - "@babel/core": "^7.11.6", - "@babel/preset-env": "^7.11.5", - "@cypress/webpack-preprocessor": "^5.4.6", + "@axe-core/react": "4.4.3", + "@babel/core": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@cypress/code-coverage": "^3.10.3", + "@cypress/webpack-preprocessor": "^5.11.1", + "@netlify/plugin-nextjs": "4.30.4", "@next/bundle-analyzer": "^10.1.0", + "@testing-library/react": "14.0.0", + "@testing-library/user-event": "^14.4.3", "@types/mapbox__mapbox-sdk": "^0.13.2", - "@types/node": "^12.12.67", - "@types/react": "^16.9.52", - "babel-loader": "^8.1.0", + "@types/node": "18.15.5", + "aria-query": "5.1.3", + "babel-loader": "^9.1.2", "concurrently": "^5.3.0", - "cypress": "^4.12.1", + "cypress": "^12.8.1", + "cypress-file-upload": "^5.0.8", + "jest": "^26.5.3", "js-levenshtein": "^1.1.6", - "next-transpile-modules": "^8.0.0", + "msw": "^0.46.0", + "next-transpile-modules": "^10.0.0", + "nyc": "^15.1.0", "postcss": "^8.3.6", "sass-loader": "^10.0.3", - "typescript": "^3.9.7", - "webpack": "^4.44.2" + "typescript": "4.6.4", + "webpack": "^5.69.1" } } diff --git a/sites/partners/page_content/locale_overrides/general.json b/sites/partners/page_content/locale_overrides/general.json deleted file mode 100644 index 472959c793..0000000000 --- a/sites/partners/page_content/locale_overrides/general.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "welcome": { - "seeRentalListings": "Browse Rentals" - }, - "application": { - "details": { - "countyName": "Alameda County" - } - } -} diff --git a/sites/partners/pages/_app.tsx b/sites/partners/pages/_app.tsx deleted file mode 100644 index 117fa542d2..0000000000 --- a/sites/partners/pages/_app.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useMemo } from "react" -import { SWRConfig } from "swr" -import type { AppProps } from "next/app" - -import "@bloom-housing/ui-components/src/global/css-imports.scss" -import "@bloom-housing/ui-components/src/global/app-css.scss" -import { - addTranslation, - ConfigProvider, - AuthProvider, - RequireLogin, - NavigationContext, - GenericRouter, -} from "@bloom-housing/ui-components" - -// TODO: Make these not-global -import "ag-grid-community/dist/styles/ag-grid.css" -import "ag-grid-community/dist/styles/ag-theme-alpine.css" - -import LinkComponent from "../src/LinkComponent" -import { translations, overrideTranslations } from "../src/translations" - -const signInMessage = "Login is required to view this page." - -function BloomApp({ Component, router, pageProps }: AppProps) { - const { locale } = router - const skipLoginRoutes = ["/forgot-password", "/reset-password", "/users/confirm"] - - useMemo(() => { - addTranslation(translations.general, true) - if (locale && locale !== "en" && translations[locale]) { - addTranslation(translations[locale]) - } - - if (overrideTranslations[locale]) { - addTranslation(overrideTranslations[locale]) - } - }, [locale]) - - return ( - { - if (error.response.status === 403) { - window.location.href = "/unauthorized" - } - }, - }} - > - - - - -
- {typeof window === "undefined" ? null : } -
-
-
-
-
-
- ) -} - -export default BloomApp diff --git a/sites/partners/pages/_error.tsx b/sites/partners/pages/_error.tsx deleted file mode 100644 index 97af22a522..0000000000 --- a/sites/partners/pages/_error.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Layout from "../layouts" -import Head from "next/head" -import { Hero, MarkdownSection, t } from "@bloom-housing/ui-components" - -const Error = () => { - const pageTitle = t("errors.notFound.title") - - return ( - - - {pageTitle} - - - {t("errors.notFound.message")} - -
- An error has occurred. -
-
- ) -} - -export default Error diff --git a/sites/partners/pages/application/[id]/edit.tsx b/sites/partners/pages/application/[id]/edit.tsx deleted file mode 100644 index 3fe51c418a..0000000000 --- a/sites/partners/pages/application/[id]/edit.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react" -import Head from "next/head" -import { useRouter } from "next/router" -import { PageHeader, t } from "@bloom-housing/ui-components" -import Layout from "../../../layouts" -import PaperApplicationForm from "../../../src/applications/PaperApplicationForm/PaperApplicationForm" -import { useSingleApplicationData } from "../../../lib/hooks" -import { ApplicationContext } from "../../../src/applications/ApplicationContext" - -const NewApplication = () => { - const router = useRouter() - const applicationId = router.query.id as string - - const { application } = useSingleApplicationData(applicationId) - - if (!application) return false - - return ( - - - - {t("nav.siteTitlePartners")} - - - -

- {t("t.edit")}: {application.applicant.firstName} {application.applicant.lastName} -

- -

{application.id}

- - } - /> - - -
-
- ) -} - -export default NewApplication diff --git a/sites/partners/pages/application/[id]/index.tsx b/sites/partners/pages/application/[id]/index.tsx deleted file mode 100644 index 078d1047c5..0000000000 --- a/sites/partners/pages/application/[id]/index.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useMemo, useState, useContext } from "react" -import { useRouter } from "next/router" -import Head from "next/head" -import { - AppearanceStyleType, - PageHeader, - t, - Tag, - Button, - AuthContext, - AlertBox, - SiteAlert, -} from "@bloom-housing/ui-components" -import { useSingleApplicationData } from "../../../lib/hooks" - -import Layout from "../../../layouts" -import { ApplicationStatus } from "@bloom-housing/backend-core/types" -import { - DetailsMemberDrawer, - MembersDrawer, -} from "../../../src/applications/PaperApplicationDetails/DetailsMemberDrawer" - -import { ApplicationContext } from "../../../src/applications/ApplicationContext" -import { DetailsApplicationData } from "../../../src/applications/PaperApplicationDetails/sections/DetailsApplicationData" -import { DetailsPrimaryApplicant } from "../../../src/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant" -import { DetailsAlternateContact } from "../../../src/applications/PaperApplicationDetails/sections/DetailsAlternateContact" -import { DetailsHouseholdMembers } from "../../../src/applications/PaperApplicationDetails/sections/DetailsHouseholdMembers" -import { DetailsHouseholdDetails } from "../../../src/applications/PaperApplicationDetails/sections/DetailsHouseholdDetails" -import { DetailsPreferences } from "../../../src/applications/PaperApplicationDetails/sections/DetailsPreferences" -import { DetailsHouseholdIncome } from "../../../src/applications/PaperApplicationDetails/sections/DetailsHouseholdIncome" -import { DetailsTerms } from "../../../src/applications/PaperApplicationDetails/sections/DetailsTerms" -import { Aside } from "../../../src/applications/Aside" - -export default function ApplicationsList() { - const router = useRouter() - const applicationId = router.query.id as string - const { application } = useSingleApplicationData(applicationId) - - const { applicationsService } = useContext(AuthContext) - const [errorAlert, setErrorAlert] = useState(false) - - const [membersDrawer, setMembersDrawer] = useState(null) - - async function deleteApplication() { - try { - await applicationsService.delete({ applicationId }) - void router.push(`/listings/${application?.listing?.id}/applications`) - } catch (err) { - setErrorAlert(true) - } - } - - const applicationStatus = useMemo(() => { - switch (application?.status) { - case ApplicationStatus.submitted: - return ( - - {t(`application.details.applicationStatus.submitted`)} - - ) - case ApplicationStatus.removed: - return ( - - {t(`application.details.applicationStatus.removed`)} - - ) - default: - return ( - - {t(`application.details.applicationStatus.draft`)} - - ) - } - }, [application]) - - if (!application) return null - - return ( - - - - {t("nav.siteTitlePartners")} - - - -

- {application.applicant.firstName} {application.applicant.lastName} -

- -

{application.id}

- - } - > -
- -
-
-
-
- - -
{applicationStatus}
-
-
- -
-
- {errorAlert && ( - setErrorAlert(false)} - closeable - type="alert" - > - {t("authentication.signIn.errorGenericMessage")} - - )} - -
-
- - - - - - - - - - - - - - - -
- -
-
-
-
-
-
- - -
- ) -} diff --git a/sites/partners/pages/forgot-password.tsx b/sites/partners/pages/forgot-password.tsx deleted file mode 100644 index 7f1d7c48b4..0000000000 --- a/sites/partners/pages/forgot-password.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useState, useContext } from "react" -import { useRouter } from "next/router" -import { useForm } from "react-hook-form" -import { - AppearanceStyleType, - Button, - Field, - Form, - FormCard, - Icon, - AuthContext, - t, - AlertBox, - SiteAlert, - setSiteAlertMessage, -} from "@bloom-housing/ui-components" -import { emailRegex } from "../lib/helpers" -import FormsLayout from "../layouts/forms" - -const ForgotPassword = () => { - const router = useRouter() - const { forgotPassword } = useContext(AuthContext) - /* Form Handler */ - // This is causing a linting issue with unbound-method, see open issue as of 10/21/2020: - // https://github.com/react-hook-form/react-hook-form/issues/2887 - // eslint-disable-next-line @typescript-eslint/unbound-method - const { register, handleSubmit, errors } = useForm() - const [requestError, setRequestError] = useState() - - const onSubmit = async (data: { email: string }) => { - const { email } = data - - try { - await forgotPassword(email) - setSiteAlertMessage(t(`authentication.forgotPassword.success`), "success") - await router.push("/") - window.scrollTo(0, 0) - } catch (err) { - const { status, data } = err.response || {} - if (status === 400) { - setRequestError(`${t(`authentication.forgotPassword.errors.${data.message}`)}`) - } else { - console.error(err) - setRequestError(`${t("authentication.forgotPassword.errors.generic")}`) - } - } - } - - return ( - - -
- -

{t("authentication.forgotPassword.sendEmail")}

-
- {requestError && ( - setRequestError(undefined)} type="alert"> - {requestError} - - )} - -
-
- - -
- -
- - -
-
-
- ) -} - -export { ForgotPassword as default, ForgotPassword } diff --git a/sites/partners/pages/index.tsx b/sites/partners/pages/index.tsx deleted file mode 100644 index 71082ba6ee..0000000000 --- a/sites/partners/pages/index.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import React, { useMemo, useContext, useState } from "react" -import Head from "next/head" -import { - PageHeader, - t, - lRoute, - AuthContext, - Button, - LocalizedLink, - AgPagination, - AG_PER_PAGE_OPTIONS, -} from "@bloom-housing/ui-components" -import moment from "moment" -import { AgGridReact } from "ag-grid-react" -import { GridOptions } from "ag-grid-community" - -import { useListingsData } from "../lib/hooks" -import Layout from "../layouts" -import { MetaTags } from "../src/MetaTags" - -class formatLinkCell { - link: HTMLAnchorElement - - init(params) { - this.link = document.createElement("a") - this.link.classList.add("text-blue-700") - this.link.setAttribute("href", lRoute(`/listings/${params.data.id}/applications`)) - this.link.innerText = params.valueFormatted || params.value - } - - getGui() { - return this.link - } -} - -class ApplicationsLink extends formatLinkCell { - init(params) { - super.init(params) - this.link.setAttribute("href", lRoute(`/listings/${params.data.id}/applications`)) - } -} - -class ListingsLink extends formatLinkCell { - init(params) { - super.init(params) - this.link.setAttribute("href", lRoute(`/listings/${params.data.id}`)) - } -} - -export default function ListingsList() { - const { profile } = useContext(AuthContext) - const isAdmin = profile.roles?.isAdmin || false - - /* Pagination */ - const [itemsPerPage, setItemsPerPage] = useState(AG_PER_PAGE_OPTIONS[0]) - const [currentPage, setCurrentPage] = useState(1) - - const metaDescription = t("pageDescription.welcome", { regionName: t("region.name") }) - const metaImage = "" // TODO: replace with hero image - - class formatWaitlistStatus { - text: HTMLSpanElement - - init({ data }) { - const isWaitlistOpen = data.waitlistCurrentSize < data.waitlistMaxSize - - this.text = document.createElement("span") - this.text.innerHTML = isWaitlistOpen ? t("t.yes") : t("t.no") - } - - getGui() { - return this.text - } - } - - const gridOptions: GridOptions = { - components: { - ApplicationsLink, - formatLinkCell, - formatWaitlistStatus, - ListingsLink, - }, - } - - const columnDefs = useMemo(() => { - const columns = [ - { - headerName: t("listings.listingName"), - field: "name", - sortable: false, - filter: false, - resizable: true, - cellRenderer: "ListingsLink", - }, - { - headerName: t("listings.listingStatusText"), - field: "status", - sortable: false, - filter: false, - resizable: true, - flex: 1, - valueFormatter: ({ value }) => t(`listings.${value}`), - cellRenderer: "ApplicationsLink", - }, - { - headerName: t("listings.applicationDeadline"), - field: "applicationDueDate", - sortable: false, - filter: false, - resizable: true, - valueFormatter: ({ value }) => (value ? moment(value).format("MM/DD/YYYY") : t("t.none")), - }, - { - headerName: t("listings.availableUnits"), - field: "unitsAvailable", - sortable: false, - filter: false, - resizable: true, - }, - { - headerName: t("listings.waitlist.open"), - field: "waitlistCurrentSize", - sortable: false, - filter: false, - resizable: true, - cellRenderer: "formatWaitlistStatus", - }, - ] - return columns - }, []) - - const { listingDtos, listingsLoading, listingsError } = useListingsData({ - page: currentPage, - limit: itemsPerPage, - userId: !isAdmin ? profile?.id : undefined, - }) - - if (listingsLoading) return "Loading..." - if (listingsError) return "An error has occurred." - - return ( - - - {t("nav.siteTitlePartners")} - - - -
-
-
-
-
-
- {isAdmin && ( - - - - )} -
-
- -
- - - setCurrentPage(1)} - /> -
-
-
-
-
- ) -} diff --git a/sites/partners/pages/listings/[id]/applications/add.tsx b/sites/partners/pages/listings/[id]/applications/add.tsx deleted file mode 100644 index 11677d0638..0000000000 --- a/sites/partners/pages/listings/[id]/applications/add.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react" -import Head from "next/head" -import { PageHeader, SiteAlert, t } from "@bloom-housing/ui-components" -import Layout from "../../../../layouts" -import PaperApplicationForm from "../../../../src/applications/PaperApplicationForm/PaperApplicationForm" -import { useRouter } from "next/router" - -const NewApplication = () => { - const router = useRouter() - const listingId = router.query.id as string - - return ( - - - {t("nav.siteTitlePartners")} - - - -
- -
-
- - -
- ) -} - -export default NewApplication diff --git a/sites/partners/pages/listings/[id]/applications/index.tsx b/sites/partners/pages/listings/[id]/applications/index.tsx deleted file mode 100644 index e62687f526..0000000000 --- a/sites/partners/pages/listings/[id]/applications/index.tsx +++ /dev/null @@ -1,309 +0,0 @@ -import React, { useState, useEffect, useRef, useMemo, useContext, useCallback } from "react" -import { useRouter } from "next/router" -import moment from "moment" -import Head from "next/head" -import { - Field, - t, - Button, - debounce, - lRoute, - LocalizedLink, - AuthContext, - SiteAlert, - setSiteAlertMessage, - AgPagination, - AG_PER_PAGE_OPTIONS, -} from "@bloom-housing/ui-components" -import { - useApplicationsData, - useSingleListingData, - useFlaggedApplicationsList, -} from "../../../../lib/hooks" -import { ApplicationSecondaryNav } from "../../../../src/applications/ApplicationSecondaryNav" -import Layout from "../../../../layouts" -import { useForm } from "react-hook-form" -import { AgGridReact } from "ag-grid-react" -import { getColDefs } from "../../../../src/applications/ApplicationsColDefs" -import { GridOptions, ColumnApi, ColumnState } from "ag-grid-community" -import { - EnumApplicationsApiExtraModelOrder, - EnumApplicationsApiExtraModelOrderBy, -} from "@bloom-housing/backend-core/types" - -type ApplicationsListSortOptions = { - orderBy: EnumApplicationsApiExtraModelOrderBy - order: EnumApplicationsApiExtraModelOrder -} - -const ApplicationsList = () => { - const COLUMN_STATE_KEY = "column-state" - - const { applicationsService } = useContext(AuthContext) - const router = useRouter() - // eslint-disable-next-line @typescript-eslint/unbound-method - const { register, watch } = useForm() - - const [gridColumnApi, setGridColumnApi] = useState(null) - - /* Filter input */ - const filterField = watch("filter-input", "") - const [delayedFilterValue, setDelayedFilterValue] = useState(filterField) - - /* Pagination */ - const [itemsPerPage, setItemsPerPage] = useState(AG_PER_PAGE_OPTIONS[0]) - const [currentPage, setCurrentPage] = useState(1) - - /* OrderBy columns */ - const [sortOptions, setSortOptions] = useState({ - orderBy: null, - order: null, - }) - - const listingId = router.query.id as string - const { appsData } = useApplicationsData( - currentPage, - itemsPerPage, - listingId, - delayedFilterValue, - sortOptions.orderBy, - sortOptions.order - ) - const { listingDto } = useSingleListingData(listingId) - const countyCode = listingDto?.countyCode - const listingName = listingDto?.name - - const { data: flaggedApps } = useFlaggedApplicationsList({ - listingId, - page: 1, - limit: 1, - }) - - /* CSV export */ - const [csvExportLoading, setCsvExportLoading] = useState(false) - const [csvExportError, setCsvExportError] = useState(false) - - const fetchFilteredResults = (value: string) => { - setDelayedFilterValue(value) - } - - // Load a table state on initial render & pagination change (because the new data comes from the API) - useEffect(() => { - const savedColumnState = sessionStorage.getItem(COLUMN_STATE_KEY) - - if (gridColumnApi && savedColumnState) { - const parsedState: ColumnState[] = JSON.parse(savedColumnState) - - gridColumnApi.applyColumnState({ - state: parsedState, - applyOrder: true, - }) - } - }, [gridColumnApi, currentPage]) - - function saveColumnState(api: ColumnApi) { - const columnState = api.getColumnState() - const columnStateJSON = JSON.stringify(columnState) - sessionStorage.setItem(COLUMN_STATE_KEY, columnStateJSON) - } - - function onGridReady(params) { - setGridColumnApi(params.columnApi) - } - - const debounceFilter = useRef(debounce((value: string) => fetchFilteredResults(value), 1000)) - - // reset page to 1 when user change limit - useEffect(() => { - setCurrentPage(1) - }, [itemsPerPage]) - - // fetch filtered data - useEffect(() => { - setCurrentPage(1) - debounceFilter.current(filterField) - }, [filterField]) - - const applications = appsData?.items || [] - const appsMeta = appsData?.meta - - const onExport = async () => { - setCsvExportError(false) - setCsvExportLoading(true) - - try { - const content = await applicationsService.listAsCsv({ - listingId, - includeHeaders: true, - }) - - const now = new Date() - const dateString = moment(now).format("YYYY-MM-DD_HH:mm:ss") - - const blob = new Blob([content], { type: "text/csv" }) - const fileLink = document.createElement("a") - fileLink.setAttribute("download", `applications-${listingId}-${dateString}.csv`) - fileLink.href = URL.createObjectURL(blob) - - fileLink.click() - } catch (err) { - setCsvExportError(true) - setSiteAlertMessage(t("errors.alert.timeoutPleaseTryAgain"), "alert") - console.error(err) - } - - setCsvExportLoading(false) - } - - // ag grid settings - class formatLinkCell { - linkWithId: HTMLSpanElement - - init(params) { - this.linkWithId = document.createElement("button") - this.linkWithId.classList.add("text-blue-700") - - this.linkWithId.innerText = params.value - - this.linkWithId.addEventListener("click", function () { - void saveColumnState(params.columnApi) - void router.push(lRoute(`/application/${params.value}`)) - }) - } - - getGui() { - return this.linkWithId - } - } - - // update table items order on sort change - const initialLoadOnSort = useRef(false) - const onSortChange = useCallback((columns: ColumnState[]) => { - // prevent multiple fetch on initial render - if (!initialLoadOnSort.current) { - initialLoadOnSort.current = true - return - } - - const sortedBy = columns.find((col) => col.sort) - const { colId, sort } = sortedBy || {} - - const allowedSortColIds: string[] = Object.values(EnumApplicationsApiExtraModelOrderBy) - - if (allowedSortColIds.includes(colId)) { - const name = EnumApplicationsApiExtraModelOrderBy[colId] - - setSortOptions({ - orderBy: name, - order: sort.toUpperCase() as EnumApplicationsApiExtraModelOrder, - }) - } - }, []) - - const gridOptions: GridOptions = { - onSortChanged: (params) => { - saveColumnState(params.columnApi) - onSortChange(params.columnApi.getColumnState()) - }, - onColumnMoved: (params) => saveColumnState(params.columnApi), - components: { - formatLinkCell: formatLinkCell, - }, - } - - const defaultColDef = { - resizable: true, - maxWidth: 300, - } - - // get the highest value from householdSize and limit to 6 - const maxHouseholdSize = useMemo(() => { - let max = 1 - - appsData?.items.forEach((item) => { - if (item.householdSize > max) { - max = item.householdSize - } - }) - - return max < 6 ? max : 6 - }, [appsData]) - - const columnDefs = useMemo(() => { - return getColDefs(maxHouseholdSize, countyCode) - }, [maxHouseholdSize, countyCode]) - - if (!applications) return null - - return ( - - - {t("nav.siteTitlePartners")} - - - - {csvExportError && ( -
- -
- )} -
- -
-
-
-
-
- -
- -
- - - - - -
-
- -
- - - -
-
-
-
-
- ) -} - -export default ApplicationsList diff --git a/sites/partners/pages/listings/[id]/edit.tsx b/sites/partners/pages/listings/[id]/edit.tsx deleted file mode 100644 index e78fcaf699..0000000000 --- a/sites/partners/pages/listings/[id]/edit.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from "react" -import Head from "next/head" -import { useRouter } from "next/router" -import { PageHeader, t } from "@bloom-housing/ui-components" -import Layout from "../../../layouts" -import PaperListingForm from "../../../src/listings/PaperListingForm" -import { useSingleListingData } from "../../../lib/hooks" -import { ListingContext } from "../../../src/listings/ListingContext" -import { MetaTags } from "../../../src/MetaTags" -import ListingGuard from "../../../src/ListingGuard" - -const EditListing = () => { - const metaDescription = "" - const metaImage = "" // TODO: replace with hero image - - const router = useRouter() - const listingId = router.query.id as string - - const { listingDto } = useSingleListingData(listingId) - - if (!listingDto) return false - - // Set listing photo from assets if necessary: - if (listingDto.image == null && listingDto.assets.length > 0) { - listingDto.image = listingDto.assets.find((asset) => asset.label == "building") - } - // If that didn't do the trick, set a default: - if (listingDto.image == null) { - listingDto.image = { fileId: "", label: "" } - } - - return ( - - - - - {t("nav.siteTitlePartners")} - - - - - -

- {t("t.edit")}: {listingDto.name} -

- -

{listingDto.id}

- - } - /> - - -
-
-
- ) -} - -export default EditListing diff --git a/sites/partners/pages/listings/[id]/flags/[flagId]/index.tsx b/sites/partners/pages/listings/[id]/flags/[flagId]/index.tsx deleted file mode 100644 index 9d62ddf076..0000000000 --- a/sites/partners/pages/listings/[id]/flags/[flagId]/index.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import React, { useMemo, useState, useCallback, useContext, useEffect } from "react" -import Head from "next/head" -import { useRouter } from "next/router" -import { AgGridReact } from "ag-grid-react" -import { GridApi, RowNode, GridOptions } from "ag-grid-community" - -import Layout from "../../../../../layouts/" -import { - t, - Button, - PageHeader, - AlertBox, - AppearanceStyleType, - useMutate, - AuthContext, - StatusBar, -} from "@bloom-housing/ui-components" -import { useSingleFlaggedApplication } from "../../../../../lib/hooks" -import { getCols } from "../../../../../src/flags/applicationsCols" -import { - EnumApplicationFlaggedSetStatus, - ApplicationFlaggedSet, -} from "@bloom-housing/backend-core/types" - -const Flag = () => { - const { applicationFlaggedSetsService } = useContext(AuthContext) - - const router = useRouter() - const flagsetId = router.query.flagId as string - const listingId = router.query.id as string - - const [gridApi, setGridApi] = useState(null) - const [selectedRows, setSelectedRows] = useState([]) - - const columns = useMemo(() => getCols(), []) - - const { data, revalidate } = useSingleFlaggedApplication(flagsetId) - - const { mutate, reset, isSuccess, isLoading, isError } = useMutate() - - const gridOptions: GridOptions = { - getRowNodeId: (data) => data.id, - } - - /* It selects all flagged rows on init and update (revalidate). */ - const selectFlaggedApps = useCallback(() => { - if (!data) return - - const duplicateIds = data.applications - .filter((item) => item.markedAsDuplicate) - .map((item) => item.id) - - gridApi.forEachNode((row) => { - if (duplicateIds.includes(row.id)) { - gridApi.selectNode(row, true) - } - }) - }, [data, gridApi]) - - useEffect(() => { - if (!gridApi) return - - selectFlaggedApps() - }, [data, gridApi, selectFlaggedApps]) - - const onGridReady = (params) => { - setGridApi(params.api) - } - - const onSelectionChanged = () => { - const selected = gridApi.getSelectedNodes() - setSelectedRows(selected) - } - - const deselectAll = useCallback(() => { - gridApi.deselectAll() - }, [gridApi]) - - const resolveFlag = useCallback(() => { - const applicationIds = selectedRows?.map((item) => ({ id: item.data.id })) || [] - - void reset() - - void mutate(() => - applicationFlaggedSetsService.resolve({ - body: { - afsId: flagsetId, - applications: applicationIds, - }, - }) - ).then(() => { - deselectAll() - void revalidate() - }) - }, [ - mutate, - reset, - revalidate, - deselectAll, - selectedRows, - applicationFlaggedSetsService, - flagsetId, - ]) - - if (!data) return null - - return ( - - - {t("nav.siteTitlePartners")} - - - -

{data.rule}

- - } - /> - -
- router.push(`/listings/${listingId}/flags`)} - > - {t("t.back")} - - } - tagStyle={ - data.status === EnumApplicationFlaggedSetStatus.resolved - ? AppearanceStyleType.success - : AppearanceStyleType.info - } - tagLabel={data.status} - /> -
- -
-
- {(isSuccess || isError) && ( - reset()} - > - {isSuccess ? "Updated" : t("account.settings.alerts.genericError")} - - )} - -
-
- - -
-
- -
-
-
-
- -
- - {t("flags.markedAsDuplicate", { - quantity: selectedRows.length, - })} - - - -
-
-
-
- ) -} - -export default Flag diff --git a/sites/partners/pages/listings/[id]/flags/index.tsx b/sites/partners/pages/listings/[id]/flags/index.tsx deleted file mode 100644 index 69085c8722..0000000000 --- a/sites/partners/pages/listings/[id]/flags/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useState, useMemo, useEffect } from "react" -import Head from "next/head" -import { useRouter } from "next/router" -import { AgGridReact } from "ag-grid-react" - -import { useFlaggedApplicationsList, useSingleListingData } from "../../../../lib/hooks" -import Layout from "../../../../layouts" -import { t, AgPagination, AG_PER_PAGE_OPTIONS } from "@bloom-housing/ui-components" -import { getFlagSetCols } from "../../../../src/flags/flagSetCols" -import { ApplicationSecondaryNav } from "../../../../src/applications/ApplicationSecondaryNav" - -const FlagsPage = () => { - const router = useRouter() - const listingId = router.query.id as string - - /* Pagination */ - const [itemsPerPage, setItemsPerPage] = useState(AG_PER_PAGE_OPTIONS[0]) - const [currentPage, setCurrentPage] = useState(1) - - // reset page to 1 when user change limit - useEffect(() => { - setCurrentPage(1) - }, [itemsPerPage]) - - const { listingDto } = useSingleListingData(listingId) - - const { data } = useFlaggedApplicationsList({ - listingId, - page: currentPage, - limit: itemsPerPage, - }) - - const listingName = listingDto?.name - - const defaultColDef = { - resizable: true, - maxWidth: 300, - } - - const columns = useMemo(() => getFlagSetCols(), []) - - if (!data) return null - - return ( - - - {t("nav.siteTitlePartners")} - - - - -
-
-
- - - -
-
-
-
- ) -} - -export default FlagsPage diff --git a/sites/partners/pages/listings/[id]/index.tsx b/sites/partners/pages/listings/[id]/index.tsx deleted file mode 100644 index 2691284ef6..0000000000 --- a/sites/partners/pages/listings/[id]/index.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React, { useMemo, useState } from "react" -import { useRouter } from "next/router" -import Head from "next/head" -import { - AppearanceStyleType, - PageHeader, - t, - Tag, - Button, - AlertBox, - SiteAlert, -} from "@bloom-housing/ui-components" -import { ListingStatus } from "@bloom-housing/backend-core/types" -import { useSingleListingData } from "../../../lib/hooks" - -import ListingGuard from "../../../src/ListingGuard" -import Layout from "../../../layouts" -import Aside from "../../../src/listings/Aside" -import { ListingContext } from "../../../src/listings/ListingContext" -import DetailListingData from "../../../src/listings/PaperListingDetails/sections/DetailListingData" -import DetailListingIntro from "../../../src/listings/PaperListingDetails/sections/DetailListingIntro" -import DetailListingPhoto from "../../../src/listings/PaperListingDetails/sections/DetailListingPhoto" -import DetailBuildingDetails from "../../../src/listings/PaperListingDetails/sections/DetailBuildingDetails" -import DetailAdditionalDetails from "../../../src/listings/PaperListingDetails/sections/DetailAdditionalDetails" -import DetailAdditionalEligibility from "../../../src/listings/PaperListingDetails/sections/DetailAdditionalEligibility" -import DetailLeasingAgent from "../../../src/listings/PaperListingDetails/sections/DetailLeasingAgent" -import DetailAdditionalFees from "../../../src/listings/PaperListingDetails/sections/DetailAdditionalFees" -import { DetailUnits } from "../../../src/listings/PaperListingDetails/sections/DetailUnits" -import DetailUnitDrawer, { - UnitDrawer, -} from "../../../src/listings/PaperListingDetails/DetailsUnitDrawer" -import DetailBuildingFeatures from "../../../src/listings/PaperListingDetails/sections/DetailBuildingFeatures" -import DetailRankingsAndResults from "../../../src/listings/PaperListingDetails/sections/DetailRankingsAndResults" -import DetailApplicationTypes from "../../../src/listings/PaperListingDetails/sections/DetailApplicationTypes" -import DetailApplicationAddress from "../../../src/listings/PaperListingDetails/sections/DetailApplicationAddress" -import DetailApplicationDates from "../../../src/listings/PaperListingDetails/sections/DetailApplicationDates" -import DetailPreferences from "../../../src/listings/PaperListingDetails/sections/DetailPreferences" -import DetailCommunityType from "../../../src/listings/PaperListingDetails/sections/DetailCommunityType" - -export default function ApplicationsList() { - const router = useRouter() - const listingId = router.query.id as string - const { listingDto } = useSingleListingData(listingId) - const [errorAlert, setErrorAlert] = useState(false) - const [unitDrawer, setUnitDrawer] = useState(null) - - const listingStatus = useMemo(() => { - switch (listingDto?.status) { - case ListingStatus.active: - return ( - - {t(`listings.listingStatus.active`)} - - ) - case ListingStatus.closed: - return ( - - {t(`listings.listingStatus.closed`)} - - ) - default: - return ( - - {t(`listings.listingStatus.pending`)} - - ) - } - }, [listingDto?.status]) - - if (!listingDto) return null - - return ( - - - <> - - - {t("nav.siteTitlePartners")} - - - -

{listingDto.name}

- -

{listingDto.id}

- - } - > -
- -
-
-
-
- - -
{listingStatus}
-
-
- -
-
- {errorAlert && ( - setErrorAlert(false)} - closeable - type="alert" - > - {t("authentication.signIn.errorGenericMessage")} - - )} - -
-
- - - - - - - - - - - - - - - - -
- -
-
-
-
-
-
- - - -
-
- ) -} diff --git a/sites/partners/pages/listings/add.tsx b/sites/partners/pages/listings/add.tsx deleted file mode 100644 index 55a06a1e8f..0000000000 --- a/sites/partners/pages/listings/add.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react" -import Head from "next/head" -import { PageHeader, SiteAlert, t } from "@bloom-housing/ui-components" -import Layout from "../../layouts" -import PaperListingForm from "../../src/listings/PaperListingForm" -import { MetaTags } from "../../src/MetaTags" -import ListingGuard from "../../src/ListingGuard" - -const NewListing = () => { - const metaDescription = "" - const metaImage = "" // TODO: replace with hero image - - return ( - - - - {t("nav.siteTitlePartners")} - - - - -
- -
-
- - -
-
- ) -} - -export default NewListing diff --git a/sites/partners/pages/reset-password.tsx b/sites/partners/pages/reset-password.tsx deleted file mode 100644 index d10f12f5b8..0000000000 --- a/sites/partners/pages/reset-password.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useState, useContext } from "react" -import { useRouter } from "next/router" -import { useForm } from "react-hook-form" -import { - AppearanceStyleType, - Button, - Field, - Form, - FormCard, - Icon, - AuthContext, - t, - AlertBox, - SiteAlert, - setSiteAlertMessage, -} from "@bloom-housing/ui-components" -import FormsLayout from "../layouts/forms" - -const ResetPassword = () => { - const router = useRouter() - const { token } = router.query - const { resetPassword } = useContext(AuthContext) - /* Form Handler */ - // This is causing a linting issue with unbound-method, see open issue as of 10/21/2020: - // https://github.com/react-hook-form/react-hook-form/issues/2887 - // eslint-disable-next-line @typescript-eslint/unbound-method - const { register, handleSubmit, errors } = useForm() - const [requestError, setRequestError] = useState() - - const onSubmit = async (data: { password: string; passwordConfirmation: string }) => { - const { password, passwordConfirmation } = data - - try { - const user = await resetPassword(token.toString(), password, passwordConfirmation) - setSiteAlertMessage(t(`authentication.signIn.success`, { name: user.firstName }), "success") - await router.push("/") - window.scrollTo(0, 0) - } catch (err) { - const { status, data } = err.response || {} - if (status === 400) { - setRequestError(`${t(`authentication.forgotPassword.errors.${data.message}`)}`) - } else { - console.error(err) - setRequestError(`${t("authentication.forgotPassword.errors.generic")}`) - } - } - } - - return ( - - -
- -

{t("authentication.forgotPassword.changePassword")}

-
- {requestError && ( - setRequestError(undefined)} type="alert"> - {requestError} - - )} - -
-
- - - -
- -
- -
-
-
- ) -} - -export { ResetPassword as default, ResetPassword } diff --git a/sites/partners/pages/sign-in.tsx b/sites/partners/pages/sign-in.tsx deleted file mode 100644 index da81bc9960..0000000000 --- a/sites/partners/pages/sign-in.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useContext } from "react" -import { useForm } from "react-hook-form" -import { useRouter } from "next/router" -import Link from "next/link" -import { - AlertBox, - AppearanceStyleType, - Button, - Field, - Form, - FormCard, - Icon, - AuthContext, - t, -} from "@bloom-housing/ui-components" -import { emailRegex } from "../lib/helpers" -import FormsLayout from "../layouts/forms" - -const SignIn = () => { - const { login } = useContext(AuthContext) - /* Form Handler */ - // eslint-disable-next-line @typescript-eslint/unbound-method - const { register, handleSubmit, errors, setError, clearErrors } = useForm() - const router = useRouter() - - const onSubmit = async (data: { email: string; password: string }) => { - const { email, password } = data - - try { - await login(email, password) - await router.push("/") - } catch (err) { - const { status } = err.response || {} - if (status === 401) { - console.warn(err.message) - setError("authentication", { - type: "manual", - message: t("authentication.signIn.cantFindAccount"), - }) - } else { - console.error(err) - setError("authentication", { - type: "manual", - message: `${t("authentication.signIn.error")} ${t( - "authentication.signIn.errorGenericMessage" - )}`, - }) - } - } - } - - const onError = () => { - window.scrollTo(0, 0) - } - - return ( - - -
- -

Partners Sign In

-
- {Object.entries(errors).length > 0 && ( - - {errors.authentication ? errors.authentication.message : t("t.errorsToResolve")} - - )} - -
-
- clearErrors("authentication"), - }} - /> - - - - clearErrors("authentication"), - }} - /> - -
- -
- -
-
-
- ) -} - -export default SignIn diff --git a/sites/partners/pages/users/index.tsx b/sites/partners/pages/users/index.tsx deleted file mode 100644 index 53ce5fa3d7..0000000000 --- a/sites/partners/pages/users/index.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React, { useMemo, useState } from "react" -import Head from "next/head" -import { AgGridReact } from "ag-grid-react" -import moment from "moment" -import { - PageHeader, - AgPagination, - Button, - t, - Drawer, - AG_PER_PAGE_OPTIONS, - SiteAlert, -} from "@bloom-housing/ui-components" - -import Layout from "../../layouts" -import { useUserList, useListingsData } from "../../lib/hooks" -import { FormUserAdd } from "../../src/users/FormUserAdd" - -const defaultColDef = { - resizable: true, - maxWidth: 300, -} - -const Users = () => { - /* Ag Grid column definitions */ - const columns = useMemo(() => { - return [ - { - headerName: t("t.name"), - field: "", - valueGetter: ({ data }) => { - const { firstName, lastName } = data - return `${firstName} ${lastName}` - }, - }, - { - headerName: t("t.email"), - field: "email", - }, - { - headerName: t("t.listing"), - field: "leasingAgentInListings", - valueFormatter: ({ value }) => { - return value.map((item) => item.name).join(", ") - }, - }, - { - headerName: t("t.role"), - field: "roles", - valueFormatter: ({ value }) => { - const { isAdmin, isPartner } = value || {} - - const roles = [] - - if (isAdmin) { - roles.push(t("users.administrator")) - } - - if (isPartner) { - roles.push(t("users.partner")) - } - - return roles.join(", ") - }, - }, - { - headerName: t("listings.details.createdDate"), - field: "createdAt", - valueFormatter: ({ value }) => moment(value).format("MM/DD/YYYY"), - }, - { - headerName: t("listings.unit.status"), - field: "confirmedAt", - valueFormatter: ({ value }) => (value ? t("users.confirmed") : t("users.unconfirmed")), - }, - ] - }, []) - - /* Pagination */ - const [itemsPerPage, setItemsPerPage] = useState(AG_PER_PAGE_OPTIONS[0]) - const [currentPage, setCurrentPage] = useState(1) - - /* Add user drawer */ - const [isDrawerOpen, setDrawerOpen] = useState(false) - - /* Fetch user list */ - const { data: userList } = useUserList({ - page: currentPage, - limit: itemsPerPage, - }) - - /* Fetch listings */ - const { listingDtos } = useListingsData({ - limit: "all", - }) - - const resetPagination = () => { - setCurrentPage(1) - } - - if (!userList) return null - - return ( - - - {t("nav.siteTitlePartners")} - - - -
- - -
-
- -
-
-
-
-
-
- -
-
-
- - - -
-
-
-
- - setDrawerOpen(false)} - > - setDrawerOpen(false)} /> - -
- ) -} - -export default Users diff --git a/sites/partners/public/favicon.ico b/sites/partners/public/favicon.ico index 62cfe653d1..d5f53ef79d 100644 Binary files a/sites/partners/public/favicon.ico and b/sites/partners/public/favicon.ico differ diff --git a/sites/partners/public/images/detroit-logo-white.png b/sites/partners/public/images/detroit-logo-white.png new file mode 100644 index 0000000000..25416bba0e Binary files /dev/null and b/sites/partners/public/images/detroit-logo-white.png differ diff --git a/sites/partners/public/images/detroit-logo.png b/sites/partners/public/images/detroit-logo.png new file mode 100644 index 0000000000..047208fe65 Binary files /dev/null and b/sites/partners/public/images/detroit-logo.png differ diff --git a/sites/partners/public/static/fonts/Montserrat.css b/sites/partners/public/static/fonts/Montserrat.css new file mode 100644 index 0000000000..e98df7333f --- /dev/null +++ b/sites/partners/public/static/fonts/Montserrat.css @@ -0,0 +1,35 @@ +/* latin */ +@font-face { + font-family: "Montserrat"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./montserrat-latin-400.woff2) format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, + U+FFFD; +} + +/* latin */ +@font-face { + font-family: "Montserrat"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(./montserrat-latin-600.woff2) format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, + U+FFFD; +} + +/* latin */ +@font-face { + font-family: "Montserrat"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(./montserrat-latin-700.woff2) format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, + U+FFFD; +} diff --git a/sites/partners/public/static/fonts/montserrat-latin-400.woff2 b/sites/partners/public/static/fonts/montserrat-latin-400.woff2 new file mode 100644 index 0000000000..8f098a5a42 Binary files /dev/null and b/sites/partners/public/static/fonts/montserrat-latin-400.woff2 differ diff --git a/sites/partners/public/static/fonts/montserrat-latin-600.woff2 b/sites/partners/public/static/fonts/montserrat-latin-600.woff2 new file mode 100644 index 0000000000..8f098a5a42 Binary files /dev/null and b/sites/partners/public/static/fonts/montserrat-latin-600.woff2 differ diff --git a/sites/partners/public/static/fonts/montserrat-latin-700.woff2 b/sites/partners/public/static/fonts/montserrat-latin-700.woff2 new file mode 100644 index 0000000000..8f098a5a42 Binary files /dev/null and b/sites/partners/public/static/fonts/montserrat-latin-700.woff2 differ diff --git a/sites/partners/src/LinkComponent.tsx b/sites/partners/src/LinkComponent.tsx deleted file mode 100644 index 3cdc91661e..0000000000 --- a/sites/partners/src/LinkComponent.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { PropsWithChildren } from "react" -import { LinkProps } from "@bloom-housing/ui-components" -import Link from "next/link" - -const LinkComponent = (props: PropsWithChildren) => { - const anchorProps = { ...props } - delete anchorProps.href - - return ( - - - - ) -} - -export default LinkComponent diff --git a/sites/partners/src/applications/Aside.tsx b/sites/partners/src/applications/Aside.tsx deleted file mode 100644 index 64cd5fb89f..0000000000 --- a/sites/partners/src/applications/Aside.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, { useContext, useMemo, useState } from "react" -import moment from "moment" -import { - t, - StatusAside, - Button, - GridCell, - AppearanceStyleType, - StatusMessages, - LocalizedLink, - Modal, - AppearanceBorderType, - LinkButton, -} from "@bloom-housing/ui-components" -import { ApplicationContext } from "./ApplicationContext" - -type AsideProps = { - type: AsideType - listingId: string - onDelete: () => void - triggerSubmitAndRedirect?: () => void -} - -type AsideType = "add" | "edit" | "details" - -const Aside = ({ listingId, type, onDelete, triggerSubmitAndRedirect }: AsideProps) => { - const application = useContext(ApplicationContext) - const [deleteModal, setDeleteModal] = useState(false) - - const applicationId = application?.id - - const applicationUpdated = useMemo(() => { - if (!application) return null - - const momentDate = moment(application.updatedAt) - - return momentDate.format("MMMM DD, YYYY") - }, [application]) - - const actions = useMemo(() => { - const elements = [] - - const cancel = ( - - - {t("t.cancel")} - - - ) - - if (type === "details") { - elements.push( - - - - - , - - - - ) - } - - if (type === "add" || type === "edit") { - elements.push( - - - - ) - - if (type === "add") { - elements.push( - - - , - cancel - ) - } - - if (type === "edit") { - elements.push( -
- {cancel} - - - -
- ) - } - } - - return elements - }, [applicationId, listingId, triggerSubmitAndRedirect, type]) - - return ( - <> - - {type === "edit" && } - - - setDeleteModal(false)} - actions={[ - , - , - ]} - > - {t("application.deleteApplicationDescription")} - - - ) -} - -export { Aside as default, Aside } diff --git a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsAlternateContact.tsx b/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsAlternateContact.tsx deleted file mode 100644 index 607dadb446..0000000000 --- a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsAlternateContact.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useContext } from "react" -import { t, GridSection, ViewItem, GridCell } from "@bloom-housing/ui-components" -import { ApplicationContext } from "../../ApplicationContext" -import { DetailsAddressColumns, AddressColsType } from "../DetailsAddressColumns" - -const DetailsAlternateContact = () => { - const application = useContext(ApplicationContext) - - return ( - - - - - {application.alternateContact.firstName || t("t.n/a")} - - - - - - {application.alternateContact.lastName || t("t.n/a")} - - - - - - {(() => { - if (!application.alternateContact.type) return t("t.n/a") - - if (application.alternateContact.otherType) - return application.alternateContact.otherType - - return t( - `application.alternateContact.type.options.${application.alternateContact.type}` - ) - })()} - - - - { - - - {application.alternateContact.agency || t("t.n/a")} - - - } - - - - {application.alternateContact.emailAddress || t("t.n/a")} - - - - - - {application.alternateContact.phoneNumber || t("t.n/a")} - - - - - - - - - ) -} - -export { DetailsAlternateContact as default, DetailsAlternateContact } diff --git a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsApplicationData.tsx b/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsApplicationData.tsx deleted file mode 100644 index 05a846f08e..0000000000 --- a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsApplicationData.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useContext, useMemo } from "react" -import { t, GridSection, ViewItem, GridCell } from "@bloom-housing/ui-components" -import { ApplicationContext } from "../../ApplicationContext" -import { convertDataToPst } from "../../../../lib/helpers" -import { ApplicationSubmissionType } from "@bloom-housing/backend-core/types" - -const DetailsApplicationData = () => { - const application = useContext(ApplicationContext) - - const applicationDate = useMemo(() => { - if (!application) return null - - return convertDataToPst( - application?.submissionDate, - application?.submissionType || ApplicationSubmissionType.electronical - ) - }, [application]) - - return ( - - - {application.id} - - - {application.submissionType && ( - - - {t(`application.details.submissionType.${application.submissionType}`)} - - - )} - - - {applicationDate.date} - - - - {applicationDate.time} - - - - - {application.language ? t(`languages.${application.language}`) : t("t.n/a")} - - - - - - {!application.householdSize ? 1 : application.householdSize} - - - - - - {application.applicant.firstName && application.applicant.lastName - ? `${application.applicant.firstName} ${application.applicant.lastName}` - : t("t.n/a")} - - - - ) -} - -export { DetailsApplicationData as default, DetailsApplicationData } diff --git a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsHouseholdDetails.tsx b/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsHouseholdDetails.tsx deleted file mode 100644 index 67440d6cb9..0000000000 --- a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsHouseholdDetails.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useContext, Fragment } from "react" -import { t, GridSection, ViewItem, GridCell, sortUnitTypes } from "@bloom-housing/ui-components" -import { ApplicationContext } from "../../ApplicationContext" - -const DetailsHouseholdDetails = () => { - const application = useContext(ApplicationContext) - - const accessibilityLabels = (accessibility) => { - const labels = [] - if (accessibility.mobility) labels.push(t("application.ada.mobility")) - if (accessibility.vision) labels.push(t("application.ada.vision")) - if (accessibility.hearing) labels.push(t("application.ada.hearing")) - if (labels.length === 0) labels.push(t("t.no")) - - return labels - } - - const preferredUnits = sortUnitTypes(application?.preferredUnit) - - return ( - - - - {(() => { - if (!preferredUnits.length) return t("t.n/a") - - return preferredUnits?.map((item) => ( - - {t(`application.household.preferredUnit.options.${item.name}`)} -
-
- )) - })()} -
-
- - - - {accessibilityLabels(application.accessibility).map((item) => ( - - {item} -
-
- ))} -
-
-
- ) -} - -export { DetailsHouseholdDetails as default, DetailsHouseholdDetails } diff --git a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsHouseholdMembers.tsx b/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsHouseholdMembers.tsx deleted file mode 100644 index 4600d00883..0000000000 --- a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsHouseholdMembers.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useContext, useMemo } from "react" -import { t, GridSection, MinimalTable, Button } from "@bloom-housing/ui-components" -import { ApplicationContext } from "../../ApplicationContext" -import { MembersDrawer } from "../DetailsMemberDrawer" -import { YesNoAnswer } from "../../PaperApplicationForm/FormTypes" - -type DetailsHouseholdMembersProps = { - setMembersDrawer: (member: MembersDrawer) => void -} - -const DetailsHouseholdMembers = ({ setMembersDrawer }: DetailsHouseholdMembersProps) => { - const application = useContext(ApplicationContext) - - const householdMembersHeaders = { - name: t("t.name"), - relationship: t("t.relationship"), - birth: t("application.household.member.dateOfBirth"), - sameResidence: t("application.add.sameResidence"), - workInRegion: t("application.details.workInRegion"), - action: "", - } - - const householdMembersData = useMemo(() => { - const checkAvailablility = (property) => { - if (property === YesNoAnswer.Yes) { - return t("t.yes") - } else if (property === "no") { - return t("t.no") - } - - return t("t.n/a") - } - return application?.householdMembers?.map((item) => ({ - name: `${item.firstName} ${item.middleName} ${item.lastName}`, - relationship: item.relationship - ? t(`application.form.options.relationship.${item.relationship}`) - : t("t.n/a"), - birth: - item.birthMonth && item.birthDay && item.birthYear - ? `${item.birthMonth}/${item.birthDay}/${item.birthYear}` - : t("t.n/a"), - sameResidence: checkAvailablility(item.sameAddress), - workInRegion: checkAvailablility(item.workInRegion), - action: ( - - ), - })) - }, [application, setMembersDrawer]) - - return ( - - {application.householdSize >= 1 ? ( - - ) : ( - {t("t.none")} - )} - - ) -} - -export { DetailsHouseholdMembers as default, DetailsHouseholdMembers } diff --git a/sites/partners/src/applications/PaperApplicationForm/sections/FormDemographics.tsx b/sites/partners/src/applications/PaperApplicationForm/sections/FormDemographics.tsx deleted file mode 100644 index 4f38384333..0000000000 --- a/sites/partners/src/applications/PaperApplicationForm/sections/FormDemographics.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React, { useMemo } from "react" -import { useFormContext } from "react-hook-form" -import { - t, - GridSection, - ViewItem, - GridCell, - Select, - FieldGroup, -} from "@bloom-housing/ui-components" -import { - ethnicityKeys, - raceKeys, - genderKeys, - sexualOrientation, - howDidYouHear, -} from "@bloom-housing/shared-helpers" - -const FormDemographics = () => { - const formMethods = useFormContext() - - // eslint-disable-next-line @typescript-eslint/unbound-method - const { register } = formMethods - - const howDidYouHearOptions = useMemo(() => { - return howDidYouHear?.map((item) => ({ - id: item.id, - label: t(`application.review.demographics.howDidYouHearOptions.${item.id}`), - register, - })) - }, [register]) - - return ( - - - - - - - - - - - - - - - - - - - - ) -} - -export { FormDemographics as default, FormDemographics } diff --git a/sites/partners/src/applications/PaperApplicationForm/sections/FormHouseholdDetails.tsx b/sites/partners/src/applications/PaperApplicationForm/sections/FormHouseholdDetails.tsx deleted file mode 100644 index 1efe1e03f1..0000000000 --- a/sites/partners/src/applications/PaperApplicationForm/sections/FormHouseholdDetails.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useEffect, useState } from "react" -import { useFormContext } from "react-hook-form" -import { t, GridSection, ViewItem, GridCell, Field, FieldGroup } from "@bloom-housing/ui-components" -import { getUniqueUnitTypes } from "@bloom-housing/ui-components/src/helpers/unitTypes" -import { Unit, UnitType } from "@bloom-housing/backend-core/types" - -type FormHouseholdDetailsProps = { - listingUnits: Unit[] - applicationUnitTypes: UnitType[] -} - -const FormHouseholdDetails = ({ - listingUnits, - applicationUnitTypes, -}: FormHouseholdDetailsProps) => { - const formMethods = useFormContext() - - // eslint-disable-next-line @typescript-eslint/unbound-method - const { register } = formMethods - - const unitTypes = getUniqueUnitTypes(listingUnits) - - const preferredUnitOptions = unitTypes?.map((item) => { - const isChecked = !!applicationUnitTypes?.find((unit) => unit.id === item.id) ?? false - - return { - id: item.id, - label: t(`application.household.preferredUnit.options.${item.name}`), - value: item.id, - defaultChecked: isChecked, - } - }) - - return ( - - - - - - - - -
- - - - - -
-
-
-
- ) -} - -export { FormHouseholdDetails as default, FormHouseholdDetails } diff --git a/sites/partners/src/applications/PaperApplicationForm/sections/FormPreferences.tsx b/sites/partners/src/applications/PaperApplicationForm/sections/FormPreferences.tsx deleted file mode 100644 index 3f39b5ee28..0000000000 --- a/sites/partners/src/applications/PaperApplicationForm/sections/FormPreferences.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React, { useMemo } from "react" -import { - Field, - t, - ExtraField, - getPreferenceOptionName, - GridSection, - ViewItem, - GridCell, - SelectOption, - getExclusivePreferenceOptionName, - getExclusiveKeys, - setExclusive, -} from "@bloom-housing/ui-components" - -import { useFormContext } from "react-hook-form" -import { Preference, FormMetadataExtraData } from "@bloom-housing/backend-core/types" - -type FormPreferencesProps = { - county: string - preferences: Preference[] - hhMembersOptions?: SelectOption[] -} - -const FormPreferences = ({ county, preferences, hhMembersOptions }: FormPreferencesProps) => { - const formMethods = useFormContext() - - // eslint-disable-next-line @typescript-eslint/unbound-method - const { register, setValue, watch } = formMethods - - const hasMetaData = useMemo(() => { - return !!preferences?.filter((preference) => preference?.formMetadata)?.length - }, [preferences]) - - const allOptionFieldNames = useMemo(() => { - const keys = [] - preferences?.forEach((preference) => - preference?.formMetadata?.options.forEach((option) => - keys.push(getPreferenceOptionName(option.key, preference?.formMetadata.key)) - ) - ) - - return keys - }, [preferences]) - - const watchPreferences = watch(allOptionFieldNames) - - const exclusiveKeys = getExclusiveKeys(preferences) - - if (!hasMetaData) { - return null - } - - const getOption = ( - optionKey: string | null, - optionName: string, - exclusive: boolean, - extraData: FormMetadataExtraData[], - preference: Preference, - label?: string - ) => { - return ( - - { - if (exclusive && e.target.checked) { - setExclusive(true, setValue, exclusiveKeys, optionName, preference) - } - if (!exclusive) { - setExclusive(false, setValue, exclusiveKeys, optionName, preference) - } - }, - }} - /> - {watchPreferences[optionName] && - extraData?.map((extra) => ( - - ))} - - ) - } - - return ( - - - {preferences?.map((preference) => { - const metaKey = preference?.formMetadata?.key - - return ( - - -
- {preference?.formMetadata?.options?.map((option) => { - return getOption( - option.key, - getPreferenceOptionName(option.key, preference.formMetadata.key), - option.exclusive, - option.extraData, - preference - ) - })} - - {preference?.formMetadata && - !preference.formMetadata.hideGenericDecline && - getOption( - null, - getExclusivePreferenceOptionName(preference?.formMetadata?.key), - true, - [], - preference, - t("application.preferences.dontWant") - )} -
-
-
- ) - })} -
-
- ) -} - -export { FormPreferences as default, FormPreferences } diff --git a/sites/partners/src/applications/PaperApplicationForm/sections/FormTerms.tsx b/sites/partners/src/applications/PaperApplicationForm/sections/FormTerms.tsx deleted file mode 100644 index 996a0fee6a..0000000000 --- a/sites/partners/src/applications/PaperApplicationForm/sections/FormTerms.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from "react" -import { useFormContext } from "react-hook-form" -import { t, GridSection, ViewItem, GridCell, Field } from "@bloom-housing/ui-components" -import { YesNoAnswer } from "../FormTypes" - -const FormTerms = () => { - const formMethods = useFormContext() - // eslint-disable-next-line @typescript-eslint/unbound-method - const { register } = formMethods - - return ( - - - -
- - - -
-
-
-
- ) -} - -export { FormTerms as default, FormTerms } diff --git a/sites/partners/src/applications/ApplicationContext.ts b/sites/partners/src/components/applications/ApplicationContext.ts similarity index 100% rename from sites/partners/src/applications/ApplicationContext.ts rename to sites/partners/src/components/applications/ApplicationContext.ts diff --git a/sites/partners/src/applications/ApplicationSecondaryNav.tsx b/sites/partners/src/components/applications/ApplicationSecondaryNav.tsx similarity index 87% rename from sites/partners/src/applications/ApplicationSecondaryNav.tsx rename to sites/partners/src/components/applications/ApplicationSecondaryNav.tsx index 42940e289b..e805aec58e 100644 --- a/sites/partners/src/applications/ApplicationSecondaryNav.tsx +++ b/sites/partners/src/components/applications/ApplicationSecondaryNav.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from "react" -import { PageHeader, t, TabNav, TabNavItem, AppearanceSizeType } from "@bloom-housing/ui-components" +import { t, TabNav, TabNavItem, AppearanceSizeType, PageHeader } from "@bloom-housing/ui-components" import { useRouter } from "next/router" type ApplicationSecondaryNavProps = { @@ -52,7 +52,11 @@ const ApplicationSecondaryNav = ({ }, [currentPath, tabNavElements]) return ( - + {children} ) diff --git a/sites/partners/src/applications/ApplicationsColDefs.ts b/sites/partners/src/components/applications/ApplicationsColDefs.ts similarity index 98% rename from sites/partners/src/applications/ApplicationsColDefs.ts rename to sites/partners/src/components/applications/ApplicationsColDefs.ts index f760b6e1bf..ad29ed4b6a 100644 --- a/sites/partners/src/applications/ApplicationsColDefs.ts +++ b/sites/partners/src/components/applications/ApplicationsColDefs.ts @@ -1,13 +1,15 @@ import { t, formatYesNoLabel } from "@bloom-housing/ui-components" import { IncomePeriod, ApplicationSubmissionType } from "@bloom-housing/backend-core/types" import { convertDataToPst, formatIncome } from "../../lib/helpers" -import moment from "moment" +import dayjs from "dayjs" +import customParseFormat from "dayjs/plugin/customParseFormat" +dayjs.extend(customParseFormat) function compareDates(a, b, node, nextNode, isInverted) { const dateStringFormat = "MM/DD/YYYY at hh:mm:ss A" - const dateA = moment(a, dateStringFormat) - const dateB = moment(b, dateStringFormat) + const dateA = dayjs(a, dateStringFormat) + const dateB = dayjs(b, dateStringFormat) if (a && b && dateA.isSame(dateB)) { return 0 @@ -60,7 +62,7 @@ export function getColDefs(maxHouseholdSize: number, countyCode: string) { }, { headerName: t("application.details.number"), - field: "id", + field: "confirmationCode", sortable: false, filter: false, width: 150, diff --git a/sites/partners/src/components/applications/Aside.tsx b/sites/partners/src/components/applications/Aside.tsx new file mode 100644 index 0000000000..6670085313 --- /dev/null +++ b/sites/partners/src/components/applications/Aside.tsx @@ -0,0 +1,169 @@ +import React, { useContext, useMemo, useState } from "react" +import dayjs from "dayjs" +import { + t, + GridCell, + AppearanceStyleType, + StatusMessages, + LocalizedLink, + AppearanceBorderType, + Button, + LinkButton, + Modal, +} from "@bloom-housing/ui-components" +import { ApplicationContext } from "./ApplicationContext" +import { StatusAside } from "../shared/StatusAside" + +type AsideProps = { + type: AsideType + listingId: string + onDelete: () => void + triggerSubmitAndRedirect?: () => void +} + +type AsideType = "add" | "edit" | "details" + +const Aside = ({ listingId, type, onDelete, triggerSubmitAndRedirect }: AsideProps) => { + const application = useContext(ApplicationContext) + const [deleteModal, setDeleteModal] = useState(false) + + const applicationId = application?.id + + const applicationUpdated = useMemo(() => { + if (!application) return null + + const dayjsDate = dayjs(application.updatedAt) + + return dayjsDate.format("MMMM DD, YYYY") + }, [application]) + + const actions = useMemo(() => { + const elements = [] + + const cancel = ( + + + {t("t.cancel")} + + + ) + + if (type === "details") { + elements.push( + + + + + , + + + + ) + } + + if (type === "add" || type === "edit") { + elements.push( + + + + ) + + if (type === "add") { + elements.push( + + + , + cancel + ) + } + + if (type === "edit") { + elements.push( +
+ {cancel} + + + +
+ ) + } + } + + return elements + }, [applicationId, listingId, triggerSubmitAndRedirect, type]) + + return ( + <> + + {type === "edit" && } + + + setDeleteModal(false)} + actions={[ + , + , + ]} + > + {t("application.deleteApplicationDescription")} + + + ) +} + +export { Aside as default, Aside } diff --git a/sites/partners/src/applications/PaperApplicationDetails/DetailsAddressColumns.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/DetailsAddressColumns.tsx similarity index 76% rename from sites/partners/src/applications/PaperApplicationDetails/DetailsAddressColumns.tsx rename to sites/partners/src/components/applications/PaperApplicationDetails/DetailsAddressColumns.tsx index 851e58e54a..8b8b81c35c 100644 --- a/sites/partners/src/applications/PaperApplicationDetails/DetailsAddressColumns.tsx +++ b/sites/partners/src/components/applications/PaperApplicationDetails/DetailsAddressColumns.tsx @@ -4,13 +4,14 @@ import { HouseholdMemberUpdate, AddressCreate, } from "@bloom-housing/backend-core/types" -import { YesNoAnswer } from "../PaperApplicationForm/FormTypes" +import { YesNoAnswer } from "../../../lib/helpers" type DetailsAddressColumnsProps = { type: AddressColsType application?: Application addressObject?: AddressCreate householdMember?: HouseholdMemberUpdate + dataTestId?: string } export enum AddressColsType { @@ -28,6 +29,7 @@ const DetailsAddressColumns = ({ application, addressObject, householdMember, + dataTestId, }: DetailsAddressColumnsProps) => { const address = { city: "", @@ -88,23 +90,36 @@ const DetailsAddressColumns = ({ return ( <> - {address.street} + + {address.street} + - {address.street2} + + {address.street2} + - {address.city} + + {address.city} + - {address.state} + + {address.state} + - {address.zipCode} + + {address.zipCode} + ) diff --git a/sites/partners/src/applications/PaperApplicationDetails/DetailsMemberDrawer.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/DetailsMemberDrawer.tsx similarity index 98% rename from sites/partners/src/applications/PaperApplicationDetails/DetailsMemberDrawer.tsx rename to sites/partners/src/components/applications/PaperApplicationDetails/DetailsMemberDrawer.tsx index 054a374eef..161f88301b 100644 --- a/sites/partners/src/applications/PaperApplicationDetails/DetailsMemberDrawer.tsx +++ b/sites/partners/src/components/applications/PaperApplicationDetails/DetailsMemberDrawer.tsx @@ -1,15 +1,15 @@ import React from "react" import { AppearanceStyleType, - t, - GridSection, - ViewItem, Button, Drawer, + GridSection, + t, + ViewItem, } from "@bloom-housing/ui-components" import { AddressColsType, DetailsAddressColumns } from "./DetailsAddressColumns" import { Application, HouseholdMemberUpdate } from "@bloom-housing/backend-core/types" -import { YesNoAnswer } from "../PaperApplicationForm/FormTypes" +import { YesNoAnswer } from "../../../lib/helpers" export type MembersDrawer = HouseholdMemberUpdate | null diff --git a/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsAlternateContact.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsAlternateContact.tsx new file mode 100644 index 0000000000..1b92e74ec4 --- /dev/null +++ b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsAlternateContact.tsx @@ -0,0 +1,76 @@ +import React, { useContext } from "react" +import { t, GridSection, GridCell, ViewItem } from "@bloom-housing/ui-components" +import { ApplicationContext } from "../../ApplicationContext" +import { DetailsAddressColumns, AddressColsType } from "../DetailsAddressColumns" + +const DetailsAlternateContact = () => { + const application = useContext(ApplicationContext) + + return ( + + + + + {application.alternateContact.firstName || t("t.n/a")} + + + + + + {application.alternateContact.lastName || t("t.n/a")} + + + + + + {(() => { + if (!application.alternateContact.type) return t("t.n/a") + + if (application.alternateContact.otherType) + return application.alternateContact.otherType + + return t( + `application.alternateContact.type.options.${application.alternateContact.type}` + ) + })()} + + + + { + + + {application.alternateContact.agency || t("t.n/a")} + + + } + + + + {application.alternateContact.emailAddress || t("t.n/a")} + + + + + + {application.alternateContact.phoneNumber || t("t.n/a")} + + + + + + + + + ) +} + +export { DetailsAlternateContact as default, DetailsAlternateContact } diff --git a/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsApplicationData.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsApplicationData.tsx new file mode 100644 index 0000000000..d4aa8aa0e9 --- /dev/null +++ b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsApplicationData.tsx @@ -0,0 +1,74 @@ +import React, { useContext, useMemo } from "react" +import { t, GridSection, GridCell, ViewItem } from "@bloom-housing/ui-components" +import { ApplicationContext } from "../../ApplicationContext" +import { convertDataToPst } from "../../../../lib/helpers" +import { ApplicationSubmissionType } from "@bloom-housing/backend-core/types" + +const DetailsApplicationData = () => { + const application = useContext(ApplicationContext) + + const applicationDate = useMemo(() => { + if (!application) return null + + return convertDataToPst( + application?.submissionDate, + application?.submissionType || ApplicationSubmissionType.electronical + ) + }, [application]) + + return ( + + + + {application.confirmationCode || application.id} + + + + {application.submissionType && ( + + + {t(`application.details.submissionType.${application.submissionType}`)} + + + )} + + + + {applicationDate.date} + + + + + + {applicationDate.time} + + + + + + {application.language ? t(`languages.${application.language}`) : t("t.n/a")} + + + + + + {!application.householdSize ? 1 : application.householdSize} + + + + + + {application.applicant.firstName && application.applicant.lastName + ? `${application.applicant.firstName} ${application.applicant.lastName}` + : t("t.n/a")} + + + + ) +} + +export { DetailsApplicationData as default, DetailsApplicationData } diff --git a/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsHouseholdDetails.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsHouseholdDetails.tsx new file mode 100644 index 0000000000..640b6a024c --- /dev/null +++ b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsHouseholdDetails.tsx @@ -0,0 +1,78 @@ +import React, { useContext, Fragment } from "react" +import { t, GridSection, GridCell, ViewItem } from "@bloom-housing/ui-components" +import { sortUnitTypes } from "@bloom-housing/shared-helpers" +import { ApplicationContext } from "../../ApplicationContext" + +const DetailsHouseholdDetails = () => { + const application = useContext(ApplicationContext) + + const accessibilityLabels = (accessibility) => { + const labels = [] + if (accessibility.mobility) labels.push(t("application.ada.mobility")) + if (accessibility.vision) labels.push(t("application.ada.vision")) + if (accessibility.hearing) labels.push(t("application.ada.hearing")) + if (labels.length === 0) labels.push(t("t.no")) + + return labels + } + + const preferredUnits = sortUnitTypes(application?.preferredUnit) + + return ( + + + + {(() => { + if (!preferredUnits.length) return t("t.n/a") + + return preferredUnits?.map((item) => ( + + {t(`application.household.preferredUnit.options.${item.name}`)} +
+
+ )) + })()} +
+
+ + + + {accessibilityLabels(application.accessibility).map((item) => ( + + {item} +
+
+ ))} +
+
+ + + {application.householdExpectingChanges ? t("t.yes") : t("t.no")} + + + + + + {application.householdStudent ? t("t.yes") : t("t.no")} + + +
+ ) +} + +export { DetailsHouseholdDetails as default, DetailsHouseholdDetails } diff --git a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsHouseholdIncome.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsHouseholdIncome.tsx similarity index 85% rename from sites/partners/src/applications/PaperApplicationDetails/sections/DetailsHouseholdIncome.tsx rename to sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsHouseholdIncome.tsx index d04e70e171..84e4c75ac3 100644 --- a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsHouseholdIncome.tsx +++ b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsHouseholdIncome.tsx @@ -1,5 +1,5 @@ import React, { useContext } from "react" -import { t, GridSection, ViewItem, GridCell } from "@bloom-housing/ui-components" +import { t, GridSection, GridCell, ViewItem } from "@bloom-housing/ui-components" import { ApplicationContext } from "../../ApplicationContext" import { IncomePeriod } from "@bloom-housing/backend-core/types" import { formatIncome } from "../../../../lib/helpers" @@ -14,7 +14,7 @@ const DetailsHouseholdIncome = () => { inset > - + {application.incomePeriod === IncomePeriod.perYear ? formatIncome( parseFloat(application.income), @@ -26,7 +26,7 @@ const DetailsHouseholdIncome = () => { - + {application.incomePeriod === IncomePeriod.perMonth ? formatIncome( parseFloat(application.income), @@ -38,7 +38,7 @@ const DetailsHouseholdIncome = () => { - + {(() => { if (application.incomeVouchers === null) return t("t.n/a") diff --git a/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsHouseholdMembers.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsHouseholdMembers.tsx new file mode 100644 index 0000000000..fd5a6211f4 --- /dev/null +++ b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsHouseholdMembers.tsx @@ -0,0 +1,80 @@ +import React, { useContext, useMemo } from "react" +import { Button, GridSection, MinimalTable, t } from "@bloom-housing/ui-components" +import { ApplicationContext } from "../../ApplicationContext" +import { MembersDrawer } from "../DetailsMemberDrawer" +import { YesNoAnswer } from "../../../../lib/helpers" + +type DetailsHouseholdMembersProps = { + setMembersDrawer: (member: MembersDrawer) => void +} + +const DetailsHouseholdMembers = ({ setMembersDrawer }: DetailsHouseholdMembersProps) => { + const application = useContext(ApplicationContext) + + const householdMembersHeaders = { + name: t("t.name"), + relationship: t("t.relationship"), + birth: t("application.household.member.dateOfBirth"), + sameResidence: t("application.add.sameResidence"), + workInRegion: t("application.details.workInRegion"), + action: "", + } + + const householdMembersData = useMemo(() => { + const checkAvailablility = (property) => { + if (property === YesNoAnswer.Yes) { + return t("t.yes") + } else if (property === "no") { + return t("t.no") + } + + return t("t.n/a") + } + return application?.householdMembers?.map((item) => ({ + name: { content: `${item.firstName} ${item.middleName} ${item.lastName}` }, + relationship: { + content: item.relationship + ? t(`application.form.options.relationship.${item.relationship}`) + : t("t.n/a"), + }, + birth: { + content: + item.birthMonth && item.birthDay && item.birthYear + ? `${item.birthMonth}/${item.birthDay}/${item.birthYear}` + : t("t.n/a"), + }, + sameResidence: { content: checkAvailablility(item.sameAddress) }, + workInRegion: { content: checkAvailablility(item.workInRegion) }, + action: { + content: ( + + ), + }, + })) + }, [application, setMembersDrawer]) + + return ( + + {application.householdSize >= 1 ? ( + + ) : ( + {t("t.none")} + )} + + ) +} + +export { DetailsHouseholdMembers as default, DetailsHouseholdMembers } diff --git a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsPreferences.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPreferences.tsx similarity index 84% rename from sites/partners/src/applications/PaperApplicationDetails/sections/DetailsPreferences.tsx rename to sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPreferences.tsx index 1595a8daec..0cd1f08283 100644 --- a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsPreferences.tsx +++ b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPreferences.tsx @@ -1,5 +1,5 @@ import React, { useContext, useMemo } from "react" -import { t, GridSection, ViewItem, GridCell } from "@bloom-housing/ui-components" +import { t, GridSection, GridCell, ViewItem } from "@bloom-housing/ui-components" import { ApplicationContext } from "../../ApplicationContext" import { InputType, AddressCreate } from "@bloom-housing/backend-core/types" import { DetailsAddressColumns, AddressColsType } from "../DetailsAddressColumns" @@ -14,11 +14,13 @@ const DetailsPreferences = ({ listingId }: DetailsPreferencesProps) => { const application = useContext(ApplicationContext) - const listingPreferences = listingDto?.preferences + const listingPreferences = listingDto?.listingPreferences const preferences = application?.preferences const hasMetaData = useMemo(() => { - return !!listingPreferences?.filter((preference) => preference?.formMetadata)?.length + return !!listingPreferences?.filter( + (listingPreference) => listingPreference.preference?.formMetadata + )?.length }, [listingPreferences]) if (!hasMetaData) { @@ -33,11 +35,11 @@ const DetailsPreferences = ({ listingId }: DetailsPreferencesProps) => { columns={2} > {listingPreferences?.map((listingPreference) => { - const metaKey = listingPreference?.formMetadata?.key + const metaKey = listingPreference?.preference?.formMetadata?.key const optionDetails = preferences.find((item) => item.key === metaKey) return ( - + { const extra = option.extraData?.map((extra) => { if (extra.type === InputType.text) return ( - - {extra.value} + + <>{extra.value} ) diff --git a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant.tsx similarity index 75% rename from sites/partners/src/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant.tsx rename to sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant.tsx index b77989d27b..67577dd282 100644 --- a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant.tsx +++ b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPrimaryApplicant.tsx @@ -1,8 +1,8 @@ import React, { useContext } from "react" -import { t, GridSection, ViewItem, GridCell } from "@bloom-housing/ui-components" +import { t, GridSection, GridCell, ViewItem } from "@bloom-housing/ui-components" import { ApplicationContext } from "../../ApplicationContext" import { DetailsAddressColumns, AddressColsType } from "../DetailsAddressColumns" -import { YesNoAnswer } from "../../PaperApplicationForm/FormTypes" +import { YesNoAnswer } from "../../../../lib/helpers" const DetailsPrimaryApplicant = () => { const application = useContext(ApplicationContext) @@ -16,25 +16,25 @@ const DetailsPrimaryApplicant = () => { > - + {application.applicant.firstName || t("t.n/a")} - + {application.applicant.middleName || t("t.n/a")} - + {application.applicant.lastName || t("t.n/a")} - + {(() => { const { birthMonth, birthDay, birthYear } = application?.applicant @@ -48,7 +48,7 @@ const DetailsPrimaryApplicant = () => { - + {application.applicant.emailAddress || t("t.n/a")} @@ -60,6 +60,7 @@ const DetailsPrimaryApplicant = () => { application.applicant.phoneNumberType && t(`application.contact.phoneNumberTypes.${application.applicant.phoneNumberType}`) } + dataTestId="phoneNumber" > {application.applicant.phoneNumber || t("t.n/a")} @@ -72,13 +73,14 @@ const DetailsPrimaryApplicant = () => { application.additionalPhoneNumber && t(`application.contact.phoneNumberTypes.${application.additionalPhoneNumberType}`) } + dataTestId="additionalPhoneNumber" > {application.additionalPhoneNumber || t("t.n/a")} - + {(() => { if (!application.contactPreferences.length) return t("t.n/a") @@ -93,7 +95,7 @@ const DetailsPrimaryApplicant = () => { - + {(() => { if (!application.applicant.workInRegion) return t("t.n/a") @@ -104,15 +106,27 @@ const DetailsPrimaryApplicant = () => { - + - + - + ) diff --git a/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPrograms.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPrograms.tsx new file mode 100644 index 0000000000..59bcb0c759 --- /dev/null +++ b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsPrograms.tsx @@ -0,0 +1,69 @@ +import React, { useContext } from "react" +import { t, GridSection, GridCell, ViewItem } from "@bloom-housing/ui-components" +import { ApplicationContext } from "../../ApplicationContext" +import { useSingleListingData } from "../../../../lib/hooks" + +type DetailsProgramsProps = { + listingId: string +} + +const DetailsPrograms = ({ listingId }: DetailsProgramsProps) => { + const { listingDto } = useSingleListingData(listingId) + + const application = useContext(ApplicationContext) + + const listingPrograms = listingDto?.listingPrograms + const programs = application?.programs + + const hasMetaData = !!listingPrograms?.some( + (listingProgram) => listingProgram.program?.formMetadata + ) + + if (!hasMetaData) { + return null + } + + return ( + + {listingPrograms?.map((listingProgram) => { + const metaKey = listingProgram?.program?.formMetadata?.key + const optionDetails = programs?.find((item) => item.key === metaKey) + + return ( + + + {(() => { + if (!optionDetails?.claimed) return t("t.none") + + const options = optionDetails.options.filter((option) => option.checked) + + return options.map((option) => ( +
+

+ {option.key === "preferNotToSay" + ? t("t.preferNotToSay") + : t(`application.programs.${metaKey}.${option.key}.label`, { + county: listingDto?.countyCode, + })} +

+
+ )) + })()} +
+
+ ) + })} +
+ ) +} + +export { DetailsPrograms as default, DetailsPrograms } diff --git a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsTerms.tsx b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsTerms.tsx similarity index 89% rename from sites/partners/src/applications/PaperApplicationDetails/sections/DetailsTerms.tsx rename to sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsTerms.tsx index 5ad5fa6081..ad299133f0 100644 --- a/sites/partners/src/applications/PaperApplicationDetails/sections/DetailsTerms.tsx +++ b/sites/partners/src/components/applications/PaperApplicationDetails/sections/DetailsTerms.tsx @@ -1,5 +1,5 @@ import React, { useContext } from "react" -import { t, GridSection, ViewItem, GridCell } from "@bloom-housing/ui-components" +import { t, GridSection, GridCell, ViewItem } from "@bloom-housing/ui-components" import { ApplicationContext } from "../../ApplicationContext" const DetailsTerms = () => { @@ -13,7 +13,7 @@ const DetailsTerms = () => { grid={false} > - + {(() => { if (application.acceptedTerms === null) { return t("t.n/a") diff --git a/sites/partners/src/applications/PaperApplicationForm/FormMember.tsx b/sites/partners/src/components/applications/PaperApplicationForm/FormMember.tsx similarity index 96% rename from sites/partners/src/applications/PaperApplicationForm/FormMember.tsx rename to sites/partners/src/components/applications/PaperApplicationForm/FormMember.tsx index d7c480a9ef..fde61eb001 100644 --- a/sites/partners/src/applications/PaperApplicationForm/FormMember.tsx +++ b/sites/partners/src/components/applications/PaperApplicationForm/FormMember.tsx @@ -1,23 +1,23 @@ import React, { useMemo } from "react" import { HouseholdMember, Member } from "@bloom-housing/backend-core/types" import { - t, - GridSection, - ViewItem, - GridCell, + AppearanceBorderType, + AppearanceStyleType, + Button, DOBField, Field, - Select, - AppearanceStyleType, - AppearanceBorderType, FieldGroup, - Button, Form, FormAddress, + GridCell, + GridSection, + Select, + t, + ViewItem, } from "@bloom-housing/ui-components" -import { relationshipKeys } from "@bloom-housing/shared-helpers" +import { relationshipKeys, stateKeys } from "@bloom-housing/shared-helpers" import { useForm } from "react-hook-form" -import { YesNoAnswer } from "./FormTypes" +import { YesNoAnswer } from "../../../lib/helpers" type ApplicationFormMemberProps = { onSubmit: (member: HouseholdMember) => void @@ -211,8 +211,8 @@ const FormMember = ({ onSubmit, onClose, members, editedMemberId }: ApplicationF )} @@ -220,8 +220,8 @@ const FormMember = ({ onSubmit, onClose, members, editedMemberId }: ApplicationF )}
@@ -231,6 +231,7 @@ const FormMember = ({ onSubmit, onClose, members, editedMemberId }: ApplicationF type="button" onClick={() => onFormSubmit()} styleType={AppearanceStyleType.primary} + dataTestId={"submitAddMemberForm"} > {t("t.submit")} diff --git a/sites/partners/src/applications/PaperApplicationForm/PaperApplicationForm.tsx b/sites/partners/src/components/applications/PaperApplicationForm/PaperApplicationForm.tsx similarity index 87% rename from sites/partners/src/applications/PaperApplicationForm/PaperApplicationForm.tsx rename to sites/partners/src/components/applications/PaperApplicationForm/PaperApplicationForm.tsx index 9ac636e0ac..4a4f0da872 100644 --- a/sites/partners/src/applications/PaperApplicationForm/PaperApplicationForm.tsx +++ b/sites/partners/src/components/applications/PaperApplicationForm/PaperApplicationForm.tsx @@ -1,19 +1,18 @@ import React, { useState, useContext, useEffect } from "react" import { useRouter } from "next/router" import { - AuthContext, - t, - Form, AlertBox, - setSiteAlertMessage, - LoadingOverlay, - StatusBar, AppearanceStyleType, Button, + Form, + LoadingOverlay, + setSiteAlertMessage, + t, } from "@bloom-housing/ui-components" +import { AuthContext } from "@bloom-housing/shared-helpers" import { useForm, FormProvider } from "react-hook-form" import { HouseholdMember, Application, ApplicationStatus } from "@bloom-housing/backend-core/types" -import { mapFormToApi, mapApiToForm } from "../../../lib/formatApplicationData" +import { mapFormToApi, mapApiToForm } from "../../../lib/applications/formatApplicationData" import { useSingleListingData } from "../../../lib/hooks" import { FormApplicationData } from "./sections/FormApplicationData" import { FormPrimaryApplicant } from "./sections/FormPrimaryApplicant" @@ -24,9 +23,11 @@ import { FormPreferences } from "./sections/FormPreferences" import { FormHouseholdIncome } from "./sections/FormHouseholdIncome" import { FormDemographics } from "./sections/FormDemographics" import { FormTerms } from "./sections/FormTerms" +import { FormPrograms } from "./sections/FormPrograms" import { Aside } from "../Aside" -import { FormTypes } from "./FormTypes" +import { FormTypes } from "../../../lib/applications/FormTypes" +import { StatusBar } from "../../../components/shared/StatusBar" type ApplicationFormProps = { listingId: string @@ -40,7 +41,8 @@ type AlertErrorType = "api" | "form" const ApplicationForm = ({ listingId, editMode, application }: ApplicationFormProps) => { const { listingDto } = useSingleListingData(listingId) - const preferences = listingDto?.preferences + const preferences = listingDto?.listingPreferences + const programs = listingDto?.listingPrograms const countyCode = listingDto?.countyCode const units = listingDto?.units @@ -97,12 +99,17 @@ const ApplicationForm = ({ listingId, editMode, application }: ApplicationFormPr ...data, } - const body = mapFormToApi(formData, listingId, editMode) + const body = mapFormToApi({ + data: formData, + listingId, + editMode, + programs: programs.map((item) => item.program), + }) try { const result = editMode ? await applicationsService.update({ - applicationId: application.id, + id: application.id, body: { id: application.id, ...body }, }) : await applicationsService.create({ body }) @@ -138,7 +145,8 @@ const ApplicationForm = ({ listingId, editMode, application }: ApplicationFormPr async function deleteApplication() { try { - await applicationsService.delete({ applicationId: application?.id }) + await applicationsService.delete({ body: { id: application?.id } }) + void router.push(`/listings/${listingId}/applications`) } catch (err) { setAlert("api") @@ -200,13 +208,16 @@ const ApplicationForm = ({ listingId, editMode, application }: ApplicationFormPr + + - +
diff --git a/sites/partners/src/applications/PaperApplicationForm/sections/FormAlternateContact.tsx b/sites/partners/src/components/applications/PaperApplicationForm/sections/FormAlternateContact.tsx similarity index 97% rename from sites/partners/src/applications/PaperApplicationForm/sections/FormAlternateContact.tsx rename to sites/partners/src/components/applications/PaperApplicationForm/sections/FormAlternateContact.tsx index b6a4cc1fcc..6073e35fa4 100644 --- a/sites/partners/src/applications/PaperApplicationForm/sections/FormAlternateContact.tsx +++ b/sites/partners/src/components/applications/PaperApplicationForm/sections/FormAlternateContact.tsx @@ -1,17 +1,17 @@ import React, { useEffect } from "react" import { useFormContext } from "react-hook-form" import { - t, - GridSection, - ViewItem, - Select, - GridCell, - Field, emailRegex, - PhoneField, + Field, FormAddress, + GridCell, + GridSection, + PhoneField, + Select, + t, + ViewItem, } from "@bloom-housing/ui-components" -import { altContactRelationshipKeys } from "@bloom-housing/shared-helpers" +import { altContactRelationshipKeys, stateKeys } from "@bloom-housing/shared-helpers" const FormAlternateContact = () => { const formMethods = useFormContext() @@ -141,8 +141,8 @@ const FormAlternateContact = () => { diff --git a/sites/partners/src/applications/PaperApplicationForm/sections/FormApplicationData.tsx b/sites/partners/src/components/applications/PaperApplicationForm/sections/FormApplicationData.tsx similarity index 97% rename from sites/partners/src/applications/PaperApplicationForm/sections/FormApplicationData.tsx rename to sites/partners/src/components/applications/PaperApplicationForm/sections/FormApplicationData.tsx index c6fa784d6a..b38f8ee8d3 100644 --- a/sites/partners/src/applications/PaperApplicationForm/sections/FormApplicationData.tsx +++ b/sites/partners/src/components/applications/PaperApplicationForm/sections/FormApplicationData.tsx @@ -1,14 +1,14 @@ import React, { useEffect } from "react" import { - t, + DateField, + DateFieldValues, GridSection, - ViewItem, Select, - applicationLanguageKeys, + t, TimeField, - DateField, - DateFieldValues, + ViewItem, } from "@bloom-housing/ui-components" +import { applicationLanguageKeys } from "@bloom-housing/shared-helpers" import { useFormContext } from "react-hook-form" const FormApplicationData = () => { diff --git a/sites/partners/src/components/applications/PaperApplicationForm/sections/FormDemographics.tsx b/sites/partners/src/components/applications/PaperApplicationForm/sections/FormDemographics.tsx new file mode 100644 index 0000000000..ccc8e1876e --- /dev/null +++ b/sites/partners/src/components/applications/PaperApplicationForm/sections/FormDemographics.tsx @@ -0,0 +1,107 @@ +import React, { useMemo } from "react" +import { useFormContext } from "react-hook-form" +import { + FieldGroup, + GridCell, + GridSection, + Select, + t, + ViewItem, +} from "@bloom-housing/ui-components" +import { ethnicityKeys, raceKeys, howDidYouHear } from "@bloom-housing/shared-helpers" +import { Demographics } from "@bloom-housing/backend-core/types" + +type FormDemographicsProps = { + formValues: Demographics +} + +const FormDemographics = ({ formValues }: FormDemographicsProps) => { + const formMethods = useFormContext() + + // eslint-disable-next-line @typescript-eslint/unbound-method + const { register } = formMethods + + const howDidYouHearOptions = useMemo(() => { + return howDidYouHear?.map((item) => ({ + id: item.id, + label: t(`application.review.demographics.howDidYouHearOptions.${item.id}`), + register, + })) + }, [register]) + + // Does a key exist in a root field or a sub array + const isKeyIncluded = (raceKey: string) => { + let keyExists = false + formValues?.race?.forEach((value) => { + if (value.includes(raceKey)) { + keyExists = true + } + }) + return keyExists + } + + // Get the value of a field that is storing a custom value, i.e. "otherAsian: Custom Race Input" + const getCustomValue = (subKey: string) => { + const customValues = formValues?.race?.filter((value) => value.split(":")[0] === subKey) + return customValues?.length ? customValues[0].split(":")[1]?.substring(1) : "" + } + + const raceOptions = useMemo(() => { + return Object.keys(raceKeys).map((rootKey) => ({ + id: rootKey, + label: t(`application.review.demographics.raceOptions.${rootKey}`), + value: rootKey, + additionalText: rootKey.indexOf("other") >= 0, + defaultChecked: isKeyIncluded(rootKey), + defaultText: getCustomValue(rootKey), + subFields: raceKeys[rootKey].map((subKey) => ({ + id: subKey, + label: t(`application.review.demographics.raceOptions.${subKey}`), + value: subKey, + defaultChecked: isKeyIncluded(subKey), + additionalText: subKey.indexOf("other") >= 0, + defaultText: getCustomValue(subKey), + })), + })) + }, [register, isKeyIncluded, getCustomValue]) + + return ( + + + + + + + + + + - - -
- - + {!draft ? ( + + ) : ( + + )} + + +
+ + ) +} + +export default UnitsSummaryAmiForm diff --git a/sites/partners/src/components/listings/PaperListingForm/UnitsSummaryForm.tsx b/sites/partners/src/components/listings/PaperListingForm/UnitsSummaryForm.tsx new file mode 100644 index 0000000000..13a885bb70 --- /dev/null +++ b/sites/partners/src/components/listings/PaperListingForm/UnitsSummaryForm.tsx @@ -0,0 +1,674 @@ +import React, { useEffect, useState, useCallback, useMemo } from "react" +import { + AppearanceBorderType, + AppearanceSizeType, + AppearanceStyleType, + Button, + Drawer, + Field, + FieldGroup, + Form, + GridCell, + GridSection, + MinimalTable, + Modal, + numberOptions, + Select, + t, + ViewItem, +} from "@bloom-housing/ui-components" +import { useForm, useFormContext } from "react-hook-form" +import { TempUnitsSummary, TempAmiLevel } from "../../../lib/listings/formTypes" + +import { + AmiChart, + MonthlyRentDeterminationType, + UnitAccessibilityPriorityType, +} from "@bloom-housing/backend-core/types" +import { useUnitPriorityList, useAmiChartList } from "../../../lib/hooks" +import { arrayToFormOptions } from "../../../lib/helpers" +import UnitsSummaryAmiForm from "./UnitsSummaryAmiForm" + +interface FieldSingle { + id: string + label: string + value?: string +} + +type UnitsSummaryFormProps = { + onSubmit: (unit: TempUnitsSummary) => void + onClose: () => void + summaries: TempUnitsSummary[] + currentTempId: number + unitTypeOptions: FieldSingle[] +} + +const UnitsSummaryForm = ({ + onSubmit, + onClose, + summaries, + currentTempId, + unitTypeOptions, +}: UnitsSummaryFormProps) => { + const [current, setCurrent] = useState(null) + const [summaryDrawer, setSummaryDrawer] = useState(null) + const [amiDeleteModal, setAmiDeleteModal] = useState(null) + const [options, setOptions] = useState({ + unitPriorities: [], + amiCharts: [], + }) + + // eslint-disable-next-line @typescript-eslint/unbound-method + const { watch } = useFormContext() + const jurisdiction: string = watch("jurisdiction.id") + + // eslint-disable-next-line @typescript-eslint/unbound-method + const { register, errors, trigger, getValues, reset, clearErrors, watch: formWatch } = useForm({ + defaultValues: { + unitType: current?.unitType, + floorMin: current?.floorMin, + floorMax: current?.floorMax, + sqFeetMin: current?.sqFeetMin, + sqFeetMax: current?.sqFeetMax, + bathroomMin: current?.bathroomMin, + bathroomMax: current?.bathroomMax, + minOccupancy: current?.minOccupancy, + maxOccupancy: current?.maxOccupancy, + totalCount: current?.totalCount, + totalAvailable: current?.totalAvailable, + priorityType: current?.priorityType, + openWaitlist: current?.openWaitlist, + amiLevels: current?.amiLevels, + }, + }) + + const unitType = formWatch("unitType") + const minOccupancy = formWatch("minOccupancy") + const maxOccupancy = formWatch("maxOccupancy") + const sqFeetMin = formWatch("sqFeetMin") + const sqFeetMax = formWatch("sqFeetMax") + const floorMin = formWatch("floorMin") + const floorMax = formWatch("floorMax") + const bathroomMin = formWatch("bathroomMin") + const bathroomMax = formWatch("bathroomMax") + const totalAvailable = formWatch("totalAvailable") + const totalCount = formWatch("totalCount") + + /** + * fetch form options + */ + const { data: unitPriorities = [] } = useUnitPriorityList() + const { data: amis = [] } = useAmiChartList(jurisdiction) + + useEffect(() => { + const summary = summaries.find((summary) => summary.tempId === currentTempId) + setCurrent(summary) + reset({ + ...summary, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore:next-line + unitType: summary?.unitType?.map((elem) => elem.id ?? elem.toString()), + openWaitListQuestion: summary?.openWaitListQuestion || summary?.openWaitlist?.toString(), + }) + }, [summaries, reset, currentTempId, setCurrent]) + + async function onFormSubmit() { + // Triggers validation across the form. + const validation = await trigger() + if (!validation) return + + const data = getValues() + + if (data.priorityType?.id) { + const priority = unitPriorities.find((priority) => priority.id === data.priorityType.id) + data.priorityType = priority + } else { + delete data.priorityType + } + + let amiLevels + if (current) { + amiLevels = current.amiLevels?.map((level) => ({ + ...level, + amiChart: amis.find((a) => a.id === level.amiChartId), + })) + } else if (data?.amiLevels) { + data.amiLevels = data.amiLevels.map((level) => ({ + ...level, + amiChart: amis.find((a) => a.id === level.amiChartId), + })) + } + + const formData = { + createdAt: undefined, + updatedAt: undefined, + ...data, + } + + if (current) { + onSubmit({ + ...formData, + id: current.id, + tempId: current.tempId || currentTempId, + amiLevels, + }) + } else { + onSubmit({ + ...formData, + id: undefined, + tempId: (summaries.length || 0) + 1, + }) + } + onClose() + } + + useEffect(() => { + setOptions({ + unitPriorities: arrayToFormOptions( + unitPriorities, + "name", + "id", + undefined, + true + ), + amiCharts: arrayToFormOptions(amis, "name", "id", undefined, true), + }) + }, [unitPriorities, amis]) + + const editAmi = useCallback( + (tempId: number) => { + setSummaryDrawer(tempId) + }, + [setSummaryDrawer] + ) + + const saveAmiSummary = (newAmiLevel: TempAmiLevel) => { + const exists = current?.amiLevels?.some((amiLevel) => amiLevel.tempId === newAmiLevel.tempId) + if (exists) { + setCurrent({ + ...current, + amiLevels: current.amiLevels.map((amiLevel) => + amiLevel.tempId === newAmiLevel.tempId ? newAmiLevel : amiLevel + ), + }) + } else { + if (current?.amiLevels) { + setCurrent({ ...current, amiLevels: [...current.amiLevels, newAmiLevel] }) + } else { + setCurrent({ ...current, amiLevels: [newAmiLevel] }) + } + } + } + + const amiSummaryTableData = useMemo( + () => + current?.amiLevels?.map((ami) => { + const selectedAmiChart = options.amiCharts.find((chart) => chart.value === ami.amiChartId) + + let rentValue = undefined + let monthlyRentDeterminationType = undefined + if (ami.monthlyRentDeterminationType === MonthlyRentDeterminationType.flatRent) { + rentValue = `${ami.flatRentValue ? `$${ami.flatRentValue}` : ""}` + monthlyRentDeterminationType = t("listings.unitsSummary.flatRent") + } else if ( + ami.monthlyRentDeterminationType === MonthlyRentDeterminationType.percentageOfIncome + ) { + rentValue = `${ami.percentageOfIncomeValue ? `${ami.percentageOfIncomeValue}%` : ""}` + monthlyRentDeterminationType = t("listings.unitsSummary.percentIncome") + } + + return { + amiChartName: { content: selectedAmiChart?.label || "" }, + amiPercentage: { content: `${ami.amiPercentage ? `${ami.amiPercentage}%` : ""}` }, + monthlyRentDeterminationType: { content: monthlyRentDeterminationType }, + rentValue: { content: rentValue }, + action: { + content: ( +
+ + +
+ ), + }, + } + }), + [current, options.amiCharts, editAmi, setAmiDeleteModal] + ) + + const deleteAmi = useCallback( + (tempId: number) => { + const updateAmis = current?.amiLevels + .filter((summary) => summary.tempId !== tempId) + .map((updatedAmi, index) => ({ + ...updatedAmi, + tempId: index + 1, + })) + + setCurrent({ ...current, amiLevels: updateAmis }) + setAmiDeleteModal(null) + }, + [setAmiDeleteModal, setCurrent, current] + ) + + const openWaitlistOptions = [ + { + id: "true", + label: t("listings.unitsSummary.open"), + value: "true", + defaultChecked: true, + }, + { + id: "false", + label: t("listings.unitsSummary.closed"), + value: "false", + }, + ] + + const amiSummariesHeaders = { + amiChartName: "listings.unitsSummary.amiChart", + amiPercentage: "listings.unitsSummary.amiLevel", + monthlyRentDeterminationType: "listings.unitsSummary.rentType", + rentValue: "listings.unitsSummary.flatRentValue", + action: "", + } + + const bathroomOptions = [ + { label: "", value: "" }, + { label: ".5", value: "0.5" }, + { label: "1", value: "1" }, + { label: "1.5", value: "1.5" }, + { label: "2", value: "2" }, + { label: "3", value: "3" }, + { label: "4", value: "4" }, + { label: "5", value: "5" }, + ] + + useEffect(() => { + if (unitType?.length && errors?.unitType) { + clearErrors("unitType") + } + }, [unitType, errors, clearErrors]) + + return ( + <> +
false}> +
+ + + + + + + + + + + + { + void trigger("totalCount") + void trigger("totalAvailable") + }, + }} + dataTestId="totalCount" + /> + + + + + + + { + void trigger("minOccupancy") + void trigger("maxOccupancy") + }, + }} + dataTestId="maxOccupancy" + /> + + + + + + + { + void trigger("sqFeetMin") + void trigger("sqFeetMax") + }, + }} + dataTestId="sqFeetMin" + /> + + + + + { + void trigger("sqFeetMin") + void trigger("sqFeetMax") + }, + }} + dataTestId="sqFeetMax" + /> + + + + + + + { + void trigger("floorMin") + void trigger("floorMax") + }, + }} + dataTestId="floorMax" + /> + + + + + + + { + void trigger("bathroomMin") + void trigger("bathroomMax") + }, + }} + dataTestId="bathroomMax" + /> + + + + + + + + { + void trigger("totalCount") + void trigger("totalAvailable") + }, + }} + dataTestId="totalAvailable" + /> + + + + + + + + + + +
+ {current?.amiLevels?.length ? ( +
+ +
+ ) : null} + +
+
+
+
+
+ + + +
+
+ setSummaryDrawer(null)} + > + saveAmiSummary(amiLevel)} + onClose={() => setSummaryDrawer(null)} + amiLevels={current?.amiLevels || []} + currentTempId={summaryDrawer} + amiCharOptions={options.amiCharts} + amiInfo={amis} + /> + + deleteAmi(amiDeleteModal)}> + {t("t.delete")} + , + , + ]} + > + {t("listings.unitsSummary.deleteAmiConf")} + + + ) +} + +export default UnitsSummaryForm diff --git a/sites/partners/src/components/listings/PaperListingForm/index.tsx b/sites/partners/src/components/listings/PaperListingForm/index.tsx new file mode 100644 index 0000000000..3056344739 --- /dev/null +++ b/sites/partners/src/components/listings/PaperListingForm/index.tsx @@ -0,0 +1,596 @@ +import React, { useState, useCallback, useContext, useEffect } from "react" +import axios from "axios" +import { useRouter } from "next/router" +import dayjs from "dayjs" +import { + AlertBox, + AppearanceBorderType, + AppearanceStyleType, + Button, + Form, + LatitudeLongitude, + LoadingOverlay, + Modal, + setSiteAlertMessage, + t, + Tab, + TabList, + TabPanel, + Tabs, +} from "@bloom-housing/ui-components" +import { AuthContext } from "@bloom-housing/shared-helpers" +import { useForm, FormProvider } from "react-hook-form" +import { ListingStatus, ListingEventType, Program } from "@bloom-housing/backend-core/types" +import { + AlertErrorType, + FormListing, + TempEvent, + TempUnit, + formDefaults, + TempUnitsSummary, +} from "../../../lib/listings/formTypes" +import ListingDataPipeline from "../../../lib/listings/ListingDataPipeline" + +import Aside from "../Aside" +import AdditionalDetails from "./sections/AdditionalDetails" +import AdditionalEligibility from "./sections/AdditionalEligibility" +import LeasingAgent from "./sections/LeasingAgent" +import AdditionalFees from "./sections/AdditionalFees" +import Units from "./sections/Units" +import BuildingDetails from "./sections/BuildingDetails" +import ListingIntro from "./sections/ListingIntro" +import ListingPhotos from "./sections/ListingPhotos" +import BuildingFeatures from "./sections/BuildingFeatures" +import RankingsAndResults from "./sections/RankingsAndResults" +import ApplicationAddress from "./sections/ApplicationAddress" +import ApplicationDates from "./sections/ApplicationDates" +import LotteryResults from "./sections/LotteryResults" +import ApplicationTypes from "./sections/ApplicationTypes" +import SelectAndOrder from "./sections/SelectAndOrder" +import Verification from "./sections/Verification" +import BuildingSelectionCriteria from "./sections/BuildingSelectionCriteria" +import { getReadableErrorMessage } from "../PaperListingDetails/sections/helpers" +import { useJurisdictionalProgramList } from "../../../lib/hooks" +import NeighborhoodAmenities from "./sections/NeighborhoodAmenities" +import { StatusBar } from "../../../components/shared/StatusBar" + +type ListingFormProps = { + listing?: FormListing + editMode?: boolean +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ListingForm = ({ listing, editMode }: ListingFormProps) => { + const defaultValues = editMode ? listing : formDefaults + const formMethods = useForm({ + defaultValues, + shouldUnregister: false, + }) + + const router = useRouter() + + const { listingsService, profile } = useContext(AuthContext) + + const [tabIndex, setTabIndex] = useState(0) + const [alert, setAlert] = useState(null) + const [loading, setLoading] = useState(false) + const [jumpToVerify, setJumpToVerify] = useState(false) + const [units, setUnits] = useState([]) + const [unitsSummaries, setUnitsSummaries] = useState([]) + const [openHouseEvents, setOpenHouseEvents] = useState([]) + const [verifyAlert, setVerifyAlert] = useState(false) + + const [programs, setPrograms] = useState( + listing?.listingPrograms.map((program) => { + return program.program + }) ?? [] + ) + + const [latLong, setLatLong] = useState({ + latitude: listing?.buildingAddress?.latitude ?? null, + longitude: listing?.buildingAddress?.longitude ?? null, + }) + const [customMapPositionChosen, setCustomMapPositionChosen] = useState( + listing?.customMapPin || false + ) + + const setLatitudeLongitude = (latlong: LatitudeLongitude) => { + if (!loading) { + setLatLong(latlong) + } + } + + /** + * Close modal + */ + const [closeModal, setCloseModal] = useState(false) + + /** + * Publish modal + */ + const [publishModal, setPublishModal] = useState(false) + + /** + * Lottery results drawer + */ + const [lotteryResultsDrawer, setLotteryResultsDrawer] = useState(false) + + /** + * Save already-live modal + */ + const [listingIsAlreadyLiveModal, setListingIsAlreadyLiveModal] = useState(false) + + useEffect(() => { + if (listing?.events) { + setOpenHouseEvents( + listing.events + .filter((event) => event.type === ListingEventType.openHouse) + .map((event) => { + return { + ...event, + startTime: new Date(event.startTime), + endTime: new Date(event.endTime), + } + }) + .sort((a, b) => (dayjs(a.startTime).isAfter(b.startTime) ? 1 : -1)) + ) + } + + if (listing?.isVerified === false) { + setVerifyAlert(true) + } + }, [listing?.events, listing?.isVerified]) + + useEffect(() => { + if (listing?.unitGroups && !unitsSummaries.length) { + const tempSummaries = listing.unitGroups.map((summary, i) => ({ + ...summary, + tempId: i + 1, + amiLevels: summary?.amiLevels?.map((elem, index) => ({ ...elem, tempId: index + 1 })), + openWaitListQuestion: summary?.openWaitlist?.toString(), + })) + setUnitsSummaries(tempSummaries) + } + }, []) + + const scrollToVerify = () => { + document.getElementById("isVerifiedContainer").scrollIntoView({ behavior: "smooth" }) + } + + useEffect(() => { + if (jumpToVerify && tabIndex === 0) { + scrollToVerify() + setJumpToVerify(false) + } + }, [tabIndex, jumpToVerify]) + + // eslint-disable-next-line @typescript-eslint/unbound-method + const { getValues, setError, clearErrors, reset } = formMethods + + const triggerSubmitWithStatus = ( + confirm?: boolean, + status?: ListingStatus, + newData?: Partial + ) => { + if (confirm && status === ListingStatus.active) { + if (listing?.status === ListingStatus.active) { + setListingIsAlreadyLiveModal(true) + } else { + setPublishModal(true) + } + return + } + let formData = { ...defaultValues, ...getValues(), ...(newData || {}) } + if (status) { + formData = { ...formData, status } + } + void onSubmit(formData) + } + + const onSubmit = useCallback( + async (formData: FormListing) => { + if (!loading) { + try { + setLoading(true) + clearErrors() + + const dataPipeline = new ListingDataPipeline(formData, { + programs, + units, + unitGroups: unitsSummaries, + openHouseEvents, + profile, + latLong, + customMapPositionChosen, + }) + const formattedData = await dataPipeline.run() + + const result = editMode + ? await listingsService.update( + { + id: listing.id, + body: { id: listing.id, ...formattedData }, + }, + { headers: { "x-purge-cache": true } } + ) + : await listingsService.create( + { body: formattedData }, + { headers: { "x-purge-cache": true } } + ) + reset(formData) + + if (result) { + /** + * Send purge request to Nginx. + * Wrapped in try catch, because it's possible that content may not be cached in between edits, + * and will return a 404, which is expected. + * listings* purges all /listings locations (with args, details), so if we decide to clear on certain locations, + * like all lists and only the edited listing, then we can do that here (with a corresponding update to nginx config) + */ + if (process.env.backendProxyBase) { + try { + // clear individual listing's cache + await axios.request({ + url: `${process.env.backendProxyBase}/listings/${result.id}*`, + method: "purge", + }) + // clear list caches if published + if (result.status !== ListingStatus.pending) { + await axios.request({ + url: `${process.env.backendProxyBase}/listings?*`, + method: "purge", + }) + } + } catch (e) { + console.log("purge error = ", e) + } + } + + setSiteAlertMessage( + editMode ? t("listings.listingUpdated") : t("listings.listingSubmitted"), + "success" + ) + + await router.push(`/listings/${result.id}`) + } + setLoading(false) + } catch (err) { + reset(formData) + setLoading(false) + clearErrors() + const { data } = err.response || {} + if (data?.statusCode === 400) { + data?.message?.forEach((errorMessage: string) => { + const fieldName = errorMessage.split(" ")[0] + const readableError = getReadableErrorMessage(errorMessage) + if (readableError) { + setError(fieldName, { message: readableError }) + if (fieldName === "buildingAddress" || fieldName === "buildingAddress.nested") { + const setIfEmpty = ( + fieldName: string, + fieldValue: string, + errorMessage: string + ) => { + if (!fieldValue) { + setError(fieldName, { message: errorMessage }) + } + } + const address = formData.buildingAddress + setIfEmpty(`buildingAddress.city`, address.city, readableError) + setIfEmpty(`buildingAddress.state`, address.state, readableError) + setIfEmpty(`buildingAddress.street`, address.street, readableError) + setIfEmpty(`buildingAddress.zipCode`, address.zipCode, readableError) + } + } + }) + setAlert("form") + } else { + setAlert("api") + } + } + } + }, + [ + loading, + clearErrors, + programs, + units, + unitsSummaries, + openHouseEvents, + profile, + listingsService, + listing, + router, + programs, + latLong, + customMapPositionChosen, + editMode, + listingsService, + listing?.id, + reset, + router, + setError, + ] + ) + return loading === true ? null : ( + <> + + <> + (editMode ? router.push(`/listings/${listing?.id}`) : router.back())} + > + {t("t.back")} + + } + tagStyle={(() => { + switch (listing?.status) { + case ListingStatus.active: + return AppearanceStyleType.success + case ListingStatus.closed: + return AppearanceStyleType.closed + default: + return AppearanceStyleType.primary + } + })()} + tagLabel={ + listing?.status + ? t(`listings.listingStatus.${listing.status}`) + : t(`listings.listingStatus.pending`) + } + /> + + +
+
+ {alert && ( + setAlert(null)} closeable type="alert"> + {alert === "form" ? t("listings.fieldError") : t("errors.alert.badRequest")} + + )} + {verifyAlert && ( + setVerifyAlert(false)} + closeable + type="alert" + > + + {t("listings.dataUpToDate")}.{" "} + { + e.preventDefault() + if (tabIndex === 1) { + setJumpToVerify(true) + setTabIndex(0) + } else { + scrollToVerify() + } + }} + > + {t("listings.verifyData")} + + + + )} + +
+
+
+ setTabIndex(index)} + > + + {t("listing.details.title")} + {t("listings.applicationProcess")} + + + + + + + + + + + + + + + + +
+ +
+
+ + + + + + + +
+ +
+
+
+ + {listing?.status === ListingStatus.closed && ( + { + triggerSubmitWithStatus(false, ListingStatus.closed, data) + }} + drawerState={lotteryResultsDrawer} + showDrawer={(toggle: boolean) => setLotteryResultsDrawer(toggle)} + /> + )} +
+ +
+
+
+
+ + + + + setCloseModal(false)} + actions={[ + , + , + ]} + > + {t("listings.closeThisListing")} + + + setPublishModal(false)} + actions={[ + , + , + ]} + > + {t("listings.publishThisListing")} + + + setListingIsAlreadyLiveModal(false)} + actions={[ + , + , + ]} + > + {t("listings.listingIsAlreadyLive")} + + + ) +} + +export default ListingForm diff --git a/sites/partners/src/listings/PaperListingForm/sections/AdditionalDetails.tsx b/sites/partners/src/components/listings/PaperListingForm/sections/AdditionalDetails.tsx similarity index 89% rename from sites/partners/src/listings/PaperListingForm/sections/AdditionalDetails.tsx rename to sites/partners/src/components/listings/PaperListingForm/sections/AdditionalDetails.tsx index 3379943af2..2d74310ddb 100644 --- a/sites/partners/src/listings/PaperListingForm/sections/AdditionalDetails.tsx +++ b/sites/partners/src/components/listings/PaperListingForm/sections/AdditionalDetails.tsx @@ -23,6 +23,7 @@ const AdditionalDetails = () => { id={"requiredDocuments"} fullWidth={true} register={register} + maxLength={2000} />