Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ARP POA Submission] (#3) Decision Creation Controller Action (#101919) #20747

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

pixiitech
Copy link
Contributor

Note: Delete the description statements, complete each step. None are optional, but can be justified as to why they cannot be completed as written. Provide known gaps to testing that may raise the risk of merging to production.

Summary

  • This work is behind a feature toggle (flipper): YES/NO
  • (Summarize the changes that have been made to the platform)
  • (If bug, how to reproduce)
  • (What is the solution, why is this the solution?)
  • (Which team do you work for, does your team own the maintenance of this component?)
  • (If introducing a flipper, what is the success criteria being targeted?)

Related issue(s)

Testing done

  • New code is covered by unit tests
  • Describe what the old behavior was prior to the change
  • Describe the steps required to verify your changes are working as expected. Exclusively stating 'Specs run' is NOT acceptable as appropriate testing
  • If this work is behind a flipper:
    • Tests need to be written for both the flipper on and flipper off scenarios. Docs.
    • What is the testing plan for rolling out the feature?

Screenshots

Note: Optional

What areas of the site does it impact?

(Describe what parts of the site are impacted andifcode touched other areas)

Acceptance criteria

  • I fixed|updated|added unit tests and integration tests for each feature (if applicable).
  • No error nor warning in the console.
  • Events are being sent to the appropriate logging solution
  • Documentation has been updated (link to documentation)
  • No sensitive information (i.e. PII/credentials/internal URLs/etc.) is captured in logging, hardcoded, or specs
  • Feature/bug has a monitor built into Datadog (if applicable)
  • If app impacted requires authentication, did you login to a local build and verify all authenticated routes work as expected
  • I added a screenshot of the developed feature

Requested Feedback

(OPTIONAL)What should the reviewers know in addition to the above. Is there anything specific you wish the reviewer to assist with. Do you have any concerns with this PR, why?

@pixiitech pixiitech changed the base branch from master to art/101919/lighthouse-api-integration February 11, 2025 22:36
@pixiitech pixiitech changed the title [ARP POA submission] (#4) decision submission controller action [ARP POA submission] (#3) decision submission controller action Feb 11, 2025
@pixiitech pixiitech changed the title [ARP POA submission] (#3) decision submission controller action [ARP POA submission] (#3) decision submission controller action (#101919) Feb 11, 2025
Copy link

github-actions bot commented Feb 11, 2025

1 Warning
⚠️ This PR changes 274 LoC (not counting whitespace/newlines).

In order to ensure each PR receives the proper attention it deserves, we recommend not exceeding
200. Expect some delays getting reviews.

File Summary

Files

  • config/settings/test.yml (+3/-0)

  • modules/accredited_representative_portal/app/controllers/accredited_representative_portal/v0/power_of_attorney_request_decisions_controller.rb (+10/-16)

  • modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_form_submission.rb (+9/-0)

  • modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request.rb (+10/-0)

  • modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_decision.rb (+15/-0)

  • modules/accredited_representative_portal/app/models/accredited_representative_portal/power_of_attorney_request_resolution.rb (+8/-0)

  • modules/accredited_representative_portal/app/services/accredited_representative_portal/power_of_attorney_request_service/accept.rb (+116/-0)

  • modules/accredited_representative_portal/config/routes.rb (+1/-1)

  • modules/accredited_representative_portal/spec/requests/accredited_representative_portal/v0/power_of_attorney_request_decisions_spec.rb (+61/-24)

    Note: We exclude files matching the following when considering PR size:

    *.csv, *.json, *.tsv, *.txt, *.md, Gemfile.lock, app/swagger, modules/mobile/docs, spec/fixtures/, spec/support/vcr_cassettes/, modules/mobile/spec/support/vcr_cassettes/, db/seeds, modules/vaos/app/docs, modules/meb_api/app/docs, modules/appeals_api/app/swagger/, *.bru, *.pdf
    

Big PRs are difficult to review, often become stale, and cause delays.

Generated by 🚫 Danger

@va-vfs-bot va-vfs-bot temporarily deployed to art/101919/2122-submission-controller-action/main/main February 11, 2025 22:54 Inactive
@pixiitech pixiitech force-pushed the art/101919/lighthouse-api-integration branch from 81c62b8 to 376a2b6 Compare February 12, 2025 16:41
@pixiitech pixiitech force-pushed the art/101919/2122-submission-controller-action branch from 8913b2c to 39e7b0a Compare February 12, 2025 22:08
@va-vfs-bot va-vfs-bot temporarily deployed to art/101919/2122-submission-controller-action/main/main February 12, 2025 22:09 Inactive
@pixiitech pixiitech force-pushed the art/101919/2122-submission-controller-action branch from 39e7b0a to 222c36c Compare February 13, 2025 14:52
@va-vfs-bot va-vfs-bot temporarily deployed to art/101919/2122-submission-controller-action/main/main February 13, 2025 14:53 Inactive
Copy link
Contributor

@nihil2501 nihil2501 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness and error handling pivot.

The PRs and their sizes have been very approachable.


module AccreditedRepresentativePortal
class PowerOfAttorneyFormSubmission < ApplicationRecord
enum :status, %w[enqueue_succeeded enqueue_failed succeeded failed].index_by(&:itself)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, this is what I'll do for these from now on. Have done values.zip(values).to_h before.

@@ -7,7 +7,7 @@
post 'form21a', to: 'form21a#submit'

resources :in_progress_forms, only: %i[update show destroy]
resources :power_of_attorney_requests, only: %i[index show] do
resources :power_of_attorney_requests, only: %i[index show create] do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snuck in?

resolving = PowerOfAttorneyRequestDecision.create!(type:, creator:)
case decision_params[:type]
when 'acceptance'
raise UnprocessableEntity, 'Reason must be blank' if reason.present?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revert to letting our model validates declarations carry out this error logic. But we can then map them to errors that are contextual to our Accept service within that service (just reiterating the comment that describes the new strategy).

)
service = PowerOfAttorneyRequestService::Accept.new(@poa_request, creator, reason)
poa_form_submission = service.call
raise UnprocessableEntity, poa_form_submission.error_message if poa_form_submission.enqueue_failed?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, looks like service raises in the enqueue-failed case so we would never have hit this code.

raise e
# All other errors: save error data on form submission
rescue => e
create_error_submission(e.message)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't make a second form submission. Rather we should update the one form submission as failing to enqueue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I left this here from before the API call had to be moved out of the transaction.

Comment on lines 21 to 22
service = BenefitsClaims::Service.new(poa_request.claimant.icn)
response = service.submit2122(attributes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could encapsulate all 3 of the methods here.

The line of thinking that led me here (and I think there are others) was that attributes isn't a good name, so then I thought of service_payload for accuracy against the current code structure, but then the redundancy of service_ is potentially a smell (I really wouldn't go as far as calling it that though).

I don't think we necessarily need a class for it, but maybe a single entry function that might call helper functions with repetitively prefixed names (e.g. submit and submit_payload), just some thoughts.

The name attributes really does stick out though.

def attributes
{}.tap do |a|
a[:veteran] = veteran_data
a[:serviceOrganization] = service_org_data
Copy link
Contributor

@nihil2501 nihil2501 Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is a nitpick that doesn't need to be changed—

About naming, I tend to avoid abbreviations unless it gets really dire (poa_request seems like a good convention for local vars for example). Here organization_data comes to mind.

end

def form_data
@form_data ||= JSON.parse(poa_request.power_of_attorney_form.data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PowerOfAttorneyForm#parsed_data is already this method implementation and it is memoized so you can call it repeatedly too.

form_submission
# TODO: call PowerOfAttorneyFormSubmissionJob.perform_async(poa_form_submission)
# Transient 5xx errors: delete objects created, re-raise error
rescue *transient_error_types => e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this sticks around I would actually make it a constant. Fine to do a calculation of another constant like that (and freeze it potentially): TRANSIENT_ERROR_TYPES.


post "/accredited_representative_portal/v0/power_of_attorney_requests/#{poa_request.id}/decision",
params: { decision: { type: 'acceptance', reason: nil } }
VCR.use_cassette('lighthouse/benefits_claims/power_of_attorney_decision/404_response.yml') do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible that VCR comments on PR stack PR preceding this one will lead to changes here. I'll wait until later to review more closely.

Copy link
Contributor

@nihil2501 nihil2501 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Streamlined service control flow
  • Consistent transient error handling
  • Resolution & resolving creation encapsulation
  • Note about service vs controller separation of concerns

Comment on lines 63 to 74
def accept!(creator, reason)
resolving = PowerOfAttorneyRequestDecision.create!(
type: PowerOfAttorneyRequestDecision::Types::ACCEPTANCE, creator:
)
# This form triggers the uniqueness validation, while the `create_resolution!` form triggers
# a more obscure `RecordNotSaved` error that is less functional for getting validation errors.
##
PowerOfAttorneyRequestResolution.create!(power_of_attorney_request: self, resolving:, reason:)
end

def decline!(creator, reason)
resolving = PowerOfAttorneyRequestDecision.create!(
type: PowerOfAttorneyRequestDecision::Types::DECLINATION, creator:
)
# This form triggers the uniqueness validation, while the `create_resolution!` form triggers
# a more obscure `RecordNotSaved` error that is less functional for getting validation errors.
PowerOfAttorneyRequestResolution.create!(
power_of_attorney_request: self,
resolving:,
reason:
)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's finally encapsulate and hide this implementation for co-creating the resolution & resolving:

  • Creating the two records in the correct sequence
  • Invoking AR's validations in a way that is useful to us
  • Wrapping them in a transaction
module AccreditedRepresentativePortal
  class PowerOfAttorneyRequestDecision < ApplicationRecord
    class << self
      def create_acceptance!(**attrs)
        create_with_resolution!(type: Types::ACCEPTANCE, **attrs)
      end

      def create_declination!(**attrs)
        create_with_resolution!(type: Types::DECLINATION, **attrs)
      end

      private

      def create_with_resolution!(creator:, type:, **resolution_attrs)
        PowerOfAttorneyRequestResolution.create_with_resolving!(
          resolving: new(type:, creator:),
          **resolution_attrs
        )
      end
    end
  end
end

module AccreditedRepresentativePortal
  class PowerOfAttorneyRequestResolution < ApplicationRecord
    class << self
      ##
      # Adding this public class method in addition to `create!` because this
      # implementation causes the uniqueness validation to be expressed.
      #
      def create_with_resolving!(resolving:, **attrs)
        transaction do
          resolving.create!
          create!(resolving:, **attrs)
        end
      end
    end
  end
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note the use of keyword args to mimic AR model interface.

Comment on lines 63 to 74
def accept!(creator, reason)
resolving = PowerOfAttorneyRequestDecision.create!(
type: PowerOfAttorneyRequestDecision::Types::ACCEPTANCE, creator:
)
# This form triggers the uniqueness validation, while the `create_resolution!` form triggers
# a more obscure `RecordNotSaved` error that is less functional for getting validation errors.
##
PowerOfAttorneyRequestResolution.create!(power_of_attorney_request: self, resolving:, reason:)
end

def decline!(creator, reason)
resolving = PowerOfAttorneyRequestDecision.create!(
type: PowerOfAttorneyRequestDecision::Types::DECLINATION, creator:
)
# This form triggers the uniqueness validation, while the `create_resolution!` form triggers
# a more obscure `RecordNotSaved` error that is less functional for getting validation errors.
PowerOfAttorneyRequestResolution.create!(
power_of_attorney_request: self,
resolving:,
reason:
)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only downside I see for exposing #accept! and #decline on PowerOfAttorneyRequest is that these verbs could potentially lead someone to believe that this performs all the business logic that it needs to, but it doesn't. We lifted these verbs from the model layer to the service layer where it would be appropriate to handle all the business logic.

On this point I am mostly indifferent though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are mark_accepted! and mark_declined! more descriptive?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pixiitech I think that's a good idea. I've seen mark_* plenty of times.

This makes me think of what methods ActiveRecord::Enum exposes e.g. accepted! and tempts to do that...

Up to you!

Comment on lines 55 to 54
# All other errors: save error data on form submission, will result in a 500
rescue => e
update_submission_with_error(e.message)
raise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected errors are transient and should be handled accordingly (blow up and roll back the resolution & resolving).

raise Error.new(e.message, :bad_request)
# Transient 5xx errors: delete objects created, raise TransientError
rescue *TRANSIENT_ERROR_TYPES => e
form_submission.delete
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A form submission wouldn't have been created if we're in this rescue. This use of memoization is creating and then instantly deleting this record. Can we create a more streamlined flow? I think using local variables is potentially the exact control flow we want, while memoized instance variables is a complicating departure from a direct control flow.

@resolution = poa_request.accept!(creator, reason)
response = service.submit2122(form_payload)

form_submission.update(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have straightforward control flow that is based around two operations:

  • Make a failed submission on a failure
  • Make a successful submission on a success

Comment on lines +34 to +35
rescue PowerOfAttorneyRequestService::Accept::Error => e
render json: { errors: [e.message] }, status: e.status
Copy link
Contributor

@nihil2501 nihil2501 Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can get away with this for now, but I suspect the correct layered design is as follows:

Service surfaces a variety of kinds of errors that express what went wrong semantically. The service understands the internals of what went wrong but is happy to obfuscate those internal details that are too much information for the service's caller to need to know about.

This controller is a caller of the service. It should have the responsibility of interpreting the variety of kinds of (semantic) errors that the service exposes and then it decides what response should be returned from the controller.

For now:
If the responses returned from the controller look principled from the outside world, but the design doesn't follow above, that is fine; we can punt on such a refactoring until later.

@pixiitech pixiitech requested a review from nihil2501 February 19, 2025 21:28
@va-vfs-bot va-vfs-bot temporarily deployed to art/101919/2122-submission-controller-action/main/main February 19, 2025 21:29 Inactive
@nihil2501 nihil2501 changed the title [ARP POA submission] (#3) decision submission controller action (#101919) [ARP POA Submission] (#3) Decision Creation Controller Action (#101919) Feb 19, 2025
@va-vfs-bot va-vfs-bot temporarily deployed to art/101919/2122-submission-controller-action/main/main February 20, 2025 19:55 Inactive
Copy link
Contributor

@nihil2501 nihil2501 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@pixiitech pixiitech marked this pull request as ready for review February 20, 2025 20:27
@pixiitech pixiitech requested review from a team as code owners February 20, 2025 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants