diff --git a/.rubocop.yml b/.rubocop.yml index 7ef1e76199f..8a486b6dcc0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -72,6 +72,10 @@ Rails/LexicallyScopedActionFilter: Rails/SkipsModelValidations: Enabled: false +Rails/ApplicationController: + Exclude: + - app/controllers/idt/api/v1/base_controller.rb + Rails/FilePath: Enabled: false diff --git a/MAC_INTEL.md b/MAC_INTEL.md index dc4a757491c..96e2951e042 100644 --- a/MAC_INTEL.md +++ b/MAC_INTEL.md @@ -3,6 +3,7 @@ [<< Back](README.md) **Pre-requisites for setup:** +**Some steps require completing VA On-boarding** 1. Create GitHub user account [VA github access process](https://department-of-veterans-affairs.github.io/github-handbook/guides/onboarding/getting-access) @@ -20,46 +21,37 @@ 2. Install Homebrew * a. Using BAH Self Service if BAH employee Run ```brew install git-lfs .``` This is required to clone caseflow-facols repo -3. Create a caseflow-setup folder by typing: `mkdir caseflow-setup` (step can be skipped if you have the file transfer files) +3. Create an appeals folder by typing: `mkdir ~/appeals` -4. Change directory to caseflow-setup by typing: `cd caseflow-setup` +4. Change directory to appeals by typing: `cd appeals` 5. Navigate to [instant client](https://www.oracle.com/database/tecdchnologies/instant-client/linux-x86-64-downloads.html) -6. Download the following zip files to caseflow-setup directory (Copy from downloads to the caseflow-setup directory if they download to downloads) (Step can be skipped if you received the file transfer files) +6. Download the following zip files (Step can be skipped if you received the file transfer files) * instantclient-basic-linux.x64-12.2.0.1.0.zip * instantclient-sqlplus-linux.x64-12.2.0.1.0.zip * instantclient-sdk-linux.x64-12.2.0.1.0.zip -7. Clone caseflow repositories required for setup into caseflow-setup directory (caseflow-facols requires github account and the account has to be in the VA org in github and can be found in the file transfer files) pwd - * HTTP protocol - * `git clone https://github.com/department-of-veterans-affairs/caseflow.git` - * `git clone https://github.com/department-of-veterans-affairs/caseflow-facols.git` - * Upon completion, navigate to caseflow-facols (`cd ~/caseflow-setup/caseflow-facols`) - * Run: `git lfs install` (needed to initialize large file storage in repo) - * Run: `git lfs pull` (this will pull the large zipfile) - * If you do not have VA access yet to clone caseflow-vacols can contact (Your Tech lead or the bid_appeals_mac_support channel) to receive a zip of the repository +7. Clone the following caseflow repositories required into appeals directory + * https://github.com/department-of-veterans-affairs/caseflow + * https://github.com/department-of-veterans-affairs/caseflow-facols -**SSH protocol** +8. Upon completion, navigate to caseflow-facols (`cd ~/appeals/caseflow-facols`) + 1. Run: `git lfs install` (needed to initialize large file storage in repo) + 2. Run: `git lfs pull` (this will pull the large zipfile) + 3. Copy the `~/appeals/caseflow-facols/build_facols` directory into `~/appeals/caseflow/local/vacols/` directory. -1. `git clone git@github.com:department-of-veterans-affairs/caseflow.git` +9. Navigate to the caseflow directory in your terminal (type: `cd ~/appeals/caseflow`) and checkout the grant/setup-no-aws branch `git checkout grant/setup-no-aws` -2. `git clone git@github.com:department-of-veterans-affairs/caseflow-facols.git` - * Upon completion, navigate to caseflow-facols (`cd ~/caseflow-setup/caseflow-facols`) - * Run: `git lfs install` (needed to initialize large file storage in repo) - * Run: `git lfs pull` (this will pull the large zipfile) +10. Navigate to caseflow/docker-bin directory (type: `cd docker-bin`) -3. Navigate to the caseflow directory in your terminal (type: `cd ~/caseflow-setup/caseflow`) and checkout the grant/setup-no-aws branch `git checkout grant/setup-no-aws` +11. Create oracle_libs subdirectory (type: `mkdir oracle_libs`) -4. Navigate to caseflow/docker-bin directory (type: `cd docker-bin`) +12. Copy the 3 instant-client zip files from step 6 into the oracle_libs directory -5. Create oracle_libs subdirectory (type: `mkdir oracle_libs`) +13. Navigate to the caseflow root directory (type: `cd ~/appeals/caseflow`) -6. Copy the 3 instant-client zip files from the caseflow-setup directory into the oracle_libs directory - -7. Navigate to the caseflow root directory (type: `cd ..`) - -8. Run scripts/dev_env_setup_step1.sh script from bash terminal (How to run script in Mac Terminal) (Will be prompted for a password will be the SUDO password which is the password used to log into mac after restart) +14. Run `scripts/dev_env_setup_step1.sh` script from bash terminal [[How to run script in Mac Terminal](https://apple.stackexchange.com/questions/235128/how-do-i-run-a-sh-or-command-file-in-terminal)] (Will be prompted for a password will be the SUDO password which is the password used to log into mac after restart) * If/When mac says Chromedriver cannot be opened do this: * Click cancel on the warning modal * Push Command + Space @@ -68,7 +60,7 @@ * Click General tab * Click the lock icon and put in your BAH pin Click allow anyway on chromedriver warning Click the lock icon to re lock -9. Setup Docker to use 4 CPUs and 8G memory and sign-in to your personal DockerHub account +15. Setup Docker to use 4 CPUs and 8G memory and sign-in to your personal DockerHub account * To get to these settings: * Command + Space * Type docker @@ -76,42 +68,47 @@ * Click the gear icon * Click Resources -10. The script updated your bash profile and you need to resource it into the terminal by typing: `source ~/.bash_profile` +16. The script updated your bash profile and you need to resource it into the terminal by typing: `source ~/.bash_profile` * If using zsh, will need to update and `source ~/.zshrc` instead -11. `brew install shared-mime-info` +17. `brew install shared-mime-info` -12. `brew install v8@3.15` +18. `brew install v8@3.15` -13. Run scripts/dev_env_setup_step2.sh script (may take a while to run) +19. Run `scripts/dev_env_setup_step2.sh` script (may take a while to run) -14. Run `gem install bundler` - * Copy the caseflow-facols/build_facols directory to the caseflow/local/vacols subdirectory. (Ensure you have a caseflow/local/vacols/build_facols directory with all the files before continuing to the next step) +20. Run `gem install bundler` -15. Navigate to caseflow/local/vacols in terminal `cd ~/caseflow- setup/caseflow/local/vacols` +21. Navigate to `~/appeals/caseflow/local/vacols` in terminal (type: `cd ~/appeals/caseflow/local/vacols`) -16. Run `./build_push.sh local` - * Requires the oracle database image to have been pulled after running scripts/dev_env_setup_step1.sh script +22. To install the latest and enterprise Oracle Database version follow (https://seanstacey.org/deploying-an-oracle-database-19c-as-a-docker-container/2020/09/) guide. + 1. Go to http://container-registry.oracle.com/ (Here log in and opt for Database) + 2. On command line `docker login container-registry.oracle.com` + 3. On command line `docker pull container-registry.oracle.com/database/enterprise:latest` -17. Navigate to caseflow root directory `cd ~/caseflow-setup/caseflow` +23. Run `./build_push.sh local` -18. Run `docker-compose up –d` +24. Navigate to caseflow root directory `cd ~/appeals/caseflow` -19. Run `bundle exec rake db:create` - * If you get connection issues stating no file to be found, run the following: - * `rm /opt/homebrew/var/postgres/postmaster.pid` or possibly `rm /usr/local/var/postgres/postmaster.pid` - * `brew services restart postgresql` +25. Run `ln -s Makefile.example Makefile` -20. Run `bundle exec rake local:vacols:seed` +26. Run `make up` -21. Run `bundle exec rake db:schema:load db:seed` +27. Run `make reset` + * If issues occur: + 1. Run `bundle exec rake db:create` + * If you get connection issues stating no file to be found, run the following: + * `rm /opt/homebrew/var/postgres/postmaster.pid` or possibly `rm /usr/local/var/postgres/postmaster.pid` + * `brew services restart postgresql` + 2. Run `bundle exec rake local:vacols:seed` + 3. Run `bundle exec rake db:schema:load db:seed` -22. Open a new tab in terminal +27. Open a new tab in terminal -23. In new tab run make: ```run-backend``` +28. In new tab run make: ```run-backend``` -24. In the old tab run: ```make run-frontend``` +29. In the old tab run: ```make run-frontend``` -25. Navigate to localhost:3000 in browser to see the application +30. Navigate to localhost:3000 in browser to see the application [<< Back](README.md) diff --git a/MAC_M1.md b/MAC_M1.md index 86721974577..603c98e0b71 100644 --- a/MAC_M1.md +++ b/MAC_M1.md @@ -1,163 +1,145 @@ -## Mac M1 and M2 ####################################################### +# Mac M1 and M2 (Apple Silicon) Installation ####################################################### [<< Back](README.md) -***Ensure command line tools are installed via Self Service Portal prior to starting*** - -**Clone these Repos** - -1. Create a `~/dev/appeals/` directory +How to verify that you have an Apple Silicon processor: +1. Click the Apple icon in the top-left of the screen +2. Click 'About This Mac' +3. Look for the 'Chip' section at the top of the list + 1. Apple Silicon processors will begin with 'Apple'; as of 7/2023, these a variant of 'Apple M1' or 'Apple M2' -2. Clone the following repos using git clone into this directory \ - a. \ - b. \ - c. +Frequently Asked Question: -3. Optional for setting up a machine, clone if can \ - a. \ - b. \ - c. +1. Why is this so complicated? -4. If cannot clone the above might need to do +Apple Silicon processors use a different architecture (arm64/aarch64) than Intel processors (x86_64). Oracle, which is used for the VACOLS database, does not have binaries to run any of their database tools natively on arm64 for MacOS. Additionally, the Ruby gems `therubyracer` and `jshint` require the library v8@3.15, which can also only be compiled and installed on x86_64 processors. To work around this we use Rosetta to emulate x86_64 processors in the terminal, installing most of the Caseflow dependencies via the x86_64 version of Homebrew. It is important that while setting up your environment, you ensure you are *in the correct terminal type* and *in the correct directory* so that you do not install or compile a dependency with the wrong architecture. -**Homebrew Installation** +***Ensure command line tools are installed via Self Service Portal prior to starting*** -1. Install homebrew from self-service portal +***Follow these instructions as closely as possible. If a folder is specified for running terminal commands, ensure you are in that directory prior to running the command(s). If you can't complete a step, ask for help in the #bid_appeals_mac_support channel of the Benefits Integrated Delivery (BID) Slack workspace.*** -**Docker Installation** +Clone these Repos +--- +1. Create a `~/dev/appeals/` directory -1. Navigate to [docker website](https://docs.docker.com/desktop/install/mac-install/) +2. Clone the following repo using `git clone` into this directory + * -2. Install “Docker Desktop for Mac Apple silicon” +3. Optional for setting up a machine, though you may work with these in the future + * + * + * + * + * -3. Pop up of app will come up to transfer to application folder +4. If you cannot clone the above, you might need to do [this setup](https://docs.github.com/en/enterprise-server@3.4/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) -4. Right click on the Docker app and select “install with privilege management” +Homebrew Installation +--- +1. Install homebrew from self-service portal -5. Open terminal and run - * a. `arch –arm64 brew install docker docker-compose colima` - * b. `mkdir -p ~/.docker/cli-plugins` - * c. `ln -sfn /opt/homebrew/opt/docker-compose/bin/docker-compose~/.docker/cli-plugins/docker-compose` - * d. `sudo mv homebrew /usr/local/homebrew` - * i. (moves homebrew from /opt to /usr/local) - * e. `eval $(/usr/local/homebrew/bin/brew shellenv)` +Docker Installation +--- +Note: We do not use Docker Desktop due to licensing. We recommend using Colima to run the docker service. -**UTM and Vacols VM** +1. Open terminal and run: + 1. `brew install docker docker-compose colima` + 2. `mkdir -p ~/.docker/cli-plugins` + 3. `ln -sfn /opt/homebrew/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose` +Install UTM and VACOLS VM +--- 1. Download UTM from this [link](https://github.com/utmapp/UTM/releases/latest/download/UTM.dmg) - 2. Right click UTM.app and select “install with privilege management” then open the UTM.app - 3. Download the Vacols VM from this [link](https://boozallen-my.sharepoint.com/:u:/r/personal/622659_bah_com/Documents/Appeals%20Vacols%20VM%20May%202023.utm.zip?csf=1&web=1&e=TnDe7c) - 4. After the file downloads, right click on it in “Finder” and select “Show Package Contents” and delete the view.plist file if it exists - 5. Right click on the application and select “Open With > UTM.app (default)” - 6. Select the “Play” button when it pops up in UTM - -7. The virtual machine will open. To login, the password is “password” \ -**Booz Allen Hamilton Internal** - -8. Leave this running in the background. If you close the window, you can open it back up by repeating steps 5-7 - -**Chromedriver Installation** - -1. Open terminal and run \ - a. `brew install --cask chromedriver` - -2. Once it successfully installs, run command \ - a. `chromedriver –version` - +7. Leave this running in the background. If you close the window, you can open it back up by repeating steps 5-7 + +Chromedriver Installation +--- +1. Open terminal and run + * `brew install --cask chromedriver` +2. Once it successfully installs, run command + * `chromedriver –version` 3. There will be a pop up. Before clicking “OK,” navigate to System Settings > Privacy & Security - -4. Scroll down and it will say “chromedriver was blocked form use because it is not from an identified +4. Scroll down to the security section and it will say “chromedriver was blocked from use because it is not from an identified developer” - 5. Select “Allow Anyway” - 6. Select “Yes” from pop up - -7. Open terminal and once again run `chromedriver –version` - +7. Reopen terminal and once again run `chromedriver –version` 8. Select “Open” -**PDFtk Server** +Note: you may need to run ```sudo spctl --global-disable``` in the terminal if you have issues with security -1. Download from this [link](https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/pdftk_server-2.02-mac_osx-10.11-setup.pkg) +Install PDFtk Server and wkhtmltox +--- +1. Download and install from this [link](https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/pdftk_server-2.02-mac_osx-10.11-setup.pkg) +2. Download this [file](https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-2/wkhtmltox-0.12.6-2.macos-cocoa.pkg) and move through the prompts -**Configure x86_64 Homebrew** +Note: you may need to run ```sudo spctl --global-disable``` in the terminal if you have issues with security -1. Create a homebrew directory under your home directory \ - a. ```mkdir homebrew``` +Configure x86_64 Homebrew +--- +Run the below commands **from your home directory** -2. Open terminal and run \ - a. ```curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew``` +1. In a terminal, create a homebrew directory under your home directory + * ```mkdir homebrew``` +2. In a terminal, run + * ```curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew``` +3. If you get a chdir error, run + * ``mkdir homebrew && curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew`` +4. Using sudo, move the homebrew directory to /usr/local/ + * ```sudo mv homebrew /usr/local/homebrew``` -3. If chdir error run \ - a. ``mkdir homebrew && curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew`` - -4. Using sudo, move the homebrew directory to /usr/local/ \ - a. ```sudo mv homebrew /usr/local/homebrew``` - -**Rosetta** - -1. Open standard terminal and run: \ - a. ```softwareupdate –install-rosetta –agree-to-license``` +Rosetta +--- +1. Open standard terminal and run: + * ```softwareupdate -–install-rosetta –-agree-to-license``` 2. Once Rosetta is installed, find the default terminal in “Finder” > Applications - 3. Right click and select “Get Info” - 4. Select “Open using Rosetta” * Note: you can copy the standard terminal executable to your desktop and enable Rosetta on that, so that you don’t need to disable rosetta on the default terminal once Caseflow setup is complete -**Booz Allen Hamilton Internal** - -**Oracle “instantclient” Files** - -1. Download these DMG files \ - a. [instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg](https://download.oracle.com/otn_software/mac/instantclient/198000/instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg) \ - b. [instantclient-sqlplus-macos.x64-19.8.0.0.0dbru.dmg](https://download.oracle.com/otn_software/mac/instantclient/198000/instantclient-sqlplus-macos.x64-19.8.0.0.0dbru.dmg) \ - c. [Instantclient-sdk-macos.x64-19.8.0.0.0dbru.dmg](https://download.oracle.com/otn_software/mac/instantclient/198000/instantclient-sdk-macos.x64-19.8.0.0.0dbru.dmg) +Oracle “instantclient” Files +--- +1. Download these DMG files + * [instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg](https://download.oracle.com/otn_software/mac/instantclient/198000/instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg) + * [instantclient-sqlplus-macos.x64-19.8.0.0.0dbru.dmg](https://download.oracle.com/otn_software/mac/instantclient/198000/instantclient-sqlplus-macos.x64-19.8.0.0.0dbru.dmg) + * [Instantclient-sdk-macos.x64-19.8.0.0.0dbru.dmg](https://download.oracle.com/otn_software/mac/instantclient/198000/instantclient-sdk-macos.x64-19.8.0.0.0dbru.dmg) +2. After downloading, double click on one of the folders and follow the instructions in INSTALL_IC_README.txt to copy the libraries -2. After downloading, click on one of the folders and follow the instructions in INSTALL_IC_README.txt - -**Postgres Download** - -1. Download from this [link](https://github.com/PostgresApp/PostgresApp/releases/download/v2.5.8/Postgres-2.5.8-14.dmg) - -**OpenSSL** +Postgres Download +--- +1. Download and install from this [link](https://github.com/PostgresApp/PostgresApp/releases/download/v2.5.8/Postgres-2.5.8-14.dmg) +OpenSSL +--- 1. Download openssl@1.1 and openssl@3 from this [link](https://boozallen.sharepoint.com/teams/VABID/appeals/Documents/Forms/AllItems.aspx?id=%2Fteams%2FVABID%2Fappeals%2FDocuments%2FDevelopment%2FDeveloper%20Setup%20Resources%2FM1%20Mac%20Developer%20Setup&viewid=8a8eaf3e%2D2c12%2D4c87%2Db95f%2D4eab3428febd) - 2. Open “Finder” and find the two folders under “Downloads” - -3. Open openssl@1.1 and find the child folder 1.1.1s - -4. Click 1.1.1s, duplicate it, and rename the duplicate folder 1.1.1t - -5. Open openssl@3 and find the child folder 3.0.7 - -6. Click 3.0.7, duplicate it, and rename the duplicate folder 3.1.0 - -7. Open a second “Finder” window and navigate to /usr/local/homebrew/Cellar - -8. Move openssl@1.1 and openssl@3 to the Cellar folder - -9. Run command (from a rosetta terminal) \ - a. brew link --force openssl@1.1 \ - b. If the one above doesn’t work run: `brew link openssl@1.1 --force` - -**.zshrc File** - +3. Extract the `.tar.gz` files +4. In each of the extracted folders: + 1. Navigate to the `/usr/local/homebrew/Cellar` subfolder + 2. Copy the openssl folder to your local machine's `/usr/local/homebrew/Cellar` folder + 3. If the folder `Cellar` in `/usr/local/homebrew` does not exist, create it with `mkdir /usr/local/homebrew/Cellar` + * Note: moving these folders can be done using finder or a terminal +5. Run command (from a rosetta terminal) + 1. `brew link --force openssl@1.1` + 2. If the one above doesn’t work run: `brew link openssl@1.1 --force` + * Note: don't link openssl@3 unless you run into issues farther in the setup + +Modify your .zshrc File +--- 1. Run command `open ~/.zshrc` - 2. Add the following lines, if any of these are already set make sure to comment previous settings: ``` -export PATH=/usr/local/homebrew/bin:/opt/homebrew/bin:${PATH} +export PATH=/usr/local/homebrew/bin:${PATH} eval "$(/usr/local/homebrew/bin/rbenv init -)" eval "$(/usr/local/homebrew/bin/nodenv init -)" +eval "$(/usr/local/homebrew/bin/pyenv init --path)" + # Add Postgres environment variables for CaseFlow export POSTGRES_HOST=localhost export POSTGRES_USER=postgres @@ -167,74 +149,94 @@ export FREEDESKTOP_MIME_TYPES_PATH=/usr/local/homebrew/share/mime/packages/freed ``` 3. Save file - 4. Go to your active terminal and enter source ~/.zshrc or create a new terminal +Note: until rbenv, nodenv, and pyenv are installed, the `eval` commands will display a 'command not found' error when launching a terminal -**Booz Allen Hamilton Internal** - -**Configure shell env for Rosetta and x86_64 homebrew** - -1. Open the terminal that was set up for “Open using Rosetta” above - -2. Run the following command: `eval $(/usr/local/homebrew/bin/brew shellenv)` - -**Run dev setup scripts in Caseflow repo** +Run dev setup scripts in Caseflow repo +--- +**VERY IMPORTANT NOTE: The below commands must be run *in a Rosetta terminal* until you reach the 'Running Caseflow' section** *Script 1* -1. Enter a rosetta terminal and ensure you are in the directory you cloned Caseflow repo into -(~/dev/appeals/caseflow), run commands \ - a. ```git checkout grant/setup-m1``` \ - b. ```./scripts/dev_env_setup_step1.sh``` \ - **If this fails, double check the “OpenSSL” section as well as your .zshrc file +1. Enter a **Rosetta** terminal and ensure you are in the directory you cloned Caseflow repo into (~/dev/appeals/caseflow) and run commands: + 1. ```git checkout grant/setup-m1``` + 2. ```./scripts/dev_env_setup_step1.sh``` + * If this fails, double check your .zshrc file to ensure your PATH has only the x86_64 brew *Script 2* -1. Open a rosetta terminal and navigate to /usr/local, run commands \ - a. ```sudo ln -s /usr/local/homebrew/opt optsource``` \ - b. ```sudo ln -s /usr/local/homebrew/Cellar Cellar``` \ - c. ```sudo spctl --global-disable``` - -2. Navigate to caseflow/scripts/dev_env_setup_step2.sh and open in VSCode - -3. Make sure you are on the branch grant/setup-m1 - -4. Comment out line 12 (should be like the line in #5a) - -5. In terminal navigate to caseflow folder, run - * a. ```RUBY_CONFIGURE_OPTS="--with-openssl-dir=/usr/local/opt/openssl@1.1" rbenv install 2.7.3``` - * b. `gem install pg:1.1.4 -- --with-pg-config=/Applications/Postgres.app/Contents/Versions/latest/bin/pg_config` - * c. ```gem install therubyracer -- --with-v8-dir=/usr/local/homebrew/opt/v8@3.15``` - * i. If step c. fails try running these commands first - - ``` zsh - brew install v8@3.15 - bundle config build.libv8 --with-system-v8 - bundle config build.therubyracer --with-v8-dir=$(brew --prefix v8@3.15) - bundle install - ``` - - * d. ```./scripts/dev_env_setup_step2.sh``` - * If you get a permission error while running gem install or bundle install, do not run using sudo. - Set the permissions back to you for every directory under /.rbenv - * Enter command: `sudo chown -R /Users//.rbenv` - * For example, if my name is Eli Brown, the command will be: - `sudo chown –R elibrown /Users/elibrown/.rbenv` - -6. If there are no obvious errors messages, run `bundle install` to ensure all gems are, in fact, installed - -**Running Caseflow** - -1. Open caseflow in VSCode (optional) - -2. Ensure you are on master branch and up to date by running \ - a. ```git checkout master```\ - b. ```git fetch```\ - c. ```git pull origin master``` \ -**Booz Allen Hamilton Internal** -3. Start Vacols UTM VM and log into it. Open the Docker and Postgres applications. Leave all running in background - -4. In caseflow, run +1. Open a **Rosetta** terminal and navigate to /usr/local, run the command ```sudo spctl --global-disable``` +2. In the **Rosetta** terminal, install pyenv and the required python2 version: + 1. `brew install pyenv` + 2. `pyenv install 2.7.18` + 3. In the caseflow directory, run `pyenv local 2.7.18` to set the version +3. In the **Rosetta** terminal navigate to caseflow folder: + 1. set ```RUBY_CONFIGURE_OPTS="--with-openssl-dir=/usr/local/homebrew/Cellar/openssl@1.1"``` + 2. run `rbenv install 2.7.3` + 3. run `gem install pg:1.1.4 -- --with-pg-config=/Applications/Postgres.app/Contents/Versions/latest/bin/pg_config` + 4. Install v8@3.15 by doing the following (these steps assume that vi/vim is the default editor): + 1. run `brew edit v8@3.15` + 2. go to line 21 in the editor by typing `:21` + Note: the line being removed is `disable! date: "2023-06-19", because: "depends on Python 2 to build"` + 3. delete the line by pressing `d` twice + 4. save and quit by typing `:x` + 5. Configure build opts for gem `therubyracer`: + 1. `bundle config build.libv8 --with-system-v8` + 2. `bundle config build.therubyracer --with-v8-dir=$(brew --prefix v8@3.15)` + 6. run ```./scripts/dev_env_setup_step2.sh``` + If you get a permission error while running gem install or bundle install, **do not run using sudo.** + Set the permissions back to you for every directory under /.rbenv + * Enter command: `sudo chown -R /Users//.rbenv` + * For example, if my name is Eli Brown, the command will be: + `sudo chown –R elibrown /Users/elibrown/.rbenv` +4. Optional: If there are no errors messages, run `bundle install` to ensure all gems are installed + +Running Caseflow +--- +**VERY IMPORTANT NOTE TWO: This is where you switch back to a *standard* (non-Rosetta) terminal** + +1. Once your installation of all gems is complete, switch back to a standard MacOS terminal: + 1. open your ~/.zshrc file + 2. comment the line `export PATH=/usr/local/homebrew/bin:$PATH` + 3. uncomment the line `export PATH=/opt/homebrew/bin:$PATH` + 4. add the line `export PATH=$HOME/.nodenv/shims:$HOME/.rbenv/shims:$HOME/.pyenv/shims:$PATH` + 5. comment the lines `eval "$({binary} init -)"` for rbenv, pyenv, and nodenv if applicable + 6. if you added the line `eval $(/usr/local/homebrew/bin/brew shellenv)` after installing x86_64 homebrew, comment it out +2. Open a terminal verify: + 1. that you are on arm64 by doing `arch` and checking the output + 2. that you are using arm64 brew by doing `which brew` and ensuring the output is `/opt/homebrew/bin/brew` +3. Open caseflow in VSCode (optional), or navigate to the caseflow directory in your terminal and: + 1. `brew install yarn` +4. Ensure you are on master branch and up to date by running + 1. ```git checkout master``` + 2. ```git fetch``` + 3. ```git pull origin master``` +5. Start Vacols UTM VM (if not already running) +6. run `make up-m1` to create the docker containers and volumes +7. run `make reset` to (re)create and seed the database; this takes a while (~45 minutes) + 1. if you get a database not found error, run `bundle exec rake db:drop db:create db:schema:load`, and then run `make reset` again +8. open a second terminal tab/window +9. run `make run-backend-m1` in one tab, and `make run-frontend` in the other + 1. the backend should launch on `localhost:3000`. go there in your browser to access Caseflow + 2. the frontend launches on `localhost:3500`; this is a server that enables hot-reloading of modules during development. going to this address in a browser will not work + 3. optionally, you can do `make run-m1` to launch both at the same time with Foreman, but if one server errors the other can remain running and lead to headache trying to get them back up + +To launch caseflow after a machine restart: +--- +1. Start the VACOLS VM via UTM; login is not required +2. In terminal, run `colima start` to start the docker daemon +3. In terminal, go to the caseflow directory and run: + * `make up-m1` to start the docker containers +4. Open a second terminal window/tab, and from the caseflow directory run: + * `make run-backend-m1` in one tab + * `make run-frontend` in the other tab + +Note: It takes several minutes for the VACOLS VM to go through its startup and launch the Oracle DB service, and about a minute for the Postgres DB to initialize after running `make up-m1`. + +--- + +The following steps are an alternative to step 7 of the Running Caseflow section in the event that you absolutely cannot get those commands to work: +1. In caseflow, run * a. `make down` * i. Removes appeals-db, appeals-redis, and localstack docker containers * b. `docker-compose down –v` @@ -245,44 +247,13 @@ export FREEDESKTOP_MIME_TYPES_PATH=/usr/local/homebrew/share/mime/packages/freed * i. Resets caseflow and ETL database schemas, seeds databases, and enables feature flags * **If `make reset` returns database not found error: * a. Run command `bundle exec rake db:drop` - * b. Download caseflow-db-backup.gz (not able to share this download via policy, message me) + * b. Download caseflow-db-backup.gz (not able to share this download via policy, ask in the slack channel) * c. Enter terminal, navigate to ~/Downloads * e. Run command * i. `gzip -dck caseflow-db-backup.gz | docker exec -i appeals-db psql -U postgres` * ii. (this command will link the caseflow_certification_database to docker) - * f. Download this [file](https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-2/wkhtmltox-0.12.6-2.macos-cocoa.pkg) and move through the prompts (wkhtmltopdf => unrelated?) - * g. Enter terminal, navigate to caseflow, and run \ - **Booz Allen Hamilton Internal** + * f. Enter terminal, navigate to caseflow, and run * i. `make up-m1` * ii. `make reset` (this will take a while) - * iii. **Alternative fix for db not found error? - * a. ```bundle exec rake db:drop db:create db:schema:load``` - * b. `bundle exec rake db:seed` - * c. `make install` - * d. `make test` - * i. This will time out before the command is finished executing. Have been told this is normal behavior if you have completed the VM Trigger Fix - * e. `make run-m1` - * i. Click to open caseflow in browser - -5. After you are finished, go back to caseflow in VSCode, enter terminal, and run - * a. `make install` - * b. `make test` - * i. This will time out before the command is finished executing. Have been told this is normal behavior if you have finished step 5 - * c. `make run-m1` - * i. Click to open caseflow in browser - * ii. If make run returns a message that the port is already running. Do the following: - 1. Find the port the error message is referring to. It should say close to the top of the error message - 2. Run command `lsof -i TCP:` - 3. Run command `kill -9 ` - 4. Run command `make run-m1 again` - -After this, anytime I want to run Caseflow follow these steps: - -1. Open the Docker app, Postgres app, and Vacols UTM, log in and leave running in background -2. Go to ~/Downloads and run `gzip -dck caseflow-db-backup.gz | docker exec -i appeals-db psql -U postgres` -3. `make up-m1` -4. `make run-m1` (if bootsnap errors, run this again and you should see a port is already running) -5. Run `lsof -i TCP:`, then `kill –9 ` -6. Run `make run-m1` again [<< Back](README.md) diff --git a/Makefile.example b/Makefile.example index ac980f9923e..6b6ac721986 100644 --- a/Makefile.example +++ b/Makefile.example @@ -151,9 +151,21 @@ db: ## Connect to your dev postgres (caseflow) db audit: ## Create caseflow_audit schema, tables, and triggers in postgres bundle exec rails r db/scripts/audit/create_caseflow_audit_schema.rb - bundle exec rails r db/scripts/audit/create_appeal_states_audit.rb - bundle exec rails r db/scripts/audit/add_row_to_appeal_states_audit_table_function.rb - bundle exec rails r db/scripts/audit/create_appeal_states_audit_trigger.rb + bundle exec rails r db/scripts/audit/tables/create_appeal_states_audit.rb + bundle exec rails r db/scripts/audit/tables/create_vbms_communication_packages_audit.rb + bundle exec rails r db/scripts/audit/tables/create_vbms_distributions_audit.rb + bundle exec rails r db/scripts/audit/tables/create_vbms_distribution_destinations_audit.rb + bundle exec rails r db/scripts/audit/tables/create_vbms_uploaded_documents_audit.rb + bundle exec rails r db/scripts/audit/functions/add_row_to_appeal_states_audit_table_function.rb + bundle exec rails r db/scripts/audit/functions/add_row_to_vbms_communication_packages_audit_table_function.rb + bundle exec rails r db/scripts/audit/functions/add_row_to_vbms_distributions_audit_table_function.rb + bundle exec rails r db/scripts/audit/functions/add_row_to_vbms_distribution_destinations_audit_table_function.rb + bundle exec rails r db/scripts/audit/functions/add_row_to_vbms_uploaded_documents_audit_table_function.rb + bundle exec rails r db/scripts/audit/triggers/create_appeal_states_audit_trigger.rb + bundle exec rails r db/scripts/audit/triggers/create_vbms_communication_packages_audit_trigger.rb + bundle exec rails r db/scripts/audit/triggers/create_vbms_distributions_audit_trigger.rb + bundle exec rails r db/scripts/audit/triggers/create_vbms_distribution_destinations_audit_trigger.rb + bundle exec rails r db/scripts/audit/triggers/create_vbms_uploaded_documents_audit_trigger.rb audit-remove: ## Remove caseflow_audit schema, tables and triggers in postgres bundle exec rails r db/scripts/audit/remove_caseflow_audit_schema.rb diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 6718602ffb2..618e6e5529d 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,5 @@ -Resolves #{jira issue link} + +Resolves [Jira Issue Title](https://jira.devops.va.gov/browse/JIRA-12345) # Description Please explain the changes you made here. @@ -7,7 +8,8 @@ Please explain the changes you made here. - [ ] Code compiles correctly ## Testing Plan -1. Go to #{jira issue link} or list them below + +1. Go to [Jira Issue/Test Plan Link](https://jira.devops.va.gov/browse/JIRA-12345) or list them below - [ ] For feature branches merging into master: Was this deployed to UAT? @@ -19,7 +21,7 @@ Please explain the changes you made here. ---|--- ## Storybook Story -*For Frontend (Presentationa) Components* +*For Frontend (Presentation) Components* * [ ] Add a [Storybook](https://github.com/department-of-veterans-affairs/caseflow/wiki/Documenting-React-Components-with-Storybook) file alongside the component file (e.g. create `MyComponent.stories.js` alongside `MyComponent.jsx`) * [ ] Give it a title that reflects the component's location within the overall Caseflow hierarchy * [ ] Write a separate story (within the same file) for each discrete variation of the component diff --git a/app/controllers/api/docs/pacman/idt-pacman-spec.yml b/app/controllers/api/docs/pacman/idt-pacman-spec.yml new file mode 100644 index 00000000000..0d094879c00 --- /dev/null +++ b/app/controllers/api/docs/pacman/idt-pacman-spec.yml @@ -0,0 +1,1281 @@ +openapi: 3.0.2 +info: + title: IDT-Caseflow-Package Manager Bridge API + description: >- + # [Caseflow Wiki Article](https://github.com/department-of-veterans-affairs/caseflow/wiki/Lighthouse-API-Implementation) + + The document outlines a number of endpoints that can be utilized within a IDT-Caseflow-Package Manager workflow. + termsOfService: https://developer.va.gov/terms-of-service + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + version: 1.0.0 +servers: +- url: https://appeals.cf.uat.ds.va.gov + description: UAT/Staging server +- url: http://localhost:3000 + description: Local Development server +paths: + /idt/api/v1/addresses/validate: + post: + tags: + - Address Validation + summary: A passthrough to the Lighthouse Address Validation API - Validates Mailing Addresses + description: >- + This route is largely a passthrough to the Lighthouse Address Validation API. In most cases, any messages Caseflow receives from its upstream source will simply be forwarded back to the requestor (IDT). + + The upstream Lighthouse Address Validation API adheres to [USPS Publication 28](https://pe.usps.com/text/pub28/welcome.htm) standards for domestic, military, and US territory address. + +

Data Definitions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DataDescription
request_address.address_line_1Solely the first line of the requested address without city, state, or zip. Cannot be null if addressLine2 and addressLine3 are null.
request_address.address_line_2Solely the second line of the requested address without city, state, or zip. Cannot be null if addressLine1 and addressLine3 are null.
request_address.address_line_3Solely the third line of the requested address without city, state, or zip. Cannot be null if addressLine1 and addressLine2 are null.
request_address.cityThe name of the city of the requested address. Must only contain letters.
request_address.zip_code_5The five digit postal code of the requested address. Must only contain five digits and only used for domestic or military addresses.
request_address.zip_code_4The four digit postal code of the requested address. Must only contain four digits and only used for domestic or military addresses.
request_address.international_postal_codeThe postal code for an international address. This can contain numbers and letters and used for international addresses.
request_address.state_province.codeThe two digit code for state/province of the requested address. Must only contain two digits.
request_address.state_province.nameThe name of the state/province of the requested address.
request_address.country_nameThe name of the country of the requested address.
request_address.country_codeThe ISO2, ISO3, or FIPS country code of the requested address. Must only contain two or three letters.
request_address.address_pouShould be either RESIDENCE, CHOICE, or CORRESPONDENCE. Optional
+
+

Upstream Data Input Requirements

+ One of: +
    +
  • Address Line 1
  • +
  • Address Line 2
  • +
  • Address Line 3
  • +
+ AND: +
    +
  • country
  • +
+ + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AddressValidationRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CandidateAddressResponseV2' + '400': + description: An error occurred either between IDT and Caseflow, or Caseflow and the Lighthouse API. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/LighthouseGenericError' + - $ref: '#/components/schemas/CaseflowMissingTokenError' + examples: + LighthouseGenericError: + summary: Whenever Caseflow experiences an error while communicating with the Lighthouse API. + value: + errors: + - status: 400 + title: 'An unexpected error occurred.' + message: 'An unexpected error occurred.' + CaseflowMissingTokenError: + summary: Whenever a token is not provided to the Caseflow IDT endpoint. + value: + message: Missing token + '403': + description: The token used for authorization with the Caseflow IDT API is invalid. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: + message: Invalid token + '429': + description: Caseflow is being rate-limited by the Lighthouse API. + content: + application/json: + schema: + type: object + properties: + errors: + type: array + items: + type: object + properties: + status: + type: number + format: int32 + example: 429 + title: + type: string + example: Service is temporarily unavailable, please try again later. + detail: + type: string + example: Service is temporarily unavailable, please try again later. + '500': + description: There was an error encountered processing the request. This will mean that Caseflow ran into an issue while contacting the Lighthouse Address Validation API. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: + message: 'Lighthouse API Error ID: 322483ae-7953-4d3c-9738-e108506d52c2 An unexpected error occurred, please try again.' + /idt/api/v1/appeals/{appeal_id}/upload_document: + post: + tags: + - Document Posting + summary: Used for uploading documents to eFolder for specific appeals/veterans + parameters: + - in: path + name: appeal_id + schema: + type: string + required: true + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/UploadDocumentRequestWithRecipientInformation' + - $ref: '#/components/schemas/UploadDocumentRequest' + examples: + "With recipient information for Package Manager": + $ref: '#/components/schemas/UploadDocumentRequestWithRecipientInformation' + "Without recipient information for Package Manager": + value: + veteran_identifier: '555555555' + document_type: 'BVA Decision' + document_subject: 'Test' + document_name: 'Test Doc' + file: 'VGhpcyBpcyBhIHRlc3QuIERvIG5vdCBiZSBhbGFybWVkLgo=' + responses: + '200': + description: 'Document was successfully placed into an S3 bucket and queued for uploading to eFolder.' + content: + application/json: + schema: + allOf: + - type: object + properties: + message: + type: string + example: "Document successfully queued for upload." + - $ref: '#/components/schemas/DistributionUUIDList' + '400': + description: 'An unknown, blank, or invalid required parameter was provided.' + content: + application/json: + schema: + oneOf: + - type: object + properties: + message: + type: string + example: "Document type is not recognized" + - type: object + properties: + message: + type: string + example: "File can't be blank" + - type: object + properties: + message: + type: string + example: "The appeal was unable to be found." + - type: object + properties: + message: + type: string + example: "The veteran was unable to be found." + examples: + DocumentTypeNotRecognized: + summary: Document type not recognized + value: + message: Document type is not recognized + FileBlank: + summary: File is blank + value: + message: File can't be blank + AppealNotFound: + summary: Appeal does not exist + value: + message: The appeal was unable to be found. + VeteranNotFound: + summary: Veteran does not exist + value: + message: The veteran was unable to be found. + InvalidCopiesValue: + summary: "Scenario: Copies out of range (1-500 inclusive, default value of 1)" + value: + message: "IDT Exception ID: 67ce1137-4d94-43ec-ba0a-1daf43fc6a65 Recipient information received was invalid or incomplete." + errors: "Copies must be between 1 and 500 (inclusive)" + RecipientTypeIsAbsent: + summary: 'Scenario: recipient_type is not included in the list: ["organization", "person", "system", "ro-colocated"]' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Recipient type is not included in the list" + NameBlank: + summary: 'Scenario: recipient_type is "system", "organization", or "ro-colocated" and name is blank' + value: + message: "IDT Exception ID: d8cecfbd-c664-440e-919b-bb1d91e77f1d Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Name can't be blank" + PoaCodeBlank: + summary: 'Scenario: recipient_type is "ro-colocated" and poa_code is blank' + value: + message: "IDT Exception ID: 8af4913f-1e96-4437-9294-0c31b0f545df Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Poa code can't be blank" + ClaimantStationOfJurisdictionBlank: + summary: 'Scenario: recipient_type is "ro-colocated" and claimant_station_of_jurisdiction is blank' + value: + message: "IDT Exception ID: cf9e0208-e5de-4fb6-9dd5-2e182acdb8c3 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Claimant station of jurisdiction can't be blank" + DestinationTypeInvalid: + summary: 'Scenario: destination_type is not included in the list: ["domesticAddress", "internationalAddress", "militaryAddress", "derived"]' + value: + message: "IDT Exception ID: d7d581fa-4e31-445c-a513-ff40c49a3b95 Recipient information received was invalid or incomplete." + errors": + "distribution 1": "Destination type is not included in the list" + AddressLine1Blank: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and address_line_1 is blank"' + value: + message: "IDT Exception ID: 578abf44-0292-49b6-a77f-27878d734145 Recipient information received was invalid or incomplete." + "errors": + "distribution 1": "Address line 1 can't be blank" + PersonRecipientMissingFirstName: + summary: 'Scenario: recipient_type is "person" and first_name is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "First name can't be blank" + PersonRecipientMissingLastName: + summary: 'Scenario: recipient_type is "person" and last_name is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Last name can't be blank" + CityIsMissing: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and city is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "City can't be blank" + CountryCodeIsMissing: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and county_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Country code can't be blank, Country code is not a valid ISO 3166-2 code" + ProvidedCountryCodeIsInvalid: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and county_code is not a valid ISO 3166-code' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Country code is not a valid ISO 3166-2 code" + DomesticOrMilitaryNeedsNonBlankState: + summary: 'Scenario: destination_type is "domesticAddress" or "militaryAddress" and state_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "State can't be blank, State is not a valid ISO 3166-2 code" + DomesticOrMilitaryNeedsValidState: + summary: 'Scenario: destination_type is "domesticAddress" or "militaryAddress" and state_code is not a valid ISO 3166-code' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "State is not a valid ISO 3166-2 code" + DomesticOrMilitaryNeedsPostalCode: + summary: 'Scenario: destination_type is "domesticAddress" or "militaryAddress" and postal_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Postal code can't be blank" + InternationalAddressNeedsCountryName: + summary: 'Scenario: destination_type is "internationalAddress" and country_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Country name can't be blank" + TreatLine2AsAddresseeTrueMissingAddressLine2: + summary: 'Scenario: treat_line_2_as_addressee is true and address_line_2 is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Address line 2 can't be blank" + TreatLine3AsAddresseeTrueMissingAddressLine3: + summary: 'Scenario: treat_line_3_as_addressee is true and address_line_3 is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Treat line 2 as addressee cannot be false if line 3 is treated as addressee" + TreatLine3AsAddresseeDependentValueMissing: + summary: 'Scenario: treat_line_3_as_addressee is true and treat_line_2_as_addressee is false' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Address line 3 can't be blank" + '500': + description: 'A server error occurred.' + content: + application/json: + schema: + oneOf: + - type: object + properties: + message: + type: string + example: "Unexpected error: job error" + /idt/api/v1/upload_document: + post: + tags: + - Document Posting + summary: Used for uploading documents to eFolder for specific appeals/veterans + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/UploadDocumentRequestWithRecipientInformation' + - $ref: '#/components/schemas/UploadDocumentRequest' + examples: + "With recipient information for Package Manager": + $ref: '#/components/schemas/UploadDocumentRequestWithRecipientInformation' + "Without recipient information for Package Manager": + value: + veteran_identifier: '555555555' + document_type: 'BVA Decision' + document_subject: 'Test' + document_name: 'Test Doc' + file: 'VGhpcyBpcyBhIHRlc3QuIERvIG5vdCBiZSBhbGFybWVkLgo=' + responses: + '200': + description: 'Document was successfully placed into an S3 bucket and queued for uploading to eFolder.' + content: + application/json: + schema: + allOf: + - type: object + properties: + message: + type: string + example: "Document successfully queued for upload." + - $ref: '#/components/schemas/DistributionUUIDList' + examples: + WithRecipientInformationPresent: + summary: "With recipient information for Package Manager" + value: + message: "Document successfully queued for upload." + distribution_ids: + - "1234" + - "1235" + - "1236" + WithoutRecipientInformationPresent: + summary: "Without recipient information for Package Manager" + value: + message: "Document successfully queued for upload." + '400': + description: 'An unknown, blank, or invalid required parameter was provided.' + content: + application/json: + schema: + oneOf: + - type: object + properties: + message: + type: string + example: "Document type is not recognized" + - type: object + properties: + message: + type: string + example: "File can't be blank" + - type: object + properties: + message: + type: string + example: "The appeal was unable to be found." + - type: object + properties: + message: + type: string + example: "The veteran was unable to be found." + examples: + DocumentTypeNotRecognized: + summary: Document type not recognized + value: + message: Document type is not recognized + FileBlank: + summary: File is blank + value: + message: File can't be blank + AppealNotFound: + summary: Appeal does not exist + value: + message: The appeal was unable to be found. + VeteranNotFound: + summary: Veteran does not exist + value: + message: The veteran was unable to be found. + InvalidCopiesValue: + summary: "Scenario: Copies out of range (1-500 inclusive, default value of 1)" + value: + message: "IDT Exception ID: 67ce1137-4d94-43ec-ba0a-1daf43fc6a65 Recipient information received was invalid or incomplete." + errors: "Copies must be between 1 and 500 (inclusive)" + RecipientTypeIsAbsent: + summary: 'Scenario: recipient_type is not included in the list: ["organization", "person", "system", "ro-colocated"]' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Recipient type is not included in the list" + NameBlank: + summary: 'Scenario: recipient_type is "system", "organization", or "ro-colocated" and name is blank' + value: + message: "IDT Exception ID: d8cecfbd-c664-440e-919b-bb1d91e77f1d Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Name can't be blank" + PoaCodeBlank: + summary: 'Scenario: recipient_type is "ro-colocated" and poa_code is blank' + value: + message: "IDT Exception ID: 8af4913f-1e96-4437-9294-0c31b0f545df Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Poa code can't be blank" + ClaimantStationOfJurisdictionBlank: + summary: 'Scenario: recipient_type is "ro-colocated" and claimant_station_of_jurisdiction is blank' + value: + message: "IDT Exception ID: cf9e0208-e5de-4fb6-9dd5-2e182acdb8c3 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Claimant station of jurisdiction can't be blank" + DestinationTypeInvalid: + summary: 'Scenario: destination_type is not included in the list: ["domesticAddress", "internationalAddress", "militaryAddress", "derived"]' + value: + message: "IDT Exception ID: d7d581fa-4e31-445c-a513-ff40c49a3b95 Recipient information received was invalid or incomplete." + errors": + "distribution 1": "Destination type is not included in the list" + AddressLine1Blank: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and address_line_1 is blank"' + value: + message: "IDT Exception ID: 578abf44-0292-49b6-a77f-27878d734145 Recipient information received was invalid or incomplete." + "errors": + "distribution 1": "Address line 1 can't be blank" + PersonRecipientMissingFirstName: + summary: 'Scenario: recipient_type is "person" and first_name is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "First name can't be blank" + PersonRecipientMissingLastName: + summary: 'Scenario: recipient_type is "person" and last_name is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Last name can't be blank" + CityIsMissing: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and city is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "City can't be blank" + CountryCodeIsMissing: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and county_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Country code can't be blank, Country code is not a valid ISO 3166-2 code" + ProvidedCountryCodeIsInvalid: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and county_code is not a valid ISO 3166-code' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Country code is not a valid ISO 3166-2 code" + DomesticOrMilitaryNeedsNonBlankState: + summary: 'Scenario: destination_type is "domesticAddress" or "militaryAddress" and state_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "State can't be blank, State is not a valid ISO 3166-2 code" + DomesticOrMilitaryNeedsValidState: + summary: 'Scenario: destination_type is "domesticAddress" or "militaryAddress" and state_code is not a valid ISO 3166-code' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "State is not a valid ISO 3166-2 code" + DomesticOrMilitaryNeedsPostalCode: + summary: 'Scenario: destination_type is "domesticAddress" or "militaryAddress" and postal_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Postal code can't be blank" + InternationalAddressNeedsCountryName: + summary: 'Scenario: destination_type is "internationalAddress" and country_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Country name can't be blank" + TreatLine2AsAddresseeTrueMissingAddressLine2: + summary: 'Scenario: treat_line_2_as_addressee is true and address_line_2 is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Address line 2 can't be blank" + TreatLine3AsAddresseeTrueMissingAddressLine3: + summary: 'Scenario: treat_line_3_as_addressee is true and address_line_3 is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Treat line 2 as addressee cannot be false if line 3 is treated as addressee" + TreatLine3AsAddresseeDependentValueMissing: + summary: 'Scenario: treat_line_3_as_addressee is true and treat_line_2_as_addressee is false' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Address line 3 can't be blank" + + /idt/api/v2/appeals/{appeal_id}/outcode: + post: + tags: + - Document Posting + summary: Route used for outcoding decision reviews + parameters: + - in: path + name: appeal_id + schema: + type: string + required: true + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/OutcodeRequestWithRecipientInformation' + - $ref: '#/components/schemas/OutcodeRequest' + examples: + "With recipient information for Package Manager": + $ref: '#/components/schemas/OutcodeRequestWithRecipientInformation' + "Without recipient information for Package Manager": + value: + citation_number: "A19062122" + decision_date: "December 21, 2022" + redacted_document_location: "\\path.va.gov\\archdata$\\some-file.pdf" + file: "VGVzdGluZyAxMjMK" + responses: + '200': + description: 'Decision review has been successfully outcoded.' + content: + application/json: + schema: + allOf: + - type: object + properties: + message: + type: string + example: "Success!" + - $ref: '#/components/schemas/DistributionUUIDList' + examples: + WithRecipientInformationPresent: + summary: "With recipient information for Package Manager" + value: + message: "Success!" + distribution_ids: + - "1234" + - "1235" + - "1236" + WithoutRecipientInformationPresent: + summary: "Without recipient information for Package Manager" + value: + message: "Success!" + + '400': + description: 'Occurs if a valid decision review-BVA Dispatch Task combination cannot be located given the params provided.' + content: + application/json: + schema: + oneOf: + - type: array + items: + type: object + properties: + title: + type: string + example: "Appeal 12345, task ID 54321 has already been outcoded. Cannot outcode the same appeal and task combination more than once" + detail: + type: string + example: "Appeal 12345, task ID 54321 has already been outcoded. Cannot outcode the same appeal and task combination more than once" + - type: array + items: + type: object + properties: + title: + type: string + example: "Expected 1 BvaDispatchTask received 0 tasks for appeal 12345, user 1" + detail: + type: string + example: "Expected 1 BvaDispatchTask received 0 tasks for appeal 12345, user 1" + - type: array + items: + type: object + properties: + title: + type: string + example: "Citation number already exists" + detail: + type: string + example: "Citation number already exists" + examples: + AlreadyOutcodedExample: + summary: "Whenever a decision review has already been outcoded." + value: + - title: "Appeal 12345, task ID 54321 has already been outcoded. Cannot outcode the same appeal and task combination more than once" + detail: "Appeal 12345, task ID 54321 has already been outcoded. Cannot outcode the same appeal and task combination more than once" + IncorrectBvaDispatchTasksExample: + summary: "User has either more or fewer than 1 BvaDispatchTask assigned to them for the decision review being outcoded." + value: + - title: "Expected 1 BvaDispatchTask received 0 tasks for appeal 12345, user 1" + detail: "Expected 1 BvaDispatchTask received 0 tasks for appeal 12345, user 1" + CitationNumberExistsExample: + summary: "Citation number provided is already associated with another outcoded appeal." + value: + - title: "Citation number already exists" + detail: "Citation number already exists" + CitationNumberInvalidExample: + summary: "Thrown whenever citation number provided fails to match with a set regular expression." + value: + message: "Citation number is invalid" + MissingParamsExample: + summary: Whenever one or more required parameters are absent. + value: + message: "Decision date can't be blank, Redacted document location can't be blank, File can't be blank" + InvalidCopiesValue: + summary: "Scenario: Copies out of range (1-500 inclusive, default value of 1)" + value: + message: "IDT Exception ID: 67ce1137-4d94-43ec-ba0a-1daf43fc6a65 Recipient information received was invalid or incomplete." + errors: "Copies must be between 1 and 500 (inclusive)" + RecipientTypeIsAbsent: + summary: 'Scenario: recipient_type is not included in the list: ["organization", "person", "system", "ro-colocated"]' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Recipient type is not included in the list" + NameBlank: + summary: 'Scenario: recipient_type is "system", "organization", or "ro-colocated" and name is blank' + value: + message: "IDT Exception ID: d8cecfbd-c664-440e-919b-bb1d91e77f1d Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Name can't be blank" + PoaCodeBlank: + summary: 'Scenario: recipient_type is "ro-colocated" and poa_code is blank' + value: + message: "IDT Exception ID: 8af4913f-1e96-4437-9294-0c31b0f545df Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Poa code can't be blank" + ClaimantStationOfJurisdictionBlank: + summary: 'Scenario: recipient_type is "ro-colocated" and claimant_station_of_jurisdiction is blank' + value: + message: "IDT Exception ID: cf9e0208-e5de-4fb6-9dd5-2e182acdb8c3 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Claimant station of jurisdiction can't be blank" + DestinationTypeInvalid: + summary: 'Scenario: destination_type is not included in the list: ["domesticAddress", "internationalAddress", "militaryAddress", "derived"]' + value: + message: "IDT Exception ID: d7d581fa-4e31-445c-a513-ff40c49a3b95 Recipient information received was invalid or incomplete." + errors": + "distribution 1": "Destination type is not included in the list" + AddressLine1Blank: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and address_line_1 is blank"' + value: + message: "IDT Exception ID: 578abf44-0292-49b6-a77f-27878d734145 Recipient information received was invalid or incomplete." + "errors": + "distribution 1": "Address line 1 can't be blank" + PersonRecipientMissingFirstName: + summary: 'Scenario: recipient_type is "person" and first_name is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "First name can't be blank" + PersonRecipientMissingLastName: + summary: 'Scenario: recipient_type is "person" and last_name is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Last name can't be blank" + CityIsMissing: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and city is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "City can't be blank" + CountryCodeIsMissing: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and county_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Country code can't be blank, Country code is not a valid ISO 3166-2 code" + ProvidedCountryCodeIsInvalid: + summary: 'Scenario: destination_type is "domesticAddress", "internationalAddress", or "militaryAddress" and county_code is not a valid ISO 3166-code' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Country code is not a valid ISO 3166-2 code" + DomesticOrMilitaryNeedsNonBlankState: + summary: 'Scenario: destination_type is "domesticAddress" or "militaryAddress" and state_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "State can't be blank, State is not a valid ISO 3166-2 code" + DomesticOrMilitaryNeedsValidState: + summary: 'Scenario: destination_type is "domesticAddress" or "militaryAddress" and state_code is not a valid ISO 3166-code' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "State is not a valid ISO 3166-2 code" + DomesticOrMilitaryNeedsPostalCode: + summary: 'Scenario: destination_type is "domesticAddress" or "militaryAddress" and postal_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Postal code can't be blank" + InternationalAddressNeedsCountryName: + summary: 'Scenario: destination_type is "internationalAddress" and country_code is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Country name can't be blank" + TreatLine2AsAddresseeTrueMissingAddressLine2: + summary: 'Scenario: treat_line_2_as_addressee is true and address_line_2 is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Address line 2 can't be blank" + TreatLine3AsAddresseeTrueMissingAddressLine3: + summary: 'Scenario: treat_line_3_as_addressee is true and address_line_3 is blank' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Treat line 2 as addressee cannot be false if line 3 is treated as addressee" + TreatLine3AsAddresseeDependentValueMissing: + summary: 'Scenario: treat_line_3_as_addressee is true and treat_line_2_as_addressee is false' + value: + message: "IDT Exception ID: 2b26f07f-b742-4056-8781-be2a8ccb5d35 Recipient information received was invalid or incomplete." + errors: + "distribution 1": "Address line 3 can't be blank" + '500': + description: 'The server encountered an error.' + content: + application/json: + schema: + oneOf: + - type: array + items: + type: object + properties: + title: + type: string + example: "VBMS::FilenumberDoesNotExist" + detail: + type: string + example: "The veteran file number does not match the file number in VBMS" + /idt/api/v2/distributions/{distribution_id}: + get: + tags: + - Distribution Tracking + summary: Queries Package Manager for information on a specified distribution. + parameters: + - in: path + name: distribution_id + schema: + type: string + required: true + responses: + '200': + description: 'Distribution information has been successfully obtained.' + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionStatusResponse' + examples: + DistributionIsEstablishedExample: + summary: Distribution is established + $ref: '#/components/schemas/DistributionStatusResponse' + DistributionIsPendingEstablishmentInPacManExample: + summary: Distribution is pending establishment + value: + id: 1234 + status: "PENDING_ESTABLISHMENT" + '403': + description: 'IDT token provided is invalid.' + '400': + description: 'Distribution ID provided is of an invalid format.' + '404': + description: 'A distribution could not be located given the ID provided.' + '500': + description: 'An error has occurred while trying to process the request.' + content: + application/json: + examples: + ServerError: + summary: Server Error + value: + message: 'An error has occurred while trying to process the request. Sentry ID: 02658f1e-31f1-4162-8903-63e70b81364d' + DistributionEstablishmentFailed: + summary: Distribution establishment failed + value: + id: 1234 + status: "ESTABLISHMENT_FAILED" + message: "The error message given to us by Package Manager." +components: + schemas: + DistributionUUIDList: + type: object + properties: + distribution_ids: + type: array + items: + type: string + format: number + DistributionStatusResponse: + $ref: '#/components/schemas/Distribution' + Distribution: + type: object + properties: + id: + type: integer + example: 1234 + description: The primary key of the distribution in our database. There is an initial delay while we establish \ + the distribution in Package Manager where its UUID is unavailable. This is why we utilize our primary key instead. + distribution_uuid: + type: string + format: uuid + recipient: + $ref: '#/components/schemas/DistributionRecipient' + destinations: + type: array + items: + $ref: '#/components/schemas/DistributionDestination' + status: + type: string + enum: ['IN_PROGRESS', 'SUCCESS', 'DRAFT', 'FAIL', 'ELECTRONIC_NOTIFICATION'] + sent_to_cbcm_date: + type: string + DistributionRecipient: + type: object + properties: + type: + type: string + enum: ['organization', 'person', 'system', 'ro-colocated'] + id: + type: string + format: uuid + name: + type: string + DistributionDestination: + type: object + properties: + type: + type: string + address_line_1: + type: string + address_line_2: + type: string + address_line_3: + type: string + address_line_4: + type: string + address_line_5: + type: string + address_line_6: + type: string + treat_line_2_as_addressee: + type: boolean + treat_line_3_as_addressee: + type: boolean + city: + type: string + state: + type: string + postal_code: + type: string + country_name: + type: string + country_code: + type: string + UploadDocumentRequestWithRecipientInformation: + allOf: + - $ref: '#/components/schemas/UploadDocumentRequest' + - type: object + properties: + recipient_info: + type: array + items: + type: object + $ref: '#/components/schemas/RecipientRequestInformation' + UploadDocumentRequest: + type: object + properties: + veteran_identifier: + type: string + example: '555555555' + document_type: + type: string + example: 'BVA Decision' + document_subject: + type: string + example: 'Test' + document_name: + type: string + example: 'Test Doc' + file: + type: string + format: base64 + example: 'VGhpcyBpcyBhIHRlc3QuIERvIG5vdCBiZSBhbGFybWVkLgo=' + required: + - document_type + - file + OutcodeRequestWithRecipientInformation: + allOf: + - $ref: '#/components/schemas/OutcodeRequest' + - type: object + properties: + recipient_info: + type: array + items: + type: object + $ref: '#/components/schemas/RecipientRequestInformation' + RecipientRequestInformation: + type: object + properties: + recipient_type: + type: string + enum: ['organization', 'person', 'system', 'ro-colocated'] + name: + description: 'Required if recipient_type is organization, system, or ro-colocated. Unused for people.' + type: string + first_name: + type: string + middle_name: + type: string + last_name: + type: string + claimant_station_of_jurisdiction: + type: string + description: 'Required if recipient_type is ro-colocated.' + postal_code: + type: string + description: 'Required if recipient_type is ro-colocated.' + destination_type: + type: string + enum: ['domesticAddress', 'internationalAddress', 'militaryAddress'] + address_line_1: + type: string + address_line_2: + type: string + address_line_3: + type: string + address_line_4: + type: string + address_line_5: + type: string + address_line_6: + type: string + treat_line_2_as_addressee: + type: boolean + treat_line_3_as_addressee: + type: boolean + city: + type: string + state: + type: string + country_name: + type: string + country_code: + type: string + copies: + type: integer + example: 1 + OutcodeRequest: + type: object + properties: + citation_number: + type: string + format: /\AA?\d{8}\Z/i + decision_date: + type: string + example: "December 21, 2022" + file: + type: string + format: base64 + redacted_document_location: + type: string + LighthouseGenericError: + type: object + properties: + errors: + type: array + items: + type: object + properties: + status: + type: number + format: int32 + title: + type: string + detail: + type: string + CaseflowMissingTokenError: + type: object + properties: + message: + type: string + example: Missing token + Message: + required: + - code + - key + - severity + type: object + properties: + code: + type: string + key: + type: string + text: + type: string + severity: + type: string + enum: + - INFO + - WARN + - ERROR + - FATAL + potentiallySelfCorrectingOnRetry: + type: boolean + ServiceResponse: + type: object + properties: + messages: + type: array + items: + $ref: '#/components/schemas/Message' + AddressValidationRequest: + required: + - request_address + type: object + properties: + request_address: + $ref: '#/components/schemas/RequestAddress' + description: Request format to describe the address to correct and validate. + RequestAddress: + type: object + properties: + address_line_1: + type: string + address_line_2: + type: string + address_line_3: + type: string + city: + type: string + zip_code_5: + type: string + zip_code_4: + type: string + international_post_code: + type: string + state_province: + $ref: '#/components/schemas/StateProvince' + request_country: + $ref: '#/components/schemas/RequestCountry' + address_pou: + type: string + enum: + - RESIDENCE/CHOICE + - CORRESPONDENCE + RequestCountry: + type: object + properties: + country_code: + type: string + StateProvince: + type: object + properties: + code: + type: string + Address: + required: + - country + - county + - state_province + type: object + properties: + address_line_1: + type: string + address_line_2: + type: string + address_line_3: + type: string + city: + type: string + zip_code_5: + type: string + zip_code_4: + type: string + international_post_code: + type: string + county: + $ref: '#/components/schemas/County' + state_province: + $ref: '#/components/schemas/StateProvince' + country: + $ref: '#/components/schemas/Country' + AddressMetaData: + type: object + properties: + confidence_score: + type: number + format: double + address_type: + type: string + delivery_point_validation: + type: string + enum: + - CONFIRMED + - STREET_NUMBER_VALIDATED_BUT_MISSING_UNIT_NUMBER + - STREET_NUMBER_VALIDATED_BUT_BAD_UNIT_NUMBER + - MULTIPLE_MATCHES_FOUND + - UNDELIVERABLE + - MISSING_ZIP + - FALSE_POSITIVE + residential_delivery_indicator: + type: string + enum: + - RESIDENTIAL + - BUSINESS + - MIXED + non_postal_input_data: + maxItems: 100 + minItems: 0 + type: array + items: + type: string + validation_key: + type: integer + format: int32 + AddressValidationResponse: + type: object + properties: + messages: + type: array + items: + $ref: '#/components/schemas/Message' + address: + $ref: '#/components/schemas/Address' + geocode: + $ref: '#/components/schemas/Geocode' + us_congressional_district: + type: string + address_metadata: + $ref: '#/components/schemas/AddressMetaData' + CandidateAddressResponseV2: + type: object + properties: + messages: + type: array + items: + $ref: '#/components/schemas/Message' + candidate_addresses: + maxItems: 100 + minItems: 0 + type: array + items: + $ref: '#/components/schemas/AddressValidationResponse' + Country: + type: object + properties: + name: + type: string + code: + type: string + fips_code: + type: string + iso2_code: + type: string + iso3_code: + type: string + County: + type: object + properties: + name: + type: string + county_fips_code: + type: string + Geocode: + type: object + properties: + calc_date: + type: string + format: date-time + location_precision: + type: number + format: double + latitude: + type: number + format: double + longitude: + type: number + format: double + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: TOKEN diff --git a/app/controllers/concerns/mail_package_concern.rb b/app/controllers/concerns/mail_package_concern.rb new file mode 100644 index 00000000000..12461498df3 --- /dev/null +++ b/app/controllers/concerns/mail_package_concern.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# shared code for building mail packages to submit to Package Manager external service + +module MailPackageConcern + extend ActiveSupport::Concern + + private + + def recipient_info + params[:recipient_info] + end + + def copies + # Default value of 1 for copies + return 1 if params[:copies].blank? + + params[:copies] + end + + def mail_package + return nil if recipient_info.blank? + + { distributions: json_mail_requests, copies: copies, created_by_id: user.id } + end + + # Purpose: - Creates and validates a MailRequest object for each recipient + # - Calls #call method on each MailRequest to save corresponding VbmsDistirbution and + # VbmsDistributionDestination to the db + # - Stores the distribution IDs (to be returned to the IDT user as an immediate means of tracking + # each distribution) + # + def build_mail_package + return if recipient_info.blank? + + throw_error_if_copies_out_of_range + mail_requests.map do |request| + request.call + distribution_ids << request.vbms_distribution_id + end + end + + def mail_requests + @mail_requests ||= create_mail_requests_and_track_errors + end + + def json_mail_requests + mail_requests.map(&:to_json) + end + + def create_mail_requests_and_track_errors + requests = recipient_info.map.with_index do |recipient, idx| + MailRequest.new(recipient).tap do |request| + if request.invalid? + recipient_errors["distribution #{idx + 1}"] = request.errors.full_messages.join(", ") + end + end + end + throw_error_if_recipient_info_invalid + requests + end + + def throw_error_if_copies_out_of_range + unless (1..500).cover?(copies) + fail Caseflow::Error::MissingRecipientInfo, "Copies must be between 1 and 500 (inclusive)".to_json + end + end + + def throw_error_if_recipient_info_invalid + return unless recipient_errors.any? + + fail Caseflow::Error::MissingRecipientInfo, recipient_errors.to_json + end + + def recipient_errors + @recipient_errors ||= {} + end + + def distribution_ids + @distribution_ids ||= [] + end +end diff --git a/app/controllers/idt/api/v1/base_controller.rb b/app/controllers/idt/api/v1/base_controller.rb index 8b2f72cf16f..03fd90fe8d4 100644 --- a/app/controllers/idt/api/v1/base_controller.rb +++ b/app/controllers/idt/api/v1/base_controller.rb @@ -16,7 +16,9 @@ class Idt::Api::V1::BaseController < ActionController::Base if error.class.method_defined?(:serialize_response) render(error.serialize_response) else - render json: { message: "IDT Standard Error ID: " + uuid + " Unexpected error: #{error.message}" }, status: :internal_server_error + render json: { + message: "IDT Standard Error ID: " + uuid + " Unexpected error: #{error.message}" + }, status: :internal_server_error end end # :nocov: @@ -32,7 +34,19 @@ class Idt::Api::V1::BaseController < ActionController::Base log_error(error) uuid = SecureRandom.uuid Rails.logger.error("IDT Standard Error ID: " + uuid) - render(json: { message: "IDT Standard Error ID: " + uuid + " Please enter a file number in the 'FILENUMBER' header" }, status: :unprocessable_entity) + render(json: + { message: + "IDT Standard Error ID: " + + uuid + + " Please enter a file number in the 'FILENUMBER' header" }, + status: :unprocessable_entity) + end + + rescue_from Caseflow::Error::MissingRecipientInfo do |error| + log_error(error) + uuid = SecureRandom.uuid + render(json: { message: "IDT Exception ID: " + uuid + " Recipient information received was invalid or incomplete.", + errors: JSON.parse(error.message) }, status: :bad_request) end rescue_from Caseflow::Error::VeteranNotFound do |error| diff --git a/app/controllers/idt/api/v1/upload_vbms_document_controller.rb b/app/controllers/idt/api/v1/upload_vbms_document_controller.rb index a0ac88a66a2..a3b6aa80834 100644 --- a/app/controllers/idt/api/v1/upload_vbms_document_controller.rb +++ b/app/controllers/idt/api/v1/upload_vbms_document_controller.rb @@ -2,40 +2,73 @@ class Idt::Api::V1::UploadVbmsDocumentController < Idt::Api::V1::BaseController include ApiRequestLoggingConcern + include MailPackageConcern protect_from_forgery with: :exception skip_before_action :verify_authenticity_token, only: [:create] before_action :verify_access - def bgs - @bgs ||= BGSService.new - end - def create - appeal = nil - # Find veteran from appeal id and check with db - if params["appeal_id"].present? - appeal = LegacyAppeal.find_by_vacols_id(params["appeal_id"]) || Appeal.find_by_uuid(params["appeal_id"]) - if appeal.nil? - fail Caseflow::Error::AppealNotFound, "IDT Standard Error ID: " + SecureRandom.uuid + " The appeal was unable to be found." - else - params["veteran_file_number"] = appeal.veteran_file_number - end - - else - file_number = bgs.fetch_veteran_info(params["veteran_identifier"])&.dig(:file_number) || bgs.fetch_file_number_by_ssn(params["veteran_identifier"]) - if file_number.nil? - fail Caseflow::Error::VeteranNotFound, "IDT Standard Error ID: " + SecureRandom.uuid + " The veteran was unable to be found." - end + # Create distributions for Package Manager mail service if recipient info present + build_mail_package - params["veteran_file_number"] = file_number - end - result = PrepareDocumentUploadToVbms.new(params, current_user, appeal).call + result = PrepareDocumentUploadToVbms.new(params, current_user, appeal, mail_package).call if result.success? - render json: { message: "Document successfully queued for upload." } + success_message = { message: "Document successfully queued for upload." } + if recipient_info.present? + success_message[:distribution_ids] = distribution_ids + end + render json: success_message else render json: result.errors[0], status: :bad_request end end + + private + + # Find veteran from appeal id and check with db + def appeal + if appeal_id.blank? + find_file_number_by_veteran_identifier + return nil + end + + @appeal ||= find_veteran_by_appeal_id + end + + def appeal_id + params[:appeal_id] + end + + def veteran_identifier + params[:veteran_identifier] + end + + def bgs + @bgs ||= BGSService.new + end + + def find_veteran_by_appeal_id + appeal = LegacyAppeal.find_by_vacols_id(appeal_id) || Appeal.find_by_uuid(appeal_id) + throw_not_found_error(Caseflow::Error::AppealNotFound, "appeal") if appeal.nil? + update_veteran_file_number(appeal.veteran_file_number) + appeal + end + + def find_file_number_by_veteran_identifier + file_number = bgs.fetch_veteran_info(veteran_identifier)&.dig(:file_number) || + bgs.fetch_file_number_by_ssn(veteran_identifier) + throw_not_found_error(Caseflow::Error::VeteranNotFound, "veteran") if file_number.nil? + update_veteran_file_number(file_number) + end + + def update_veteran_file_number(file_number) + params["veteran_file_number"] = file_number + end + + def throw_not_found_error(error, name) + uuid = SecureRandom.uuid + fail error, uuid + " The #{name} was unable to be found." + end end diff --git a/app/controllers/idt/api/v2/appeals_controller.rb b/app/controllers/idt/api/v2/appeals_controller.rb index 628dafc81d9..ce1d9526698 100644 --- a/app/controllers/idt/api/v2/appeals_controller.rb +++ b/app/controllers/idt/api/v2/appeals_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Idt::Api::V2::AppealsController < Idt::Api::V1::BaseController + include MailPackageConcern + protect_from_forgery with: :exception before_action :verify_access @@ -23,10 +25,17 @@ def details end def outcode - result = BvaDispatchTask.outcode(appeal, outcode_params, user) + # Create distributions for Package Manager mail service if recipient info present + build_mail_package + + result = BvaDispatchTask.outcode(appeal, outcode_params, user, mail_package) if result.success? - return render json: { message: "Success!" } + success_response = { message: "Successful dispatch!" } + if recipient_info.present? + success_response[:distribution_ids] = distribution_ids + end + return render json: success_response end render json: { message: result.errors[0] }, status: :bad_request @@ -151,4 +160,17 @@ def load_tags_by_doc_id def outcode_params params.permit(:citation_number, :decision_date, :redacted_document_location, :file) end + + def mail_params + params.permit(:copies, recipient_info: recipient_keys) + end + + def recipient_keys + [ + :recipient_type, :name, :first_name, :last_name, :claimant_station_of_jurisdiction, :postal_code, + :destination_type, :address_line_1, :address_line_2, :address_line_3, :address_line_4, :address_line_5, + :address_line_6, :treat_line_2_as_addressee, :treat_line_3_as_addressee, :city, :state, :country_name, + :country_code + ] + end end diff --git a/app/controllers/idt/api/v2/distributions_controller.rb b/app/controllers/idt/api/v2/distributions_controller.rb new file mode 100644 index 00000000000..2b35a8435ff --- /dev/null +++ b/app/controllers/idt/api/v2/distributions_controller.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class Idt::Api::V2::DistributionsController < Idt::Api::V1::BaseController + protect_from_forgery with: :exception + before_action :verify_access + + def distribution + distribution_id = params[:distribution_id] + # Checks if the distribution id is blank and if it exists with the database + if distribution_id.blank? || !valid_id?(distribution_id) + return render_error(400, "Distribution Does Not Exist Or Id is blank", distribution_id) + end + + distribution_uuid = distribution_uuid_from_id(distribution_id) + + return pending_establishment(distribution_id) unless distribution_uuid + + begin + # Retrieves the distribution package from the PacMan API + distribution_response = PacmanService.get_distribution_request(distribution_uuid) + + response_code = distribution_response.code + + fail StandardError if response_code != 200 + # Handles errors when making any requests both from Pacman and the DB + rescue StandardError + return render_error(response_code, "Internal Server Error", distribution_id) + end + + render json: format_response(distribution_response) + end + + private + + def pending_establishment(distribution_id) + render json: { id: distribution_id, status: "PENDING_ESTABLISHMENT" }, status: :ok + end + + def format_response(response) + response_body = response.raw_body + + begin + parsed_response = JSON.parse(response_body) + + # Convert keys from camelCase to snake_case + parsed_response.deep_transform_keys do |key| + key.to_s.underscore.gsub(/e(\d)/, 'e_\1') + end + rescue JSON::ParseError => error + log_error(error + " Distribution ID: #{params[:distribution_id]}") + + response_body + end + end + + # Checks if the distribution exists in the database before sending request to Pacman + def valid_id?(distribution_id) + VbmsDistribution.exists?(id: distribution_id) + end + + def distribution_uuid_from_id(pk_id) + VbmsDistribution.find(pk_id).uuid + end + + # Renders errors and logs and tracks the here within Raven + # :reek:FeatureEnvy + def render_error(status, message, distribution_id) + error_uuid = SecureRandom.uuid + error_message = "[IDT] Http Status Code: #{status}, #{message}, (Distribution ID: #{distribution_id})" + Rails.logger.error(error_message.to_s + "Error ID: " + error_uuid) + Raven.capture_exception(error_message, extra: { error_uuid: error_uuid }) + render json: { message: error_message + " #{error_uuid}" }, status: status + end +end diff --git a/app/jobs/mail_request_job.rb b/app/jobs/mail_request_job.rb new file mode 100644 index 00000000000..962cc47bcea --- /dev/null +++ b/app/jobs/mail_request_job.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +class MailRequestJob < CaseflowJob + queue_with_priority :low_priority + application_attr :api + + # Purpose: performs job + # + # takes in VbmsUploadedDocument object and JSON payload + # mail_package looks like this: + # { + # "distributions": [ + # { + # "recipient_info": json of MailRequest object + # } + # ] + # "copies": integer value, + # "created_by_id": integer value + # } + # + # Response: n/a + def perform(document_to_mail, mail_package) + begin + package_response = PacmanService.send_communication_package_request( + document_to_mail.veteran_file_number, + get_package_name(document_to_mail), + document_referenced(document_to_mail.document_version_reference_id, mail_package[:copies]) + ) + log_info(package_response) + + fail Caseflow::Error::PacmanApiError if package_response.error? + rescue Caseflow::Error::PacmanApiError => error + log_error(error) + else + ActiveRecord::Base.transaction do + vbms_comm_package = create_package(document_to_mail, mail_package) + vbms_comm_package.update!(status: "success", uuid: parse_pacman_id(package_response)) + create_distribution_request(vbms_comm_package.id, mail_package) + end + end + end + + private + + # Purpose: Parses responses from the PacMan API and pulls out the + # + # ID (UUID) of the entity created in the request. + # + # Response: The UUID of the communication package or distribution just created + def parse_pacman_id(pacman_response) + response_body = pacman_response.body + + parsed_body = if response_body.is_a?(ActiveSupport::HashWithIndifferentAccess) + response_body + else + JSON.parse(response_body) + end + + parsed_body.with_indifferent_access[:id] + end + + # Purpose: arranges id and copies to pass into package post request + # + # takes in VbmsUploadedDocument id and copies integer + # + # Response: Array of json with document id and copies + def document_referenced(doc_id, copies) + [{ "id": doc_id, "copies": copies }] + end + + # Purpose: Creates new VbmsCommunicationPackage + # + # takes in VbmsUploadedDocument object and MailRequest object + # + # Response: new VbmsCommunicationPackage object + # :reek:FeatureEnvy + def create_package(document_to_mail, mail_package) + VbmsCommunicationPackage.new( + comm_package_name: get_package_name(document_to_mail), + created_at: Time.zone.now, + created_by_id: mail_package[:created_by_id], + copies: mail_package[:copies], + file_number: document_to_mail.veteran_file_number, + status: nil, + updated_at: Time.zone.now, + updated_by_id: mail_package[:created_by_id], + document_mailable_via_pacman: document_to_mail + ) + end + + def get_package_name(document_to_mail) + "#{document_to_mail.document_name}_#{Time.now.utc.strftime('%Y%m%d%k%M%S')}" + end + + # Purpose: Find a VbmsDistribution from various input formats + # + # Response: The corresponding VbmsDistribution record + def find_associated_vbms_distribution_record(distribution_info) + parsed_distro = if [ActiveSupport::HashWithIndifferentAccess, Hash].include?(distribution_info.class) + distribution_info + else + JSON.parse(distribution_info) + end + + VbmsDistribution.find(parsed_distro.with_indifferent_access[:vbms_distribution_id]) + end + + # Purpose: sends distribution POST request to Pacman API + # + # takes in VbmsCommunicationPackage id (string) and MailRequest object + # + # Response: n/a + def create_distribution_request(package_id, mail_package) + distributions = mail_package[:distributions] + + distributions.each do |dist| + begin + distribution = find_associated_vbms_distribution_record(dist) + distribution_responses = PacmanService.send_distribution_request( + VbmsCommunicationPackage.find(package_id).uuid, + get_recipient_hash(distribution), + get_destinations_hash(dist) + ) + distribution_responses.each do |response| + log_info(response) + distribution.update!(vbms_communication_package_id: package_id, uuid: parse_pacman_id(response)) + end + rescue Caseflow::Error::PacmanApiError => error + log_error(error) + end + end + end + + # Purpose: creates recipient hash from VbmsDistribution attributes + # + # takes in VbmsDistribution object + # + # Response: hash that is needed in Pacman API distribution POST requests + def get_recipient_hash(distribution) + { + type: distribution.recipient_type, + name: distribution.name, + first_name: distribution.first_name, + middle_name: distribution.middle_name, + last_name: distribution.last_name, + participant_id: distribution.participant_id, + poa_code: distribution.poa_code, + claimant_station_of_jurisdiction: distribution.claimant_station_of_jurisdiction + } + end + + # Purpose: Find root of available destination information + # + # Response: Hash containing the root of available destination information + def parse_recipient_info_from_destinaton(destination_info) + parsed_destination = if [ActiveSupport::HashWithIndifferentAccess, Hash].include?(destination_info.class) + destination_info + else + JSON.parse(destination_info).with_indifferent_access + end + + parsed_destination[:recipient_info] || parsed_destination["recipient_info"] || parsed_destination + end + + # Purpose: creates destination hash from VbmsDistributionDestination attributes + # + # takes in VbmsDistributionDestination object + # + # Response: array that holds a hash + def get_destinations_hash(destination) + recipient_info = parse_recipient_info_from_destinaton(destination) + + [{ + type: recipient_info[:destination_type], + addressLine1: recipient_info[:address_line_1], + addressLine2: recipient_info[:address_line_2], + addressLine3: recipient_info[:address_line_3], + addressLine4: recipient_info[:address_line_4], + addressLine5: recipient_info[:address_line_5], + addressLine6: recipient_info[:address_line_6], + treatLine2AsAddressee: recipient_info[:treat_line_2_as_addressee], + treatLine3AsAddressee: recipient_info[:treat_line_3_as_addressee], + city: recipient_info[:city], + state: recipient_info[:state], + postalCode: recipient_info[:postal_code], + countryName: recipient_info[:country_name], + countryCode: recipient_info[:country_code] + }] + end + + # Purpose: logging error in Rails and in Raven + # + # takes in error message (string) + # + # Response: n/a + def log_error(error) + uuid = SecureRandom.uuid + error_msg = ERROR_MESSAGES[error.code] || "#{error.code} Unknown error has occurred." + + Rails.logger.error(error_msg + "Error ID: " + uuid) + Raven.capture_exception(error, extra: { error_uuid: uuid }) + end + + ERROR_MESSAGES = { + 400 => "400 PacmanBadRequestError The server cannot create the new communication package due to a client error.", + 403 => "403 PacmanForbiddenError The server cannot create the new communication package" \ + "due to insufficient privileges.", + 404 => "404 PacmanNotFoundError The communication package could not be found but may be available" \ + "again in the future. Subsequent requests by the client are permissible.", + 500 => "500 PacmanInternalServerError The request was unable to be completed." + }.freeze + + # Purpose: logs information in Rails logger + # + # takes in info message (string) + # + # Response: n/a + def log_info(info_message) + uuid = SecureRandom.uuid + + Rails.logger.info("#{info_message.body} - ID: #{uuid}") + end +end diff --git a/app/jobs/process_decision_document_job.rb b/app/jobs/process_decision_document_job.rb index 742f42cdd48..e2d0e9f0cee 100644 --- a/app/jobs/process_decision_document_job.rb +++ b/app/jobs/process_decision_document_job.rb @@ -4,10 +4,10 @@ class ProcessDecisionDocumentJob < CaseflowJob queue_with_priority :low_priority application_attr :intake - def perform(decision_document_id) + def perform(decision_document_id, mail_package = nil) RequestStore.store[:application] = "idt" RequestStore.store[:current_user] = User.system_user - DecisionDocument.find(decision_document_id).process! + DecisionDocument.find(decision_document_id).process!(mail_package) end end diff --git a/app/jobs/upload_document_to_vbms_job.rb b/app/jobs/upload_document_to_vbms_job.rb index fa66a304c83..e0337e31ad4 100644 --- a/app/jobs/upload_document_to_vbms_job.rb +++ b/app/jobs/upload_document_to_vbms_job.rb @@ -8,21 +8,37 @@ class UploadDocumentToVbmsJob < CaseflowJob # Params: document_id - integer to search for VbmsUploadedDocument # initiator_css_id - string to find a user by css_id # application - string with a default value of "idt" but can be overwritten + # mail_package - Payload with distributions value (array of JSON-formatted MailRequest objects), + # copies value (integer), and created_by_id value (integer) to be submitted to + # Package Manager if optional recipient info is present # # Return: nil - def perform(document_id:, initiator_css_id:, application: "idt") + def perform(params) + @params = params RequestStore.store[:application] = application RequestStore.store[:current_user] = User.system_user - - @document = VbmsUploadedDocument.find_by(id: document_id) - @initiator = User.find_by_css_id(initiator_css_id) + @document = VbmsUploadedDocument.find(params[:document_id]) + @initiator = User.find_by_css_id(params[:initiator_css_id]) add_context_to_sentry UploadDocumentToVbms.new(document: document).call + queue_mail_request_job(mail_package) unless mail_package.nil? end private - attr_reader :document, :initiator + attr_reader :document, :initiator, :params + + def application + return "idt" if params[:application].blank? + + params[:application] + end + + def mail_package + return nil if params[:mail_package].blank? + + params[:mail_package] + end def add_context_to_sentry if initiator.present? @@ -39,4 +55,17 @@ def add_context_to_sentry veteran_file_number: document.veteran_file_number ) end + + def queue_mail_request_job(mail_package) + return unless document.uploaded_to_vbms_at + + MailRequestJob.perform_later(document, mail_package) + info_message = "MailRequestJob for document #{document.id} queued for submission to Package Manager" + log_info(info_message) + end + + def log_info(info_message) + uuid = SecureRandom.uuid + Rails.logger.info(info_message + " ID: " + uuid) + end end diff --git a/app/models/cavc_dashboard.rb b/app/models/cavc_dashboard.rb index 0fd35592b33..209dce3f251 100644 --- a/app/models/cavc_dashboard.rb +++ b/app/models/cavc_dashboard.rb @@ -26,9 +26,14 @@ def set_attributes_from_cavc_remand end def remand_request_issues - return cavc_remand.remand_appeal&.request_issues.order(:id) if cavc_remand.remand_appeal + remand_appeal_issues = cavc_remand.remand_appeal ? cavc_remand.remand_appeal&.request_issues.order(:id) : [] + source_issues = cavc_remand.source_appeal&.request_issues.order(:id) - cavc_remand.source_appeal&.request_issues.order(:id) + remand_appeal_issues + source_issues.reject do |ri| + remand_appeal_issues.any? do |rai| + rai.description.gsub(/#\d+\z/, "").rstrip == ri.description && rai.benefit_type == ri.benefit_type + end + end end def create_dispositions_for_remand_request_issues diff --git a/app/models/decision_document.rb b/app/models/decision_document.rb index 1be7c4c54c8..e81ebc23075 100644 --- a/app/models/decision_document.rb +++ b/app/models/decision_document.rb @@ -19,6 +19,7 @@ class NotYetSubmitted < StandardError; end S3_SUB_BUCKET = "decisions" delegate :veteran, to: :appeal + delegate :file_number, to: :veteran, prefix: true include BelongsToPolymorphicAppealConcern # Sets up belongs_to association with :appeal and provides `ama_appeal` used by `has_many` call @@ -26,6 +27,22 @@ class NotYetSubmitted < StandardError; end has_many :ama_decision_issues, -> { includes(:ama_decision_documents).references(:decision_documents) }, through: :ama_appeal, source: :decision_issues + has_many :vbms_communication_packages, as: :document_mailable_via_pacman + + def self.create_document!(params, mail_package) + create!(params).tap { |document| document.add_mail_package(mail_package) } + end + + def add_mail_package(mail_package) + @mail_package = mail_package + end + + def pdf_name + appeal.external_id + ".pdf" + end + + alias document_name pdf_name + def decision_issues ama_decision_issues if appeal_type == "Appeal" # LegacyAppeals do not have decision_issue records @@ -53,17 +70,18 @@ def submit_for_processing!(delay: processing_delay) super if not_processed_or_decision_date_not_in_the_future? - ProcessDecisionDocumentJob.perform_later(id) + ProcessDecisionDocumentJob.perform_later(id, mail_package) end end - def process! + def process!(mail_package) return if processed? fail NotYetSubmitted unless submitted_and_ready? attempted! upload_to_vbms! + queue_mail_request_job!(mail_package) unless mail_package.nil? if appeal.is_a?(Appeal) create_board_grant_effectuations! @@ -109,6 +127,8 @@ def all_contention_records(epe) private + attr_reader :mail_package + def create_board_grant_effectuations! appeal.decision_issues.granted.each do |granted_decision_issue| BoardGrantEffectuation.find_or_create_by(granted_decision_issue: granted_decision_issue) @@ -134,12 +154,13 @@ def update_decision_issue_decision_dates! def upload_to_vbms! return if uploaded_to_vbms_at - VBMSService.upload_document_to_vbms(appeal, self) - update!(uploaded_to_vbms_at: Time.zone.now) - end + response = VBMSService.upload_document_to_vbms(appeal, self) - def pdf_name - appeal.external_id + ".pdf" + update!( + uploaded_to_vbms_at: Time.zone.now, + document_version_reference_id: response.dig(:upload_document_response, :@new_document_version_ref_id), + document_series_reference_id: response.dig(:upload_document_response, :@document_series_ref_id) + ) end def s3_location @@ -184,4 +205,18 @@ def send_outcode_email(appeal) Rails.logger.warn("BVADispatchEmail #{log}") end end + + # Queues mail request job if recipient info present and dispatch completed + def queue_mail_request_job!(mail_package) + return unless uploaded_to_vbms_at + + MailRequestJob.perform_later(self, mail_package) + info_message = "MailRequestJob for citation #{citation_number} queued for submission to Package Manager" + log_info(info_message) + end + + def log_info(info_message) + uuid = SecureRandom.uuid + Rails.logger.info(info_message + " ID: " + uuid) + end end diff --git a/app/models/etl/decision_document.rb b/app/models/etl/decision_document.rb index 6a6ab0b65d2..3cf81fac966 100644 --- a/app/models/etl/decision_document.rb +++ b/app/models/etl/decision_document.rb @@ -6,6 +6,9 @@ class ETL::DecisionDocument < ETL::Record class << self private + ATTRS_TO_OMIT = %w[created_at updated_at + document_series_reference_id document_version_reference_id].freeze + # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/CyclomaticComplexity @@ -14,7 +17,7 @@ def merge_original_attributes_to_target(original, target) # To-do: ETL legacy appeals; AMA appeals are sufficient for now return unless original.appeal_type == "Appeal" - target.attributes = original.attributes.reject { |key| %w[created_at updated_at].include?(key) } + target.attributes = original.attributes.reject { |key| ATTRS_TO_OMIT.include?(key) } target.decision_document_created_at = original.created_at target.decision_document_updated_at = original.updated_at diff --git a/app/models/prepend/va_notify/appeal_decision_mailed.rb b/app/models/prepend/va_notify/appeal_decision_mailed.rb index 48a8674bfc9..034e4d32acd 100644 --- a/app/models/prepend/va_notify/appeal_decision_mailed.rb +++ b/app/models/prepend/va_notify/appeal_decision_mailed.rb @@ -12,7 +12,7 @@ module AppealDecisionMailed # Params: none # # Response: returns true if successfully processed, returns false if not successfully processed (will not notify) - def process! + def process!(mail_package = nil) super_return_value = super if processed? AppellantNotification.appeal_mapper(appeal.id, appeal.class.to_s, "decision_mailed") diff --git a/app/models/tasks/bva_dispatch_task.rb b/app/models/tasks/bva_dispatch_task.rb index 3d475f407c1..71ddb629232 100644 --- a/app/models/tasks/bva_dispatch_task.rb +++ b/app/models/tasks/bva_dispatch_task.rb @@ -37,11 +37,12 @@ def ready_for_dispatch?(appeal) true end - def outcode(appeal, params, user) + # Passes mail distributions to Package Manager service if recipient info present + def outcode(appeal, params, user, mail_package = nil) if appeal.is_a?(Appeal) - AmaAppealDispatch.new(appeal: appeal, user: user, params: params).call + AmaAppealDispatch.new(appeal: appeal, params: params, user: user, mail_package: mail_package).call elsif appeal.is_a?(LegacyAppeal) - LegacyAppealDispatch.new(appeal: appeal, params: params).call + LegacyAppealDispatch.new(appeal: appeal, params: params, mail_package: mail_package).call end end end diff --git a/app/models/vbms_communication_package.rb b/app/models/vbms_communication_package.rb new file mode 100644 index 00000000000..f09be6354b8 --- /dev/null +++ b/app/models/vbms_communication_package.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class VbmsCommunicationPackage < CaseflowRecord + belongs_to :document_mailable_via_pacman, polymorphic: true, optional: false + has_many :vbms_distributions + + validates :file_number, :comm_package_name, :copies, presence: true + validates :comm_package_name, length: { in: 1..255 }, format: { with: /\A[\w !*+,-.:;=?]{1,255}\Z/ } + validates :copies, numericality: { only_integer: true, greater_than: 0, less_than: 501 } +end diff --git a/app/models/vbms_distribution.rb b/app/models/vbms_distribution.rb new file mode 100644 index 00000000000..f7af0f5ef39 --- /dev/null +++ b/app/models/vbms_distribution.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class VbmsDistribution < CaseflowRecord + include MailRequestValidator::Distribution + + belongs_to :vbms_communication_package + has_many :vbms_distribution_destinations +end diff --git a/app/models/vbms_distribution_destination.rb b/app/models/vbms_distribution_destination.rb new file mode 100644 index 00000000000..80bbd6b1730 --- /dev/null +++ b/app/models/vbms_distribution_destination.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class VbmsDistributionDestination < CaseflowRecord + include MailRequestValidator::DistributionDestination + + belongs_to :vbms_distribution, optional: false +end diff --git a/app/models/vbms_uploaded_document.rb b/app/models/vbms_uploaded_document.rb index 6a767c13c2f..dc5f9a55c73 100644 --- a/app/models/vbms_uploaded_document.rb +++ b/app/models/vbms_uploaded_document.rb @@ -4,6 +4,8 @@ class VbmsUploadedDocument < CaseflowRecord include BelongsToPolymorphicAppealConcern belongs_to_polymorphic_appeal :appeal + has_many :vbms_communication_packages, as: :document_mailable_via_pacman + validates :document_type, presence: true attribute :file, :string diff --git a/app/queries/veteran_record_requests_open_for_vre_query.rb b/app/queries/veteran_record_requests_open_for_vre_query.rb new file mode 100644 index 00000000000..f4e5766f166 --- /dev/null +++ b/app/queries/veteran_record_requests_open_for_vre_query.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class VeteranRecordRequestsOpenForVREQuery + # @return [ActiveRecord::Relation] VeteranRecordRequest tasks that are + # both open and assigned to the 'Veterans Readiness and Employment' business + # line (f.k.a 'Vocational Rehabilitation and Employment') + def self.call + vre_business_line = + BusinessLine.where(name: Constants::BENEFIT_TYPES["voc_rehab"]) + + VeteranRecordRequest.open.where(assigned_to: vre_business_line) + end +end diff --git a/app/services/concerns/jwt_generator.rb b/app/services/concerns/jwt_generator.rb new file mode 100644 index 00000000000..102ab0fa91f --- /dev/null +++ b/app/services/concerns/jwt_generator.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module JwtGenerator + extend ActiveSupport::Concern + + module ClassMethods + # Purpose: Remove any illegal characters and keeps source at proper format + # + # Params: string + # + # Return: sanitized string + def base64url(source) + encoded_source = Base64.encode64(source) + encoded_source = encoded_source.sub(/=+$/, "") + encoded_source = encoded_source.tr("+", "-") + encoded_source = encoded_source.tr("/", "_") + encoded_source + end + end +end diff --git a/app/services/external_api/pacman_service.rb b/app/services/external_api/pacman_service.rb new file mode 100644 index 00000000000..b309d4a8b2f --- /dev/null +++ b/app/services/external_api/pacman_service.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require "json" +require "base64" +require "digest" + +class ExternalApi::PacmanService + include JwtGenerator + + BASE_URL = ENV["PACMAN_API_URL"] + SEND_DISTRIBUTION_ENDPOINT = "/package-manager-service/distribution" + SEND_PACKAGE_ENDPOINT = "/package-manager-service/communication-package" + GET_DISTRIBUTION_ENDPOINT = "/package-manager-service/distribution/" + HEADERS = { + "Content-Type": "application/json", Accept: "application/json" + }.freeze + + class << self + # Purpose: Creates and sends communication package + # POST: /package-manager-service/communication-package + # + # takes in file_number(string), name(string), document_reference(array of strings) + # + # Response: JSON of created package from Pacman API + # Example response can be seen in lib/fakes/pacman_service.rb under 'fake_package_request' method + def send_communication_package_request(file_number, name, document_references) + request = package_request(file_number, name, document_references.first) + send_pacman_request(request) + end + + # Purpose: Creates and sends distribution + # POST: /package-manager-service/distribution + # + # takes in package_id(string), recipient(json of strings), destinations(array of strings) + # + # Response: JSON of created distribution from Pacman API + # Example response can be seen in lib/fakes/pacman_service.rb under 'fake_distribution_request' method + def send_distribution_request(package_id, recipient, destinations) + destinations.map do |destination| + request = distribution_request(package_id, recipient, destination) + send_pacman_request(request) + end + end + + # Purpose: Gets distribution from distribution id + # POST: /package-manager-service/distribution + # + # takes in distribution_uuid(string) + # + # Response: JSON of distribution from Pacman API + # Example response can be seen in lib/fakes/pacman_service.rb under 'fake_distribution_response' method + def get_distribution_request(distribution_uuid) + request = { + endpoint: GET_DISTRIBUTION_ENDPOINT + distribution_uuid, method: :get + } + send_pacman_request(request) + end + + private + + # Purpose: Builds package request + # + # takes in file_number(string), name(string), document_reference(array of strings) + # + # Response: package request hash + def package_request(file_number, name, document_reference) + { + body: { + fileNumber: file_number, + name: name, + documentReferences: [{ + id: document_reference[:id], + copies: document_reference[:copies] + }] + }, + headers: HEADERS, + endpoint: SEND_PACKAGE_ENDPOINT, method: :post + } + end + + # Purpose: Builds distribution request + # + # takes in package_id(string), recipient(json of strings), destinations(array of strings) + # + # Response: Distribution request hash + def distribution_request(package_id, recipient, destination) + { + body: { + communicationPackageId: package_id, + recipient: recipient_data(recipient), + destinations: destinations_data(destination) + }, + headers: HEADERS, + endpoint: SEND_DISTRIBUTION_ENDPOINT, method: :post + }.compact + end + + # Purpose: Builds recipient json for distribution request + # + # takes in recipient(json of strings) + # + # Response: json of recipient data for distribution request hash + def recipient_data(recipient) + { + type: recipient[:type], + name: recipient[:name], + firstName: recipient[:first_name], + middleName: recipient[:middle_name], + lastName: recipient[:last_name], + participantId: recipient[:participant_id], + poaCode: recipient[:poa_code], + claimantStationOfJurisdiction: recipient[:claimant_station_of_jurisdiction] + } + end + + # Purpose: Builds destinations array for distribution request + # + # takes in destination(array of strings) + # + # Response: array of destination data for distribution request hashh + def destinations_data(destination) + [{ + type: destination[:type], + addressLine1: destination[:addressLine1], + addressLine2: destination[:addressLine2], + addressLine3: destination[:addressLine3], + addressLine4: destination[:addressLine4], + addressLine5: destination[:addressLine5], + addressLine6: destination[:addressLine6], + treatLine2AsAddressee: destination[:treatLine2AsAddressee], + treatLine3AsAddressee: destination[:treatLine3AsAddressee], + city: destination[:city], + state: destination[:state], + postalCode: destination[:postalCode], + countryName: destination[:countryName], + countryCode: destination[:countryCode] + }] + end + + def jwt_payload + current_epoch_timestamp = DateTime.now.strftime("%Q").to_i / 1000.floor + + { + iat: current_epoch_timestamp, + iss: ENV["PACMAN_API_TOKEN_ISSUER"], + aud: ENV["PACMAN_API_TOKEN_ISSUER"], + samlToken: ENV["PACMAN_API_SAML_TOKEN"]&.encode("UTF-8"), + externalSystemSource: ENV["PACMAN_API_SYS_ACCOUNT"] + } + end + + # Purpose: Generate the JWT token + # + # Params: none + # + # Return: token needed for authentication + def generate_token + header = { + alg: ENV["PACMAN_API_TOKEN_ALG"] + } + + stringified_header = header.to_json.encode("UTF-8") + encoded_header = base64url(stringified_header) + stringified_data = jwt_payload.to_json.encode("UTF-8") + encoded_data = base64url(stringified_data) + token = "#{encoded_header}.#{encoded_data}" + signature = OpenSSL::HMAC.digest("SHA512", ENV["PACMAN_API_TOKEN_SECRET"], token) + + # Signed Token + "#{token}.#{base64url(signature)}" + end + + # Purpose: Build and send the request to the server + # + # Params: general requirements for HTTP request + # + # Return: service_response: JSON from Pacman or error + # :reek:LongParameterList + def send_pacman_request(headers: {}, endpoint:, method: :get, body: nil) + url = BASE_URL + endpoint + request = HTTPI::Request.new(url) + request.open_timeout = 30 + request.read_timeout = 30 + request.body = body.to_json unless body.nil? + request.auth.ssl.ssl_version = :TLSv1_2 + request.auth.ssl.ca_cert_file = ENV["SSL_CERT_FILE"] + request.headers = headers.merge("X-Forwarded-User": ENV["PACMAN_API_JWT"]) + sleep 1 + + MetricsService.record("Pacman Service #{method.to_s.upcase} request to #{url}", + service: :pacman, + name: endpoint) do + case method + when :get + HTTPI.get(request) + when :post + HTTPI.post(request) + end + end + end + end +end diff --git a/app/services/external_api/pacman_service/response.rb b/app/services/external_api/pacman_service/response.rb new file mode 100644 index 00000000000..1e491e5c836 --- /dev/null +++ b/app/services/external_api/pacman_service/response.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class ExternalApi::PacmanService::Response + attr_reader :resp, :code + + def initialize(resp) + @resp = resp + @code = @resp.code + end + + def data; end + + # Wrapper method to check for errors + def error + check_for_error + end + + # Checks if there is no error + def success? + !resp.error? + end + + # Parses response body to an object + def body + @body ||= begin + JSON.parse(resp.body).with_indifferent_access + rescue JSON::ParserError + log(JSON::ParserError) + {} + end + end + + private + + # Error codes and their associated error + ERROR_LOOKUP = { + 400 => Caseflow::Error::PacmanBadRequestError, + 403 => Caseflow::Error::PacmanForbiddenError, + 404 => Caseflow::Error::PacmanNotFoundError, + 500 => Caseflow::Error::PacmanInternalServerError + }.freeze + + # Checks for error and returns if found + def check_for_error + return if success? + + message = error_message + + if ERROR_LOOKUP.key? code + ERROR_LOOKUP[code].new(code: code, message: message) + else + Caseflow::Error::PacmanApiError.new(code: code, message: message) + end + end + + def log_error(error) + uuid = SecureRandom.uuid + Rails.logger.error(error.name + " " + error.message + "Error ID: " + uuid) + Raven.capture_exception(error.name + " " + error.message, extra: { error_uuid: uuid }) + end + + # Gets the error message from the response + def error_message + return "No error message from Pacman" if body.empty? + + body&.error || "No error message from Pacman" + end +end diff --git a/app/services/external_api/va_notify_service.rb b/app/services/external_api/va_notify_service.rb index 38a6febf294..5f38a03e0c0 100644 --- a/app/services/external_api/va_notify_service.rb +++ b/app/services/external_api/va_notify_service.rb @@ -4,6 +4,8 @@ require "base64" require "digest" class ExternalApi::VANotifyService + include JwtGenerator + BASE_URL = ENV["VA_NOTIFY_API_URL"] CLIENT_SECRET = ENV["VA_NOTIFY_API_KEY"] SERVICE_ID = ENV["VA_NOTIFY_SERVICE_ID"] @@ -100,19 +102,6 @@ def generate_token signed_token end - # Purpose: Remove any illegal characters and keeps source at proper format - # - # Params: string - # - # Return: sanitized string - def base64url(source) - encoded_source = Base64.encode64(source) - encoded_source = encoded_source.sub(/=+$/, "") - encoded_source = encoded_source.tr("+", "-") - encoded_source = encoded_source.tr("/", "_") - encoded_source - end - # Purpose: Build an email request object # # Params: Details from appeal for notification diff --git a/app/validators/mail_request_validator.rb b/app/validators/mail_request_validator.rb new file mode 100644 index 00000000000..3f3acbcc273 --- /dev/null +++ b/app/validators/mail_request_validator.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module MailRequestValidator + # Validations for VbmsDistribution model and MailRequest object + module Distribution + extend ActiveSupport::Concern + + included do + with_options presence: true do + validates :recipient_type, inclusion: { in: %w[organization person system ro-colocated] } + validates :first_name, :last_name, if: -> { recipient_type == "person" } + validates :name, if: :not_a_person? + validates :poa_code, :claimant_station_of_jurisdiction, if: -> { recipient_type == "ro-colocated" } + end + end + + private + + def not_a_person? + %w[organization system ro-colocated].include?(recipient_type) + end + end + + # Validations for VbmsDistributionDestination model and MailRequest object + module DistributionDestination + extend ActiveSupport::Concern + + included do + with_options presence: true do + validates :destination_type, inclusion: { in: %w[domesticAddress internationalAddress militaryAddress derived] } + validates :address_line_1, :city, :country_code, if: :physical_mail? + validates :address_line_2, if: :treat_line_2_as_addressee + validates :address_line_3, if: :treat_line_3_as_addressee + validates :state, :postal_code, if: :us_address? + validates :country_name, if: -> { destination_type == "internationalAddress" } + end + + validates :treat_line_2_as_addressee, + inclusion: { in: [true], message: "cannot be false if line 3 is treated as addressee" }, + if: -> { treat_line_3_as_addressee == true } + + validate :valid_country_code?, if: :physical_mail? + validate :valid_us_state_code?, if: :us_address? + end + + private + + def physical_mail? + %w[domesticAddress internationalAddress militaryAddress].include?(destination_type) + end + + def us_address? + %w[domesticAddress militaryAddress].include?(destination_type) + end + + def valid_country_code? + unless iso_country_codes.include?(country_code) + errors.add(:country_code, "is not a valid ISO 3166-2 code") + end + end + + def valid_us_state_code? + unless iso_us_state_codes.include?(state) + errors.add(:state, "is not a valid ISO 3166-2 code") + end + end + + def iso_country_codes + ISO3166::Country.codes + end + + def iso_us_state_codes + ISO3166::Country.find_country_by_alpha2("US").subdivisions.keys + end + end +end diff --git a/app/views/queue/index.html.erb b/app/views/queue/index.html.erb index 7d357ca9017..7e96ef7f0d1 100644 --- a/app/views/queue/index.html.erb +++ b/app/views/queue/index.html.erb @@ -53,7 +53,8 @@ cavc_remand_granted_substitute_appellant: FeatureToggle.enabled?(:cavc_remand_granted_substitute_appellant, user: current_user), cavc_dashboard_workflow: FeatureToggle.enabled?(:cavc_dashboard_workflow, user: current_user), cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, user: current_user), - metricsBrowserError: FeatureToggle.enabled_metric?(:metrics_browser_error, user: current_user) + metricsBrowserError: FeatureToggle.enabled_metric?(:metrics_browser_error, user: current_user), + cc_vacatur_visibility: FeatureToggle.enabled?(:cc_vacatur_visibility, user: current_user) } }) %> <% end %> diff --git a/app/workflows/ama_appeal_dispatch.rb b/app/workflows/ama_appeal_dispatch.rb index cd10f06950d..01618ee3627 100644 --- a/app/workflows/ama_appeal_dispatch.rb +++ b/app/workflows/ama_appeal_dispatch.rb @@ -4,14 +4,11 @@ class AmaAppealDispatch include ActiveModel::Model include DecisionDocumentValidator - def initialize(appeal:, params:, user:) - @appeal = appeal + def initialize(appeal:, params:, user:, mail_package: nil) @params = params.merge(appeal_id: appeal.id, appeal_type: "Appeal") + @appeal = appeal @user = user - @citation_number = params[:citation_number] - @decision_date = params[:decision_date] - @redacted_document_location = params[:redacted_document_location] - @file = params[:file] + @mail_package = mail_package end def call @@ -27,8 +24,23 @@ def call private - attr_reader :appeal, :params, :user, :success, :citation_number, - :decision_date, :redacted_document_location, :file + attr_reader :params, :appeal, :user, :mail_package, :success + + def citation_number + params[:citation_number] + end + + def decision_date + params[:decision_date] + end + + def redacted_document_location + params[:redacted_document_location] + end + + def file + params[:file] + end def dispatch_tasks @dispatch_tasks ||= BvaDispatchTask.not_cancelled.where(appeal: appeal, assigned_to: user) @@ -73,7 +85,7 @@ def outcode_appeal end def create_decision_document_and_submit_for_processing!(params) - DecisionDocument.create!(params).tap(&:submit_for_processing!) + DecisionDocument.create_document!(params, mail_package).tap(&:submit_for_processing!) end def complete_dispatch_task! diff --git a/app/workflows/legacy_appeal_dispatch.rb b/app/workflows/legacy_appeal_dispatch.rb index 086d73d6b7e..67d087f6099 100644 --- a/app/workflows/legacy_appeal_dispatch.rb +++ b/app/workflows/legacy_appeal_dispatch.rb @@ -4,13 +4,10 @@ class LegacyAppealDispatch include ActiveModel::Model include DecisionDocumentValidator - def initialize(appeal:, params:) - @appeal = appeal + def initialize(appeal:, params:, mail_package: nil) @params = params.merge(appeal_id: appeal.id, appeal_type: "LegacyAppeal") - @citation_number = params[:citation_number] - @decision_date = params[:decision_date] - @redacted_document_location = params[:redacted_document_location] - @file = params[:file] + @appeal = appeal + @mail_package = mail_package end def call @@ -26,11 +23,26 @@ def call private - attr_reader :appeal, :params, :success, :citation_number, - :decision_date, :redacted_document_location, :file + attr_reader :params, :appeal, :mail_package, :success + + def citation_number + params[:citation_number] + end + + def decision_date + params[:decision_date] + end + + def redacted_document_location + params[:redacted_document_location] + end + + def file + params[:file] + end def create_decision_document_and_submit_for_processing!(params) - DecisionDocument.create!(params).tap(&:submit_for_processing!) + DecisionDocument.create_document!(params, mail_package).tap(&:submit_for_processing!) end def complete_root_task! diff --git a/app/workflows/mail_request.rb b/app/workflows/mail_request.rb new file mode 100644 index 00000000000..81da60840b4 --- /dev/null +++ b/app/workflows/mail_request.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +class MailRequest + include ActiveModel::Model + include ActiveModel::Validations + + include MailRequestValidator::Distribution + include MailRequestValidator::DistributionDestination + + attr_reader :vbms_distribution_id, :comm_package_id + + # Purpose: initializes a mail_request object making use of the passed in hash and also initializing + # the attributes of vbms_distribution_id and a comm_package_id. Both set to nil until set + # otherwise. + # + # Params: recipient_and_destination_hash - expected parameters that that hold information + # that will be used to create a valid VbmsDistribution and valid VbmsDistributionDestination. + # + # Return: nil + def initialize(recipient_and_destination_hash) + @recipient_info = recipient_and_destination_hash + @vbms_distribution_id = nil + @comm_package_id = nil + end + + # Purpose: With the passed in parameters, the call method creates both a valid VBMSDistribution and + # valid VBMSDistributionDestination. If there is an error it will fail and that information will be provided + # to the IDT user. + # + def call + if valid? + distribution = create_a_vbms_distribution + @vbms_distribution_id = distribution.id + create_a_vbms_distribution_destination + else + fail Caseflow::Error::MissingRecipientInfo + end + end + + private + + def create_a_vbms_distribution + VbmsDistribution.create!(recipient_params_parse) + end + + def create_a_vbms_distribution_destination + VbmsDistributionDestination.create!(destination_params_parse) + end + + def destination_params_parse + { + destination_type: destination_type, + address_line_1: address_line_1, + address_line_2: address_line_2, + address_line_3: address_line_3, + address_line_4: address_line_4, + address_line_5: address_line_5, + address_line_6: address_line_6, + city: city, + country_code: country_code, + postal_code: postal_code, + state: state, + treat_line_2_as_addressee: treat_line_2_as_addressee, + treat_line_3_as_addressee: treat_line_3_as_addressee, + country_name: country_name, + vbms_distribution_id: vbms_distribution_id + } + end + + def recipient_params_parse + { + recipient_type: recipient_type, + name: name, + first_name: first_name, + middle_name: middle_name, + last_name: last_name, + participant_id: participant_id, + poa_code: poa_code, + claimant_station_of_jurisdiction: claimant_station_of_jurisdiction, + created_by_id: RequestStore[:current_user].id + } + end + + def recipient_type + @recipient_info[:recipient_type] + end + + def name + @recipient_info[:name] + end + + def first_name + @recipient_info[:first_name] + end + + def middle_name + @recipient_info[:middle_name] + end + + def last_name + @recipient_info[:last_name] + end + + def participant_id + @recipient_info[:participant_id] + end + + def poa_code + @recipient_info[:poa_code] + end + + def claimant_station_of_jurisdiction + @recipient_info[:claimant_station_of_jurisdiction] + end + + def destination_type + @recipient_info[:destination_type] + end + + # :reek:UncommunicativeMethodName + def address_line_1 + @recipient_info[:address_line_1] + end + + # :reek:UncommunicativeMethodName + def address_line_2 + @recipient_info[:address_line_2] + end + + # :reek:UncommunicativeMethodName + def address_line_3 + @recipient_info[:address_line_3] + end + + # :reek:UncommunicativeMethodName + def address_line_4 + @recipient_info[:address_line_4] + end + + # :reek:UncommunicativeMethodName + def address_line_5 + @recipient_info[:address_line_5] + end + + # :reek:UncommunicativeMethodName + def address_line_6 + @recipient_info[:address_line_6] + end + + def city + @recipient_info[:city] + end + + def country_code + @recipient_info[:country_code] + end + + def postal_code + @recipient_info[:postal_code] + end + + def state + @recipient_info[:state] + end + + def treat_line_2_as_addressee + @recipient_info[:treat_line_2_as_addressee] + end + + def treat_line_3_as_addressee + @recipient_info[:treat_line_3_as_addressee] + end + + def country_name + @recipient_info[:country_name] + end +end diff --git a/app/workflows/prepare_document_upload_to_vbms.rb b/app/workflows/prepare_document_upload_to_vbms.rb index f83f59f8ed6..8270e8ca595 100644 --- a/app/workflows/prepare_document_upload_to_vbms.rb +++ b/app/workflows/prepare_document_upload_to_vbms.rb @@ -10,11 +10,15 @@ class PrepareDocumentUploadToVbms # Params: params - hash containing file and document_type at minimum # user - current user that is preparing the document for upload # appeal - Appeal object (optional if ssn or file number are passed into params) - def initialize(params, user, appeal = nil) + # mail_package - Payload with distributions value (array of JSON-formatted MailRequest objects), + # copies value (integer), and created_by_id value (integer) to be submitted to + # Package Manager if optional recipient info is present + # + def initialize(params, user, appeal = nil, mail_package = nil) @params = params.slice(:veteran_file_number, :document_type, :document_subject, :document_name, :file, :application) - @document_type = @params[:document_type] @user = user @appeal = appeal + @mail_package = mail_package end # Purpose: Queues a job to upload a document to vbms @@ -28,11 +32,7 @@ def call @params[:veteran_file_number] = throw_error_if_file_number_not_match_bgs VbmsUploadedDocument.create(document_params).tap do |document| document.cache_file - UploadDocumentToVbmsJob.perform_later( - document_id: document.id, - initiator_css_id: user.css_id, - application: @params[:application] - ) + UploadDocumentToVbmsJob.perform_later(upload_job_params(document)) end end @@ -42,22 +42,26 @@ def call private attr_accessor :success - attr_reader :document_type, :params, :user + attr_reader :params, :user, :mail_package, :document def veteran_file_number - @params[:veteran_file_number] + params[:veteran_file_number] end def document_subject - @params[:document_subject] + params[:document_subject] end def document_name - @params[:document_name] + params[:document_name] end def file - @params[:file] + params[:file] + end + + def document_type + params[:document_type] end def valid_document_type @@ -84,6 +88,15 @@ def document_params } end + def upload_job_params(document) + { + document_id: document.id, + initiator_css_id: user.css_id, + application: params[:application], + mail_package: mail_package + } + end + def response_errors return if success diff --git a/app/workflows/upload_document_to_vbms.rb b/app/workflows/upload_document_to_vbms.rb index dfcda597753..c95b815569d 100644 --- a/app/workflows/upload_document_to_vbms.rb +++ b/app/workflows/upload_document_to_vbms.rb @@ -15,6 +15,7 @@ def call submit_for_processing! upload_to_vbms! set_processed_at_to_current_time + log_info("Document #{document.id} uploaded to VBMS") rescue StandardError => error save_rescued_error!(error.to_s) raise error @@ -88,6 +89,11 @@ def file_number document.veteran_file_number end + def log_info(info_message) + uuid = SecureRandom.uuid + Rails.logger.info(info_message + " ID: " + uuid) + end + # Purpose: Get the s3_sub_bucket based on the document type # S3_SUB_BUCKET was previously a constant defined for this class. # diff --git a/client/COPY.json b/client/COPY.json index dafc32b9699..9bd85705be1 100644 --- a/client/COPY.json +++ b/client/COPY.json @@ -386,6 +386,12 @@ "MTV_CHECKOUT_RETURN_TO_JUDGE_MODAL_INSTRUCTIONS_LABEL": "Provide instructions and context for this action", "MTV_CHECKOUT_RETURN_TO_JUDGE_SUCCESS_TITLE": "%s's Motion to Vacate has been returned to %s", "MTV_CHECKOUT_RETURN_TO_JUDGE_SUCCESS_DETAILS": "If you made a mistake, please email your judge to resolve the issue.", + + "MTV_TASK_INSTRUCTIONS": "**Motion To Vacate:** \n", + "MTV_TASK_INSTRUCTIONS_TYPE": "**Type:** ", + "MTV_TASK_INSTRUCTIONS_DETAIL": "**Detail:** ", + "MTV_TASK_INSTRUCTIONS_HYPERLINK": "**Hyperlink:** ", + "VACATE_AND_DE_NOVO_TASK_LABEL": "Vacate and De Novo", "VACATE_AND_READJUDICATION_TASK_LABEL": "Vacate and Readjudication", "STRAIGHT_VACATE_TASK_LABEL": "Straight Vacate", diff --git a/client/app/queue/QueueApp.jsx b/client/app/queue/QueueApp.jsx index f78d4574909..f1b6d83a52c 100644 --- a/client/app/queue/QueueApp.jsx +++ b/client/app/queue/QueueApp.jsx @@ -719,7 +719,7 @@ class QueueApp extends React.PureComponent { /> diff --git a/client/app/queue/constants.js b/client/app/queue/constants.js index 7fa63f479e9..f02c465abb3 100644 --- a/client/app/queue/constants.js +++ b/client/app/queue/constants.js @@ -179,7 +179,7 @@ export const PAGE_TITLES = { ATTORNEY: 'Select Remand Reasons' }, REVIEW_CASES: 'Review Cases', - UNASSIGED_CASES: 'Unassgined Cases', + UNASSIGNED_CASES: 'Unassigned Cases', CASE_DETAILS: 'Case Details', DRAFT_DECISION: 'Draft Decision', EVALUATE_DECISION: 'Evaluate Decision', diff --git a/client/app/queue/mtv/AddressMotionToVacateView.jsx b/client/app/queue/mtv/AddressMotionToVacateView.jsx index 825fd14c2d5..8cf14c6e30a 100644 --- a/client/app/queue/mtv/AddressMotionToVacateView.jsx +++ b/client/app/queue/mtv/AddressMotionToVacateView.jsx @@ -18,6 +18,9 @@ export const AddressMotionToVacateView = () => { const task = useSelector((state) => taskById(state, { taskId })); const appeal = useSelector((state) => appealWithDetailSelector(state, { appealId })); + const vacateTypeFeatureToggle = useSelector( + (state) => state.ui.featureToggles.cc_vacatur_visibility + ); const { selected, options } = taskActionData({ task, match }); @@ -42,6 +45,7 @@ export const AddressMotionToVacateView = () => { task={task} attorneys={attyOptions} selectedAttorney={selected} + vacateTypeFeatureToggle = {vacateTypeFeatureToggle} appeal={appeal} onSubmit={handleSubmit} returnToLitSupportLink={`${match.url}/${JUDGE_RETURN_TO_LIT_SUPPORT.value}`} diff --git a/client/app/queue/mtv/MTVJudgeDisposition.jsx b/client/app/queue/mtv/MTVJudgeDisposition.jsx index eddde279cc9..ec89ec4d81a 100644 --- a/client/app/queue/mtv/MTVJudgeDisposition.jsx +++ b/client/app/queue/mtv/MTVJudgeDisposition.jsx @@ -13,9 +13,13 @@ import { JUDGE_ADDRESS_MTV_VACATE_TYPE_LABEL, JUDGE_ADDRESS_MTV_HYPERLINK_LABEL, JUDGE_ADDRESS_MTV_DISPOSITION_NOTES_LABEL, - JUDGE_ADDRESS_MTV_ASSIGN_ATTORNEY_LABEL + JUDGE_ADDRESS_MTV_ASSIGN_ATTORNEY_LABEL, + MTV_TASK_INSTRUCTIONS, + MTV_TASK_INSTRUCTIONS_TYPE, + MTV_TASK_INSTRUCTIONS_DETAIL, + MTV_TASK_INSTRUCTIONS_HYPERLINK } from '../../../COPY'; -import { DISPOSITION_TEXT, VACATE_TYPE_OPTIONS } from '../../../constants/MOTION_TO_VACATE'; +import { DISPOSITION_TIMELINE_TEXT, VACATE_TYPE_OPTIONS } from '../../../constants/MOTION_TO_VACATE'; import { JUDGE_RETURN_TO_LIT_SUPPORT } from '../../../constants/TASK_ACTIONS'; import SearchableDropdown from '../../components/SearchableDropdown'; import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment'; @@ -28,6 +32,7 @@ import StringUtil from '../../util/StringUtil'; import { ReturnToLitSupportAlert } from './ReturnToLitSupportAlert'; import { grantTypes, dispositionStrings } from './mtvConstants'; import { sprintf } from 'sprintf-js'; +import { isEmpty } from 'lodash'; const vacateTypeText = (val) => { const opt = VACATE_TYPE_OPTIONS.find((i) => i.value === val); @@ -35,19 +40,33 @@ const vacateTypeText = (val) => { return opt && opt.displayText; }; -const formatInstructions = ({ disposition, vacateType, hyperlink, instructions }) => { - const parts = [`I am proceeding with a ${DISPOSITION_TEXT[disposition]}.`]; +const formatInstructions = ({ vacateTypeFeatureToggle, disposition, vacateType, hyperlink, instructions }) => { + const parts = [`${MTV_TASK_INSTRUCTIONS}${DISPOSITION_TIMELINE_TEXT[disposition]}\n`]; switch (disposition) { case 'granted': case 'partially_granted': - parts.push(`This will be a ${vacateTypeText(vacateType)}`); - parts.push(instructions); + if (!vacateTypeFeatureToggle) { + parts.push(MTV_TASK_INSTRUCTIONS_TYPE); + parts.push(`${vacateTypeText(vacateType)}\n`); + + } + if (isEmpty(instructions) === false) { + parts.push(MTV_TASK_INSTRUCTIONS_DETAIL); + parts.push(`${instructions}\n`); + } break; + case 'denied': + case 'dismissed': default: - parts.push(instructions); - parts.push('\nHere is the hyperlink to the signed denial document'); - parts.push(hyperlink); + if (isEmpty(instructions) === false) { + parts.push(MTV_TASK_INSTRUCTIONS_DETAIL); + parts.push(`${instructions}\n`); + } + if (hyperlink !== null) { + parts.push(MTV_TASK_INSTRUCTIONS_HYPERLINK); + parts.push(`${hyperlink}\n`); + } break; } @@ -62,10 +81,12 @@ const styles = { }; export const MTVJudgeDisposition = ({ + attorneys, selectedAttorney, task, appeal, + vacateTypeFeatureToggle, onSubmit = () => null, submitting = false, returnToLitSupportLink = JUDGE_RETURN_TO_LIT_SUPPORT.value @@ -81,6 +102,7 @@ export const MTVJudgeDisposition = ({ const handleSubmit = () => { const formattedInstructions = formatInstructions({ + vacateTypeFeatureToggle, disposition, vacateType, hyperlink, @@ -180,7 +202,8 @@ export const MTVJudgeDisposition = ({ setInstructions(val)} value={instructions} className={['mtv-decision-instructions']} @@ -227,6 +250,7 @@ MTVJudgeDisposition.propTypes = { onSubmit: PropTypes.func.isRequired, submitting: PropTypes.bool, task: PropTypes.object.isRequired, + vacateTypeFeatureToggle: PropTypes.bool, appeal: PropTypes.object.isRequired, attorneys: PropTypes.array.isRequired, selectedAttorney: PropTypes.object, diff --git a/client/constants/MOTION_TO_VACATE.json b/client/constants/MOTION_TO_VACATE.json index eb7d4c76893..e5b1d8e44c0 100644 --- a/client/constants/MOTION_TO_VACATE.json +++ b/client/constants/MOTION_TO_VACATE.json @@ -39,6 +39,14 @@ "denied": "denial of all issues for vacatur", "dismissed": "dismissal" }, + + "DISPOSITION_TIMELINE_TEXT": { + "granted": "Full vacatur", + "partially_granted": "Partial vacatur", + "denied": "Deny all issues for vacatur", + "dismissed": "Dismiss all issues for vacatur" + }, + "DISPOSITION_RECOMMENDATIONS": { "granted": "I recommend granting a vacatur.", "partially_granted": "I recommend granting a partial vacatur.", diff --git a/client/test/app/queue/ColocatedTaskListView.test.js b/client/test/app/queue/ColocatedTaskListView.test.js index 6660012b6c0..570ec2d2e87 100644 --- a/client/test/app/queue/ColocatedTaskListView.test.js +++ b/client/test/app/queue/ColocatedTaskListView.test.js @@ -31,8 +31,9 @@ const WrapperComponent = ({ children }) => ( ); -// Date constructor uses zero-based offset for months — this is 2021-03-17 -const fakeDate = new Date(2021, 2, 17, 12); +// Date constructor uses zero-based offset for months — this is 2021-03-17. The time (11:30pm) is to ensure +// that the crossover between days doesn't affect the front end calculations for when tasks were assigned +const fakeDate = new Date(2021, 2, 17, 23, 30, 0, 0); beforeAll(() => { // Ensure consistent handling of dates across tests diff --git a/client/test/app/queue/mtv/MTVJudgeDisposition.test.js b/client/test/app/queue/mtv/MTVJudgeDisposition.test.js index 42025286c26..306965c3da0 100644 --- a/client/test/app/queue/mtv/MTVJudgeDisposition.test.js +++ b/client/test/app/queue/mtv/MTVJudgeDisposition.test.js @@ -15,40 +15,99 @@ const task = generateAmaTask({ type: 'VacateMotionMailTask', instructions: ['Lorem ipsum dolor sit amet, consectetur adipiscing'], }); +let linkField = /Insert Caseflow Reader document hyperlink to/; +let instructionsField = /Provide context and instructions on which issues should be/; + +const selectRadioField = (radioSelection) => { + const radioFieldToSelect = screen.getByLabelText(radioSelection); + + userEvent.click(radioFieldToSelect); +}; + +const enterAdditionalContext = (text, selectedField) => { + const textField = screen.getByText(selectedField); + + userEvent.type(textField, text); +}; + +const selectDisposition = async (disposition = 'grant all') => { + userEvent.click( + screen.getByLabelText(new RegExp(disposition, 'i')) + ); + + if ((/grant/i).test(disposition)) { + await waitFor(() => { + expect( + screen.getByText(/what type of vacate/i) + ).toBeInTheDocument(); + }); + } else { + await waitFor(() => { + expect( + screen.getByLabelText(/insert caseflow reader document hyperlink/i) + ).toBeInTheDocument(); + }); + } +}; + +const fillForm = async (disposition, vacateType, vacateIssues, hyperlink, instructions) => { + userEvent.click( + screen.getByLabelText(new RegExp(disposition, 'i')) + ); + + if ((/grant all/i).test(disposition)) { + await waitFor(() => { + expect( + screen.getByText(/what type of vacate/i) + ).toBeInTheDocument(); + }); + + selectRadioField(vacateType); + + } else if ((/grant partial/i).test(disposition)) { + await waitFor(() => { + expect( + screen.getByText(/which issues would you like to vacate/i) + ).toBeInTheDocument(); + }); + + selectRadioField(vacateType); + selectRadioField(vacateIssues); + + } else { + await waitFor(() => { + expect( + screen.getByLabelText(/insert caseflow reader document hyperlink/i) + ).toBeInTheDocument(); + }); + + enterAdditionalContext(hyperlink, linkField); + + } + + enterAdditionalContext(instructions, instructionsField); + + await userEvent.click( + screen.getByText('Submit') + ); +}; describe('MTVJudgeDisposition', () => { + const onSubmit = jest.fn(); + const defaults = { appeal: amaAppeal, attorneys: generateAttorneys(5), task, onSubmit, }; + const setup = (props) => render(, { wrapper: BrowserRouter, }); - const selectDisposition = async (disposition = 'grant all') => { - await userEvent.click( - screen.getByLabelText(new RegExp(disposition, 'i')) - ); - - if ((/grant/i).test(disposition)) { - await waitFor(() => { - expect( - screen.getByText(/what type of vacate/i) - ).toBeInTheDocument(); - }); - } else { - await waitFor(() => { - expect( - screen.getByLabelText(/insert caseflow reader document hyperlink/i) - ).toBeInTheDocument(); - }); - } - }; - describe('default view', () => { it('renders correctly', () => { const { container } = setup(); @@ -67,7 +126,7 @@ describe('MTVJudgeDisposition', () => { describe.each(DISPOSITION_OPTIONS.map((item) => [item.value, item]))( 'with %s disposition selected', - (disposition, { displayText: label }) => { + ({ displayText: label }) => { it('renders correctly', async () => { const { container } = setup(); @@ -87,4 +146,237 @@ describe('MTVJudgeDisposition', () => { }); } ); + + describe('Case timeline instructions, feature toggle enabled', () => { + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('grant or partial grant instructions sent', () => { + it('sends the correct instructions based on grant all disposition', async () => { + const disposition = 'grant all'; + let vacateType = 'Vacate and De Novo (2 documents)'; + let vacateIssues; + let hyperlink; + const instructions = 'instructions from judge'; + + setup({ vacateTypeFeatureToggle: false }); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** ' + + '\nFull vacatur' + + '\n' + + '\n**Type:** ' + + '\nVacate and De Novo (2 documents)' + + '\n' + + '\n**Detail:** ' + + '\ninstructions from judge' + + '\n' + ); + }); + + it('sends the correct instructions based on partially granted disposition', async () => { + const disposition = 'Grant partial vacatur'; + const vacateType = 'Straight Vacate (1 document)'; + const vacateIssues = '1. This is a description of the decision'; + let hyperlink; + const instructions = 'some instructions from judge'; + + setup({ vacateTypeFeatureToggle: false }); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch('**Motion To Vacate:** ' + + '\nPartial vacatur' + + '\n' + + '\n**Type:** ' + + '\nStraight Vacate (1 document)' + + '\n' + + '\n**Detail:** ' + + '\nsome instructions from judge' + + '\n' + ); + }); + }); + + describe('deny or dismiss instructions sent', () => { + it('sends the correct instructions based on denied disposition', async () => { + const disposition = 'deny'; + let vacateType; + let vacateIssues; + const hyperlink = 'www.caseflow.com'; + const instructions = 'testing'; + + setup(); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** \n' + + 'Deny all issues for vacatur\n\n' + + '**Detail:** \ntesting\n\n' + + '**Hyperlink:** \nwww.caseflow.com\n' + ); + }); + + it('sends the correct instructions based on dismissed disposition', async () => { + const disposition = 'dismiss'; + let vacateType; + let vacateIssues; + const hyperlink = 'www.google.com'; + const instructions = 'new instructions from judge'; + + setup(); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** \n' + + 'Dismiss all issues for vacatur\n\n**Detail:** \nnew instructions from judge\n\n' + + '**Hyperlink:** \nwww.google.com\n' + ); + }); + }); + }); + + describe('Case timeline instructions, feature toggle disabled', () => { + + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('grant or partial grant instructions sent', () => { + it('sends the correct instructions based on grant all disposition', async () => { + + const disposition = 'grant all'; + let vacateType = 'Vacate and De Novo (2 documents)'; + let vacateIssues; + let hyperlink; + const instructions = 'instructions from judge'; + + setup({ vacateTypeFeatureToggle: true }); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** ' + + '\nFull vacatur' + + '\n' + + '\n**Detail:** ' + + '\ninstructions from judge' + + '\n' + ); + + }); + + it('sends the correct instructions based on partially granted disposition', async () => { + const disposition = 'Grant partial vacatur'; + let vacateType = 'Vacate and De Novo (2 documents)'; + let vacateIssues = '1. This is a description of the decision'; + let hyperlink; + const instructions = 'some instructions from judge'; + + setup({ vacateTypeFeatureToggle: true }); + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** ' + + '\nPartial vacatur' + + '\n' + + '\n**Detail:** ' + + '\nsome instructions from judge' + + '\n' + ); + }); + }); + + describe('deny or dismiss instructions sent', () => { + it('sends the correct instructions based on denied disposition', async () => { + const disposition = 'deny'; + let vacateType; + let vacateIssues; + const hyperlink = 'www.caseflow.com'; + const instructions = 'testing'; + + setup({ vacateTypeFeatureToggle: true }); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** \n' + + 'Deny all issues for vacatur\n\n' + + '**Detail:** \ntesting\n\n' + + '**Hyperlink:** \nwww.caseflow.com\n' + ); + }); + + it('sends the correct instructions based on dismissed disposition', async () => { + const disposition = 'dismiss'; + let vacateType; + let vacateIssues; + const hyperlink = 'www.google.com'; + const instructions = 'new instructions from judge'; + + setup({ vacateTypeFeatureToggle: true }); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** \n' + + 'Dismiss all issues for vacatur\n\n**Detail:** \nnew instructions from judge\n\n' + + '**Hyperlink:** \nwww.google.com\n' + ); + }); + }); + }); }); diff --git a/client/test/app/queue/mtv/__snapshots__/MTVJudgeDisposition.test.js.snap b/client/test/app/queue/mtv/__snapshots__/MTVJudgeDisposition.test.js.snap index 94920f806ba..01fe1fb42f9 100644 --- a/client/test/app/queue/mtv/__snapshots__/MTVJudgeDisposition.test.js.snap +++ b/client/test/app/queue/mtv/__snapshots__/MTVJudgeDisposition.test.js.snap @@ -422,31 +422,77 @@ exports[`MTVJudgeDisposition with denied disposition selected renders correctly - + +
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -456,7 +502,7 @@ exports[`MTVJudgeDisposition with denied disposition selected renders correctly > - Provide context and instructions on which issues should be denied + Provide context and instructions on which issues should be granted @@ -470,14 +516,114 @@ exports[`MTVJudgeDisposition with denied disposition selected renders correctly name="instructions" />
+
+
- + +
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -719,7 +911,7 @@ exports[`MTVJudgeDisposition with dismissed disposition selected renders correct > - Provide context and instructions on which issues should be dismissed + Provide context and instructions on which issues should be granted @@ -733,14 +925,114 @@ exports[`MTVJudgeDisposition with dismissed disposition selected renders correct name="instructions" />
+
+
-
- - - - Which issues would you like to vacate? - - - -
- - -
-
- - -
-
@@ -1478,7 +1729,7 @@ exports[`MTVJudgeDisposition with partially_granted disposition selected renders > - Provide context and instructions on which issues should be partially_granted + Provide context and instructions on which issues should be granted diff --git a/client/test/data/queue/taskLists/index.js b/client/test/data/queue/taskLists/index.js index 08250c39455..1b99fb70dad 100644 --- a/client/test/data/queue/taskLists/index.js +++ b/client/test/data/queue/taskLists/index.js @@ -18,7 +18,7 @@ const getAmaTaskTemplate = ({ id = 1 } = {}) => ({ placed_on_hold_at: null, on_hold_duration: null, status: null, - assigned_at: formatISO(sub(new Date(), { hours: 47 })), + assigned_at: formatISO(sub(new Date(), { days: 2 })), closest_regional_office: null, assigned_to: { css_id: null, diff --git a/config/environments/development.rb b/config/environments/development.rb index d4ef038d7bb..02e591e960e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -98,6 +98,13 @@ # Notifications page eFolder link ENV["CLAIM_EVIDENCE_EFOLDER_BASE_URL"] ||= "https://vefs-claimevidence-ui-uat.stage.bip.va.gov" + ENV["PACMAN_API_SAML_TOKEN"] ||= "our-saml-token" + ENV["PACMAN_API_TOKEN_SECRET"] ||= "client-secret" + ENV["PACMAN_API_TOKEN_ALG"] ||= "HS512" + ENV["PACMAN_API_TOKEN_ISSUER"] ||= "issuer-of-our-token" + ENV["PACMAN_API_SYS_ACCOUNT"] ||= "CSS_ID_OF_OUR_ACCOUNT" + ENV["PACMAN_API_URL"] ||= "https://pacman-uat.dev.bip.va.gov/" + if ENV["WITH_TEST_EMAIL_SERVER"] config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { diff --git a/config/environments/test.rb b/config/environments/test.rb index 1e65fed84ad..89a089dabb7 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -112,4 +112,12 @@ ENV["CLAIM_EVIDENCE_EFOLDER_BASE_URL"] ||= "https://vefs-claimevidence-ui-uat.stage.bip.va.gov" ENV['TEST_VACOLS_HOST'] ||= "localhost" + + # Pacman environment variables + ENV["PACMAN_API_TOKEN_ALG"] ||= "HS512" + ENV["PACMAN_API_URL"] ||= "https://pacman-uat.dev.bip.va.gov" + ENV["PACMAN_API_SAML_TOKEN"] ||= "our-saml-token" + ENV["PACMAN_API_TOKEN_SECRET"] ||= "client-secret" + ENV["PACMAN_API_TOKEN_ISSUER"] ||= "issuer-of-our-token" + ENV["PACMAN_API_SYS_ACCOUNT"] ||= "CSS_ID_OF_OUR_ACCOUNT" end diff --git a/config/initializers/pacman.rb b/config/initializers/pacman.rb new file mode 100644 index 00000000000..481795e510d --- /dev/null +++ b/config/initializers/pacman.rb @@ -0,0 +1 @@ +PacmanService = (ApplicationController.dependencies_faked? ? Fakes::PacmanService : ExternalApi::PacmanService) diff --git a/config/routes.rb b/config/routes.rb index 5d95cf261fa..b13b2cc3cda 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,6 +80,7 @@ post 'appeals/:appeal_id/outcode', to: 'appeals#outcode' get 'appeals/:appeal_id/documents', to: 'appeals#appeal_documents' get 'appeals/:appeal_id/documents/:document_id', to: 'appeals#appeals_single_document' + get 'distributions/:distribution_id', to: 'distributions#distribution' end end end diff --git a/db/migrate/20230425144000_create_pacman_integration.rb b/db/migrate/20230425144000_create_pacman_integration.rb new file mode 100644 index 00000000000..8eda26cf944 --- /dev/null +++ b/db/migrate/20230425144000_create_pacman_integration.rb @@ -0,0 +1,54 @@ +class CreatePacmanIntegration < Caseflow::Migration + def change + create_table :vbms_communication_packages do |t| + t.string :file_number, comment: "number associated with the documents." + t.bigint :copies, default: 1 + t.string :status + t.string :comm_package_name, null: false + t.timestamps + + t.references :vbms_uploaded_document, index: true, foreign_key: { to_table: :vbms_uploaded_documents } + t.references :created_by , index: true, foreign_key: { to_table: :users } + t.references :updated_by, index: true, foreign_key: { to_table: :users } + end + + create_table :vbms_distributions do |t| + t.string :recipient_type, null: false, comment: "Must be one of [person, organization, ro-colocated, System]." + t.string :name, comment: "should only be used for non-person entity names. Not null if [recipient_type] is organization, ro-colocated, or System." + t.string :first_name, comment: "recipient's first name. If Type is [person] then it cant be null." + t.string :middle_name, comment: "recipient's middle name." + t.string :last_name, comment: "recipient's last name. If Type is [person] then it cant be null." + t.string :participant_id, comment: "recipient's participant id." + t.string :poa_code, comment: "Can't be null if [recipient_type] is ro-colocated. The recipients POA code" + t.string :claimant_station_of_jurisdiction, comment: "Can't be null if [recipient_type] is ro-colocated." + t.timestamps + + t.references :vbms_communication_package, index: true, foreign_key: { to_table: :vbms_communication_packages } + t.references :created_by , index: true, foreign_key: { to_table: :users } + t.references :updated_by, index: true, foreign_key: { to_table: :users } + + end + + create_table :vbms_distribution_destinations do |t| + t.string :destination_type, null: false, comment: "Must be 'domesticAddress', 'internationalAddress', 'militaryAddress', 'derived', 'email', or 'sms'. Cannot be 'physicalAddress'." + t.string :address_line_1, null: false, comment: "PII. If destination_type is domestic, international, or military then Must not be null." + t.string :address_line_2, comment: "PII. If treatLine2AsAddressee is [true] then must not be null" + t.string :address_line_3, comment: "PII. If treatLine3AsAddressee is [true] then must not be null" + t.string :address_line_4, comment: "PII." + t.string :address_line_5, comment: "PII." + t.string :address_line_6, comment: "PII." + t.boolean :treat_line_2_as_addressee + t.boolean :treat_line_3_as_addressee, comment: "If true, treatLine2AsAddressee must also be true" + t.string :city, comment: "PII. If type is [domestic, international, military] then Must not be null" + t.string :state, comment: "PII. Must be exactly two-letter ISO 3166-2 code. If destination_type is domestic or military then Must not be null" + t.string :postal_code + t.string :country_name + t.string :country_code,comment: "Must be exactly two-letter ISO 3166 code." + t.timestamps + + t.references :vbms_distribution, index: true, foreign_key: { to_table: :vbms_distributions } + t.references :created_by, index: true, foreign_key: { to_table: :users } + t.references :updated_by, index: true, foreign_key: { to_table: :users } + end + end +end diff --git a/db/migrate/20230627203547_add_uuid_to_pacman_tables.rb b/db/migrate/20230627203547_add_uuid_to_pacman_tables.rb new file mode 100644 index 00000000000..743876eeb23 --- /dev/null +++ b/db/migrate/20230627203547_add_uuid_to_pacman_tables.rb @@ -0,0 +1,18 @@ +class AddUuidToPacmanTables < Caseflow::Migration + def up + add_column :vbms_communication_packages, + :uuid, + :string, + comment: "UUID of the communication package in Package Manager (Pacman)" + + add_column :vbms_distributions, + :uuid, + :string, + comment: "UUID of the distrubtion in Package Manager (Pacman)" + end + + def down + remove_column :vbms_communication_packages, :uuid + remove_column :vbms_distributions, :uuid + end +end diff --git a/db/migrate/20230629172100_add_doc_reference_and_series_ids_to_decision_documents.rb b/db/migrate/20230629172100_add_doc_reference_and_series_ids_to_decision_documents.rb new file mode 100644 index 00000000000..b2d0f285f2b --- /dev/null +++ b/db/migrate/20230629172100_add_doc_reference_and_series_ids_to_decision_documents.rb @@ -0,0 +1,27 @@ +# Adds columns to the decision_documents table to retain the +# documentVersionReferenceId and documentSeriesReferenceId values that are +# returned once a document is uploaded to VBMS eFolder. +# +# These values can be used to refer to documents and +# update documents via the eFolder API. +# + +class AddDocReferenceAndSeriesIdsToDecisionDocuments < Caseflow::Migration + def up + add_column :decision_documents, + :document_version_reference_id, + :string, + comment: "UUID that is provided by eFolder that represents the specific version of the document." + + add_column :decision_documents, + :document_series_reference_id, + :string, + comment: "UUID that is provided by eFolder that represents the group of documents" \ + "this document belongs to. Think of a series as a stack of versions." + end + + def down + remove_column :decision_documents, :document_version_reference_id + remove_column :decision_documents, :document_series_reference_id + end +end diff --git a/db/migrate/20230629183146_add_polymorphic_document_association_to_comm_package_table.rb b/db/migrate/20230629183146_add_polymorphic_document_association_to_comm_package_table.rb new file mode 100644 index 00000000000..bb14cc52ea1 --- /dev/null +++ b/db/migrate/20230629183146_add_polymorphic_document_association_to_comm_package_table.rb @@ -0,0 +1,16 @@ +class AddPolymorphicDocumentAssociationToCommPackageTable < ActiveRecord::Migration[5.2] + def change + remove_index :vbms_communication_packages, :vbms_uploaded_document_id + + add_reference :vbms_communication_packages, :document_mailable_via_pacman, polymorphic: true, index: false + + VbmsCommunicationPackage.find_each do |vcp| + unless vcp.vbms_uploaded_document_id.nil? + vcp.update_attribute(:document_mailable_via_pacman_type, "VbmsUploadedDocument") + vcp.document_mailable_via_pacman_id = vcp.vbms_uploaded_document_id + end + end + + safety_assured { remove_column :vbms_communication_packages, :vbms_uploaded_document_id } + end +end diff --git a/db/migrate/20230629184615_add_index_to_polymorphic_document_association_in_comm_package_table.rb b/db/migrate/20230629184615_add_index_to_polymorphic_document_association_in_comm_package_table.rb new file mode 100644 index 00000000000..ca7140946ae --- /dev/null +++ b/db/migrate/20230629184615_add_index_to_polymorphic_document_association_in_comm_package_table.rb @@ -0,0 +1,10 @@ +class AddIndexToPolymorphicDocumentAssociationInCommPackageTable < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + add_index :vbms_communication_packages, + [:document_mailable_via_pacman_type, :document_mailable_via_pacman_id], + name: "index_vbms_communication_packages_on_pacman_document_id", + algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index e8506087e4e..c2b16e78ef4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_05_23_174750) do +ActiveRecord::Schema.define(version: 2023_06_29_184615) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -571,6 +571,8 @@ t.string "citation_number", null: false, comment: "Unique identifier for decision document" t.datetime "created_at", null: false t.date "decision_date", null: false + t.string "document_series_reference_id", comment: "UUID that is provided by eFolder that represents the group of documentsthis document belongs to. Think of a series as a stack of versions." + t.string "document_version_reference_id", comment: "UUID that is provided by eFolder that represents the specific version of the document." t.string "error", comment: "Message captured from a failed attempt" t.datetime "last_submitted_at", comment: "When the job is eligible to run (can be reset to restart the job)" t.datetime "processed_at", comment: "When the job has concluded" @@ -1816,6 +1818,68 @@ t.index ["updated_at"], name: "index_users_on_updated_at" end + create_table "vbms_communication_packages", force: :cascade do |t| + t.string "comm_package_name", null: false + t.bigint "copies", default: 1 + t.datetime "created_at", null: false + t.bigint "created_by_id" + t.bigint "document_mailable_via_pacman_id" + t.string "document_mailable_via_pacman_type" + t.string "file_number", comment: "number associated with the documents." + t.string "status" + t.datetime "updated_at", null: false + t.bigint "updated_by_id" + t.string "uuid", comment: "UUID of the communication package in Package Manager (Pacman)" + t.index ["created_by_id"], name: "index_vbms_communication_packages_on_created_by_id" + t.index ["document_mailable_via_pacman_type", "document_mailable_via_pacman_id"], name: "index_vbms_communication_packages_on_pacman_document_id" + t.index ["updated_by_id"], name: "index_vbms_communication_packages_on_updated_by_id" + end + + create_table "vbms_distribution_destinations", force: :cascade do |t| + t.string "address_line_1", null: false, comment: "PII. If destination_type is domestic, international, or military then Must not be null." + t.string "address_line_2", comment: "PII. If treatLine2AsAddressee is [true] then must not be null" + t.string "address_line_3", comment: "PII. If treatLine3AsAddressee is [true] then must not be null" + t.string "address_line_4", comment: "PII." + t.string "address_line_5", comment: "PII." + t.string "address_line_6", comment: "PII." + t.string "city", comment: "PII. If type is [domestic, international, military] then Must not be null" + t.string "country_code", comment: "Must be exactly two-letter ISO 3166 code." + t.string "country_name" + t.datetime "created_at", null: false + t.bigint "created_by_id" + t.string "destination_type", null: false, comment: "Must be 'domesticAddress', 'internationalAddress', 'militaryAddress', 'derived', 'email', or 'sms'. Cannot be 'physicalAddress'." + t.string "postal_code" + t.string "state", comment: "PII. Must be exactly two-letter ISO 3166-2 code. If destination_type is domestic or military then Must not be null" + t.boolean "treat_line_2_as_addressee" + t.boolean "treat_line_3_as_addressee", comment: "If true, treatLine2AsAddressee must also be true" + t.datetime "updated_at", null: false + t.bigint "updated_by_id" + t.bigint "vbms_distribution_id" + t.index ["created_by_id"], name: "index_vbms_distribution_destinations_on_created_by_id" + t.index ["updated_by_id"], name: "index_vbms_distribution_destinations_on_updated_by_id" + t.index ["vbms_distribution_id"], name: "index_vbms_distribution_destinations_on_vbms_distribution_id" + end + + create_table "vbms_distributions", force: :cascade do |t| + t.string "claimant_station_of_jurisdiction", comment: "Can't be null if [recipient_type] is ro-colocated." + t.datetime "created_at", null: false + t.bigint "created_by_id" + t.string "first_name", comment: "recipient's first name. If Type is [person] then it cant be null." + t.string "last_name", comment: "recipient's last name. If Type is [person] then it cant be null." + t.string "middle_name", comment: "recipient's middle name." + t.string "name", comment: "should only be used for non-person entity names. Not null if [recipient_type] is organization, ro-colocated, or System." + t.string "participant_id", comment: "recipient's participant id." + t.string "poa_code", comment: "Can't be null if [recipient_type] is ro-colocated. The recipients POA code" + t.string "recipient_type", null: false, comment: "Must be one of [person, organization, ro-colocated, System]." + t.datetime "updated_at", null: false + t.bigint "updated_by_id" + t.string "uuid", comment: "UUID of the distrubtion in Package Manager (Pacman)" + t.bigint "vbms_communication_package_id" + t.index ["created_by_id"], name: "index_vbms_distributions_on_created_by_id" + t.index ["updated_by_id"], name: "index_vbms_distributions_on_updated_by_id" + t.index ["vbms_communication_package_id"], name: "index_vbms_distributions_on_vbms_communication_package_id" + end + create_table "vbms_uploaded_documents", force: :cascade do |t| t.bigint "appeal_id", comment: "Appeal/LegacyAppeal ID; use as FK to appeals/legacy_appeals" t.string "appeal_type", comment: "'Appeal' or 'LegacyAppeal'" @@ -2098,6 +2162,14 @@ add_foreign_key "unrecognized_appellants", "users", column: "created_by_id" add_foreign_key "user_quotas", "team_quotas" add_foreign_key "user_quotas", "users" + add_foreign_key "vbms_communication_packages", "users", column: "created_by_id" + add_foreign_key "vbms_communication_packages", "users", column: "updated_by_id" + add_foreign_key "vbms_distribution_destinations", "users", column: "created_by_id" + add_foreign_key "vbms_distribution_destinations", "users", column: "updated_by_id" + add_foreign_key "vbms_distribution_destinations", "vbms_distributions" + add_foreign_key "vbms_distributions", "users", column: "created_by_id" + add_foreign_key "vbms_distributions", "users", column: "updated_by_id" + add_foreign_key "vbms_distributions", "vbms_communication_packages" add_foreign_key "virtual_hearing_establishments", "virtual_hearings" add_foreign_key "virtual_hearings", "users", column: "created_by_id" add_foreign_key "virtual_hearings", "users", column: "updated_by_id" diff --git a/db/scripts/audit/add_row_to_appeal_states_audit_table_function.rb b/db/scripts/audit/add_row_to_appeal_states_audit_table_function.rb deleted file mode 100644 index ec98d929ba0..00000000000 --- a/db/scripts/audit/add_row_to_appeal_states_audit_table_function.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require "pg" - -conn = CaseflowRecord.connection -conn.execute( - "create or replace function caseflow_audit.add_row_to_appeal_states_audit() returns trigger - as - $appeal_states_audit$ - begin - if (TG_OP = 'DELETE') then - insert into caseflow_audit.appeal_states_audit select nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), 'D', OLD.*; - elsif (TG_OP = 'UPDATE') then - insert into caseflow_audit.appeal_states_audit select nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), 'U', NEW.*; - elsif (TG_OP = 'INSERT') then - insert into caseflow_audit.appeal_states_audit select nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), 'I', NEW.*; - end if; - return null; - end; - $appeal_states_audit$ - language plpgsql;" -) diff --git a/db/scripts/audit/add_row_to_appeal_states_audit_table_function.sql b/db/scripts/audit/add_row_to_appeal_states_audit_table_function.sql deleted file mode 100644 index d3cb5a534d5..00000000000 --- a/db/scripts/audit/add_row_to_appeal_states_audit_table_function.sql +++ /dev/null @@ -1,15 +0,0 @@ -create or replace function caseflow_audit.add_row_to_appeal_states_audit() returns trigger -as -$appeal_states_audit$ -begin - if (TG_OP = 'DELETE') then - insert into caseflow_audit.appeal_states_audit select nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), 'D', OLD.*; - elsif (TG_OP = 'UPDATE') then - insert into caseflow_audit.appeal_states_audit select nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), 'U', NEW.*; - elsif (TG_OP = 'INSERT') then - insert into caseflow_audit.appeal_states_audit select nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), 'I', NEW.*; - end if; - return null; -end; -$appeal_states_audit$ -language plpgsql; \ No newline at end of file diff --git a/db/scripts/audit/create_caseflow_audit_schema.rb b/db/scripts/audit/create_caseflow_audit_schema.rb index 034de127e11..fc70b54e1a8 100644 --- a/db/scripts/audit/create_caseflow_audit_schema.rb +++ b/db/scripts/audit/create_caseflow_audit_schema.rb @@ -4,3 +4,4 @@ conn = CaseflowRecord.connection conn.execute("create schema caseflow_audit;") +conn.close diff --git a/db/scripts/audit/functions/add_row_to_appeal_states_audit_table_function.rb b/db/scripts/audit/functions/add_row_to_appeal_states_audit_table_function.rb new file mode 100644 index 00000000000..d729b2a7111 --- /dev/null +++ b/db/scripts/audit/functions/add_row_to_appeal_states_audit_table_function.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute( + "create or replace function caseflow_audit.add_row_to_appeal_states_audit() returns trigger + as + $add_row$ + begin + if (TG_OP = 'DELETE') then + insert into caseflow_audit.appeal_states_audit + select + nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), + 'D', + OLD.id, + OLD.appeal_cancelled, + OLD.appeal_docketed, + OLD.appeal_id, + OLD.appeal_type, + OLD.created_at, + OLD.created_by_id, + OLD.decision_mailed, + OLD.hearing_postponed, + OLD.hearing_scheduled, + OLD.hearing_withdrawn, + OLD.privacy_act_complete, + OLD.privacy_act_pending, + OLD.scheduled_in_error, + OLD.updated_at, + OLD.updated_by_id, + OLD.vso_ihp_complete, + OLD.vso_ihp_pending; + elsif (TG_OP = 'UPDATE') then + insert into caseflow_audit.appeal_states_audit + select + nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), + 'U', + NEW.id, + NEW.appeal_cancelled, + NEW.appeal_docketed, + NEW.appeal_id, + NEW.appeal_type, + NEW.created_at, + NEW.created_by_id, + NEW.decision_mailed, + NEW.hearing_postponed, + NEW.hearing_scheduled, + NEW.hearing_withdrawn, + NEW.privacy_act_complete, + NEW.privacy_act_pending, + NEW.scheduled_in_error, + NEW.updated_at, + NEW.updated_by_id, + NEW.vso_ihp_complete, + NEW.vso_ihp_pending; + elsif (TG_OP = 'INSERT') then + insert into caseflow_audit.appeal_states_audit + select + nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), + 'I', + NEW.id, + NEW.appeal_cancelled, + NEW.appeal_docketed, + NEW.appeal_id, + NEW.appeal_type, + NEW.created_at, + NEW.created_by_id, + NEW.decision_mailed, + NEW.hearing_postponed, + NEW.hearing_scheduled, + NEW.hearing_withdrawn, + NEW.privacy_act_complete, + NEW.privacy_act_pending, + NEW.scheduled_in_error, + NEW.updated_at, + NEW.updated_by_id, + NEW.vso_ihp_complete, + NEW.vso_ihp_pending; + end if; + return null; + end; + $add_row$ + language plpgsql;" +) +conn.close diff --git a/db/scripts/audit/functions/add_row_to_appeal_states_audit_table_function.sql b/db/scripts/audit/functions/add_row_to_appeal_states_audit_table_function.sql new file mode 100644 index 00000000000..22c40f0218d --- /dev/null +++ b/db/scripts/audit/functions/add_row_to_appeal_states_audit_table_function.sql @@ -0,0 +1,78 @@ +create or replace function caseflow_audit.add_row_to_appeal_states_audit() returns trigger +as +$add_row$ +begin + if (TG_OP = 'DELETE') then + insert into caseflow_audit.appeal_states_audit + select + nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), + 'D', + OLD.id, + OLD.appeal_cancelled, + OLD.appeal_docketed, + OLD.appeal_id, + OLD.appeal_type, + OLD.created_at, + OLD.created_by_id, + OLD.decision_mailed, + OLD.hearing_postponed, + OLD.hearing_scheduled, + OLD.hearing_withdrawn, + OLD.privacy_act_complete, + OLD.privacy_act_pending, + OLD.scheduled_in_error, + OLD.updated_at, + OLD.updated_by_id, + OLD.vso_ihp_complete, + OLD.vso_ihp_pending; + elsif (TG_OP = 'UPDATE') then + insert into caseflow_audit.appeal_states_audit + select + nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), + 'U', + NEW.id, + NEW.appeal_cancelled, + NEW.appeal_docketed, + NEW.appeal_id, + NEW.appeal_type, + NEW.created_at, + NEW.created_by_id, + NEW.decision_mailed, + NEW.hearing_postponed, + NEW.hearing_scheduled, + NEW.hearing_withdrawn, + NEW.privacy_act_complete, + NEW.privacy_act_pending, + NEW.scheduled_in_error, + NEW.updated_at, + NEW.updated_by_id, + NEW.vso_ihp_complete, + NEW.vso_ihp_pending; + elsif (TG_OP = 'INSERT') then + insert into caseflow_audit.appeal_states_audit + select + nextval('caseflow_audit.appeal_states_audit_id_seq'::regclass), + 'I', + NEW.id, + NEW.appeal_cancelled, + NEW.appeal_docketed, + NEW.appeal_id, + NEW.appeal_type, + NEW.created_at, + NEW.created_by_id, + NEW.decision_mailed, + NEW.hearing_postponed, + NEW.hearing_scheduled, + NEW.hearing_withdrawn, + NEW.privacy_act_complete, + NEW.privacy_act_pending, + NEW.scheduled_in_error, + NEW.updated_at, + NEW.updated_by_id, + NEW.vso_ihp_complete, + NEW.vso_ihp_pending; + end if; + return null; +end; +$add_row$ +language plpgsql; diff --git a/db/scripts/audit/functions/add_row_to_vbms_communication_packages_audit_table_function.rb b/db/scripts/audit/functions/add_row_to_vbms_communication_packages_audit_table_function.rb new file mode 100644 index 00000000000..8a0b5105fa6 --- /dev/null +++ b/db/scripts/audit/functions/add_row_to_vbms_communication_packages_audit_table_function.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute( + "create or replace function caseflow_audit.add_row_to_vbms_communication_packages_audit() returns trigger + as + $add_row$ + begin + if (TG_OP = 'DELETE') then + insert into caseflow_audit.vbms_communication_packages_audit + select + nextval('caseflow_audit.vbms_communication_packages_audit_id_seq'::regclass), + 'D', + OLD.id, + OLD.file_number, + OLD.copies, + OLD.status, + OLD.comm_package_name, + OLD.created_at, + OLD.updated_at, + OLD.document_mailable_via_pacman_id, + OLD.document_mailable_via_pacman_type, + OLD.created_by_id, + OLD.updated_by_id, + OLD.uuid; + elsif (TG_OP = 'UPDATE') then + insert into caseflow_audit.vbms_communication_packages_audit + select + nextval('caseflow_audit.vbms_communication_packages_audit_id_seq'::regclass), + 'U', + NEW.id, + NEW.file_number, + NEW.copies, + NEW.status, + NEW.comm_package_name, + NEW.created_at, + NEW.updated_at, + NEW.document_mailable_via_pacman_id, + NEW.document_mailable_via_pacman_type, + NEW.created_by_id, + NEW.updated_by_id, + NEW.uuid; + elsif (TG_OP = 'INSERT') then + insert into caseflow_audit.vbms_communication_packages_audit + select + nextval('caseflow_audit.vbms_communication_packages_audit_id_seq'::regclass), + 'I', + NEW.id, + NEW.file_number, + NEW.copies, + NEW.status, + NEW.comm_package_name, + NEW.created_at, + NEW.updated_at, + NEW.document_mailable_via_pacman_id, + NEW.document_mailable_via_pacman_type, + NEW.created_by_id, + NEW.updated_by_id, + NEW.uuid; + end if; + return null; + end; + $add_row$ + language plpgsql;" +) +conn.close diff --git a/db/scripts/audit/functions/add_row_to_vbms_communication_packages_audit_table_function.sql b/db/scripts/audit/functions/add_row_to_vbms_communication_packages_audit_table_function.sql new file mode 100644 index 00000000000..fd8841b5bf9 --- /dev/null +++ b/db/scripts/audit/functions/add_row_to_vbms_communication_packages_audit_table_function.sql @@ -0,0 +1,60 @@ +create or replace function caseflow_audit.add_row_to_vbms_communication_packages_audit() returns trigger +as +$add_row$ +begin + if (TG_OP = 'DELETE') then + insert into caseflow_audit.vbms_communication_packages_audit + select + nextval('caseflow_audit.vbms_communication_packages_audit_id_seq'::regclass), + 'D', + OLD.id, + OLD.file_number, + OLD.copies, + OLD.status, + OLD.comm_package_name, + OLD.created_at, + OLD.updated_at, + OLD.document_mailable_via_pacman_id, + OLD.document_mailable_via_pacman_type, + OLD.created_by_id, + OLD.updated_by_id, + OLD.uuid; + elsif (TG_OP = 'UPDATE') then + insert into caseflow_audit.vbms_communication_packages_audit + select + nextval('caseflow_audit.vbms_communication_packages_audit_id_seq'::regclass), + 'U', + NEW.id, + NEW.file_number, + NEW.copies, + NEW.status, + NEW.comm_package_name, + NEW.created_at, + NEW.updated_at, + NEW.document_mailable_via_pacman_id, + NEW.document_mailable_via_pacman_type, + NEW.created_by_id, + NEW.updated_by_id, + NEW.uuid; + elsif (TG_OP = 'INSERT') then + insert into caseflow_audit.vbms_communication_packages_audit + select + nextval('caseflow_audit.vbms_communication_packages_audit_id_seq'::regclass), + 'I', + NEW.id, + NEW.file_number, + NEW.copies, + NEW.status, + NEW.comm_package_name, + NEW.created_at, + NEW.updated_at, + NEW.document_mailable_via_pacman_id, + NEW.document_mailable_via_pacman_type, + NEW.created_by_id, + NEW.updated_by_id, + NEW.uuid; + end if; + return null; +end; +$add_row$ +language plpgsql; diff --git a/db/scripts/audit/functions/add_row_to_vbms_distribution_destinations_audit_table_function.rb b/db/scripts/audit/functions/add_row_to_vbms_distribution_destinations_audit_table_function.rb new file mode 100644 index 00000000000..b84153084ef --- /dev/null +++ b/db/scripts/audit/functions/add_row_to_vbms_distribution_destinations_audit_table_function.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute( + "create or replace function caseflow_audit.add_row_to_vbms_distribution_destinations_audit() returns trigger + as + $add_row$ + begin + if (TG_OP = 'DELETE') then + insert into caseflow_audit.vbms_distribution_destinations_audit + select + nextval('caseflow_audit.vbms_distribution_destinations_audit_id_seq'::regclass), + 'D', + OLD.id, + OLD.destination_type, + OLD.address_line_1, + OLD.address_line_2, + OLD.address_line_3, + OLD.address_line_4, + OLD.address_line_5, + OLD.address_line_6, + OLD.treat_line_2_as_addressee, + OLD.treat_line_3_as_addressee, + OLD.city, + OLD.state, + OLD.postal_code, + OLD.country_name, + OLD.country_code, + OLD.created_at, + OLD.updated_at, + OLD.vbms_distribution_id, + OLD.created_by_id, + OLD.updated_by_id; + elsif (TG_OP = 'UPDATE') then + insert into caseflow_audit.vbms_distribution_destinations_audit + select + nextval('caseflow_audit.vbms_distribution_destinations_audit_id_seq'::regclass), + 'U', + NEW.id, + NEW.destination_type, + NEW.address_line_1, + NEW.address_line_2, + NEW.address_line_3, + NEW.address_line_4, + NEW.address_line_5, + NEW.address_line_6, + NEW.treat_line_2_as_addressee, + NEW.treat_line_3_as_addressee, + NEW.city, + NEW.state, + NEW.postal_code, + NEW.country_name, + NEW.country_code, + NEW.created_at, + NEW.updated_at, + NEW.vbms_distribution_id, + NEW.created_by_id, + NEW.updated_by_id; + elsif (TG_OP = 'INSERT') then + insert into caseflow_audit.vbms_distribution_destinations_audit + select + nextval('caseflow_audit.vbms_distribution_destinations_audit_id_seq'::regclass), + 'I', + NEW.id, + NEW.destination_type, + NEW.address_line_1, + NEW.address_line_2, + NEW.address_line_3, + NEW.address_line_4, + NEW.address_line_5, + NEW.address_line_6, + NEW.treat_line_2_as_addressee, + NEW.treat_line_3_as_addressee, + NEW.city, + NEW.state, + NEW.postal_code, + NEW.country_name, + NEW.country_code, + NEW.created_at, + NEW.updated_at, + NEW.vbms_distribution_id, + NEW.created_by_id, + NEW.updated_by_id; + end if; + return null; + end; + $add_row$ + language plpgsql;" +) +conn.close diff --git a/db/scripts/audit/functions/add_row_to_vbms_distribution_destinations_audit_table_function.sql b/db/scripts/audit/functions/add_row_to_vbms_distribution_destinations_audit_table_function.sql new file mode 100644 index 00000000000..6c8f9b1aa9e --- /dev/null +++ b/db/scripts/audit/functions/add_row_to_vbms_distribution_destinations_audit_table_function.sql @@ -0,0 +1,84 @@ +create or replace function caseflow_audit.add_row_to_vbms_distribution_destinations_audit() returns trigger +as +$add_row$ +begin + if (TG_OP = 'DELETE') then + insert into caseflow_audit.vbms_distribution_destinations_audit + select + nextval('caseflow_audit.vbms_distribution_destinations_audit_id_seq'::regclass), + 'D', + OLD.id, + OLD.destination_type, + OLD.address_line_1, + OLD.address_line_2, + OLD.address_line_3, + OLD.address_line_4, + OLD.address_line_5, + OLD.address_line_6, + OLD.treat_line_2_as_addressee, + OLD.treat_line_3_as_addressee, + OLD.city, + OLD.state, + OLD.postal_code, + OLD.country_name, + OLD.country_code, + OLD.created_at, + OLD.updated_at, + OLD.vbms_distribution_id, + OLD.created_by_id, + OLD.updated_by_id; + elsif (TG_OP = 'UPDATE') then + insert into caseflow_audit.vbms_distribution_destinations_audit + select + nextval('caseflow_audit.vbms_distribution_destinations_audit_id_seq'::regclass), + 'U', + NEW.id, + NEW.destination_type, + NEW.address_line_1, + NEW.address_line_2, + NEW.address_line_3, + NEW.address_line_4, + NEW.address_line_5, + NEW.address_line_6, + NEW.treat_line_2_as_addressee, + NEW.treat_line_3_as_addressee, + NEW.city, + NEW.state, + NEW.postal_code, + NEW.country_name, + NEW.country_code, + NEW.created_at, + NEW.updated_at, + NEW.vbms_distribution_id, + NEW.created_by_id, + NEW.updated_by_id; + elsif (TG_OP = 'INSERT') then + insert into caseflow_audit.vbms_distribution_destinations_audit + select + nextval('caseflow_audit.vbms_distribution_destinations_audit_id_seq'::regclass), + 'I', + NEW.id, + NEW.destination_type, + NEW.address_line_1, + NEW.address_line_2, + NEW.address_line_3, + NEW.address_line_4, + NEW.address_line_5, + NEW.address_line_6, + NEW.treat_line_2_as_addressee, + NEW.treat_line_3_as_addressee, + NEW.city, + NEW.state, + NEW.postal_code, + NEW.country_name, + NEW.country_code, + NEW.created_at, + NEW.updated_at, + NEW.vbms_distribution_id, + NEW.created_by_id, + NEW.updated_by_id; + end if; + return null; +end; +$add_row$ +language plpgsql; diff --git a/db/scripts/audit/functions/add_row_to_vbms_distributions_audit_table_function.rb b/db/scripts/audit/functions/add_row_to_vbms_distributions_audit_table_function.rb new file mode 100644 index 00000000000..93f1d2e4fe9 --- /dev/null +++ b/db/scripts/audit/functions/add_row_to_vbms_distributions_audit_table_function.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute( + "create or replace function caseflow_audit.add_row_to_vbms_distributions_audit() returns trigger + as + $add_row$ + begin + if (TG_OP = 'DELETE') then + insert into caseflow_audit.vbms_distributions_audit + select + nextval('caseflow_audit.vbms_distributions_audit_id_seq'::regclass), + 'D', + OLD.id, + OLD.recipient_type, + OLD.name, + OLD.first_name, + OLD.middle_name, + OLD.last_name, + OLD.participant_id, + OLD.poa_code, + OLD.claimant_station_of_jurisdiction, + OLD.created_at, + OLD.updated_at, + OLD.vbms_communication_package_id, + OLD.created_by_id, + OLD.updated_by_id, + OLD.uuid; + elsif (TG_OP = 'UPDATE') then + insert into caseflow_audit.vbms_distributions_audit + select + nextval('caseflow_audit.vbms_distributions_audit_id_seq'::regclass), + 'U', + NEW.id, + NEW.recipient_type, + NEW.name, + NEW.first_name, + NEW.middle_name, + NEW.last_name, + NEW.participant_id, + NEW.poa_code, + NEW.claimant_station_of_jurisdiction, + NEW.created_at, + NEW.updated_at, + NEW.vbms_communication_package_id, + NEW.created_by_id, + NEW.updated_by_id, + NEW.uuid; + elsif (TG_OP = 'INSERT') then + insert into caseflow_audit.vbms_distributions_audit + select + nextval('caseflow_audit.vbms_distributions_audit_id_seq'::regclass), + 'I', + NEW.id, + NEW.recipient_type, + NEW.name, + NEW.first_name, + NEW.middle_name, + NEW.last_name, + NEW.participant_id, + NEW.poa_code, + NEW.claimant_station_of_jurisdiction, + NEW.created_at, + NEW.updated_at, + NEW.vbms_communication_package_id, + NEW.created_by_id, + NEW.updated_by_id, + NEW.uuid; + end if; + return null; + end; + $add_row$ + language plpgsql;" +) +conn.close diff --git a/db/scripts/audit/functions/add_row_to_vbms_distributions_audit_table_function.sql b/db/scripts/audit/functions/add_row_to_vbms_distributions_audit_table_function.sql new file mode 100644 index 00000000000..b5b74937baf --- /dev/null +++ b/db/scripts/audit/functions/add_row_to_vbms_distributions_audit_table_function.sql @@ -0,0 +1,69 @@ +create or replace function caseflow_audit.add_row_to_vbms_distributions_audit() returns trigger +as +$add_row$ +begin + if (TG_OP = 'DELETE') then + insert into caseflow_audit.vbms_distributions_audit + select + nextval('caseflow_audit.vbms_distributions_audit_id_seq'::regclass), + 'D', + OLD.id, + OLD.recipient_type, + OLD.name, + OLD.first_name, + OLD.middle_name, + OLD.last_name, + OLD.participant_id, + OLD.poa_code, + OLD.claimant_station_of_jurisdiction, + OLD.created_at, + OLD.updated_at, + OLD.vbms_communication_package_id, + OLD.created_by_id, + OLD.updated_by_id, + OLD.uuid; + elsif (TG_OP = 'UPDATE') then + insert into caseflow_audit.vbms_distributions_audit + select + nextval('caseflow_audit.vbms_distributions_audit_id_seq'::regclass), + 'U', + NEW.id, + NEW.recipient_type, + NEW.name, + NEW.first_name, + NEW.middle_name, + NEW.last_name, + NEW.participant_id, + NEW.poa_code, + NEW.claimant_station_of_jurisdiction, + NEW.created_at, + NEW.updated_at, + NEW.vbms_communication_package_id, + NEW.created_by_id, + NEW.updated_by_id, + NEW.uuid; + elsif (TG_OP = 'INSERT') then + insert into caseflow_audit.vbms_distributions_audit + select + nextval('caseflow_audit.vbms_distributions_audit_id_seq'::regclass), + 'I', + NEW.id, + NEW.recipient_type, + NEW.name, + NEW.first_name, + NEW.middle_name, + NEW.last_name, + NEW.participant_id, + NEW.poa_code, + NEW.claimant_station_of_jurisdiction, + NEW.created_at, + NEW.updated_at, + NEW.vbms_communication_package_id, + NEW.created_by_id, + NEW.updated_by_id, + NEW.uuid; + end if; + return null; +end; +$add_row$ +language plpgsql; diff --git a/db/scripts/audit/functions/add_row_to_vbms_uploaded_documents_audit_table_function.rb b/db/scripts/audit/functions/add_row_to_vbms_uploaded_documents_audit_table_function.rb new file mode 100644 index 00000000000..1b13c9a74f4 --- /dev/null +++ b/db/scripts/audit/functions/add_row_to_vbms_uploaded_documents_audit_table_function.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute( + "create or replace function caseflow_audit.add_row_to_vbms_uploaded_documents_audit() returns trigger + as + $add_row$ + begin + if (TG_OP = 'DELETE') then + insert into caseflow_audit.vbms_uploaded_documents_audit + select + nextval('caseflow_audit.vbms_uploaded_documents_audit_id_seq'::regclass), + 'D', + OLD.id, + OLD.appeal_id, + OLD.appeal_type, + OLD.attempted_at, + OLD.canceled_at, + OLD.created_at, + OLD.document_name, + OLD.document_series_reference_id, + OLD.document_subject, + OLD.document_type, + OLD.document_version_reference_id, + OLD.error, + OLD.last_submitted_at, + OLD.processed_at, + OLD.submitted_at, + OLD.updated_at, + OLD.uploaded_to_vbms_at, + OLD.veteran_file_number; + elsif (TG_OP = 'UPDATE') then + insert into caseflow_audit.vbms_uploaded_documents_audit + select + nextval('caseflow_audit.vbms_uploaded_documents_audit_id_seq'::regclass), + 'U', + NEW.id, + NEW.appeal_id, + NEW.appeal_type, + NEW.attempted_at, + NEW.canceled_at, + NEW.created_at, + NEW.document_name, + NEW.document_series_reference_id, + NEW.document_subject, + NEW.document_type, + NEW.document_version_reference_id, + NEW.error, + NEW.last_submitted_at, + NEW.processed_at, + NEW.submitted_at, + NEW.updated_at, + NEW.uploaded_to_vbms_at, + NEW.veteran_file_number; + elsif (TG_OP = 'INSERT') then + insert into caseflow_audit.vbms_uploaded_documents_audit + select + nextval('caseflow_audit.vbms_uploaded_documents_audit_id_seq'::regclass), + 'I', + NEW.id, + NEW.appeal_id, + NEW.appeal_type, + NEW.attempted_at, + NEW.canceled_at, + NEW.created_at, + NEW.document_name, + NEW.document_series_reference_id, + NEW.document_subject, + NEW.document_type, + NEW.document_version_reference_id, + NEW.error, + NEW.last_submitted_at, + NEW.processed_at, + NEW.submitted_at, + NEW.updated_at, + NEW.uploaded_to_vbms_at, + NEW.veteran_file_number; + end if; + return null; + end; + $add_row$ + language plpgsql;" +) +conn.close diff --git a/db/scripts/audit/functions/add_row_to_vbms_uploaded_documents_audit_table_function.sql b/db/scripts/audit/functions/add_row_to_vbms_uploaded_documents_audit_table_function.sql new file mode 100644 index 00000000000..2b366270a34 --- /dev/null +++ b/db/scripts/audit/functions/add_row_to_vbms_uploaded_documents_audit_table_function.sql @@ -0,0 +1,78 @@ +create or replace function caseflow_audit.add_row_to_vbms_uploaded_documents_audit() returns trigger +as +$add_row$ +begin + if (TG_OP = 'DELETE') then + insert into caseflow_audit.vbms_uploaded_documents_audit + select + nextval('caseflow_audit.vbms_uploaded_documents_audit_id_seq'::regclass), + 'D', + OLD.id, + OLD.appeal_id, + OLD.appeal_type, + OLD.attempted_at, + OLD.canceled_at, + OLD.created_at, + OLD.document_name, + OLD.document_series_reference_id, + OLD.document_subject, + OLD.document_type, + OLD.document_version_reference_id, + OLD.error, + OLD.last_submitted_at, + OLD.processed_at, + OLD.submitted_at, + OLD.updated_at, + OLD.uploaded_to_vbms_at, + OLD.veteran_file_number; + elsif (TG_OP = 'UPDATE') then + insert into caseflow_audit.vbms_uploaded_documents_audit + select + nextval('caseflow_audit.vbms_uploaded_documents_audit_id_seq'::regclass), + 'U', + NEW.id, + NEW.appeal_id, + NEW.appeal_type, + NEW.attempted_at, + NEW.canceled_at, + NEW.created_at, + NEW.document_name, + NEW.document_series_reference_id, + NEW.document_subject, + NEW.document_type, + NEW.document_version_reference_id, + NEW.error, + NEW.last_submitted_at, + NEW.processed_at, + NEW.submitted_at, + NEW.updated_at, + NEW.uploaded_to_vbms_at, + NEW.veteran_file_number; + elsif (TG_OP = 'INSERT') then + insert into caseflow_audit.vbms_uploaded_documents_audit + select + nextval('caseflow_audit.vbms_uploaded_documents_audit_id_seq'::regclass), + 'I', + NEW.id, + NEW.appeal_id, + NEW.appeal_type, + NEW.attempted_at, + NEW.canceled_at, + NEW.created_at, + NEW.document_name, + NEW.document_series_reference_id, + NEW.document_subject, + NEW.document_type, + NEW.document_version_reference_id, + NEW.error, + NEW.last_submitted_at, + NEW.processed_at, + NEW.submitted_at, + NEW.updated_at, + NEW.uploaded_to_vbms_at, + NEW.veteran_file_number; + end if; + return null; +end; +$add_row$ +language plpgsql; diff --git a/db/scripts/audit/pacman_integration_teardown.sql b/db/scripts/audit/pacman_integration_teardown.sql new file mode 100644 index 00000000000..386008b3bb0 --- /dev/null +++ b/db/scripts/audit/pacman_integration_teardown.sql @@ -0,0 +1,12 @@ +drop trigger vbms_communication_packages_audit_trigger; +drop trigger vbms_distributions_audit_trigger; +drop trigger vbms_distribution_destinations_audit_trigger; +drop trigger vbms_uploaded_documents_audit_trigger; +drop function caseflow_audit.add_row_to_vbms_communication_packages_audit +drop function caseflow_audit.add_row_to_vbms_distributions_audit +drop function caseflow_audit.add_row_to_vbms_distribution_destinations_audit +drop function caseflow_audit.add_row_to_vbms_uploaded_documents_audit +drop table caseflow_audit.vbms_communication_packages_audit; +drop table caseflow_audit.vbms_distributions_audit; +drop table caseflow_audit.vbms_distribution_destinations_audit; +drop table caseflow_audit.vbms_uploaded_documents_audit; diff --git a/db/scripts/audit/remove_caseflow_audit_schema.rb b/db/scripts/audit/remove_caseflow_audit_schema.rb index 78df1a206aa..344617e8aa8 100644 --- a/db/scripts/audit/remove_caseflow_audit_schema.rb +++ b/db/scripts/audit/remove_caseflow_audit_schema.rb @@ -6,3 +6,4 @@ conn.execute( "drop schema IF EXISTS caseflow_audit CASCADE;" ) +conn.close diff --git a/db/scripts/audit/create_appeal_states_audit.rb b/db/scripts/audit/tables/create_appeal_states_audit.rb similarity index 98% rename from db/scripts/audit/create_appeal_states_audit.rb rename to db/scripts/audit/tables/create_appeal_states_audit.rb index b72634dabc8..6a8c61dc23a 100644 --- a/db/scripts/audit/create_appeal_states_audit.rb +++ b/db/scripts/audit/tables/create_appeal_states_audit.rb @@ -25,3 +25,4 @@ vso_ihp_complete boolean not null, vso_ihp_pending boolean not null );") +conn.close diff --git a/db/scripts/audit/create_appeal_states_audit.sql b/db/scripts/audit/tables/create_appeal_states_audit.sql similarity index 100% rename from db/scripts/audit/create_appeal_states_audit.sql rename to db/scripts/audit/tables/create_appeal_states_audit.sql diff --git a/db/scripts/audit/tables/create_vbms_communication_packages_audit.rb b/db/scripts/audit/tables/create_vbms_communication_packages_audit.rb new file mode 100644 index 00000000000..bf0b979682e --- /dev/null +++ b/db/scripts/audit/tables/create_vbms_communication_packages_audit.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute("create table caseflow_audit.vbms_communication_packages_audit ( + id BIGSERIAL PRIMARY KEY, + type_of_change CHAR(1) not null, + vbms_communication_package_id bigint not null, + file_number varchar NULL, + copies int8 NULL DEFAULT 1, + status varchar NULL, + comm_package_name varchar NOT NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + document_mailable_via_pacman_id bigint not NULL, + document_mailable_via_pacman_type varchar not NULL, + created_by_id int8 NULL, + updated_by_id int8 NULL, + uuid varchar NULL + );") +conn.close diff --git a/db/scripts/audit/tables/create_vbms_communication_packages_audit.sql b/db/scripts/audit/tables/create_vbms_communication_packages_audit.sql new file mode 100644 index 00000000000..87a778d05d1 --- /dev/null +++ b/db/scripts/audit/tables/create_vbms_communication_packages_audit.sql @@ -0,0 +1,16 @@ +create table caseflow_audit.vbms_communication_packages_audit ( + id BIGSERIAL PRIMARY KEY, + type_of_change CHAR(1) not null, + vbms_communication_package_id bigint not null, + file_number varchar NULL, + copies int8 NULL DEFAULT 1, + status varchar NULL, + comm_package_name varchar NOT NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + document_mailable_via_pacman_id bigint not NULL, + document_mailable_via_pacman_type varchar not NULL, + created_by_id int8 NULL, + updated_by_id int8 NULL, + uuid varchar NULL + ); diff --git a/db/scripts/audit/tables/create_vbms_distribution_destinations_audit.rb b/db/scripts/audit/tables/create_vbms_distribution_destinations_audit.rb new file mode 100644 index 00000000000..ec1e1e3973b --- /dev/null +++ b/db/scripts/audit/tables/create_vbms_distribution_destinations_audit.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute("create table caseflow_audit.vbms_distribution_destinations_audit ( + id BIGSERIAL PRIMARY KEY, + type_of_change CHAR(1) not null, + vbms_distribution_destinations_id bigint not null, + destination_type varchar NOT NULL, + address_line_1 varchar NOT NULL, + address_line_2 varchar NULL, + address_line_3 varchar NULL, + address_line_4 varchar NULL, + address_line_5 varchar NULL, + address_line_6 varchar NULL, + treat_line_2_as_addressee bool NULL, + treat_line_3_as_addressee bool NULL, + city varchar NULL, + state varchar NULL, + postal_code varchar NULL, + country_name varchar NULL, + country_code varchar NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + vbms_distribution_id int8 NULL, + created_by_id int8 NULL, + updated_by_id int8 NULL + );") +conn.close diff --git a/db/scripts/audit/tables/create_vbms_distribution_destinations_audit.sql b/db/scripts/audit/tables/create_vbms_distribution_destinations_audit.sql new file mode 100644 index 00000000000..bc27fea7588 --- /dev/null +++ b/db/scripts/audit/tables/create_vbms_distribution_destinations_audit.sql @@ -0,0 +1,24 @@ +create table caseflow_audit.vbms_distribution_destinations_audit ( + id BIGSERIAL PRIMARY KEY, + type_of_change CHAR(1) not null, + vbms_distribution_destinations_id bigint not null, + destination_type varchar NOT NULL, + address_line_1 varchar NOT NULL, + address_line_2 varchar NULL, + address_line_3 varchar NULL, + address_line_4 varchar NULL, + address_line_5 varchar NULL, + address_line_6 varchar NULL, + treat_line_2_as_addressee bool NULL, + treat_line_3_as_addressee bool NULL, + city varchar NULL, + state varchar NULL, + postal_code varchar NULL, + country_name varchar NULL, + country_code varchar NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + vbms_distribution_id int8 NULL, + created_by_id int8 NULL, + updated_by_id int8 NULL + ); diff --git a/db/scripts/audit/tables/create_vbms_distributions_audit.rb b/db/scripts/audit/tables/create_vbms_distributions_audit.rb new file mode 100644 index 00000000000..c8ddddc7879 --- /dev/null +++ b/db/scripts/audit/tables/create_vbms_distributions_audit.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute("create table caseflow_audit.vbms_distributions_audit ( + id BIGSERIAL PRIMARY KEY, + type_of_change CHAR(1) not null, + vbms_distributions_id bigint not null, + recipient_type varchar NOT NULL, + name varchar NULL, + first_name varchar NULL, + middle_name varchar NULL, + last_name varchar NULL, + participant_id varchar NULL, + poa_code varchar NULL, + claimant_station_of_jurisdiction varchar NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + vbms_communication_package_id int8 NULL, + created_by_id int8 NULL, + updated_by_id int8 NULL, + uuid varchar NULL + );") +conn.close diff --git a/db/scripts/audit/tables/create_vbms_distributions_audit.sql b/db/scripts/audit/tables/create_vbms_distributions_audit.sql new file mode 100644 index 00000000000..817c9ff2d73 --- /dev/null +++ b/db/scripts/audit/tables/create_vbms_distributions_audit.sql @@ -0,0 +1,19 @@ +create table caseflow_audit.vbms_distributions_audit ( + id BIGSERIAL PRIMARY KEY, + type_of_change CHAR(1) not null, + vbms_distributions_id bigint not null, + recipient_type varchar NOT NULL, + name varchar NULL, + first_name varchar NULL, + middle_name varchar NULL, + last_name varchar NULL, + participant_id varchar NULL, + poa_code varchar NULL, + claimant_station_of_jurisdiction varchar NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + vbms_communication_package_id int8 NULL, + created_by_id int8 NULL, + updated_by_id int8 NULL, + uuid varchar NULL + ); diff --git a/db/scripts/audit/tables/create_vbms_uploaded_documents_audit.rb b/db/scripts/audit/tables/create_vbms_uploaded_documents_audit.rb new file mode 100644 index 00000000000..34d53368952 --- /dev/null +++ b/db/scripts/audit/tables/create_vbms_uploaded_documents_audit.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute("create table caseflow_audit.vbms_uploaded_documents_audit ( + id BIGSERIAL PRIMARY KEY, + type_of_change CHAR(1) not null, + vbms_uploaded_documents_id bigint not null, + appeal_id int8 NULL, + appeal_type varchar NULL, + attempted_at timestamp NULL, + canceled_at timestamp NULL, + created_at timestamp NOT NULL, + document_name varchar NULL, + document_series_reference_id varchar NULL, + document_subject varchar NULL, + document_type varchar NOT NULL, + document_version_reference_id varchar NULL, + error varchar NULL, + last_submitted_at timestamp NULL, + processed_at timestamp NULL, + submitted_at timestamp NULL, + updated_at timestamp NOT NULL, + uploaded_to_vbms_at timestamp NULL, + veteran_file_number varchar NULL + );") +conn.close diff --git a/db/scripts/audit/tables/create_vbms_uploaded_documents_audit.sql b/db/scripts/audit/tables/create_vbms_uploaded_documents_audit.sql new file mode 100644 index 00000000000..8c467e9851d --- /dev/null +++ b/db/scripts/audit/tables/create_vbms_uploaded_documents_audit.sql @@ -0,0 +1,22 @@ +create table caseflow_audit.vbms_uploaded_documents_audit ( + id BIGSERIAL PRIMARY KEY, + type_of_change CHAR(1) not null, + vbms_uploaded_documents_id bigint not null, + appeal_id int8 NULL, + appeal_type varchar NULL, + attempted_at timestamp NULL, + canceled_at timestamp NULL, + created_at timestamp NOT NULL, + document_name varchar NULL, + document_series_reference_id varchar NULL, + document_subject varchar NULL, + document_type varchar NOT NULL, + document_version_reference_id varchar NULL, + error varchar NULL, + last_submitted_at timestamp NULL, + processed_at timestamp NULL, + submitted_at timestamp NULL, + updated_at timestamp NOT NULL, + uploaded_to_vbms_at timestamp NULL, + veteran_file_number varchar NULL + ); diff --git a/db/scripts/audit/create_appeal_states_audit_trigger.rb b/db/scripts/audit/triggers/create_appeal_states_audit_trigger.rb similarity index 96% rename from db/scripts/audit/create_appeal_states_audit_trigger.rb rename to db/scripts/audit/triggers/create_appeal_states_audit_trigger.rb index 29b9b32a5a6..08c226671ce 100644 --- a/db/scripts/audit/create_appeal_states_audit_trigger.rb +++ b/db/scripts/audit/triggers/create_appeal_states_audit_trigger.rb @@ -9,3 +9,4 @@ for each row execute procedure caseflow_audit.add_row_to_appeal_states_audit();" ) +conn.close diff --git a/db/scripts/audit/create_appeal_states_audit_trigger.sql b/db/scripts/audit/triggers/create_appeal_states_audit_trigger.sql similarity index 100% rename from db/scripts/audit/create_appeal_states_audit_trigger.sql rename to db/scripts/audit/triggers/create_appeal_states_audit_trigger.sql diff --git a/db/scripts/audit/triggers/create_vbms_communication_packages_audit_trigger.rb b/db/scripts/audit/triggers/create_vbms_communication_packages_audit_trigger.rb new file mode 100644 index 00000000000..fee24fae47b --- /dev/null +++ b/db/scripts/audit/triggers/create_vbms_communication_packages_audit_trigger.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute( + "create trigger vbms_communication_packages_audit_trigger + after insert or update or delete on public.vbms_communication_packages + for each row + execute procedure caseflow_audit.add_row_to_vbms_communication_packages_audit();" +) +conn.close diff --git a/db/scripts/audit/triggers/create_vbms_communication_packages_audit_trigger.sql b/db/scripts/audit/triggers/create_vbms_communication_packages_audit_trigger.sql new file mode 100644 index 00000000000..6628fc6661b --- /dev/null +++ b/db/scripts/audit/triggers/create_vbms_communication_packages_audit_trigger.sql @@ -0,0 +1,4 @@ +create trigger vbms_communication_packages_audit_trigger +after insert or update or delete on public.vbms_communication_packages +for each row +execute procedure caseflow_audit.add_row_to_vbms_communication_packages_audit(); diff --git a/db/scripts/audit/triggers/create_vbms_distribution_destinations_audit_trigger.rb b/db/scripts/audit/triggers/create_vbms_distribution_destinations_audit_trigger.rb new file mode 100644 index 00000000000..d118a5e982f --- /dev/null +++ b/db/scripts/audit/triggers/create_vbms_distribution_destinations_audit_trigger.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute( + "create trigger vbms_distribution_destinations_audit_trigger + after insert or update or delete on public.vbms_distribution_destinations + for each row + execute procedure caseflow_audit.add_row_to_vbms_distribution_destinations_audit();" +) +conn.close diff --git a/db/scripts/audit/triggers/create_vbms_distribution_destinations_audit_trigger.sql b/db/scripts/audit/triggers/create_vbms_distribution_destinations_audit_trigger.sql new file mode 100644 index 00000000000..506a3703b07 --- /dev/null +++ b/db/scripts/audit/triggers/create_vbms_distribution_destinations_audit_trigger.sql @@ -0,0 +1,4 @@ +create trigger vbms_distribution_destinations_audit_trigger +after insert or update or delete on public.vbms_distribution_destinations +for each row +execute procedure caseflow_audit.add_row_to_vbms_distribution_destinations_audit(); diff --git a/db/scripts/audit/triggers/create_vbms_distributions_audit_trigger.rb b/db/scripts/audit/triggers/create_vbms_distributions_audit_trigger.rb new file mode 100644 index 00000000000..77a47db6060 --- /dev/null +++ b/db/scripts/audit/triggers/create_vbms_distributions_audit_trigger.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute( + "create trigger vbms_distributions_audit_trigger + after insert or update or delete on public.vbms_distributions + for each row + execute procedure caseflow_audit.add_row_to_vbms_distributions_audit();" +) +conn.close diff --git a/db/scripts/audit/triggers/create_vbms_distributions_audit_trigger.sql b/db/scripts/audit/triggers/create_vbms_distributions_audit_trigger.sql new file mode 100644 index 00000000000..03c9f8eecd6 --- /dev/null +++ b/db/scripts/audit/triggers/create_vbms_distributions_audit_trigger.sql @@ -0,0 +1,4 @@ +create trigger vbms_distributions_audit_trigger +after insert or update or delete on public.vbms_distributions +for each row +execute procedure caseflow_audit.add_row_to_vbms_distributions_audit(); diff --git a/db/scripts/audit/triggers/create_vbms_uploaded_documents_audit_trigger.rb b/db/scripts/audit/triggers/create_vbms_uploaded_documents_audit_trigger.rb new file mode 100644 index 00000000000..6856ec44376 --- /dev/null +++ b/db/scripts/audit/triggers/create_vbms_uploaded_documents_audit_trigger.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "pg" + +conn = CaseflowRecord.connection +conn.execute( + "create trigger vbms_uploaded_documents_audit_trigger + after insert or update or delete on public.vbms_uploaded_documents + for each row + execute procedure caseflow_audit.add_row_to_vbms_uploaded_documents_audit();" +) +conn.close diff --git a/db/scripts/audit/triggers/create_vbms_uploaded_documents_audit_trigger.sql b/db/scripts/audit/triggers/create_vbms_uploaded_documents_audit_trigger.sql new file mode 100644 index 00000000000..5ae25d271e5 --- /dev/null +++ b/db/scripts/audit/triggers/create_vbms_uploaded_documents_audit_trigger.sql @@ -0,0 +1,4 @@ +create trigger vbms_uploaded_documents_audit_trigger +after insert or update or delete on public.vbms_uploaded_documents +for each row +execute procedure caseflow_audit.add_row_to_vbms_uploaded_documents_audit(); diff --git a/lib/caseflow/error.rb b/lib/caseflow/error.rb index cc884eb3af4..df77673dd3b 100644 --- a/lib/caseflow/error.rb +++ b/lib/caseflow/error.rb @@ -340,6 +340,7 @@ class MustImplementInSubclass < StandardError; end class AttributeNotLoaded < StandardError; end class VeteranNotFound < StandardError; end class AppealNotFound < StandardError; end + class MissingRecipientInfo < StandardError; end class EstablishClaimFailedInVBMS < StandardError attr_reader :error_code @@ -447,4 +448,14 @@ class VANotifyNotFoundError < VANotifyApiError; end class VANotifyInternalServerError < VANotifyApiError; end class VANotifyRateLimitError < VANotifyApiError; end class EmptyQueueError < StandardError; end + + # Pacman errors + class PacmanApiError < StandardError + include Caseflow::Error::ErrorSerializer + attr_accessor :code, :message + end + class PacmanBadRequestError < PacmanApiError; end + class PacmanForbiddenError < PacmanApiError; end + class PacmanNotFoundError < PacmanApiError; end + class PacmanInternalServerError < PacmanApiError; end end diff --git a/lib/fakes/pacman_service.rb b/lib/fakes/pacman_service.rb new file mode 100644 index 00000000000..9da83eedef7 --- /dev/null +++ b/lib/fakes/pacman_service.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +class Fakes::PacmanService < ExternalApi::PacmanService + COMMUNICATION_PACKAGE_UUID = "24eb6a66-3833-4de6-bea4-4b614e55d5ac" + DISTRIBUTION_UUID = "201cef13-49ba-4f40-8741-97d06cee0270" + + class << self + def send_communication_package_request(file_number, name, document_references) + fake_package_request(file_number, name, document_references) + end + + def send_distribution_request(package_id, recipient, destinations) + [fake_distribution_request(package_id, recipient, destinations)] + end + + def get_distribution_request(distribution_uuid) + distribution = VbmsDistribution.find_by(uuid: distribution_uuid) + + return distribution_not_found_response unless distribution + + fake_distribution_response(distribution.uuid) + end + + private + + def bad_request_response + HTTPI::Response.new( + 400, + {}, + { + "error": "BadRequestError", + "message": "participant id is not valid" + }.with_indifferent_access + ) + end + + def bad_access_response + HTTPI::Response.new( + 403, + {}, + { + "error": "BadRequestError", + "message": "package cannot be created because of insufficient privileges" + }.with_indifferent_access + ) + end + + def distribution_not_found_response + HTTPI::Response.new( + 404, + {}, + { + "error": "BadRequestError", + "message": "distribution does not exist at this time" + }.with_indifferent_access + ) + end + + # POST: /package-manager-service/communication-package + def fake_package_request(file_number, name, document_references) + HTTPI::Response.new( + 201, + {}, + { + "id" => COMMUNICATION_PACKAGE_UUID, + "fileNumber": file_number, + "name": name, + "documentReferences": document_references, + "status": "NEW", + "createDate": "" + }.with_indifferent_access + ) + end + + # POST: /package-manager-service/distribution + def fake_distribution_request(package_id, recipient, destinations) + HTTPI::Response.new( + 201, + {}, + { + "id": DISTRIBUTION_UUID, + "recipient": recipient, + "description": "bad", + "communicationPackageId": package_id, + "destinations": destinations, + "status": "", + "sentToCbcmDate": "" + }.with_indifferent_access + ) + end + + # rubocop:disable Metrics/MethodLength + # GET: /package-manager-service/distribution/{id} + def fake_distribution_response(_distribution_id) + HTTPI::Response.new( + 200, + {}, + { + "id": DISTRIBUTION_UUID, + "recipient": { + "type": "system", + "id": "a050a21e-23f6-4743-a1ff-aa1e24412eff", + "name": "VBMS-C" + }, + "description": "Staging Mailing Distribution", + "communicationPackageId": 1, + "destinations": [{ + "type": "physicalAddress", + "id": "28440040-51a5-4d2a-81a2-28730827be14", + "status": "", + "cbcmSendAttemptDate": "2022-06-06T16:35:27.996", + "addressLine1": "POSTMASTER GENERAL", + "addressLine2": "UNITED STATES POSTAL SERVICE", + "addressLine3": "475 LENFANT PLZ SW RM 10022", + "addressLine4": "SUITE 123", + "addressLine5": "APO AE 09001-5275", + "addressLine6": "", + "treatLine2AsAddressee": true, + "treatLine3AsAddressee": true, + "city": "WASHINGTON DC", + "state": "DC", + "postalCode": "12345", + "countryName": "UNITED STATES", + "countryCode": "us" + }], + "status": "", + "sentToCbcmDate": "" + }.with_indifferent_access + ) + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/lib/helpers/cancel_tasks_and_descendants.rb b/lib/helpers/cancel_tasks_and_descendants.rb new file mode 100644 index 00000000000..fc87251d0d3 --- /dev/null +++ b/lib/helpers/cancel_tasks_and_descendants.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require "securerandom" + +class CancelTasksAndDescendants + LOG_TAG = "CancelTasksAndDescendants" + + # Cancels all tasks and descendant tasks for given Task relation + # + # @param task_relation [ActiveRecord::Relation] tasks to be cancelled + # @return [NilClass] + def self.call(task_relation = Task.none) + new(task_relation).__send__(:call) + end + + private + + def initialize(task_relation) + @task_relation = task_relation + @request_id = SecureRandom.uuid + @logs = [] + end + + def call + RequestStore[:current_user] = User.system_user + + with_paper_trail_options do + log_time_elapsed { log_task_count_before_and_after { cancel_tasks } } + print_logs_to_stdout + end + end + + # @note Temporarily sets the PaperTrail request options and executes the given + # block. The request options are only in effect on the current thread for + # the duration of the block. + # This is needed so that the PaperTrail `versions` records for cancelled + # tasks reflect the appropriate `whodunnit` and `request_id`. + def with_paper_trail_options(&block) + options = { whodunnit: User.system_user.id, + controller_info: { request_id: @request_id } } + + PaperTrail.request(options, &block) + end + + def cancel_tasks + @task_relation.find_each do |task| + log_cancelled(task) { task.cancel_task_and_child_subtasks } + rescue StandardError => error + log_errored(task, error) + end + end + + def log_cancelled(task, &block) + task_ids = cancellable_descendants_for(task).pluck(:id) + yield(block) + log("Task ids #{task_ids} cancelled successfully") + end + + def log_errored(task, error) + task_ids = cancellable_descendants_for(task).pluck(:id) + log("Task ids #{task_ids} not cancelled due to error - #{error}", + level: :error) + end + + def cancellable_descendants_for(task) + # Note: The result of `Task #descendants` also includes the instance itself + Task.open.where(id: task.descendants) + end + + def log_task_count_before_and_after(&block) + initial_count = count_of_cancellable_tasks + log_total_tasks_for_cancellation(initial_count) + yield(block) + final_count = initial_count - count_of_cancellable_tasks + log_cancelled_successfully(final_count) + end + + def count_of_cancellable_tasks + sum = 0 + @task_relation.find_each do |task| + sum += cancellable_descendants_for(task).count + end + sum + end + + def log_total_tasks_for_cancellation(count) + log("Total tasks for cancellation: #{count}") + end + + def log_cancelled_successfully(count) + log("Tasks cancelled successfully: #{count}") + end + + def log_time_elapsed(&block) + time_elapsed_in_seconds = Benchmark.realtime(&block) + log("Elapsed time (sec): #{time_elapsed_in_seconds}") + end + + def log(message, level: :info) + append_to_application_logs(level, message) + append_to_logs_for_stdout(message) + end + + def append_to_application_logs(level, message) + Rails.logger.tagged(LOG_TAG, @request_id) do + Rails.logger.public_send(level, message) + end + end + + def append_to_logs_for_stdout(message) + @logs << "[#{LOG_TAG}] [#{@request_id}] #{message}" + end + + def print_logs_to_stdout + puts @logs + end +end diff --git a/lib/helpers/report_load_end_product_sync.rb b/lib/helpers/report_load_end_product_sync.rb index 145aaff7b3b..eafe06669c8 100644 --- a/lib/helpers/report_load_end_product_sync.rb +++ b/lib/helpers/report_load_end_product_sync.rb @@ -29,14 +29,14 @@ def run_by_report_load(report_load) # The next two methods are part of the APPEALS-22696 initiative to priority sync # all EPs in Caseflow with VBMS # The following method is priority syncing cleared EPs - def run_for_cleared_eps(batch_limit, env) + def run_for_cleared_eps(batch_limit) RequestStore[:current_user] = User.system_user conn = ActiveRecord::Base.connection @error_log = [] @run_log = [] - error_ids = get_error_ids(env) + error_ids = get_error_ids eps_queried = get_cleared_eps(batch_limit, error_ids, conn) eps_queried.each do |x| @@ -53,14 +53,14 @@ def run_for_cleared_eps(batch_limit, env) end # Priority sync for cancelled EPs - def run_for_cancelled_eps(batch_limit, env) + def run_for_cancelled_eps(batch_limit) RequestStore[:current_user] = User.system_user conn = ActiveRecord::Base.connection @error_log = [] @run_log = [] - error_ids = get_error_ids(env) + error_ids = get_error_ids eps_queried = get_cancelled_eps(batch_limit, error_ids, conn) eps_queried.each do |x| @@ -128,14 +128,20 @@ def call_sync_by_report_load(ep_ref, rep_load, conn) #################################################################### # Grab txt file of previously errored EP reference ids from s3 and return as an array - def get_error_ids(env) - # Set Client Resources for AWS - Aws.config.update(region: "us-gov-west-1") - s3client = Aws::S3::Client.new - key_name = "ep_establishment_workaround/#{env}/ep_priority_sync/error_ids.txt" - - filepath = s3client.get_object(bucket:'appeals-dbas', key:key_name) - filepath.body.read.gsub("\r","").split("\n").map{ |obj| obj[1...-1] } + def get_error_ids + error_txt = S3Service.fetch_content(S3_BUCKET_NAME + "/error_ids.txt") + error_txt.gsub("\r","").split("\n").map{ |obj| obj[1...-1] } + end + + # Method to log errors to S3 error_ids txt file in real time in case of + # sync failure mid batch processing + # S3 does not support appending or modifying files in any way so the current txt file + # of errors has to be pulled and stored locally, modified locally, and then re-uploaded + def realtime_log_error_to_s3(reference_id) + error_txt = S3Service.fetch_content(S3_BUCKET_NAME + "/error_ids.txt") + error_txt << "\r\n" + error_txt << '"' + "#{reference_id}"+ '"' + S3Service.store_file(S3_BUCKET_NAME + "/error_ids.txt", error_txt) end # Grab cleared EPs that are out of sync @@ -228,6 +234,7 @@ def call_priority_sync(ep_ref, conn) prev_synced_status: sync_status_before, error: error.message ) + realtime_log_error_to_s3(epe.reference_id) end end diff --git a/lib/helpers/scripts/priority_sync_cancelled_eps.sh b/lib/helpers/scripts/priority_sync_cancelled_eps.sh index 867b2a57767..a0fe67b19ad 100644 --- a/lib/helpers/scripts/priority_sync_cancelled_eps.sh +++ b/lib/helpers/scripts/priority_sync_cancelled_eps.sh @@ -1,5 +1,5 @@ #! /bin/bash cd /opt/caseflow-certification/src; bin/rails c << DONETOKEN x = WarRoom::ReportLoadEndProductSync.new -x.run_for_cancelled_eps("$1", "$2") +x.run_for_cancelled_eps("$1") DONETOKEN diff --git a/lib/helpers/scripts/priority_sync_cleared_eps.sh b/lib/helpers/scripts/priority_sync_cleared_eps.sh index 4917258ce8e..893e0ec87c9 100644 --- a/lib/helpers/scripts/priority_sync_cleared_eps.sh +++ b/lib/helpers/scripts/priority_sync_cleared_eps.sh @@ -1,5 +1,5 @@ #! /bin/bash cd /opt/caseflow-certification/src; bin/rails c << DONETOKEN x = WarRoom::ReportLoadEndProductSync.new -x.run_for_cleared_eps("$1", "$2") +x.run_for_cleared_eps("$1") DONETOKEN diff --git a/lib/tasks/remediations/cancel_vrr_tasks_open_for_vre.rake b/lib/tasks/remediations/cancel_vrr_tasks_open_for_vre.rake new file mode 100644 index 00000000000..b76b86a6dd5 --- /dev/null +++ b/lib/tasks/remediations/cancel_vrr_tasks_open_for_vre.rake @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "../../../lib/helpers/cancel_tasks_and_descendants" + +namespace :remediations do + desc "Cancel VeteranRecordRequest tasks that are both open and assigned to " \ + "the 'Veterans Readiness and Employment' business line" + task cancel_vrr_tasks_open_for_vre: [:environment] do + CancelTasksAndDescendants.call( + VeteranRecordRequestsOpenForVREQuery.call + ) + end +end diff --git a/scripts/dev_env_setup_step1.sh b/scripts/dev_env_setup_step1.sh index 57259bf0bc6..e29b26b96da 100755 --- a/scripts/dev_env_setup_step1.sh +++ b/scripts/dev_env_setup_step1.sh @@ -98,13 +98,10 @@ echo "1. Run Docker and go into advanced preferences to limit Docker's resources Recommended settings are 4 CPUs, 8 GiB of internal memory, and 512 MiB of swap. " -echo "2. In a new terminal, run: - docker login -u dsvaappeals - The password is in the DSVA 1Password account. - Note you can use your personal account as well, you'll just have to accept - the license agreement for the Oracle Database docker image. - https://store.docker.com/images/oracle-database-enterprise-edition - To accept the agreement, checkout with the Oracle image on the docker store. +echo "2. To install the latest and enterprise Oracle Database version follow (https://seanstacey.org/deploying-an-oracle-database-19c-as-a-docker-container/2020/09/) guide. + 1. Go to http://container-registry.oracle.com/ (Here log in and opt for Database) + 2. On command line docker login container-registry.oracle.com + 3. On command line docker pull container-registry.oracle.com/database/enterprise:latest " echo "==> Close this terminal, open a new terminal, and run ./dev_env_setup_step2.sh diff --git a/scripts/dev_env_setup_step2.sh b/scripts/dev_env_setup_step2.sh index bbbce1ddc84..7df7a9dab77 100755 --- a/scripts/dev_env_setup_step2.sh +++ b/scripts/dev_env_setup_step2.sh @@ -46,11 +46,7 @@ ln -s Makefile.example Makefile echo " =================================== -You must do the following manually: - -AWS access is needed starting at this point. -If you need to get AWS access, follow these instructions: - https://github.com/department-of-veterans-affairs/appeals-deployment/wiki/New-Hires +Congratulations " echo 'Finish the manual set up from "Database environment setup": diff --git a/scripts/enable_features_dev.rb b/scripts/enable_features_dev.rb index 5a0a2ddf6c3..f6053eedb20 100644 --- a/scripts/enable_features_dev.rb +++ b/scripts/enable_features_dev.rb @@ -54,11 +54,12 @@ def call # - they make significantly drastic changes in Dev/Demo compared to Production # - the work around the feature has been paused # - the flag is only being used to disable functionality -disabled_flags = [ - "legacy_das_deprecation", - "cavc_dashboard_workflow", - "poa_auto_refresh", - "interface_version_2" +disabled_flags = %w[ + legacy_das_deprecation + cavc_dashboard_workflow + poa_auto_refresh + interface_version_2 + cc_vacatur_visibility ] all_features = AllFeatureToggles.new.call.flatten.uniq diff --git a/spec/controllers/appeals_controller_spec.rb b/spec/controllers/appeals_controller_spec.rb index d8c685e64b9..616680c5549 100644 --- a/spec/controllers/appeals_controller_spec.rb +++ b/spec/controllers/appeals_controller_spec.rb @@ -218,11 +218,8 @@ end context "when request header contains nonexistent Veteran file number" do - it "returns 404 error", skip: "flake" do - appeal = create(:appeal, claimants: [build(:claimant, participant_id: "CLAIMANT_WITH_PVA_AS_VSO")]) - create(:supplemental_claim, veteran_file_number: appeal.veteran_file_number) - - request.headers["HTTP_CASE_SEARCH"] = "123" + it "returns 404 error" do + request.headers["HTTP_CASE_SEARCH"] = "123456789" expect_any_instance_of(Fakes::BGSService).to_not receive(:fetch_poas_by_participant_id) diff --git a/spec/controllers/hearings/schedule_periods_controller_spec.rb b/spec/controllers/hearings/schedule_periods_controller_spec.rb index 491eb9a57cf..7d7101798a8 100644 --- a/spec/controllers/hearings/schedule_periods_controller_spec.rb +++ b/spec/controllers/hearings/schedule_periods_controller_spec.rb @@ -5,6 +5,12 @@ let!(:ro_schedule_period) { create(:ro_schedule_period) } let!(:judge_stuart) { create(:user, :with_vacols_judge_record, full_name: "Stuart Huels", css_id: "BVAHUELS") } let!(:judge_doris) { create(:user, :with_vacols_judge_record, full_name: "Doris Lamphere", css_id: "BVALAMPHERE") } + before(:all) do + # clean up is required as Hearing day ID is hard coded in the spreadsheet file. + # if for some reason the database's table pk is not reset then it will cause test failure. + + clean_up_hearing_days + end shared_context "hearing_days" do let!(:hearing_days) do @@ -121,7 +127,7 @@ context "judge assignment" do include_context "hearing_days" - it "stages hearing days for judge assignment", skip: "flake" do + it "stages hearing days for judge assignment" do base64_header = "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64," post :create, params: { schedule_period: { @@ -230,4 +236,9 @@ expect(response.status).to eq 200 end end + + def clean_up_hearing_days + HearingDay.delete_all + ActiveRecord::Base.connection.reset_pk_sequence!(HearingDay.table_name) + end end diff --git a/spec/controllers/hearings_controller_spec.rb b/spec/controllers/hearings_controller_spec.rb index a863d585317..aa3546a0646 100644 --- a/spec/controllers/hearings_controller_spec.rb +++ b/spec/controllers/hearings_controller_spec.rb @@ -453,12 +453,12 @@ expect(response.status).to eq 200 end - it "should return a 200 and update aod if provided", :aggregate_failures, skip: "flake AOD present" do + it "should return a 200 and update aod if provided", :aggregate_failures do params = { id: ama_hearing.external_id, advance_on_docket_motion: { user_id: user.id, - person_id: ama_hearing.appeal.appellant.id, + person_id: ama_hearing.appeal.appellant.person.id, reason: Constants.AOD_REASONS.age, granted: true }, @@ -467,7 +467,7 @@ patch :update, as: :json, params: params expect(response.status).to eq 200 ama_hearing.reload - expect(ama_hearing.advance_on_docket_motion.person.id).to eq ama_hearing.appeal.appellant.id + expect(ama_hearing.advance_on_docket_motion.person.id).to eq ama_hearing.appeal.appellant.person.id expect(ama_hearing.advance_on_docket_motion.reason).to eq Constants.AOD_REASONS.age expect(ama_hearing.advance_on_docket_motion.granted).to eq true end diff --git a/spec/controllers/idt/api/v1/upload_vbms_document_controller_spec.rb b/spec/controllers/idt/api/v1/upload_vbms_document_controller_spec.rb index 0c1b55944e9..33251249622 100644 --- a/spec/controllers/idt/api/v1/upload_vbms_document_controller_spec.rb +++ b/spec/controllers/idt/api/v1/upload_vbms_document_controller_spec.rb @@ -1,18 +1,63 @@ # frozen_string_literal: true RSpec.describe Idt::Api::V1::UploadVbmsDocumentController, :all_dbs, type: :controller do + include ActiveJob::TestHelper + describe "POST /idt/api/v1/appeals/:appeal_id/upload_document" do let(:user) { create(:user) } let(:appeal) { create(:appeal) } let(:veteran) { appeal.veteran } let(:file_number) { appeal.veteran.file_number } + let(:file) { "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YW" } let(:valid_document_type) { "BVA Decision" } let(:params) do { appeal_id: appeal.external_id, - file: "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YW", + file: file, document_type: valid_document_type } end + let(:mail_request_params) do + { veteran_identifier: veteran.file_number, + file: file, + document_type: valid_document_type, + recipient_info: [ + { + recipient_type: "person", + first_name: "Bob", + last_name: "Smithmets", + participant_id: "487470002", + destination_type: "domesticAddress", + address_line_1: "1235 Main Street", + treat_line_2_as_addressee: false, + treat_line_3_as_addressee: false, + city: "Orlando", + state: "FL", + postal_code: "13246", + country_code: "US" + } + ] } + end + + let(:invalid_mail_request_params) do + { veteran_identifier: veteran.file_number, + file: "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YW", + document_type: valid_document_type, + recipient_info: [ + { + recipient_type: "person", + participant_id: "487470002", + destination_type: "domesticAddress", + address_line_1: "1234 Main Street", + treat_line_2_as_addressee: false, + treat_line_3_as_addressee: false, + city: "Orlando", + state: "FL", + postal_code: "12345", + country_code: "US" + } + ] } + end + let(:params_identifier) do { veteran_identifier: veteran.file_number, file: "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YW", @@ -115,6 +160,18 @@ end end + context "when the recipient_info parameters are incomplete" do + it "returns a descriptive error to the IDT user" do + expect(Raven).to receive(:capture_exception) + post :create, params: invalid_mail_request_params, as: :json + validation_error_msgs = JSON.parse(response.body)["errors"] + expect(validation_error_msgs).to eq( + "distribution 1" => "First name can't be blank, Last name can't be blank" + ) + expect(response.status).to eq(400) + end + end + context "all parameters are valid" do let(:uploaded_document) { instance_double(VbmsUploadedDocument, id: 1) } let(:document_params) do @@ -123,13 +180,30 @@ appeal_type: appeal.class.name, veteran_file_number: file_number, document_type: params[:document_type], - file: params[:file], + file: file, document_name: nil, document_subject: nil } end shared_examples "success_with_valid_parameters" do + before do + RequestStore.store[:current_user] = User.system_user + end + + it "creates a new Mail Request object when optional params exist" do + expect_any_instance_of(MailRequest).to receive(:call) + post :create, params: mail_request_params, as: :json + end + + it "returns a list of vbms_distribution ids alongside a success message" do + post :create, params: mail_request_params, as: :json + success_message = JSON.parse(response.body)["message"] + success_id = JSON.parse(response.body)["distribution_ids"] + expect(success_message).to eq "Document successfully queued for upload." + expect(success_id).not_to eq([]) + end + it "returns a successful message and creates a new VbmsUploadedDocument" do expect { post :create, params: params }.to change(VbmsUploadedDocument, :count).by(1) @@ -144,7 +218,8 @@ expect(UploadDocumentToVbmsJob).to receive(:perform_later).with( document_id: uploaded_document.id, initiator_css_id: user.css_id, - application: anything + application: anything, + mail_package: nil ) expect(uploaded_document).to receive(:cache_file) @@ -165,6 +240,48 @@ it_behaves_like "success_with_valid_parameters" end end + + context "queues async mail request job" do + let(:recipient_info) { mail_request_params[:recipient_info] } + let(:mail_request) { MailRequest.new(recipient_info[0]) } + let(:mail_package) do + { distributions: [mail_request.to_json], + copies: 1, + created_by_id: user.id } + end + let(:uploaded_document) { create(:vbms_uploaded_document) } + let(:upload_job_params) do + { document_id: uploaded_document.id, + initiator_css_id: user.css_id, + application: nil, + mail_package: mail_package } + end + + context "document is associated with a mail package" do + it "calls #perform_later on MailRequestJob" do + post :create, params: mail_request_params, as: :json + expect(MailRequestJob).to receive(:perform_later) + perform_enqueued_jobs do + UploadDocumentToVbmsJob.perform_later(upload_job_params) + end + end + end + + context "document is not associated with a mail package" do + it "does not call #perform_later on MailRequestJob" do + mail_request_params[:recipient_info] = [] + post :create, params: mail_request_params, as: :json + expect(MailRequestJob).to_not receive(:perform_later) + end + end + + context "recipient info is incorrect" do + it "does not call #perform_later on MailRequestJob" do + post :create, params: invalid_mail_request_params, as: :json + expect(MailRequestJob).to_not receive(:perform_later) + end + end + end end end end diff --git a/spec/controllers/idt/api/v2/appeals_controller_spec.rb b/spec/controllers/idt/api/v2/appeals_controller_spec.rb index 15fe3342605..c8f293f78b8 100644 --- a/spec/controllers/idt/api/v2/appeals_controller_spec.rb +++ b/spec/controllers/idt/api/v2/appeals_controller_spec.rb @@ -510,16 +510,17 @@ citation_number: citation_number, decision_date: Date.new(1989, 12, 13).to_s, file: "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YW", - redacted_document_location: "C://Windows/User/BLOBLAW/Documents/Decision.docx" } + redacted_document_location: "C://Windows/User/BLOBLAW/Documents/Decision.docx", + recipient_info: [] } end before do - allow(controller).to receive(:verify_access).and_return(true) BvaDispatch.singleton.add_user(user) key, t = Idt::Token.generate_one_time_key_and_proposed_token Idt::Token.activate_proposed_token(key, user.css_id) request.headers["TOKEN"] = t + create(:staff, :attorney_role, sdomainid: user.css_id) end context "when some params are missing" do @@ -567,7 +568,6 @@ it "should complete the BvaDispatchTask assigned to the User and the task assigned to the BvaDispatch org" do post :outcode, params: params - expect(response.status).to eq(200) tasks = BvaDispatchTask.where(appeal: root_task.appeal, assigned_to: user) @@ -580,7 +580,60 @@ expect(task.parent.status).to eq("completed") expect(S3Service.files["decisions/" + root_task.appeal.external_id + ".pdf"]).to_not eq nil expect(DecisionDocument.find_by(appeal_id: root_task.appeal.id)&.submitted_at).to_not be_nil - expect(JSON.parse(response.body)["message"]).to eq("Success!") + expect(JSON.parse(response.body)["message"]).to eq("Successful dispatch!") + end + + context "when dispatch is associated with a mail request" do + include ActiveJob::TestHelper + + let(:recipient) do + { recipient_type: "person", + first_name: "Bob", + last_name: "Smithmetz", + participant_id: "487470002", + destination_type: "domesticAddress", + address_line_1: "1234 Main Street", + treat_line_2_as_addressee: false, + treat_line_3_as_addressee: false, + city: "Orlando", + state: "FL", + postal_code: "12345", + country_code: "US" } + end + + before { params[:recipient_info] << recipient } + + it "calls #perform_later on MailRequestJob" do + expect(MailRequestJob).to receive(:perform_later) + + perform_enqueued_jobs { post :outcode, params: params, as: :json } + end + + context "recipient info is incorrect" do + it "returns validation errors and does not call #perform_later on MailRequestJob" do + recipient[:first_name] = nil + expect(MailRequestJob).to_not receive(:perform_later) + perform_enqueued_jobs { post :outcode, params: params, as: :json } + error_message = JSON.parse(response.body)["errors"]["distribution 1"] + expect(error_message).to eq("First name can't be blank") + end + end + + context "when dispatch is not successfully processed" do + let(:citation_number) { "INVALID" } + it "does not call #perform_later on MailRequestJob" do + perform_enqueued_jobs { expect(MailRequestJob).to_not receive(:perform_later) } + post :outcode, params: params + end + end + end + + context "when dispatch is not associated with a mail request" do + it "does not call #perform_later on MailRequestJob" do + params[:recipient_info] = [] + expect(MailRequestJob).to_not receive(:perform_later) + post :outcode, params: params + end end end diff --git a/spec/controllers/idt/api/v2/distributions_controller_spec.rb b/spec/controllers/idt/api/v2/distributions_controller_spec.rb new file mode 100644 index 00000000000..8c60e4c8ed9 --- /dev/null +++ b/spec/controllers/idt/api/v2/distributions_controller_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require "rails_helper" + +# This is the command to run rspec in the console +# bundle exec rspec spec/controllers/idt/api/v2/distributions_controller_spec.rb + +RSpec.describe Idt::Api::V2::DistributionsController, type: :controller do + describe "#distribution" do + let(:user) { create(:user) } + let(:error_uuid) { "a9df0251-8350-464b-9aa4-a7d56a8ac173" } + let(:distro_uuid) { "df7fc6b2-8be3-4124-a796-6a77bdd8f66a" } + + before do + allow(SecureRandom).to receive(:uuid).and_return(error_uuid) + + key, t = Idt::Token.generate_one_time_key_and_proposed_token + Idt::Token.activate_proposed_token(key, user.css_id) + + request.headers["TOKEN"] = t + create(:staff, :attorney_role, sdomainid: user.css_id) + end + + context "when distribution_id is blank or invalid" do + let(:distribution_id) { "" } + let(:error_msg) do + "[IDT] Http Status Code: 400, Distribution Does Not Exist Or Id is blank," \ + " (Distribution ID: #{distribution_id}) #{error_uuid}" + end + + it "renders an error with status 400" do + get :distribution, params: { distribution_id: distribution_id } + + expect(response.code).to eq "400" + expect(JSON.parse(response.body)).to eq( + "message" => error_msg + ) + end + end + + context "when PacmanService fails with a 404 error" do + let!(:vbms_distribution) { create(:vbms_distribution) } + + it "renders the expected response with status 200, Pacman api has a 404" do + expected_response = { + "id" => vbms_distribution.id.to_s, + "status" => "PENDING_ESTABLISHMENT" + } + + get :distribution, params: { distribution_id: vbms_distribution.id } + + expect(response).to have_http_status(200) + expect(JSON.parse(response.body)).to eq(expected_response) + end + end + + context "when PacmanService fails with a 500 error" do + let(:error_msg) do + "[IDT] Http Status Code: 500, Internal Server Error," \ + " (Distribution ID: #{vbms_distribution.id}) #{error_uuid}" + end + let(:vbms_distribution) { create(:vbms_distribution, uuid: distro_uuid) } + + it "renders an error with status 500" do + allow(PacmanService).to receive(:get_distribution_request).with(vbms_distribution.uuid) do + OpenStruct.new(code: 500) + end + + get :distribution, params: { distribution_id: vbms_distribution.id } + + expect(response.code).to eq "500" + expect(JSON.parse(response.body)).to eq( + "message" => error_msg + ) + end + end + + context "when converting the distribution" do + let(:vbms_distribution) { create(:vbms_distribution, uuid: distro_uuid) } + let(:expected_response) do + { + "id": Fakes::PacmanService::DISTRIBUTION_UUID, + "recipient": + { + "type": "system", + "id": "a050a21e-23f6-4743-a1ff-aa1e24412eff", + "name": "VBMS-C" + }, + "description": "Staging Mailing Distribution", + "communication_package_id": 1, + "destinations": [ + { + "type": "physicalAddress", + "id": "28440040-51a5-4d2a-81a2-28730827be14", + "status": "", + "cbcm_send_attempt_date": "2022-06-06T16:35:27.996", + "address_line_1": "POSTMASTER GENERAL", + "address_line_2": "UNITED STATES POSTAL SERVICE", + "address_line_3": "475 LENFANT PLZ SW RM 10022", + "address_line_4": "SUITE 123", + "address_line_5": "APO AE 09001-5275", + "address_line_6": "", + "treat_line_2_as_addressee": true, + "treat_line_3_as_addressee": true, + "city": "WASHINGTON DC", + "state": "DC", + "postal_code": "12345", + "country_name": "UNITED STATES", + "country_code": "us" + } + ], + "status": "", + "sent_to_cbcm_date": "" + } + end + + it "returns the expected converted response" do + get :distribution, params: { distribution_id: vbms_distribution.id } + + expect(response).to have_http_status(200) + expect(JSON.parse(response.body.to_json)).to eq(expected_response.to_json) + end + end + + context "render_error" do + let(:status) { 500 } + let(:message) { "Internal Server Error" } + let(:vbms_distribution) { create(:vbms_distribution, uuid: distro_uuid) } + + it "renders the error response with correct status, message, and distribution ID" do + error_message = "[IDT] Http Status Code: #{status}, #{message}, (Distribution ID: #{vbms_distribution.id})" + expect(Rails.logger).to receive(:error).with("#{error_message}Error ID: #{error_uuid}") + + allow(PacmanService).to receive(:get_distribution_request).with(distro_uuid) do + OpenStruct.new(code: 500) + end + + get :distribution, params: { distribution_id: vbms_distribution.id } + + expect(response).to have_http_status(status) + expect(JSON.parse(response.body)).to eq( + "message" => error_message + " #{error_uuid}" + ) + end + end + end +end diff --git a/spec/controllers/organizations/users_controller_spec.rb b/spec/controllers/organizations/users_controller_spec.rb index 132826bf5af..11cee50f66e 100644 --- a/spec/controllers/organizations/users_controller_spec.rb +++ b/spec/controllers/organizations/users_controller_spec.rb @@ -274,7 +274,7 @@ let!(:params) { { organization_url: org.url, id: user.id } } let(:org) { create(:judge_team, :has_judge_team_lead_as_admin) } - let!(:user) do + let(:user) do create(:user).tap do |user| org.add_user(user) end @@ -300,7 +300,9 @@ end end - context "when user is the judge in the organization", skip: "Flake" do + context "when user is the judge in the organization" do + let(:user) { org.admin } + it "returns an error" do subject diff --git a/spec/factories/mail_request.rb b/spec/factories/mail_request.rb new file mode 100644 index 00000000000..256c80ed196 --- /dev/null +++ b/spec/factories/mail_request.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :mail_request do + recipient_type { "person" } + first_name { "Bob" } + last_name { "Smithcole" } + participant_id { "487470002" } + destination_type { "domesticAddress" } + address_line_1 { "1234 Main Street" } + city { "Orlando" } + country_code { "US" } + postal_code { "12345" } + state { "FL" } + treat_line_2_as_addressee { false } + treat_line_3_as_addressee { false } + + trait :nil_recipient_type do + recipient_type { nil } + end + + initialize_with { new(attributes) } + end +end diff --git a/spec/factories/organization.rb b/spec/factories/organization.rb index 0ccf7fc1bf4..7d5fdb08bab 100644 --- a/spec/factories/organization.rb +++ b/spec/factories/organization.rb @@ -70,6 +70,11 @@ type { "BusinessLine" } end + factory :vre_business_line, class: BusinessLine do + type { "BusinessLine" } + name { Constants::BENEFIT_TYPES["voc_rehab"] } + end + factory :hearings_management do type { "HearingsManagement" } name { "Hearings Management" } diff --git a/spec/factories/vacols/case.rb b/spec/factories/vacols/case.rb index 101e71ad02b..9fc01dfe2b5 100644 --- a/spec/factories/vacols/case.rb +++ b/spec/factories/vacols/case.rb @@ -175,6 +175,19 @@ end end + factory :case_with_multi_decision do + bfddec { 1.day.ago } + + transient do + decision_document do + [ + create(:document, type: "BVA Decision", received_at: 1.day.ago), + create(:document, type: "BVA Decision", received_at: 1.day.ago) + ] + end + end + end + factory :case_with_old_decision do bfddec { 1.day.ago } diff --git a/spec/factories/vbms_communication_package.rb b/spec/factories/vbms_communication_package.rb new file mode 100644 index 00000000000..143cce7de44 --- /dev/null +++ b/spec/factories/vbms_communication_package.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :vbms_communication_package do + association :document_mailable_via_pacman, factory: :vbms_uploaded_document + comm_package_name { "DocumentName_" + Time.zone.now.to_s } + copies { 1 } + created_at { Time.zone.now } + created_by_id { create(:user).id } + file_number { generate :veteran_file_number } + status { nil } + updated_at { Time.zone.now } + updated_by_id { nil } + uuid { nil } + end +end diff --git a/spec/factories/vbms_distribution.rb b/spec/factories/vbms_distribution.rb new file mode 100644 index 00000000000..512d9234622 --- /dev/null +++ b/spec/factories/vbms_distribution.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :vbms_distribution do + claimant_station_of_jurisdiction { nil } + created_at { Time.zone.now } + created_by_id { nil } + first_name { "Bob" } + last_name { "Bobjoe" } + middle_name { "Joe" } + name { nil } + participant_id { generate :participant_id } + poa_code { nil } + recipient_type { "person" } + updated_at { Time.zone.now } + updated_by_id { nil } + vbms_communication_package_id { nil } + uuid { nil } + end +end diff --git a/spec/factories/vbms_distribution_destination.rb b/spec/factories/vbms_distribution_destination.rb new file mode 100644 index 00000000000..0dd3b64bd9f --- /dev/null +++ b/spec/factories/vbms_distribution_destination.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :vbms_distribution_destination do + association :vbms_distribution, factory: :vbms_distribution + address_line_1 { "POSTMASTER GENERAL" } + address_line_2 { "UNITED STATES POSTAL SERVICE" } + address_line_3 { "475 LENFANT PLZ SW RM 10022" } + address_line_4 { "SUITE 123" } + address_line_5 { "APO AE 09001-5275" } + address_line_6 { nil } + city { "WASHINGTON DC" } + country_code { "US" } + country_name { "UNITED STATES" } + created_at { Time.zone.now } + created_by_id { nil } + destination_type { "domesticAddress" } + postal_code { "12345" } + state { "DC" } + treat_line_2_as_addressee { true } + treat_line_3_as_addressee { true } + updated_at { Time.zone.now } + updated_by_id { nil } + end +end diff --git a/spec/factories/vbms_uploaded_document.rb b/spec/factories/vbms_uploaded_document.rb index f6e10724896..3a2ef598365 100644 --- a/spec/factories/vbms_uploaded_document.rb +++ b/spec/factories/vbms_uploaded_document.rb @@ -6,6 +6,9 @@ document_type { "Status Letter" } appeal { create(:appeal) } + document_version_reference_id { "{#{SecureRandom.uuid.upcase}}" } + document_series_reference_id { "{#{SecureRandom.uuid.upcase}}" } + trait :for_legacy_appeal do appeal { create(:legacy_appeal, vacols_case: create(:case)) } end diff --git a/spec/feature/certification/certification_stats_spec.rb b/spec/feature/certification/certification_stats_spec.rb deleted file mode 100644 index 3195646a690..00000000000 --- a/spec/feature/certification/certification_stats_spec.rb +++ /dev/null @@ -1,186 +0,0 @@ -# frozen_string_literal: true - -RSpec.feature "Certification Stats Dashboard", :postgres, skip: "deprecated" do - before do - Timecop.freeze(Time.utc(2015, 1, 1, 17, 55, 0, rand(1000))) - - Certification.create( - nod_matching_at: 5.hours.ago, - form9_matching_at: 5.hours.ago, - soc_matching_at: 5.hours.ago, - ssocs_required: false, - ssocs_matching_at: nil, - form8_started_at: 5.hours.ago, - created_at: 5.hours.ago, - completed_at: 4.hours.ago - ) - - Certification.create( - nod_matching_at: 4.hours.ago, - form9_matching_at: 5.hours.ago, - soc_matching_at: 5.hours.ago, - ssocs_required: true, - ssocs_matching_at: 4.hours.ago, - form8_started_at: 4.hours.ago, - created_at: 5.hours.ago, - completed_at: nil - ) - - Certification.create( - nod_matching_at: 4.hours.ago, - form9_matching_at: 5.hours.ago, - soc_matching_at: 5.hours.ago, - ssocs_required: true, - ssocs_matching_at: 5.hours.ago, - form8_started_at: 4.hours.ago, - created_at: 5.hours.ago, - completed_at: 3.hours.ago - ) - - Certification.create( - nod_matching_at: 45.minutes.ago, - form9_matching_at: 45.minutes.ago, - soc_matching_at: 45.minutes.ago, - ssocs_required: true, - ssocs_matching_at: 45.minutes.ago, - form8_started_at: 45.minutes.ago, - created_at: 45.minutes.ago, - completed_at: 30.minutes.ago - ) - CertificationStats.calculate_all! - - # Necessary role to view certification_stats page - User.authenticate!(roles: ["System Admin"]) - end - - let(:leftarrow) { "d3.select(window).dispatch('keydown', { detail: { keyCode: 37 } })" } - let(:rightarrow) { "d3.select(window).dispatch('keydown', { detail: { keyCode: 39 } })" } - - scenario "Switching tab intervals" do - visit "/certification/stats" - expect(page).to have_content("Activity for 12:00–12:59 EST (so far)") - expect(page).to have_content("Certifications Started\n1") - expect(page).to have_content("Certifications Completed\n1") - expect(page).to have_content("Overall\n100 %") - expect(page).to have_content("Missing Document\n?? %") - expect(page).to have_content("Overall (median)\n15.00 min") - expect(page).to have_content("Missing Document (median)\n??") - expect(page).to have_content("Any Document\n0 %") - - click_on "Daily" - expect(page).to have_content("Activity for January 1 (so far)") - expect(page).to have_content("Certifications Started\n4") - expect(page).to have_content("Certifications Completed\n3") - expect(page).to have_content("Overall\n75 %") - expect(page).to have_content("Missing Document\n50 %") - expect(page).to have_content("Overall (median)\n1.00 hours") - expect(page).to have_content("Missing Document (median)\n2.00 hours") - expect(page).to have_content("Any Document\n50 %") - expect(page).to have_content("NOD\n50 %") - expect(page).to have_content("SOC\n0 %") - expect(page).to have_content("SSOC\n33 %") - expect(page).to have_content("Form 9\n0 %") - end - - # The stats tests don't play well with Selenium Chrome - # The mouseover effect with the stat bars is erratic - # TODO: Augment stats to disable mouseover for the tests - scenario "Check missing documents" do - Certification.create( - nod_matching_at: 45.minutes.ago, - form9_matching_at: 45.minutes.ago, - soc_matching_at: 45.minutes.ago, - ssocs_required: true, - ssocs_matching_at: 43.minutes.ago, - form8_started_at: nil, - created_at: 45.minutes.ago, - completed_at: nil - ) - - Certification.create( - nod_matching_at: 45.minutes.ago, - form9_matching_at: 45.minutes.ago, - soc_matching_at: nil, - ssocs_required: true, - ssocs_matching_at: 45.minutes.ago, - form8_started_at: nil, - created_at: 45.minutes.ago, - completed_at: nil - ) - CertificationStats.calculate_all! - - visit "/certification/stats/daily" - - # Turn mousever events off on the Stats dashboard to not confuse Chrome - page.execute_script("window.Dashboard.mouseoverEvents = false;") - - expect(page).to have_content("Activity for January 1 (so far)") - expect(page).to have_content("Certifications Started\n6") - expect(page).to have_content("Certifications Completed\n3") - expect(page).to have_content("Overall\n50 %") - expect(page).to have_content("Missing Document\n25 %") - expect(page).to have_content("Overall (median)\n1.00 hours") - expect(page).to have_content("Missing Document (median)\n2.00 hours") - - expect(page).to have_content("Any Document\n67 %") - expect(page).to have_content("NOD\n33 %") - expect(page).to have_content("SOC\n17 %") - expect(page).to have_content("SSOC\n40 %") - expect(page).to have_content("Form 9\n0 %") - end - - scenario "Toggle median to 95th percentile and navigate to past periods", - skip: "Seeing weird Time/Day related test failure" do - visit "/certification/stats" - - # Turn mouseover events off on the Stats dashboard to not confuse Chrome - page.execute_script("window.Dashboard.mouseoverEvents = false;") - - click_on "Daily" - - find("#time-to-certify-toggle").click - - expect(page).to have_content("Overall (95th percentile)") - expect(page).to have_content("December 17") - - # Scroll once more to see December 16 have no stats - page.driver.execute_script(leftarrow) - expect(page).to have_content("December 16") - expect(page).to have_content("Overall (95th percentile)") - - find("#time-to-certify-toggle").click - - # Scroll to the most recent time interval - page.driver.execute_script(rightarrow) - expect(page).to have_content("Overall (median)") - end - - scenario "Unauthorized user access" do - # Unauthenticated access - User.unauthenticate! - visit "/certification/stats" - expect(page.has_no_content?("Activity for")).to eq(true) - expect(page.has_no_content?("Certification Rate")).to eq(true) - expect(page.has_no_content?("Time to Certify")).to eq(true) - expect(page.has_no_content?("Missing Documents")).to eq(true) - - # Authenticated access with System Admin CSS role - User.tester!(roles: ["System Admin"]) - visit "/certification/stats" - expect(page.has_no_content?("Activity for")).to eq(true) - expect(page.has_no_content?("Certification Rate")).to eq(true) - expect(page.has_no_content?("Time to Certify")).to eq(true) - expect(page.has_no_content?("Missing Documents")).to eq(true) - - # Authenticated access without System Admin role - User.authenticate! - visit "/certification/stats" - expect(page.has_no_content?("Activity for")).to eq(true) - expect(page.has_no_content?("Certification Rate")).to eq(true) - expect(page.has_no_content?("Time to Certify")).to eq(true) - expect(page.has_no_content?("Missing Documents")).to eq(true) - - expect(page).to have_content("You aren't authorized to use this part of Caseflow yet.") - expect(page).to have_content("Unauthorized") - end -end diff --git a/spec/feature/dispatch/dispatch_stats_spec.rb b/spec/feature/dispatch/dispatch_stats_spec.rb deleted file mode 100644 index 9d309459471..00000000000 --- a/spec/feature/dispatch/dispatch_stats_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -RSpec.feature "Dispatch Stats Dashboard", :postgres, skip: "deprecated" do - before do - Timecop.freeze(Time.utc(2015, 1, 1, 17, 55, 0, rand(1000))) - end - - context ".show#daily" do - before do - Rails.cache.clear - DispatchStats.calculate_all! - end - it "switches between the tabs" do - User.authenticate!(roles: ["Manage Claim Establishment"]) - visit "/dispatch/stats" - expect(page).to have_content("Establish Claim Tasks Identified") - expect(page).to have_content("Establish Claim Task Activity") - expect(page).to have_content("Establish Claim Task Completion Rate") - expect(page).to have_content("Time to Claim Establishment") - expect(page).to have_content("Establish Claim Tasks Canceled") - - click_on "Daily" - expect(page).to have_content("Establish Claim Tasks Identified") - end - end - - context ".show" do - before do - Generators::EstablishClaim.create(started_at: 30.minutes.ago, completed_at: 15.minutes.ago, completion_status: 0) - Generators::EstablishClaim.create(started_at: 30.minutes.ago) - Rails.cache.clear - DispatchStats.calculate_all! - end - it "loads the correct stats" do - User.authenticate!(roles: ["Manage Claim Establishment"]) - visit "/dispatch/stats" - expect(page).to have_content("All\n2") - end - end - - scenario "Users without manager permissions cannot view page" do - User.authenticate! - visit "/dispatch/stats" - expect(page).to have_content("Drat!\nYou aren't authorized to use this part of Caseflow yet.") - end -end diff --git a/spec/feature/dispatch/establish_claim_spec.rb b/spec/feature/dispatch/establish_claim_spec.rb index 085afb747a5..15442585c9a 100644 --- a/spec/feature/dispatch/establish_claim_spec.rb +++ b/spec/feature/dispatch/establish_claim_spec.rb @@ -27,7 +27,7 @@ let(:folder) { build(:folder, tioctime: 23.days.ago.midnight) } let(:case_remand) do - create(:case_with_decision, :status_remand, folder: folder) + create(:case_with_multi_decision, :status_remand, folder: folder) end let(:appeal_remand) do @@ -491,9 +491,8 @@ Generators::Document.build(type: "BVA Decision", received_at: 6.days.ago) ] end - # :nocov: - scenario "Review page lets users choose which document to use", - skip: "This test is failing because of a stale element reference" do + + scenario "Review page lets users choose which document to use" do visit "/dispatch/establish-claim" click_on "Establish next claim" @@ -508,8 +507,7 @@ expect(page).to have_content("Benefit Type") end - scenario "the EP creation page has a link back to decision review", - skip: "This test is failing because of a stale element reference" do + scenario "the EP creation page has a link back to decision review" do visit "/dispatch/establish-claim" click_on "Establish next claim" @@ -518,7 +516,6 @@ click_on "< Back to Review Decision" expect(page).to have_content("Multiple Decision Documents") end - # :nocov: end context "For a full grant" do @@ -629,10 +626,11 @@ scenario "Assigning it to complete the claims establishment", skip: "flakey hang" do visit "/dispatch/establish-claim" click_on "Establish next claim" - expect(page).to have_current_path("/dispatch/establish-claim/#{task.id}") click_on "Route claim" expect(page).to have_current_path("/dispatch/establish-claim/#{task.id}") + expect(page).to have_content("Route Claim") + expect(page).to have_selector(:link_or_button, "Assign to Claim") click_on "Assign to Claim" # unknown reason sometimes hangs here expect(page).to have_content("Success!") @@ -652,8 +650,7 @@ aasm_state: "unassigned") end - scenario "Establish a new claim routed to ARC", - skip: "This test is failing because of a stale element reference" do + scenario "Establish a new claim routed to ARC", :aggregate_failure do # Mock the claim_id returned by VBMS's create end product Fakes::VBMSService.end_product_claim_id = "CLAIM_ID_123" @@ -700,9 +697,11 @@ suppress_acknowledgement_letter: true, claimant_participant_id: nil, limited_poa_code: nil, - limited_poa_access: nil + limited_poa_access: nil, + status_type_code: nil }, - veteran_hash: task.appeal.veteran.to_vbms_hash + veteran_hash: task.appeal.veteran.to_vbms_hash, + user: RequestStore[:current_user] ) expect(AppealRepository).to have_received(:update_vacols_after_dispatch!) @@ -713,11 +712,11 @@ expect(task.appeal.reload.dispatched_to_station).to eq("397") - click_on "Caseflow Dispatch" - expect(page).to have_current_path("/dispatch/establish-claim") + expect(page).to have_current_path("/dispatch/establish-claim/#{task.id}") # No tasks left - expect(page).to have_content("Way to go! You have completed all the claims assigned to you.") + expect(page).to have_content("Way to go!") + expect(page).to have_content("You have completed all of the total cases assigned to you today") expect(page).to have_css(".usa-button-disabled") end diff --git a/spec/feature/hearings/daily_docket/build_hearsched_spec.rb b/spec/feature/hearings/daily_docket/build_hearsched_spec.rb index 0d64bb1f58b..e02dcfac20b 100644 --- a/spec/feature/hearings/daily_docket/build_hearsched_spec.rb +++ b/spec/feature/hearings/daily_docket/build_hearsched_spec.rb @@ -72,10 +72,10 @@ let!(:hearing) { create(:hearing, :with_tasks) } let!(:postponed_hearing_day) { create(:hearing_day, scheduled_for: Date.new(2019, 3, 3)) } - scenario "User can update fields", skip: "flake" do + scenario "User can update fields" do visit "hearings/schedule/docket/" + hearing.hearing_day.id.to_s find("textarea", id: "#{hearing.external_id}-notes").click.send_keys("This is a note about the hearing!") - find("label", text: "9:00 am").click + find("label", text: "9:00 AM Eastern Time (US & Canada)").click find("label", text: "Transcript Requested").click click_button("Save") expect(page).to have_content("You have successfully updated") @@ -87,7 +87,7 @@ expect(page).to have_content("No Show") expect(page).to have_content("This is a note about the hearing!", wait: 10) # flake expect(find_field("Transcript Requested", visible: false)).to be_checked - expect(find_field("9:00 am", visible: false)).to be_checked + expect(find_field("9:00 AM", visible: false)).to be_checked end end diff --git a/spec/feature/out_of_service_spec.rb b/spec/feature/out_of_service_spec.rb index 9ebeaf5be29..ae38d2b836a 100644 --- a/spec/feature/out_of_service_spec.rb +++ b/spec/feature/out_of_service_spec.rb @@ -11,10 +11,13 @@ User.unauthenticate! end - scenario "When out of service is disabled, it shows Caseflow Home page", - skip: "This test is failing because of a bad feature toggle set somewhere" do + scenario "When out of service is disabled, it shows Caseflow Home page", :aggregate_failures do visit "/" - expect(page).to have_content("Caseflow Help") + expect(page).to have_current_path("/") + expect(page).to have_content("BVAAABSHIRE (DSUSER)") + expect(page).to have_link("Queue") + expect(page).to have_content("Search case") + expect(page).to have_content("Send feedback") expect(page.has_no_content?("Technical Difficulties")).to eq(true) end diff --git a/spec/feature/queue/case_details_spec.rb b/spec/feature/queue/case_details_spec.rb index 9688f477954..38086fbdf41 100644 --- a/spec/feature/queue/case_details_spec.rb +++ b/spec/feature/queue/case_details_spec.rb @@ -153,7 +153,7 @@ def wait_for_page_render expect(details_link.text).to eq(COPY::CASE_DETAILS_HEARING_DETAILS_LINK_COPY) end - context "the user has a VSO role", skip: "re-enable when pagination is fixed" do + context "the user has a VSO role" do let!(:vso) { create(:vso, name: "VSO", role: "VSO", url: "vso-url", participant_id: "8054") } let!(:vso_user) { create(:user, :vso_role) } let!(:vso_task) { create(:ama_vso_task, :in_progress, assigned_to: vso, appeal: appeal) } @@ -649,27 +649,43 @@ def wait_for_page_render ) ) end + # some of the below values are hardcoded in the veteran factory + let!(:inflated_bgs_veteran_record) do + { first_name: appeal.veteran.first_name, + last_name: appeal.veteran.last_name, + date_of_birth: 30.years.ago.to_date.strftime("%m/%d/%Y"), + date_of_death: nil, + name_suffix: appeal.veteran.name_suffix, + sex: "M", + address_line1: "1234 Main Street", + country: "USA", + zip_code: "12345", + state: "FL", + city: "Orlando", + file_number: appeal.veteran.file_number, + ssn: appeal.veteran.ssn, + email_address: "#{appeal.veteran.first_name}.#{appeal.veteran.last_name}@test.com", + ptcpnt_id: appeal.veteran.participant_id, + participant_id: appeal.veteran.participant_id } + end + let!(:bgs) { Fakes::BGSService.new } before do - Fakes::BGSService.inaccessible_appeal_vbms_ids = [] - Fakes::BGSService.inaccessible_appeal_vbms_ids << appeal.veteran_file_number + bgs.class.mark_veteran_not_accessible(appeal.veteran_file_number) allow_any_instance_of(Fakes::BGSService).to receive(:fetch_veteran_info) .and_raise(BGS::ShareError, "NonUniqueResultException") end - scenario "access the appeal's case details", skip: "flake" do - visit "/queue/appeals/#{appeal.external_id}" - + scenario "access the appeal's case details" do + reload_case_detail_page(appeal.external_id) expect(page).to have_content(COPY::DUPLICATE_PHONE_NUMBER_TITLE) - cache_key = Fakes::BGSService.new.can_access_cache_key(current_user, appeal.veteran_file_number) - expect(Rails.cache.exist?(cache_key)).to eq(false) + bgs.inaccessible_appeal_vbms_ids = [] + allow_any_instance_of(Fakes::BGSService).to receive(:fetch_veteran_info) + .and_return(inflated_bgs_veteran_record) - allow_any_instance_of(Fakes::BGSService).to receive(:fetch_veteran_info).and_call_original - Fakes::BGSService.inaccessible_appeal_vbms_ids = [] visit "/queue/appeals/#{appeal.external_id}" - - expect(Rails.cache.exist?(cache_key)).to eq(true) + expect(page).to have_content(appeal.veteran_full_name) end end end diff --git a/spec/feature/queue/cavc_dashboard_spec.rb b/spec/feature/queue/cavc_dashboard_spec.rb index 497ffa4690f..a2547a9c1af 100644 --- a/spec/feature/queue/cavc_dashboard_spec.rb +++ b/spec/feature/queue/cavc_dashboard_spec.rb @@ -183,10 +183,9 @@ go_to_dashboard(cavc_remand.remand_appeal.uuid) - expect(page).to have_text `CAVC appeals for #{cavc_remand.remand_appeal.veteran.name}` + expect(page).to have_text "CAVC appeals for #{cavc_remand.remand_appeal.veteran.name}" - dropdowns = page.all("div.cf-select__placeholder", exact_text: /Select option/) - dropdowns.first.click + page.all("div.cf-select__placeholder", exact_text: /Select option/).first.click page.find("div.cf-select__menu").find("div", exact_text: "Reversed").click reversed_section = page.all("div.usa-accordion-bordered").first @@ -196,10 +195,10 @@ reversed_section.find("span", exact_text: "Treatment records").click expect(page).to have_content "Decision Reasons (2)" reversed_section.find("div", exact_text: "Decision Reasons (2)").click - reversed_section.click expect(page).to have_css('div[aria-expanded="false"]') - dropdowns.last.click + scroll_to page.find("button.cf-submit", text: "Save Changes") + page.all("div.cf-select__placeholder", exact_text: /Select option/).last.click page.find("div.cf-select__menu").find("div", exact_text: "Vacated and Remanded").click v_and_r_section = page.all("div.usa-accordion-bordered").last @@ -217,6 +216,7 @@ expect(page).to have_content "Decision Reasons (1)" v_and_r_section.find("div", exact_text: "Decision Reasons (1)").click + scroll_to page.find("button", text: "Save Changes") click_button "Save Changes" reload_case_detail_page(cavc_remand.remand_appeal.uuid) expect(page).to have_current_path "/queue/appeals/#{cavc_remand.remand_appeal.uuid}" @@ -224,13 +224,14 @@ expect(page).to have_content "Decision Reasons (2)" expect(page).to have_content "Decision Reasons (1)" - expect(CavcSelectionBasis.last.basis_for_selection).to eq "Test New Basis" + page.all("div", text: "Decision Reasons (1)").last.click + scroll_to page.find("button", text: "Remove basis") + expect(page).to have_content "Test New Basis" end end end def go_to_dashboard(appeal_uuid) - visit "/queue/appeals/#{appeal_uuid}/" reload_case_detail_page(appeal_uuid) click_button "CAVC Dashboard" expect(page).to have_current_path("/queue/appeals/#{appeal_uuid}/cavc_dashboard", ignore_query: true) diff --git a/spec/feature/queue/colocated_task_queue_spec.rb b/spec/feature/queue/colocated_task_queue_spec.rb index d0f15e562a7..88a5af39375 100644 --- a/spec/feature/queue/colocated_task_queue_spec.rb +++ b/spec/feature/queue/colocated_task_queue_spec.rb @@ -56,6 +56,7 @@ find(".cf-form-dropdown", text: "Select number of tasks to assign").click find("option", text: "1 (all available tasks)").click find("button", id: "Bulk-Assign-Tasks-button-id-1").click # going by text is an ambiguous match + expect(page).to have_content("You have bulk assigned 1 Poa Clarification Colocated Task tasks") # Visit case details page for VLJ support staff. User.authenticate!(user: vlj_support_staff) @@ -84,8 +85,8 @@ # verify that the instructions from the VLJ appear on the case timeline expect(page).to have_css("h2", text: "Case Timeline") scroll_to(find("h2", text: "Case Timeline")) - poa_task = PoaClarificationColocatedTask.find_by(assigned_to_type: User.name) - click_button(COPY::TASK_SNAPSHOT_VIEW_TASK_INSTRUCTIONS_LABEL, id: poa_task.id) + poa_task = PoaClarificationColocatedTask.last + click_button(text: COPY::TASK_SNAPSHOT_VIEW_TASK_INSTRUCTIONS_LABEL, id: poa_task.id) expect(page).to have_content(return_instructions) # Expect to see draft decision option. find(".cf-select__control", text: "Select an action…").click diff --git a/spec/feature/queue/motion_to_vacate_spec.rb b/spec/feature/queue/motion_to_vacate_spec.rb index c07feb9b71d..1841031eaf7 100644 --- a/spec/feature/queue/motion_to_vacate_spec.rb +++ b/spec/feature/queue/motion_to_vacate_spec.rb @@ -435,6 +435,7 @@ def submit_and_fetch_task(judge) expect(new_task.available_actions(motions_attorney)).to include( Constants.TASK_ACTIONS.LIT_SUPPORT_PULAC_CERULLO.to_h ) + expect(new_task.instructions.join("")).to eq(instructions) end end diff --git a/spec/helpers/cancel_tasks_and_descendants_spec.rb b/spec/helpers/cancel_tasks_and_descendants_spec.rb new file mode 100644 index 00000000000..3b1bc195617 --- /dev/null +++ b/spec/helpers/cancel_tasks_and_descendants_spec.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +require "helpers/cancel_tasks_and_descendants" +require "securerandom" + +describe CancelTasksAndDescendants do + describe ".call" do + context "when task_relation is not given" do + subject(:call) { described_class.call } + + it "assigns RequestStore[:current_user]" do + expect { call }.to change { RequestStore[:current_user] } + .from(nil).to(User.system_user) + end + + it "appends appropriate logs to application logs" do + rails_logger = Rails.logger + allow(Rails).to receive(:logger).and_return(rails_logger) + + aggregate_failures do + expect(rails_logger).to receive(:info) + .with("Total tasks for cancellation: 0").ordered + + expect(rails_logger).not_to receive(:info) + .with(/Task ids \[.+\] cancelled successfully/) + + expect(rails_logger).to receive(:info) + .with(/Tasks cancelled successfully: 0/).ordered + + expect(rails_logger).to receive(:info) + .with(/Elapsed time \(sec\):/).ordered + end + + call + end + + it "appends appropriate logs to stdout" do + allow(SecureRandom).to receive(:uuid) { "dummy-request-id" } + + # rubocop:disable Layout/FirstArgumentIndentation + expect { call }.to output( + match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Total tasks for cancellation: 0" + )).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Tasks cancelled successfully: 0" + ))).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Elapsed time (sec):" + ))) + ).to_stdout + # rubocop:enable Layout/FirstArgumentIndentation + end + + it { is_expected.to be_nil } + end + + context "when task_relation is given " do + subject(:call) { described_class.call(task_relation) } + + let(:task_relation) { Task.where(id: [task_1, task_2, task_3]) } + let(:task_1) { create(:veteran_record_request_task) } + let(:task_2) { create(:veteran_record_request_task) } + let(:task_3) { create(:veteran_record_request_task) } + + before do + expect(task_relation).to receive(:find_each).at_least(:once) + .and_yield(task_1) + .and_yield(task_2) + .and_yield(task_3) + + allow(task_1).to receive(:self_and_descendants) { [task_1] } + allow(task_2).to receive(:self_and_descendants) { [task_2] } + allow(task_3).to receive(:self_and_descendants) { [task_3] } + end + + it "cancels each task and its descendants" do + aggregate_failures do + expect(task_1).to receive(:cancel_task_and_child_subtasks) + expect(task_2).to receive(:cancel_task_and_child_subtasks) + expect(task_3).to receive(:cancel_task_and_child_subtasks) + end + call + end + + it "sets PaperTrail versions data appropriately for cancelled tasks" do + request_id = SecureRandom.uuid + allow(SecureRandom).to receive(:uuid) { request_id } + + call + + task_1_version = task_1.versions.last + expect(task_1_version.whodunnit).to eq(User.system_user.id.to_s) + expect(task_1_version.request_id).to eq(request_id) + end + + it "appends appropriate logs to application logs" do + rails_logger = Rails.logger + allow(Rails).to receive(:logger).and_return(rails_logger) + + aggregate_failures do + expect(rails_logger).to receive(:info) + .with("Total tasks for cancellation: 3").ordered + + expect(rails_logger).to receive(:info) + .with(/Task ids \[#{task_1.id}\] cancelled successfully/).ordered + + expect(rails_logger).to receive(:info) + .with(/Task ids \[#{task_2.id}\] cancelled successfully/).ordered + + expect(rails_logger).to receive(:info) + .with(/Task ids \[#{task_3.id}\] cancelled successfully/).ordered + + expect(rails_logger).to receive(:info) + .with(/Tasks cancelled successfully: 3/).ordered + + expect(rails_logger).to receive(:info) + .with(/Elapsed time \(sec\):/).ordered + end + + call + end + + it "appends appropriate logs to stdout" do + allow(SecureRandom).to receive(:uuid) { "dummy-request-id" } + + # rubocop:disable Layout/FirstArgumentIndentation + expect { call }.to output( + match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Total tasks for cancellation: 3" + )).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_1.id}] cancelled successfully" + ))).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_2.id}] cancelled successfully" + ))).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_3.id}] cancelled successfully" + ))).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Tasks cancelled successfully: 3" + ))).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Elapsed time (sec):" + ))) + ).to_stdout + # rubocop:enable Layout/FirstArgumentIndentation + end + + context "when a task fails to cancel" do + before do + expect(task_2).to receive(:cancel_task_and_child_subtasks) + .and_raise(ActiveModel::ValidationError.new(Task.new)) + end + + it "does not prevent cancellation of other tasks in the relation" do + aggregate_failures do + expect(task_1).to receive(:cancel_task_and_child_subtasks) + expect(task_3).to receive(:cancel_task_and_child_subtasks) + end + call + end + + it "does not raise error" do + expect { call }.not_to raise_error + end + + it "appends appropriate logs to application logs" do + rails_logger = Rails.logger + allow(Rails).to receive(:logger).and_return(rails_logger) + + aggregate_failures do + expect(rails_logger).to receive(:info) + .with("Total tasks for cancellation: 3").ordered + + expect(rails_logger).to receive(:info) + .with(/Task ids \[#{task_1.id}\] cancelled successfully/).ordered + + expect(rails_logger).to receive(:error).with( + /Task ids \[#{task_2.id}\] not cancelled due to error - Validation failed/ + ).ordered + + expect(rails_logger).to receive(:info) + .with(/Task ids \[#{task_3.id}\] cancelled successfully/).ordered + + expect(rails_logger).to receive(:info) + .with(/Tasks cancelled successfully: 2/).ordered + + expect(rails_logger).to receive(:info) + .with(/Elapsed time \(sec\):/).ordered + end + + call + end + + it "appends appropriate logs to stdout" do + allow(SecureRandom).to receive(:uuid) { "dummy-request-id" } + + # rubocop:disable Layout/FirstArgumentIndentation + expect { call }.to output( + match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Total tasks for cancellation: 3" + )).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_1.id}] cancelled successfully" + ))).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_2.id}] not cancelled due to error - " \ + "Validation failed" + ))).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Task ids [#{task_3.id}] cancelled successfully" + ))).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Tasks cancelled successfully: 2" + ))).and(match(Regexp.escape( + "[CancelTasksAndDescendants] [dummy-request-id] Elapsed time (sec):" + ))) + ).to_stdout + # rubocop:enable Layout/FirstArgumentIndentation + end + + it { is_expected.to be_nil } + end + end + end +end diff --git a/spec/jobs/mail_request_job_spec.rb b/spec/jobs/mail_request_job_spec.rb new file mode 100644 index 00000000000..7a5f24c0694 --- /dev/null +++ b/spec/jobs/mail_request_job_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +describe MailRequestJob do + include ActiveJob::TestHelper + + let!(:current_user) { User.authenticate! } + let!(:vbms_file) { create(:vbms_uploaded_document) } + let!(:mail_request) { build(:mail_request) } + + context "Successful execution of MailRequestJob" do + it "creates a new VbmsCommunicationPackage" do + mail_request.call + mail_package = { distributions: [mail_request.to_json], copies: 1, created_by_id: current_user.id } + + expect do + perform_enqueued_jobs { MailRequestJob.perform_later(vbms_file, mail_package) } + end.to change { VbmsCommunicationPackage.count }.by(1) + + expect(find_comm_package_via_distribution_id(mail_request.vbms_distribution_id).status).to eq("success") + end + end + + context "Unsuccessful execution of MailRequestJob" do + it "VbmsCommunicationPackage is not created. VbmsDistribution's vbms_communication_package_id remains nil." do + mail_request.call + mail_package = { distributions: [mail_request.to_json], copies: 1, created_by_id: current_user.id } + + allow(PacmanService) + .to receive(:send_communication_package_request) + .and_raise(Caseflow::Error::PacmanApiError.new(code: 500, message: "Fake Error")) + + expect do + perform_enqueued_jobs { MailRequestJob.perform_later(vbms_file, mail_package) } + end.to change { VbmsCommunicationPackage.count }.by(0) + + distribution = VbmsDistribution.find(mail_request.vbms_distribution_id) + + expect(distribution.vbms_communication_package_id).to be_nil + end + end + def find_comm_package_via_distribution_id(distro_id) + distribution = VbmsDistribution.find(distro_id) + + VbmsCommunicationPackage.find(distribution.vbms_communication_package_id) + end +end diff --git a/spec/jobs/upload_document_to_vbms_job_spec.rb b/spec/jobs/upload_document_to_vbms_job_spec.rb index 2cb2a50cc67..025b6c18485 100644 --- a/spec/jobs/upload_document_to_vbms_job_spec.rb +++ b/spec/jobs/upload_document_to_vbms_job_spec.rb @@ -5,8 +5,20 @@ let(:document) { create(:vbms_uploaded_document) } let(:service) { instance_double(UploadDocumentToVbms) } let(:user) { create(:user) } + let(:mail_request) { instance_double(MailRequest) } + let(:mail_package) do + { distributions: [mail_request.to_json], + copies: 1, + created_by_id: user.id } + end + + let(:params) do + { document_id: document.id, + initiator_css_id: user.css_id, + mail_package: mail_package } + end - subject { UploadDocumentToVbmsJob.perform_now(document_id: document.id, initiator_css_id: user.css_id) } + subject { UploadDocumentToVbmsJob.perform_now(params) } it "calls #call on UploadDocumentToVbms instance" do expect(UploadDocumentToVbms).to receive(:new).with(document: document).and_return(service) @@ -15,5 +27,28 @@ expect(service).to receive(:call) subject end + + context "document is associated with a mail package" do + it "calls #perform_later on MailRequestJob" do + expect(MailRequestJob).to receive(:perform_later).with(document, mail_package) + subject + end + end + + context "document is not associated with a mail package" do + let(:mail_package) { nil } + it "does not call #perform_later on MailRequestJob" do + expect(MailRequestJob).to_not receive(:perform_later) + subject + end + end + + context "document is not successfully uploaded to vbms" do + it "does not call #perform_later on MailRequestJob" do + allow(VBMSService).to receive(:upload_document_to_vbms_veteran).and_raise(StandardError) + expect(MailRequestJob).to_not receive(:perform_later) + expect { subject }.to raise_error(StandardError) + end + end end end diff --git a/spec/lib/tasks/remediations/cancel_vrr_tasks_open_for_vre_spec.rb b/spec/lib/tasks/remediations/cancel_vrr_tasks_open_for_vre_spec.rb new file mode 100644 index 00000000000..ce95ddc0278 --- /dev/null +++ b/spec/lib/tasks/remediations/cancel_vrr_tasks_open_for_vre_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +describe "remediations/cancel_vrr_tasks_open_for_vre" do + include_context "rake" + + describe "remediations:cancel_vrr_tasks_open_for_vre" do + it "delegates to CancelTasksAndDescendants" do + task_relation = double("task_relation") + + expect(VeteranRecordRequestsOpenForVREQuery) + .to receive(:call).and_return(task_relation) + + expect(CancelTasksAndDescendants).to receive(:call).with(task_relation) + + Rake::Task["remediations:cancel_vrr_tasks_open_for_vre"].invoke + end + end +end diff --git a/spec/models/appeal_series_issues_spec.rb b/spec/models/appeal_series_issues_spec.rb index 9e40ca71516..b9215160f0b 100644 --- a/spec/models/appeal_series_issues_spec.rb +++ b/spec/models/appeal_series_issues_spec.rb @@ -68,7 +68,27 @@ end let(:combined_issues) do - AppealSeriesIssues.new(appeal_series: series).all.sort_by { |issue_hash| issue_hash[:diagnostic_code] } + AppealSeriesIssues.new(appeal_series: series).all + end + + let(:combined_issues_array) do + [ + { + description: "Service connection, limitation of thigh motion (flexion)", + diagnosticCode: "5252", + active: true, + lastAction: :remand, + date: 6.months.ago.to_date + }, + { + description: "New and material evidence to reopen claim for service connection,"\ + " shoulder or arm muscle injury", + diagnosticCode: "5301", + active: false, + lastAction: :allowed, + date: 6.months.ago.to_date + } + ] end context "#all" do @@ -77,18 +97,7 @@ context "when an issue spans a remand" do it "combines issues together" do expect(subject.length).to eq(2) - expect(subject.first[:description]).to eq( - "Service connection, limitation of thigh motion (flexion)" - ) - expect(subject.first[:active]).to be_truthy - expect(subject.first[:lastAction]).to eq(:remand) - expect(subject.first[:date]).to eq(6.months.ago.to_date) - expect(subject.last[:description]).to eq( - "New and material evidence to reopen claim for service connection, shoulder or arm muscle injury" - ) - expect(subject.last[:active]).to be_falsey - expect(subject.last[:lastAction]).to eq(:allowed) - expect(subject.last[:date]).to eq(6.months.ago.to_date) + expect(subject).to match_array(combined_issues_array) end context "when there is a draft decision" do @@ -105,9 +114,7 @@ it "does not show the draft disposition" do expect(subject.length).to eq(2) - expect(subject.first[:active]).to be_truthy - expect(subject.first[:date]).to eq(6.months.ago.to_date) - expect(subject.first[:lastAction]).to eq(:remand) + expect(subject).to match_array(combined_issues_array) end end end @@ -116,7 +123,7 @@ let(:appeals) { [original] } it "is marked as active" do - expect(subject.first[:active]).to be_truthy + expect(subject).to match_array(combined_issues_array) end end @@ -124,6 +131,7 @@ let(:original_issues) { [] } it "returns issues" do + expect(subject.length).to eq(1) expect(subject.first[:description]).to eq("Service connection, limitation of thigh motion (flexion)") end end @@ -151,7 +159,15 @@ end it "does not show as a last_action" do - expect(subject.first[:lastAction]).to eq(:remand) + expect(subject.length).to eq(3) + other_hash = { + description: "Other", + diagnosticCode: nil, + active: false, + lastAction: nil, + date: nil + } + expect(subject).to match_array(combined_issues_array.push(other_hash)) end end @@ -161,8 +177,13 @@ end it "appears as the last action" do - expect(subject.first[:lastAction]).to eq(:cavc_remand) - expect(subject.first[:date]).to eq(1.month.ago.to_date) + expect(subject.length).to eq(2) + expect(subject).to include( + a_hash_including( + lastAction: :cavc_remand, + date: 1.month.ago.to_date + ) + ) end end end diff --git a/spec/models/certification_spec.rb b/spec/models/certification_spec.rb index 08b6942717b..7329f8f16e9 100644 --- a/spec/models/certification_spec.rb +++ b/spec/models/certification_spec.rb @@ -6,7 +6,7 @@ end let(:vacols_case) do - create(:case_with_ssoc) + create(:case_with_ssoc, :representative_american_legion) end let(:certification) do @@ -356,9 +356,7 @@ context "#fetch_power_of_attorney!" do subject { certification } - - it "returns true when bgs address is found", - skip: "VACOLS rep test fails sometimes, will be changed with #5185 so ignoring it for now" do + it "fetches the power of attorney from bgs and vacols" do certification.async_start! expect(subject.bgs_rep_city).to eq "SAN FRANCISCO" expect(subject.bgs_representative_type).to eq "Attorney" diff --git a/spec/models/certification_stats_spec.rb b/spec/models/certification_stats_spec.rb deleted file mode 100644 index 5f93d178d7e..00000000000 --- a/spec/models/certification_stats_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -describe CertificationStats, :postgres do - before do - Timecop.freeze(Time.utc(2016, 2, 17, 20, 59, 0)) - Rails.cache.clear - end - - let(:monthly_stats) { Rails.cache.read("CertificationStats-2016-2") } - let(:weekly_stats) { Rails.cache.read("CertificationStats-2016-w07") } - let(:daily_stats) { Rails.cache.read("CertificationStats-2016-2-17") } - let(:hourly_stats) { Rails.cache.read("CertificationStats-2016-2-17-15") } - let(:prev_weekly_stats) { Rails.cache.read("CertificationStats-2016-w06") } - - context ".calculate_all!" do - it "calculates and saves all calculated certification_stats" do - Certification.create(completed_at: 40.days.ago) - Certification.create(completed_at: 7.days.ago) - Certification.create(completed_at: 2.days.ago) - Certification.create(completed_at: 4.hours.ago) - Certification.create(completed_at: 30.minutes.ago) - - CertificationStats.calculate_all! - - expect(monthly_stats[:certifications_completed]).to eq(4) - expect(weekly_stats[:certifications_completed]).to eq(3) - expect(daily_stats[:certifications_completed]).to eq(2) - expect(hourly_stats[:certifications_completed]).to eq(1) - expect(prev_weekly_stats[:certifications_completed]).to eq(1) - end - - it "overwrites incomplete periods" do - Certification.create(completed_at: 30.minutes.ago) - CertificationStats.calculate_all! - Certification.create(completed_at: 1.minute.ago) - CertificationStats.calculate_all! - - expect(hourly_stats[:certifications_completed]).to eq(2) - end - - it "does not recalculate complete periods" do - Certification.create(completed_at: 7.days.ago) - CertificationStats.calculate_all! - Certification.create(completed_at: 7.days.ago) - CertificationStats.calculate_all! - - expect(prev_weekly_stats[:certifications_completed]).to eq(1) - end - end -end diff --git a/spec/models/dispatch/task_spec.rb b/spec/models/dispatch/task_spec.rb index 2ed2640fb83..7f7eef117e5 100644 --- a/spec/models/dispatch/task_spec.rb +++ b/spec/models/dispatch/task_spec.rb @@ -56,7 +56,7 @@ def should_invalidate? let!(:unassigned_task) { FakeTask.create!(aasm_state: :unassigned, prepared_at: Date.yesterday) } let!(:reviewed_task) { FakeTask.create!(aasm_state: :reviewed, prepared_at: Date.yesterday) } - it { is_expected.to eq([unassigned_task, reviewed_task]) } + it { is_expected.to match_array [unassigned_task, reviewed_task] } end context ".completed_on" do @@ -79,7 +79,7 @@ def should_invalidate? end # .sort added in case ActiveRecord returns tasks out of order in subject - it { is_expected.to eq([task_completed_this_morning, task_completed_tonight].sort) } + it { is_expected.to match_array [task_completed_this_morning, task_completed_tonight] } end context ".completed_success" do diff --git a/spec/models/dispatch_stats_spec.rb b/spec/models/dispatch_stats_spec.rb deleted file mode 100644 index 6456d1a3b01..00000000000 --- a/spec/models/dispatch_stats_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -# frozen_string_literal: true - -describe DispatchStats, :postgres do - before do - Timecop.freeze(Time.utc(2016, 2, 17, 20, 59, 0)) - Rails.cache.clear - end - - let(:monthly_stats) { Rails.cache.read("DispatchStats-2016-2") } - let(:weekly_stats) { Rails.cache.read("DispatchStats-2016-w07") } - let(:daily_stats) { Rails.cache.read("DispatchStats-2016-2-17") } - let(:hourly_stats) { Rails.cache.read("DispatchStats-2016-2-17-15") } - let(:prev_weekly_stats) { Rails.cache.read("DispatchStats-2016-w06") } - - context ".throttled_calculate_all!" do - subject { DispatchStats.throttled_calculate_all! } - context "when not previously calculated" do - it "calculates stats" do - expect(DispatchStats).to receive(:calculate_all!) - subject - expect(Rails.cache.read("DispatchStats-last-calculated-timestamp")).to eq(Time.now.to_i) - end - end - - context "when last calculated more than 61 minutes ago" do - before { Rails.cache.write("DispatchStats-last-calculated-timestamp", 61.minutes.ago.to_i) } - - it "calculates stats" do - expect(DispatchStats).to receive(:calculate_all!) - subject - expect(Rails.cache.read("DispatchStats-last-calculated-timestamp")).to eq(Time.now.to_i) - end - end - - context "when last calculated less than 60 minutes ago" do - before { Rails.cache.write("DispatchStats-last-calculated-timestamp", 59.minutes.ago.to_i) } - - it "doesn't recalculate stats" do - expect(DispatchStats).to_not receive(:calculate_all!) - subject - end - end - end - - context ".calculate_all!" do - it "calculates and saves all completed dispatch_stats" do - Generators::EstablishClaim.create(completed_at: 40.days.ago) - Generators::EstablishClaim.create(completed_at: 7.days.ago) - Generators::EstablishClaim.create(completed_at: 2.days.ago) - Generators::EstablishClaim.create(completed_at: 4.hours.ago) - Generators::EstablishClaim.create(completed_at: 30.minutes.ago) - - DispatchStats.calculate_all! - - expect(monthly_stats[:establish_claim_completed]).to eq(4) - expect(weekly_stats[:establish_claim_completed]).to eq(3) - expect(daily_stats[:establish_claim_completed]).to eq(2) - expect(hourly_stats[:establish_claim_completed]).to eq(1) - expect(prev_weekly_stats[:establish_claim_completed]).to eq(1) - end - - it "calculates and saves all started dispatch_stats" do - Generators::EstablishClaim.create(started_at: 40.days.ago) - Generators::EstablishClaim.create(started_at: 7.days.ago) - Generators::EstablishClaim.create(started_at: 2.days.ago) - Generators::EstablishClaim.create(started_at: 4.hours.ago) - Generators::EstablishClaim.create(started_at: 30.minutes.ago) - - DispatchStats.calculate_all! - - expect(monthly_stats[:establish_claim_started]).to eq(4) - expect(weekly_stats[:establish_claim_started]).to eq(3) - expect(daily_stats[:establish_claim_started]).to eq(2) - expect(hourly_stats[:establish_claim_started]).to eq(1) - expect(prev_weekly_stats[:establish_claim_started]).to eq(1) - end - - it "calculates and saves all canceled dispatch_stats" do - Generators::EstablishClaim.create(completed_at: 30.minutes.ago, completion_status: 1) - - DispatchStats.calculate_all! - - expect(monthly_stats[:establish_claim_canceled]).to eq(1) - expect(weekly_stats[:establish_claim_canceled]).to eq(1) - expect(daily_stats[:establish_claim_canceled]).to eq(1) - expect(hourly_stats[:establish_claim_canceled]).to eq(1) - expect(prev_weekly_stats[:establish_claim_canceled]).to eq(0) - end - - it "filters remands and partial grants from full grants" do - remand = Generators::EstablishClaim.create(completed_at: 7.days.ago) - full_grant = Generators::EstablishClaim.create(completed_at: 7.days.ago) - ClaimEstablishment.create(task_id: remand.id, decision_type: 3) - ClaimEstablishment.create(task_id: full_grant.id, decision_type: 1) - - DispatchStats.calculate_all! - - expect(monthly_stats[:establish_claim_full_grant_completed]).to eq(1) - expect(monthly_stats[:establish_claim_partial_grant_remand_completed]).to eq(1) - end - end -end diff --git a/spec/models/docket_spec.rb b/spec/models/docket_spec.rb index 620fe365327..78e6734650b 100644 --- a/spec/models/docket_spec.rb +++ b/spec/models/docket_spec.rb @@ -213,8 +213,7 @@ end context "blocking mail tasks with status completed or cancelled" do - it "includes those appeals", - skip: "https://github.com/department-of-veterans-affairs/caseflow/issues/10516#issuecomment-503269122" do + it "includes those appeals" do with_blocking_but_closed_tasks = create(:appeal, :with_post_intake_tasks, docket_type: Constants.AMA_DOCKETS.direct_review) diff --git a/spec/models/judge_case_review_spec.rb b/spec/models/judge_case_review_spec.rb index 9e9f79e57e4..5d8131d4308 100644 --- a/spec/models/judge_case_review_spec.rb +++ b/spec/models/judge_case_review_spec.rb @@ -23,7 +23,7 @@ def complete_params(judge:, attorney:, location:, vacols_issue1:, vacols_issue2: # rubocop:disable Metrics/AbcSize def expect_decass_to_be_up_to_date(decass) decass.reload - expect(decass.demdusr).to eq "CFS456" + expect(decass.demdusr).to eq vacols_judge.slogid expect(decass.defdiff).to eq "3" expect(decass.deoq).to eq "1" expect(decass.deqr2).to eq "Y" @@ -35,7 +35,6 @@ def expect_decass_to_be_up_to_date(decass) expect(decass.decomp).to eq VacolsHelper.local_date_with_utc_timezone expect(decass.detrem).to eq "N" end -# rubocop:enable Metrics/AbcSize def expect_case_to_be_update_to_date(vacols_case, decass) expect(vacols_case.bfmemid).to eq(decass.dememid) @@ -43,6 +42,20 @@ def expect_case_to_be_update_to_date(vacols_case, decass) expect(vacols_case.bfboard).to eq(decass.deteam) end +def expect_case_review_to_match_params(case_review) + expect(case_review.valid?).to eq true + expect(case_review.complexity).to eq "hard" + expect(case_review.quality).to eq "does_not_meet_expectations" + expect(case_review.comment).to eq "do this" + expect(case_review.factors_not_considered).to match_array %w[theory_contention relevant_records] + expect(case_review.areas_for_improvement).to match_array ["process_violations"] + expect(case_review.judge).to eq judge + expect(case_review.attorney).to eq attorney + expect(case_review.appeal_type).to eq "LegacyAppeal" + expect(case_review.appeal_id).to eq LegacyAppeal.find_by_vacols_id(vacols_case.bfkey).id +end +# rubocop:enable Metrics/AbcSize + describe JudgeCaseReview, :all_dbs do before do Timecop.freeze(Time.utc(2019, 1, 1, 12, 0, 0)) @@ -84,8 +97,10 @@ def expect_case_to_be_update_to_date(vacols_case, decass) end context ".create" do - let(:judge) { User.create(css_id: "CFS123", station_id: User::BOARD_STATION_ID) } - let(:attorney) { User.create(css_id: "CFS456", station_id: "317") } + let(:judge) { create(:user, :judge) } + let!(:vacols_judge) { create(:staff, :judge_role, user: judge) } + let(:attorney) { create(:user, station_id: "317") } + let!(:vacols_attorney) { create(:staff, :attorney_role, user: attorney) } let(:probability) { JudgeCaseReview::QUALITY_REVIEW_SELECTION_PROBABILITY } subject { JudgeCaseReview.complete(params) } @@ -129,7 +144,7 @@ def expect_case_to_be_update_to_date(vacols_case, decass) let!(:decass) do create(:decass, deadtim: "2013-12-06".to_date, - defolder: "123456", + defolder: vacols_case.bfkey, deprod: work_product, deatty: "102", deteam: "BB", @@ -137,9 +152,8 @@ def expect_case_to_be_update_to_date(vacols_case, decass) dedeadline: 6.days.ago) end let!(:vacols_case) { create(:case, bfkey: "123456") } - let(:vacols_issue1) { create(:case_issue, isskey: "123456") } - let(:vacols_issue2) { create(:case_issue, isskey: "123456") } - let!(:judge_staff) { create(:staff, :judge_role, slogid: "CFS456", sdomainid: judge.css_id, sattyid: "AA") } + let(:vacols_issue1) { create(:case_issue, isskey: vacols_case.bfkey) } + let(:vacols_issue2) { create(:case_issue, isskey: vacols_case.bfkey) } context "when all parameters are present to sign a decision and VACOLS update is successful" do before do @@ -173,63 +187,42 @@ def expect_case_to_be_update_to_date(vacols_case, decass) it "should create judge case review and change the location to quality review" do allow_any_instance_of(JudgeCaseReview).to receive(:rand).and_return(probability / 2) - expect(subject.valid?).to eq true expect(subject.location).to eq "quality_review" - expect(subject.complexity).to eq "hard" - expect(subject.quality).to eq "does_not_meet_expectations" - expect(subject.comment).to eq "do this" - expect(subject.factors_not_considered).to eq %w[theory_contention relevant_records] - expect(subject.areas_for_improvement).to eq ["process_violations"] - expect(subject.judge).to eq judge - expect(subject.attorney).to eq attorney - - expect(subject.appeal_type).to eq "LegacyAppeal" - expect(subject.appeal_id).to eq LegacyAppeal.find_by_vacols_id(vacols_case.bfkey).id + expect_case_review_to_match_params(subject) - expect(decass.reload.demdusr).to eq "CFS456" - expect(decass.defdiff).to eq "3" - expect(decass.deoq).to eq "1" - expect(decass.deqr2).to eq "Y" - expect(decass.deqr6).to eq "Y" - expect(decass.deqr9).to eq "Y" - expect(decass.deqr1).to eq nil - expect(decass.deqr3).to eq nil - expect(decass.deqr4).to eq nil - expect(decass.dememid).to eq "AA" - expect(decass.decomp).to eq VacolsHelper.local_date_with_utc_timezone - expect(decass.detrem).to eq "N" + expect_decass_to_be_up_to_date(decass) expect(vacols_case.reload.bfcurloc).to eq "48" - expect(vacols_case.bfmemid).to eq "AA" + expect(vacols_case.bfmemid).to eq vacols_judge.sattyid expect(vacols_case.bfattid).to eq "102" expect(vacols_case.bfboard).to eq "BB" - vacols_issues = VACOLS::CaseIssue.where(isskey: "123456") + vacols_issues = VACOLS::CaseIssue.where(isskey: vacols_case.bfkey) # 1 vacated, 1 remanded and 1 blank issue created because of vacated disposition expect(vacols_issues.size).to eq 3 vacols_issue = vacols_issues.find_by(issseq: 1) expect(vacols_issue.issdc).to eq "5" expect(vacols_issue.issseq).to eq vacols_issue1.issseq - expect(vacols_issue.issmduser).to eq "CFS456" + expect(vacols_issue.issmduser).to eq vacols_judge.slogid vacols_issue = vacols_issues.find_by(issseq: 2) expect(vacols_issue.issdc).to eq "3" expect(vacols_issue.issseq).to eq vacols_issue2.issseq - expect(vacols_issue.issmduser).to eq "CFS456" + expect(vacols_issue.issmduser).to eq vacols_judge.slogid vacols_issue = vacols_issues.find_by(issseq: 3) expect(vacols_issue.issdc).to eq nil expect(vacols_issue.issseq).to eq(vacols_issue2.issseq + 1) - expect(vacols_issue.issaduser).to eq "CFS456" + expect(vacols_issue.issaduser).to eq vacols_judge.slogid remand_reasons = VACOLS::RemandReason.where(rmdkey: "123456", rmdissseq: vacols_issue2.issseq) expect(remand_reasons.size).to eq 1 expect(remand_reasons.first.rmdissseq).to eq vacols_issue2.issseq - expect(remand_reasons.first.rmdmdusr).to eq "CFS456" + expect(remand_reasons.first.rmdmdusr).to eq vacols_judge.slogid quality_review_record = VACOLS::DecisionQualityReview.find_by(qrfolder: vacols_case.bfkey) - expect(quality_review_record.qrsmem).to eq "AA" + expect(quality_review_record.qrsmem).to eq vacols_judge.sattyid expect(quality_review_record.qrteam).to eq "BB" expect(quality_review_record.qrseldate).to eq VacolsHelper.local_date_with_utc_timezone expect(quality_review_record.qryymm).to eq "1901" @@ -250,18 +243,8 @@ def expect_case_to_be_update_to_date(vacols_case, decass) it "should create Judge Case Review" do allow_any_instance_of(JudgeCaseReview).to receive(:rand).and_return(probability + probability) - expect(subject.valid?).to eq true expect(subject.location).to eq "bva_dispatch" - expect(subject.judge).to eq judge - expect(subject.attorney).to eq attorney - expect(subject.complexity).to eq "hard" - expect(subject.quality).to eq "does_not_meet_expectations" - expect(subject.comment).to eq "do this" - expect(subject.factors_not_considered).to eq %w[theory_contention relevant_records] - expect(subject.areas_for_improvement).to eq ["process_violations"] - - expect(subject.appeal_type).to eq "LegacyAppeal" - expect(subject.appeal_id).to eq LegacyAppeal.find_by_vacols_id(vacols_case.bfkey).id + expect_case_review_to_match_params(subject) expect_decass_to_be_up_to_date(decass) @@ -269,26 +252,26 @@ def expect_case_to_be_update_to_date(vacols_case, decass) expect(vacols_case.bfcurloc).to eq "4E" expect_case_to_be_update_to_date(vacols_case, decass) - vacols_issues = VACOLS::CaseIssue.where(isskey: "123456") + vacols_issues = VACOLS::CaseIssue.where(isskey: vacols_case.bfkey) # 1 vacated, 1 remanded and 1 blank issue created because of vacated disposition expect(vacols_issues.size).to eq 3 - expect(vacols_issues.first.issdc).to eq "5" - expect(vacols_issues.first.issseq).to eq vacols_issue1.issseq - expect(vacols_issues.first.issmduser).to eq "CFS456" + vacols_issue = vacols_issues.find_by(issseq: vacols_issue1.issseq) + expect(vacols_issue.issdc).to eq "5" + expect(vacols_issue.issmduser).to eq vacols_judge.slogid - expect(vacols_issues.second.issdc).to eq "3" - expect(vacols_issues.second.issseq).to eq vacols_issue2.issseq - expect(vacols_issues.second.issmduser).to eq "CFS456" + vacols_issue = vacols_issues.find_by(issseq: vacols_issue2.issseq) + expect(vacols_issue.issdc).to eq "3" + expect(vacols_issue.issmduser).to eq vacols_judge.slogid - expect(vacols_issues.third.issdc).to eq nil - expect(vacols_issues.third.issseq).to eq(vacols_issue2.issseq + 1) - expect(vacols_issues.third.issaduser).to eq "CFS456" + vacols_issue = vacols_issues.find_by(issseq: vacols_issue2.issseq + 1) + expect(vacols_issue.issdc).to eq nil + expect(vacols_issue.issaduser).to eq vacols_judge.slogid remand_reasons = VACOLS::RemandReason.where(rmdkey: "123456", rmdissseq: vacols_issue2.issseq) expect(remand_reasons.size).to eq 1 expect(remand_reasons.first.rmdissseq).to eq vacols_issue2.issseq - expect(remand_reasons.first.rmdmdusr).to eq "CFS456" + expect(remand_reasons.first.rmdmdusr).to eq vacols_judge.slogid expect(VACOLS::DecisionQualityReview.find_by(qrfolder: vacols_case.bfkey)).to eq nil end diff --git a/spec/models/task_filter_spec.rb b/spec/models/task_filter_spec.rb index 4204ffecc2c..98e7036659b 100644 --- a/spec/models/task_filter_spec.rb +++ b/spec/models/task_filter_spec.rb @@ -381,7 +381,7 @@ def create_cached_appeals_for_tasks(tasks, case_type) ["col=#{Constants.QUEUE_CONFIG.COLUMNS.APPEAL_TYPE.name}&val=#{case_types['1']}|#{case_types['2']}"] end - it "returns tasks with Original or Supplemental case types", skip: "flakey" do + it "returns tasks with Original or Supplemental case types" do expect(subject.map(&:id)).to match_array(tasks_type_original.map(&:id) + tasks_type_supplemental.map(&:id)) end end diff --git a/spec/models/tasks/bva_dispatch_task_spec.rb b/spec/models/tasks/bva_dispatch_task_spec.rb index cf3d50caba9..9124d11eaf8 100644 --- a/spec/models/tasks/bva_dispatch_task_spec.rb +++ b/spec/models/tasks/bva_dispatch_task_spec.rb @@ -113,7 +113,7 @@ decision_document = DecisionDocument.find_by(appeal_id: root_task.appeal.id) expect(ProcessDecisionDocumentJob).to have_received(:perform_later) - .with(decision_document.id).exactly(:once) + .with(decision_document.id, nil).exactly(:once) expect(decision_document).to_not eq nil expect(decision_document.document_type).to eq "BVA Decision" expect(decision_document.source).to eq "BVA" @@ -144,7 +144,7 @@ decision_document = DecisionDocument.find_by(appeal_id: legacy_appeal.id) expect(ProcessDecisionDocumentJob).to have_received(:perform_later) - .with(decision_document.id).exactly(:once) + .with(decision_document.id, nil).exactly(:once) expect(decision_document).to_not eq nil expect(decision_document.document_type).to eq "BVA Decision" expect(decision_document.source).to eq "BVA" @@ -248,7 +248,7 @@ decision_document = DecisionDocument.find_by(appeal_id: root_task.appeal.id) expect(ProcessDecisionDocumentJob).to have_received(:perform_later) - .with(decision_document.id).exactly(:once) + .with(decision_document.id, nil).exactly(:once) expect(decision_document).to_not eq nil expect(decision_document.document_type).to eq "BVA Decision" expect(decision_document.source).to eq "BVA" diff --git a/spec/models/validators/intake_start_validator_spec.rb b/spec/models/validators/intake_start_validator_spec.rb index fc8fba85b9c..45c0056b69c 100644 --- a/spec/models/validators/intake_start_validator_spec.rb +++ b/spec/models/validators/intake_start_validator_spec.rb @@ -21,7 +21,7 @@ context "when BGS shows a station conflict" do let(:station_conflict) { true } - it "sets error_code \"veteran_not_modifiable\" when BGS shows a station conflict", skip: "Flake" do + it "sets error_code \"veteran_not_modifiable\" when BGS shows a station conflict" do subject expect(intake.error_code).to eq "veteran_not_modifiable" @@ -33,7 +33,7 @@ context "intake user is on the BVA Intake team" do before { BvaIntake.singleton.add_user(user) } - it "sets a veteran_not_modifiable error code", skip: "Flake" do + it "sets a veteran_not_modifiable error code" do subject expect(intake.error_code).to eq "veteran_not_modifiable" diff --git a/spec/models/vbms_communication_package_spec.rb b/spec/models/vbms_communication_package_spec.rb new file mode 100644 index 00000000000..ce427dc7327 --- /dev/null +++ b/spec/models/vbms_communication_package_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +describe VbmsCommunicationPackage, :postgres do + let(:package) do + VbmsCommunicationPackage.new( + file_number: "329780002", + comm_package_name: "test package name", + copies: 1, + document_mailable_via_pacman: VbmsUploadedDocument.new + ) + end + + it "is valid with valid attributes" do + expect(package).to be_valid + end + + it "is not valid without a filenumber" do + package.file_number = nil + expect(package).to_not be_valid + expect(package.errors[:file_number]).to eq(["can't be blank"]) + end + + it "is not valid without a communication package name" do + package.comm_package_name = nil + expect(package).to_not be_valid + expect(package.errors[:comm_package_name]).to eq( + [ + "can't be blank", + "is too short (minimum is 1 character)", + "is invalid" + ] + ) + end + + it "is not valid if communication package name exceeds 255 characters" do + package.comm_package_name = "x" * 256 + expect(package).to_not be_valid + expect(package.errors[:comm_package_name]).to eq(["is too long (maximum is 255 characters)", "is invalid"]) + end + + it "is not valid without a user friendly communication package name" do + package.comm_package_name = "(test package name with parentheses)" + expect(package).to_not be_valid + expect(package.errors[:comm_package_name]).to eq(["is invalid"]) + end + + it "is not valid without a copies attribute" do + package.copies = nil + expect(package).to_not be_valid + expect(package.errors[:copies]).to eq(["can't be blank", "is not a number"]) + end + + it "is not valid with less than one copy" do + package.copies = 0 + expect(package).to_not be_valid + expect(package.errors[:copies]).to eq(["must be greater than 0"]) + end + + it "is not valid with more than 500 copies" do + package.copies = 500 + expect(package).to be_valid + + package.copies = 501 + expect(package).to_not be_valid + expect(package.errors[:copies]).to eq(["must be less than 501"]) + end + + it "is not valid without an associated document mailable via pacman" do + package.document_mailable_via_pacman = nil + expect(package).to_not be_valid + expect(package.errors[:document_mailable_via_pacman]).to eq(["must exist"]) + end +end diff --git a/spec/models/vbms_distribution_destination_spec.rb b/spec/models/vbms_distribution_destination_spec.rb new file mode 100644 index 00000000000..e23f2f1c18f --- /dev/null +++ b/spec/models/vbms_distribution_destination_spec.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +describe VbmsDistributionDestination, :postgres do + let(:distribution) { VbmsDistribution.new } + + shared_examples "destination has valid attributes" do + it "is valid with valid attributes" do + expect(destination).to be_valid + end + end + + let(:destination) do + VbmsDistributionDestination.new( + destination_type: "domesticAddress", + vbms_distribution: distribution, + address_line_1: "address line 1", + city: "city", + state: "NY", + postal_code: "11385", + country_code: "US" + ) + end + + include_examples "destination has valid attributes" + + it "is not valid without a destination type" do + destination.destination_type = nil + expect(destination).to_not be_valid + expect(destination.errors[:destination_type]).to eq(["can't be blank", "is not included in the list"]) + end + + it "is not valid with incorrect destination type" do + destination.destination_type = "DomesticAddress" + expect(destination).to_not be_valid + expect(destination.errors[:destination_type]).to eq(["is not included in the list"]) + end + + it "is not valid without an associated VbmsDistribution" do + destination.vbms_distribution = nil + expect(destination).to_not be_valid + expect(destination.errors[:vbms_distribution]).to eq(["must exist"]) + end + + shared_examples "destination is a physical mailing address" do + it "is not valid without an address line 1" do + destination.address_line_1 = nil + expect(destination).to_not be_valid + expect(destination.errors[:address_line_1]).to eq(["can't be blank"]) + end + + it "is not valid without an address line 2 if treat_line_2_as_addressee is true" do + destination.treat_line_2_as_addressee = true + destination.address_line_2 = nil + expect(destination).to_not be_valid + expect(destination.errors[:address_line_2]).to eq(["can't be blank"]) + end + + it "is not valid without an address line 3 if treat_line_3_as_addressee is true" do + destination.treat_line_3_as_addressee = true + destination.address_line_3 = nil + expect(destination).to_not be_valid + expect(destination.errors[:address_line_3]).to eq(["can't be blank"]) + end + + it "is not valid if treat_line_3_as_addressee is true and treat_line_2_as_addressee is false" do + destination.treat_line_3_as_addressee = true + destination.treat_line_2_as_addressee = false + expect(destination).to_not be_valid + expect(destination.errors[:treat_line_2_as_addressee]) + .to eq(["cannot be false if line 3 is treated as addressee"]) + end + + it "is not valid without a city" do + destination.city = nil + expect(destination).to_not be_valid + expect(destination.errors[:city]).to eq(["can't be blank"]) + end + + it "is not valid without a country code" do + destination.country_code = nil + expect(destination).to_not be_valid + expect(destination.errors[:country_code]).to eq(["can't be blank", "is not a valid ISO 3166-2 code"]) + end + + it "is not valid without a two-letter ISO 3166-2 country code" do + destination.country_code = "XX" + expect(destination).to_not be_valid + expect(destination.errors[:country_code]).to eq(["is not a valid ISO 3166-2 code"]) + end + end + + shared_examples "destination is a US address" do + it "is not valid without a state" do + destination.state = nil + expect(destination).to_not be_valid + expect(destination.errors[:state]).to eq(["can't be blank", "is not a valid ISO 3166-2 code"]) + end + + it "is not valid without a two-letter ISO 3166-2 state code" do + destination.state = "XX" + expect(destination).to_not be_valid + expect(destination.errors[:state]).to eq(["is not a valid ISO 3166-2 code"]) + end + + it "is not valid without a postal code" do + destination.postal_code = nil + expect(destination).to_not be_valid + expect(destination.errors[:postal_code]).to eq(["can't be blank"]) + end + end + + context "destination type is domesticAddress" do + include_examples "destination has valid attributes" + include_examples "destination is a physical mailing address" + include_examples "destination is a US address" + end + + context "destination type is militaryAddress" do + let(:destination) do + VbmsDistributionDestination.new( + destination_type: "militaryAddress", + vbms_distribution: distribution, + address_line_1: "address line 1", + city: "city", + state: "NY", + postal_code: "11385", + country_code: "US" + ) + end + + include_examples "destination has valid attributes" + include_examples "destination is a physical mailing address" + include_examples "destination is a US address" + end + + context "destination type is internationalAddress" do + let(:destination) do + VbmsDistributionDestination.new( + destination_type: "internationalAddress", + vbms_distribution: distribution, + address_line_1: "address line 1", + city: "city", + country_name: "France", + country_code: "FR" + ) + end + + include_examples "destination has valid attributes" + include_examples "destination is a physical mailing address" + + it "is not valid without a country name" do + destination.country_name = nil + expect(destination).to_not be_valid + expect(destination.errors[:country_name]).to eq(["can't be blank"]) + end + end +end diff --git a/spec/models/vbms_distribution_spec.rb b/spec/models/vbms_distribution_spec.rb new file mode 100644 index 00000000000..7182708ceab --- /dev/null +++ b/spec/models/vbms_distribution_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +describe VbmsDistribution, :postgres do + let(:package) { VbmsCommunicationPackage.new } + + shared_examples "distribution has valid attributes" do + it "is valid with valid attributes" do + expect(distribution).to be_valid + end + end + + let(:distribution) do + VbmsDistribution.new( + recipient_type: "person", + vbms_communication_package: package, + first_name: "First", + last_name: "Last" + ) + end + + include_examples "distribution has valid attributes" + + it "is valid without an associated VbmsCommunicationPackage" do + distribution.vbms_communication_package = nil + expect(distribution).to be_valid + end + + it "is not valid without a recipient type" do + distribution.recipient_type = nil + expect(distribution).to_not be_valid + expect(distribution.errors[:recipient_type]).to eq(["can't be blank", "is not included in the list"]) + end + + it "is not valid with incorrect recipient type" do + distribution.recipient_type = "Person" + expect(distribution).to_not be_valid + expect(distribution.errors[:recipient_type]).to eq(["is not included in the list"]) + end + + context "recipient type is person" do + it "is not valid without a first name" do + distribution.first_name = nil + expect(distribution).to_not be_valid + expect(distribution.errors[:first_name]).to eq(["can't be blank"]) + end + + it "is not valid without a last name" do + distribution.last_name = nil + expect(distribution).to_not be_valid + expect(distribution.errors[:last_name]).to eq(["can't be blank"]) + end + end + + shared_examples "recipient type is not person" do + it "is not valid without a name" do + distribution.name = nil + expect(distribution).to_not be_valid + expect(distribution.errors[:name]).to eq(["can't be blank"]) + end + end + + context "recipient type is organization" do + let(:distribution) do + VbmsDistribution.new( + recipient_type: "organization", + vbms_communication_package: package, + name: "Organization" + ) + end + + include_examples "distribution has valid attributes" + include_examples "recipient type is not person" + end + + context "recipient type is system" do + let(:distribution) do + VbmsDistribution.new( + recipient_type: "system", + vbms_communication_package: package, + name: "System" + ) + end + + include_examples "distribution has valid attributes" + include_examples "recipient type is not person" + end + + context "recipient is ro-colocated" do + let(:distribution) do + VbmsDistribution.new( + recipient_type: "ro-colocated", + vbms_communication_package: package, + name: "Ro-Colocated", + poa_code: "poa code", + claimant_station_of_jurisdiction: "claimant station" + ) + end + + include_examples "distribution has valid attributes" + include_examples "recipient type is not person" + + it "is not valid without a poa code" do + distribution.poa_code = nil + expect(distribution).to_not be_valid + expect(distribution.errors[:poa_code]).to eq(["can't be blank"]) + end + + it "is not valid without a claimant station of jurisdiction" do + distribution.claimant_station_of_jurisdiction = nil + expect(distribution).to_not be_valid + expect(distribution.errors[:claimant_station_of_jurisdiction]).to eq(["can't be blank"]) + end + end +end diff --git a/spec/queries/veteran_record_requests_open_for_vre_query_spec.rb b/spec/queries/veteran_record_requests_open_for_vre_query_spec.rb new file mode 100644 index 00000000000..8a531724f49 --- /dev/null +++ b/spec/queries/veteran_record_requests_open_for_vre_query_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +describe VeteranRecordRequestsOpenForVREQuery do + describe ".call" do + subject(:call) { described_class.call } + + context "when there are no VeteranRecordRequests" do + let!(:task) { create(:task) } + + it "is none" do + result = call + + expect(result).to be_a_kind_of(ActiveRecord::Relation) + expect(result).to be_none + end + end + + context "when there are VeteranRecordRequests" do + let!(:cancelled_for_vre) { create(:veteran_record_request_task, :cancelled, assigned_to: vre_business_line) } + let!(:complete_for_vre) { create(:veteran_record_request_task, :completed, assigned_to: vre_business_line) } + + let!(:assigned_for_vre) { create(:veteran_record_request_task, assigned_to: vre_business_line) } + let!(:in_progress_for_vre) { create(:veteran_record_request_task, :in_progress, assigned_to: vre_business_line) } + let!(:on_hold_for_vre) { create(:veteran_record_request_task, :on_hold, assigned_to: vre_business_line) } + + let!(:assigned) { create(:veteran_record_request_task, assigned_to: non_vre_organization) } + let!(:in_progress) { create(:veteran_record_request_task, :in_progress, assigned_to: non_vre_organization) } + let!(:on_hold) { create(:veteran_record_request_task, :on_hold, assigned_to: non_vre_organization) } + + let(:vre_business_line) { create(:vre_business_line) } + let(:non_vre_organization) { create(:organization) } + + it "only returns those that are both open and assigned to the VRE business line" do + result = call + + expect(result).to be_a_kind_of(ActiveRecord::Relation) + expect(result).to contain_exactly( + assigned_for_vre, + in_progress_for_vre, + on_hold_for_vre + ) + end + end + end +end diff --git a/spec/services/external_api/pacman_service_spec.rb b/spec/services/external_api/pacman_service_spec.rb new file mode 100644 index 00000000000..81409e28efa --- /dev/null +++ b/spec/services/external_api/pacman_service_spec.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +describe ExternalApi::PacmanService do + let(:error_response_body) { { "result": "error", "message": { "token": ["error"] } }.as_json } + let(:error_response) do + HTTPI::Response.new(400, {}, error_response_body) + end + let(:forbidden_response) do + HTTPI::Response.new(403, {}, error_response_body) + end + let(:not_found_response) do + HTTPI::Response.new(404, {}, error_response_body) + end + + let(:distribution) do + { + "id" => Fakes::PacmanService::DISTRIBUTION_UUID, + "recipient" => { + "type" => "system", + "id" => "a050a21e-23f6-4743-a1ff-aa1e24412eff", + "name" => "VBMS-C" + }, + "description" => "Staging Mailing Distribution", + "communicationPackageId" => 1, + "destinations" => [{ + "type" => "physicalAddress", + "id" => "28440040-51a5-4d2a-81a2-28730827be14", + "status" => "", + "cbcmSendAttemptDate" => "2022-06-06T16:35:27.996", + "addressLine1" => "POSTMASTER GENERAL", + "addressLine2" => "UNITED STATES POSTAL SERVICE", + "addressLine3" => "475 LENFANT PLZ SW RM 10022", + "addressLine4" => "SUITE 123", + "addressLine5" => "APO AE 09001-5275", + "addressLine6" => "", + "treatLine2AsAddressee" => true, + "treatLine3AsAddressee" => true, + "city" => "WASHINGTON DC", + "state" => "DC", + "postalCode" => "12345", + "countryName" => "UNITED STATES", + "countryCode" => "us" + }], + "status" => "", + "sentToCbcmDate" => "" + }.as_json + end + + let(:vbms_distribution) do + create(:vbms_distribution, uuid: SecureRandom.uuid) + end + + let(:distribution_post_request) do + { + "communicationPackageId" => "673c8b4a-cb7d-4fdf-bc4d-998d6d5d7431", + "recipient" => { + "type" => nil, + "name" => nil, + "firstName" => nil, + "middleName" => nil, + "lastName" => nil, + "participantId" => nil, + "poaCode" => nil, + "claimantStationOfJurisdiction" => nil + }, + "destinations" => [{ + "type" => nil, + "addressLine1" => nil, + "addressLine2" => nil, + "addressLine3" => nil, + "addressLine4" => nil, + "addressLine5" => nil, + "addressLine6" => nil, + "treatLine2AsAddressee" => nil, + "treatLine3AsAddressee" => nil, + "city" => nil, + "state" => nil, + "postalCode" => nil, + "countryName" => nil, + "countryCode" => nil + }] + }.as_json + end + + let(:distribution_post_response) do + { + "id" => "673c8b4a-cb7d-4fdf-bc4d-998d6d5d7431", + "recipient" => { + "type" => "system", + "id" => "2c6592fc-b3af-48ff-8263-c581c2f0a68b", + "name" => "VBMS-C" + }, + "description" => "Staging Distribution", + "communicationPackageId" => "673c8b4a-cb7d-4fdf-bc4d-998d6d5d7431", + "destinations" => [{ + "type" => "physicalAddress", + "id" => "5378bfbd-eff5-470c-bbc4-c7fd3c863a50", + "status" => "null", + "cbcmSendAttemptDate" => "2022-06-06T16:35:28.017", + "addressLine1" => "POSTMASTER GENERAL", + "addressLine2" => "UNITED STATES POSTAL SERVICE", + "addressLine3" => "475 LENFANT PLZ SW RM 10022", + "addressLine4" => "SUITE 123", + "addressLine5" => "APO AE 09001-5275", + "addressLine6" => "", + "treatLine2AsAddressee" => false, + "treatLine3AsAddressee" => false, + "city" => "WASHINGTON DC", + "state" => "DC", + "postalCode" => "12345", + "countryName" => "UNITED STATES", + "countryCode" => "us" + }], + "status" => "null", + "sentToCbcmDate" => "null" + }.as_json + end + + let(:package_post_request) do + { + "fileNumber" => "123456789", + "name" => "ABC abc 1234 !*+,-.:;=?", + "documentReferences" => [{ + "id" => "3aec91cc-a88d-4b9c-9183-84bed583bbcc", + "copies" => 1 + }] + }.as_json + end + + let(:package_post_response) do + { + "id" => "24eb6a66-3833-4de6-bea4-4b614e55d5ac", + "fileNumber" => "123456789", + "documentReferences" => [{ + "id" => "23233175-6a87-4cd4-b327-f20cf5ef1222", + "copies" => 1 + }], + "status" => "NEW", + "createDate" => "" + }.as_json + end + + let(:get_distribution_success_response) do + HTTPI::Response.new(200, {}, distribution) + end + + let(:post_distribution_success_response) do + HTTPI::Response.new(201, {}, distribution_post_response) + end + + let(:post_package_success_response) do + HTTPI::Response.new(201, {}, package_post_response) + end + + context "get distribution" do + subject { Fakes::PacmanService.get_distribution_request(vbms_distribution.uuid) } + it "gets correct distribution" do + expect(subject.body.as_json).to eq(get_distribution_success_response.body) + end + context "not found" do + subject { Fakes::PacmanService.get_distribution_request("fake") } + it "returns 404 PacmanNotFoundError" do + expect(subject.code).to eq(not_found_response.code) + end + end + end + + context "creates and submits distribution" do + subject do + ExternalApi::PacmanService.send_distribution_request(distribution_post_request["communicationPackageId"], + distribution_post_request["recipient"], + distribution_post_request["destinations"]) + end + it "successfully sends distribution" do + allow(HTTPI) + .to receive(:post) do |req| + expect(JSON.parse(req.body)).to eq distribution_post_request + end.and_return(post_distribution_success_response) + expect(subject.first.body.as_json).to eq(post_distribution_success_response.body) + end + end + + context "creates and sends communication package" do + subject do + ExternalApi::PacmanService.send_communication_package_request(package_post_request["file_number"], + package_post_request["name"], + package_post_request["documentReferences"]) + end + it "successfully sends package" do + allow(HTTPI).to receive(:post).and_return(post_package_success_response) + expect(subject.body.as_json).to include(post_package_success_response.body) + end + end + + describe "response failure" do + subject { ExternalApi::PacmanService.get_distribution_request(vbms_distribution.uuid) } + + context "400" do + it "throws Caseflow::Error::PacmanBadRequestError" do + allow(HTTPI).to receive(:get).and_return(error_response) + expect(subject).to eq(error_response) + end + end + + context "403" do + it "throws Caseflow::Error::PacmanForbiddenError" do + allow(HTTPI).to receive(:get).and_return(forbidden_response) + expect(subject).to eq(forbidden_response) + end + end + end +end diff --git a/spec/support/queue_helpers.rb b/spec/support/queue_helpers.rb index bfd61ed4c25..39d1cbabb0c 100644 --- a/spec/support/queue_helpers.rb +++ b/spec/support/queue_helpers.rb @@ -9,6 +9,10 @@ def disposition_text mtv_const.DISPOSITION_TEXT.to_h end + def disposition_timeline_text + mtv_const.DISPOSITION_TIMELINE_TEXT.to_h + end + def recommendation_text mtv_const.DISPOSITION_RECOMMENDATIONS.to_h end @@ -32,14 +36,26 @@ def format_mtv_attorney_instructions(notes:, disposition:, hyperlinks: []) end def format_mtv_judge_instructions(notes:, disposition:, vacate_type: nil, hyperlink: nil) - parts = ["I am proceeding with a #{disposition_text[disposition.to_sym]}."] - - parts += case disposition - when "granted", "partial" - ["This will be a #{vacate_types[vacate_type.to_sym]}", notes] - else - [notes, "\nHere is the hyperlink to the signed denial document", hyperlink] - end + parts = ["**Motion To Vacate:** \n#{disposition_timeline_text[disposition.to_sym]}\n"] + + case disposition + when "granted", "partially_granted" + parts += ["**Type:** "] + parts += ["#{vacate_types[vacate_type.to_sym]}\n"] + if !notes.empty? + parts += ["**Detail:** "] + parts += ["#{notes}\n"] + end + when "denied", "dismissed" + if !notes.empty? + parts += ["**Detail:** "] + parts += ["#{notes}\n"] + end + if hyperlink.present? + parts += ["**Hyperlink:** "] + parts += ["#{hyperlink}\n"] + end + end parts.join("\n") end diff --git a/spec/workflows/ama_appeal_dispatch_spec.rb b/spec/workflows/ama_appeal_dispatch_spec.rb index 05b4231b62a..62a1e336e61 100644 --- a/spec/workflows/ama_appeal_dispatch_spec.rb +++ b/spec/workflows/ama_appeal_dispatch_spec.rb @@ -1,32 +1,77 @@ # frozen_string_literal: true describe AmaAppealDispatch, :postgres do + include ActiveJob::TestHelper + + let(:user) { User.authenticate! } + let(:appeal) { create(:appeal, :advanced_on_docket_due_to_age) } + let(:root_task) { create(:root_task, appeal: appeal) } + let(:poa_participant_id) { "600153863" } + let(:bgs_poa) { instance_double(BgsPowerOfAttorney) } + let(:params) do + { citation_number: "A18123456", + decision_date: Time.zone.now, + redacted_document_location: "C://Windows/User/BLOBLAW/Documents/Decision.docx", + file: "12345678" } + end + let(:mail_package) do + { distributions: [build(:mail_request).call.to_json], + copies: 1, + created_by_id: user.id } + end + + before do + BvaDispatch.singleton.add_user(user) + BvaDispatchTask.create_from_root_task(root_task) + allow(BgsPowerOfAttorney).to receive(:find_or_create_by_file_number) + .with(appeal.veteran_file_number).and_return(bgs_poa) + allow(bgs_poa).to receive(:participant_id).and_return(poa_participant_id) + end + + before(:all) { Seeds::NotificationEvents.new.seed! } + + subject do + perform_enqueued_jobs do + AmaAppealDispatch.new(appeal: appeal, params: params, user: user, mail_package: mail_package).call + end + end + describe "#call" do it "stores current POA participant ID in the Appeals table" do - user = create(:user) - BvaDispatch.singleton.add_user(user) - appeal = create(:appeal, :advanced_on_docket_due_to_age) - root_task = create(:root_task, appeal: appeal) - BvaDispatchTask.create_from_root_task(root_task) - poa_participant_id = "600153863" - - bgs_poa = instance_double(BgsPowerOfAttorney) - allow(BgsPowerOfAttorney).to receive(:find_or_create_by_file_number) - .with(appeal.veteran_file_number).and_return(bgs_poa) - allow(bgs_poa).to receive(:participant_id).and_return(poa_participant_id) - - params = { - appeal_id: appeal.id, - appeal_type: "Appeal", - citation_number: "A18123456", - decision_date: Time.zone.now, - redacted_document_location: "C://Windows/User/BLOBLAW/Documents/Decision.docx", - file: "12345678" - } - - AmaAppealDispatch.new(appeal: appeal, params: params, user: user).call - - expect(appeal.reload.poa_participant_id).to eq poa_participant_id + subject + expect(appeal.poa_participant_id).to eq poa_participant_id + end + + context "document is associated with a mail request" do + it "calls #perform_later on MailRequestJob" do + expect(MailRequestJob).to receive(:perform_later) do |doc, pkg| + expect(doc).to be_a DecisionDocument + expect(doc.appeal_type).to eq "Appeal" + expect(doc.appeal_id).to eq appeal.id + expect(doc.redacted_document_location).to eq params[:redacted_document_location] + expect(doc.citation_number).to eq params[:citation_number] + + expect(pkg).to eq mail_package + end + + subject + end + end + + context "document is not associated with a mail request" do + let(:mail_package) { nil } + it "does not call #perform_later on MailRequestJob" do + expect(MailRequestJob).to_not receive(:perform_later) + subject + end + end + + context "document is not successfully processed" do + it "does not call #perform_later on MailRequestJob" do + allow(ProcessDecisionDocumentJob).to receive(:perform_later).and_raise(StandardError) + expect(MailRequestJob).to_not receive(:perform_later) + expect { subject }.to raise_error(StandardError) + end end end end diff --git a/spec/workflows/legacy_appeal_dispatch_spec.rb b/spec/workflows/legacy_appeal_dispatch_spec.rb index 59b729349b9..81c40e35793 100644 --- a/spec/workflows/legacy_appeal_dispatch_spec.rb +++ b/spec/workflows/legacy_appeal_dispatch_spec.rb @@ -1,61 +1,108 @@ # frozen_string_literal: true -describe LegacyAppealDispatch do +describe LegacyAppealDispatch, :all_dbs do + include ActiveJob::TestHelper + describe "#call" do - context "invalid citation number" do - it "returns an object with validation errors" do - legacy_appeal = build_stubbed(:legacy_appeal) + let(:user) { User.authenticate! } + let(:legacy_appeal) do + create(:legacy_appeal, + :with_veteran, + vacols_case: create(:case, :aod, :type_cavc_remand, bfregoff: "RO13", + folder: create(:folder, tinum: "13 11-265"))) + end + let(:root_task) { create(:root_task, appeal: legacy_appeal) } + let(:params) do + { appeal_id: legacy_appeal.id, + citation_number: "A18123456", + decision_date: Time.zone.today, + redacted_document_location: "some/filepath", + file: "some file" } + end + let(:mail_package) do + { distributions: [build(:mail_request).call.to_json], + copies: 1, + created_by_id: user.id } + end - params = { - appeal_id: legacy_appeal.id, - citation_number: "123", - decision_date: Time.zone.today, - redacted_document_location: "some/filepath", - file: "some file" - } + before(:all) { Seeds::NotificationEvents.new.seed! } - dispatch = LegacyAppealDispatch.new(appeal: legacy_appeal, params: params).call + before do + BvaDispatch.singleton.add_user(user) + BvaDispatchTask.create_from_root_task(root_task) + end - expect(dispatch).to_not be_success - expect(dispatch.errors[0]).to eq "Citation number is invalid" + subject do + perform_enqueued_jobs do + LegacyAppealDispatch.new(appeal: legacy_appeal, params: params, mail_package: mail_package).call end end - context "citation number already exists" do - it "returns an object with validation errors" do - legacy_appeal = build_stubbed(:legacy_appeal) - - params = { - appeal_id: legacy_appeal.id, - citation_number: "A18123456", - decision_date: Time.zone.today, - redacted_document_location: "some/filepath", - file: "some file" - } + context "valid parameters" do + it "successfully outcodes dispatch" do + expect(subject).to be_success + end + end - dispatch = LegacyAppealDispatch.new(appeal: legacy_appeal, params: params) - allow(dispatch).to receive(:unique_citation_number?).and_return(false) + context "invalid citation number" do + it "returns an object with validation errors" do + params[:citation_number] = "123" + expect(subject).to_not be_success + expect(subject.errors[0]).to eq "Citation number is invalid" + end + end - expect(dispatch.call).to_not be_success - expect(dispatch.call.errors[0]).to eq "Citation number already exists" + context "citation number already exists" do + it "returns an object with validation errors" do + allow_any_instance_of(LegacyAppealDispatch).to receive(:unique_citation_number?).and_return(false) + expect(subject).to_not be_success + expect(subject.errors[0]).to eq "Citation number already exists" end end context "missing required parameters" do it "returns an object with validation errors" do - legacy_appeal = build_stubbed(:legacy_appeal) - - params = { - appeal_id: legacy_appeal.id, - citation_number: "A18123456" - } + params[:decision_date] = nil + params[:redacted_document_location] = nil + params[:file] = nil - dispatch = LegacyAppealDispatch.new(appeal: legacy_appeal, params: params).call error_message = "Decision date can't be blank, Redacted document " \ "location can't be blank, File can't be blank" - expect(dispatch).to_not be_success - expect(dispatch.errors[0]).to eq error_message + expect(subject).to_not be_success + expect(subject.errors[0]).to eq error_message + end + end + + context "dispatch is associated with a mail request" do + it "calls #perform_later on MailRequestJob" do + expect(MailRequestJob).to receive(:perform_later) do |doc, pkg| + expect(doc).to be_a DecisionDocument + expect(doc.appeal_type).to eq "LegacyAppeal" + expect(doc.appeal_id).to eq params[:appeal_id] + expect(doc.citation_number).to eq params[:citation_number] + expect(doc.redacted_document_location).to eq params[:redacted_document_location] + + expect(pkg).to eq mail_package + end + + subject + end + end + + context "document is not associated with a mail request" do + let(:mail_package) { nil } + it "does not call #perform_later on MailRequestJob" do + expect(MailRequestJob).to_not receive(:perform_later) + subject + end + end + + context "document is not successfully processed" do + it "does not call #perform_later on MailRequestJob" do + allow(ProcessDecisionDocumentJob).to receive(:perform_later).and_raise(StandardError) + expect(MailRequestJob).to_not receive(:perform_later) + expect { subject }.to raise_error(StandardError) end end end diff --git a/spec/workflows/mail_request_spec.rb b/spec/workflows/mail_request_spec.rb new file mode 100644 index 00000000000..ef43959f4e7 --- /dev/null +++ b/spec/workflows/mail_request_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +describe MailRequest, :postgres do + let(:mail_request_params) do + ActionController::Parameters.new( + recipient_type: "person", + first_name: "Bob", + last_name: "Smithmetz", + participant_id: "487470002", + destination_type: "domesticAddress", + address_line_1: "1234 Main Street", + treat_line_2_as_addressee: false, + treat_line_3_as_addressee: false, + city: "Orlando", + state: "FL", + postal_code: "12345", + country_code: "US" + ) + end + + let(:invalid_mail_request_params) do + ActionController::Parameters.new( + recipient_type: nil, + last_name: "Smithmetz", + participant_id: "487470002", + destination_type: "domesticAddress", + address_line_1: "1234 Main Street", + treat_line_2_as_addressee: false, + treat_line_3_as_addressee: false, + city: "Orlando", + state: "FL", + postal_code: nil, + country_code: "US" + ) + end + + shared_examples "mail request has valid attributes" do + let(:mail_request_spec_object) { build(:mail_request) } + it "is valid with valid attributes" do + expect(mail_request_spec_object).to be_valid + end + end + + let(:mail_request_spec_object_1) { build(:mail_request, :nil_recipient_type) } + include_examples "mail request has valid attributes" + it "is not valid without a recipient type" do + expect(mail_request_spec_object_1).to_not be_valid + end + + describe "#call" do + context "when valid parameters are passed into the mail requests initialize method." do + subject { described_class.new(mail_request_params).call } + + before do + RequestStore.store[:current_user] = User.system_user + end + + it "creates a vbms_distribution" do + expect { subject }.to change(VbmsDistribution, :count).by(1) + end + + it "creates a vbms_distribution_destination" do + expect { subject }.to change(VbmsDistributionDestination, :count).by(1) + end + end + + context "when invalid parameters are passed into the mail requests initialize method." do + subject { described_class.new(invalid_mail_request_params).call } + it "raises an error" do + expect { subject }.to raise_error(Caseflow::Error::MissingRecipientInfo) + end + end + end +end