Skip to content

LG-7832: Transliterate fields for the USPS API#7866

Closed
NavaTim wants to merge 36 commits intomainfrom
tbradley/lg-7832-transliterate-usps-api
Closed

LG-7832: Transliterate fields for the USPS API#7866
NavaTim wants to merge 36 commits intomainfrom
tbradley/lg-7832-transliterate-usps-api

Conversation

@NavaTim
Copy link
Contributor

@NavaTim NavaTim commented Feb 22, 2023

🎫 Ticket

🛠 Summary of changes

  • Validate that name and address fields can be transliterated
  • Transliterate the fields when sending them to USPS

📜 Testing Plan

Provide a checklist of steps to confirm the changes.

  • Complete IPP flow up to state ID / address
  • Check that state ID and address forms reject non-transliterable characters
  • Finish IPP flow using details that need transliterated
  • Verify that info is sent to USPS in a transliterated form
  • Verify that Login stores non-transliterated info

👀 Screenshots

  • TBD

@svalexander
Copy link
Contributor

  • Complete IPP flow up to state ID / address
  • Check that state ID and address forms reject non-transliterable characters
  • Finish IPP flow using details that need transliterated
  • Verify that info is sent to USPS in a transliterated form
  • Verify that Login stores non-transliterated info
    Can you include examples in the testing plan of inputs that can be tested?

# @option fields [String,#to_s] :address2
# @option fields [String,#to_s] :city
# @return [Hash,nil] Field names mapped to error messages, or nil if there were no issues
def validate(fields)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not use kwargs here? and build a hash from that?

Suggested change
def validate(fields)
def validate(first_name: nil, last_name: nil, address1: nil, address2: nil, city: nil)
fields = { first_name:, last_name:, address1:, address2: city: }.select(&:present?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll take a look at the impact implementing this change would have on the controller. I was trying here to keep logic about which fields get validated relatively isolated to this class, but your suggested usage could be easier to work with.


private

ADDITIONAL_UNSUPPORTED_CHARS = ActiveSupport::HashWithIndifferentAccess.new(
Copy link
Contributor

Choose a reason for hiding this comment

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

if we construct the hash using keywords like above, we don't need this to have indifferent access, it could just be a plan hash?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep.

@aduth
Copy link
Contributor

aduth commented Feb 22, 2023

I haven't had a chance to dive deep into this, but am I correct that this validates the characters prior to submission from the frontend using fetch? Would it be simpler to validate this from the backend and use ValidatedFieldComponent + simple_form's built-in error handling?

FSM obfuscates the form, but the general idea being...

form.errors.add(:address1, t('in_person_proofing.form.state_id.errors.transliteration')) if has_unsupported_character?

NavaTim and others added 2 commits February 22, 2023 13:51
Co-authored-by: Andrew Duthie <andrew.duthie@gsa.gov>
@NavaTim
Copy link
Contributor Author

NavaTim commented Feb 23, 2023

I haven't had a chance to dive deep into this, but am I correct that this validates the characters prior to submission from the frontend using fetch? Would it be simpler to validate this from the backend and use ValidatedFieldComponent + simple_form's built-in error handling?

FSM obfuscates the form, but the general idea being...

form.errors.add(:address1, t('in_person_proofing.form.state_id.errors.transliteration')) if has_unsupported_character?

@aduth It's looking like the state ID and address forms may not be setup to work with this approach out-of-the box, i.e. I'm not seeing errors populate on the form with the approach you suggested. If you think it's worth pursuing that approach further, then let me know and maybe we can discuss it tomorrow.

You can check out the changes I made to test this approach in this commit.

@aduth
Copy link
Contributor

aduth commented Feb 23, 2023

@aduth It's looking like the state ID and address forms may not be setup to work with this approach out-of-the box, i.e. I'm not seeing errors populate on the form with the approach you suggested. If you think it's worth pursuing that approach further, then let me know and maybe we can discuss it tomorrow.

Yeah unfortunately it looks like FSM doesn't make it very easy to maintain values through an invalid submission to the next render. I was able to get a minimal working prototype going with some small revisions, though (diff below).

Having to touch FSM to get this to work is unfortunate. Luckily, I suspect that if FSM were refactored away, the form bits could still carry over and be useful and easier to implement.

I guess my overall concern is that I suspect the conventional Rails implementation might be more maintainable and have fewer moving parts than asynchronous frontend validation. Alternatively, is there a way we could implement the validation on the frontend without calling out to an API?

diff --git a/app/forms/idv/state_id_form.rb b/app/forms/idv/state_id_form.rb
index 5b2a1a1b7..c7635693b 100644
--- a/app/forms/idv/state_id_form.rb
+++ b/app/forms/idv/state_id_form.rb
@@ -9,2 +9,4 @@ module Idv
 
+    validate :validate_supported_characters
+
     def self.model_name
@@ -28,2 +30,6 @@ module Idv
 
+    def validate_supported_characters
+      errors.add(:first_name, I18n.t('in_person_proofing.form.state_id.errors.transliteration'))
+    end
+
     def consume_params(params)
diff --git a/app/services/flow/base_flow.rb b/app/services/flow/base_flow.rb
index 6f5779794..85d0c7266 100644
--- a/app/services/flow/base_flow.rb
+++ b/app/services/flow/base_flow.rb
@@ -40,3 +40,3 @@ module Flow
       @flow_session[:error_message] = nil
-      handler = step_handler(step)
+      handler = step_handler_instance(step)
       return failure("Unhandled step #{step}") unless handler
@@ -46,6 +46,5 @@ module Flow
     def extra_view_variables(step)
-      handler = step_handler(step)
+      handler = step_handler_instance(step)
       return failure("Unhandled step #{step}") unless handler
-      obj = handler.new(self)
-      obj.extra_view_variables
+      handler.extra_view_variables
     end
@@ -62,6 +61,10 @@ module Flow
 
+    def step_handler_instance(step)
+      @step_handler_instances ||= {}
+      @step_handler_instances[step] ||= step_handler(step)&.new(self)
+    end
+
     def wrap_send(handler)
-      obj = handler.new(self)
-      value = obj.base_call
-      form_response(obj, value)
+      value = handler.base_call
+      form_response(handler, value)
     end
diff --git a/app/services/idv/steps/in_person/state_id_step.rb b/app/services/idv/steps/in_person/state_id_step.rb
index 3afeb3f0d..ff82e91ba 100644
--- a/app/services/idv/steps/in_person/state_id_step.rb
+++ b/app/services/idv/steps/in_person/state_id_step.rb
@@ -31,2 +31,3 @@ module Idv
           {
+            form:,
             pii: flow_session[:pii_from_user],
@@ -40,4 +41,4 @@ module Idv
         def form_submit
-          Idv::StateIdForm.new(current_user).submit(
-            permit(
+          form.submit(
+            flow_params.permit(
               *Idv::StateIdForm::ATTRIBUTES,
@@ -51,2 +52,10 @@ module Idv
         end
+
+        def flow_params
+          params.require(:state_id)
+        end
+
+        def form
+          @form ||= Idv::StateIdForm.new(current_user)
+        end
       end
diff --git a/app/views/idv/in_person/state_id.html.erb b/app/views/idv/in_person/state_id.html.erb
index 3ba30bd91..1d05c544b 100644
--- a/app/views/idv/in_person/state_id.html.erb
+++ b/app/views/idv/in_person/state_id.html.erb
@@ -27,3 +27,3 @@
 
-<%= simple_form_for :doc_auth,
+<%= simple_form_for form,
                     url: url_for,

@NavaTim
Copy link
Contributor Author

NavaTim commented Feb 23, 2023

@aduth It's looking like the state ID and address forms may not be setup to work with this approach out-of-the box, i.e. I'm not seeing errors populate on the form with the approach you suggested. If you think it's worth pursuing that approach further, then let me know and maybe we can discuss it tomorrow.

Yeah unfortunately it looks like FSM doesn't make it very easy to maintain values through an invalid submission to the next render. I was able to get a minimal working prototype going with some small revisions, though (diff below).

Read more... Having to touch FSM to get this to work is unfortunate. Luckily, I suspect that if FSM were refactored away, the form bits could still carry over and be useful and easier to implement.

I guess my overall concern is that I suspect the conventional Rails implementation might be more maintainable and have fewer moving parts than asynchronous frontend validation. Alternatively, is there a way we could implement the validation on the frontend without calling out to an API?

diff --git a/app/forms/idv/state_id_form.rb b/app/forms/idv/state_id_form.rb
index 5b2a1a1b7..c7635693b 100644
--- a/app/forms/idv/state_id_form.rb
+++ b/app/forms/idv/state_id_form.rb
@@ -9,2 +9,4 @@ module Idv
 
+    validate :validate_supported_characters
+
     def self.model_name
@@ -28,2 +30,6 @@ module Idv
 
+    def validate_supported_characters
+      errors.add(:first_name, I18n.t('in_person_proofing.form.state_id.errors.transliteration'))
+    end
+
     def consume_params(params)
diff --git a/app/services/flow/base_flow.rb b/app/services/flow/base_flow.rb
index 6f5779794..85d0c7266 100644
--- a/app/services/flow/base_flow.rb
+++ b/app/services/flow/base_flow.rb
@@ -40,3 +40,3 @@ module Flow
       @flow_session[:error_message] = nil
-      handler = step_handler(step)
+      handler = step_handler_instance(step)
       return failure("Unhandled step #{step}") unless handler
@@ -46,6 +46,5 @@ module Flow
     def extra_view_variables(step)
-      handler = step_handler(step)
+      handler = step_handler_instance(step)
       return failure("Unhandled step #{step}") unless handler
-      obj = handler.new(self)
-      obj.extra_view_variables
+      handler.extra_view_variables
     end
@@ -62,6 +61,10 @@ module Flow
 
+    def step_handler_instance(step)
+      @step_handler_instances ||= {}
+      @step_handler_instances[step] ||= step_handler(step)&.new(self)
+    end
+
     def wrap_send(handler)
-      obj = handler.new(self)
-      value = obj.base_call
-      form_response(obj, value)
+      value = handler.base_call
+      form_response(handler, value)
     end
diff --git a/app/services/idv/steps/in_person/state_id_step.rb b/app/services/idv/steps/in_person/state_id_step.rb
index 3afeb3f0d..ff82e91ba 100644
--- a/app/services/idv/steps/in_person/state_id_step.rb
+++ b/app/services/idv/steps/in_person/state_id_step.rb
@@ -31,2 +31,3 @@ module Idv
           {
+            form:,
             pii: flow_session[:pii_from_user],
@@ -40,4 +41,4 @@ module Idv
         def form_submit
-          Idv::StateIdForm.new(current_user).submit(
-            permit(
+          form.submit(
+            flow_params.permit(
               *Idv::StateIdForm::ATTRIBUTES,
@@ -51,2 +52,10 @@ module Idv
         end
+
+        def flow_params
+          params.require(:state_id)
+        end
+
+        def form
+          @form ||= Idv::StateIdForm.new(current_user)
+        end
       end
diff --git a/app/views/idv/in_person/state_id.html.erb b/app/views/idv/in_person/state_id.html.erb
index 3ba30bd91..1d05c544b 100644
--- a/app/views/idv/in_person/state_id.html.erb
+++ b/app/views/idv/in_person/state_id.html.erb
@@ -27,3 +27,3 @@
 
-<%= simple_form_for :doc_auth,
+<%= simple_form_for form,
                     url: url_for,

@aduth Thanks for the additional clarification. I mentioned in our discussion today a concern with the stateful Ruby class instances being reused, but after looking into the actual usage that appears not to be the case. The flow is fully re-initialized on each request in app/services/flow/flow_state_machine.rb:

module Flow
  module FlowStateMachine
    extend ActiveSupport::Concern

    included do
      before_action :initialize_flow_state_machine # this line
      before_action :ensure_correct_step, only: :show
    end

    # ...

This is good since it means that there's no risk of the state being inappropriately preserved between requests.

I'll test to make sure that the errors appear in the expected manner using this approach. If so, then I'll check in with design since it would effectively remove one of the error states we discussed, then likely proceed with implementation.

@NavaTim
Copy link
Contributor Author

NavaTim commented Mar 1, 2023

@zachmargolis @svalexander I resolved some of the comments here because I am switching to the implementation suggested by @aduth. I'll let you know when the updated implementation is ready for review.

@success
end

def analytics_hash
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 adding another method to the form response object specifically for this is not a good approach. I see that it's skipping certain errors, but I would recommend finding another way if possible

see notes in our docs about creating separate properties for sensitive values, because the entire point of the to_h method on this class is for logging things to our events log

Copy link
Contributor Author

@NavaTim NavaTim Mar 3, 2023

Choose a reason for hiding this comment

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

Based on how the form is behaving when I attempt to set the sensitive properties, I think the current flow/form system simply does not support form errors that could contain PII.

i.e. returning the errors via FormResponse is inherently connected to logging those same errors. Those need to be decoupled.

I can attempt to make the approach more lightweight (e.g. by using a pii_ prefix convention in the error type), but it looks like something similar to this will need to change in order to support this scenario.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@zachmargolis The guidance to add properties during #submit simply did not work when I tried it, so I pushed up code with a slightly different approach. If you think there's a problem with the new approach, then I'd like to discuss alternative approaches on Monday.

If you think the new approach is acceptable, then I can add a description of the new option to the guidance.

The new option automatically redacts the message if you mark an error with pii: true, but it keeps the error type (which is ultimately what the error_details code is getting, though that's a little obscured). It also adds to :pii_like_keypaths so the error type doesn't trigger the PII check.

end
end

context 'transliteration'
Copy link
Contributor

Choose a reason for hiding this comment

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

this doesn't throw an error, but it also doesn't get printed when running tests; we should follow convention and make it a block that wraps the new test below

  context 'transliteration' do
    ...
  end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I intended to use a block here. Thanks for pointing out the issue.

@NavaTim
Copy link
Contributor Author

NavaTim commented Mar 7, 2023

@NavaTim NavaTim closed this Mar 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants