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/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/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: + + AND: + + + 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/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/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 88b5f34ca42..e0ba7a1038a 100644 --- a/app/views/queue/index.html.erb +++ b/app/views/queue/index.html.erb @@ -52,7 +52,8 @@ split_appeal_workflow: FeatureToggle.enabled?(:split_appeal_workflow, user: current_user), 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) + cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, 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/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/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/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 b3390b552a2..81670f08808 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 f5bf3fde845..0fa2ef9ac77 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_08_224138) 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" @@ -1789,6 +1791,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'" @@ -2070,6 +2134,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/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/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/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/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/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/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/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/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/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