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

3090 v2 distribution confirmation #4367

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

danielabar
Copy link
Contributor

Resolves #3090

Description

Introduce a Stimulus controller for the new distribution confirmation feature. It intercepts the Save form submission (only for New distributions, not Edit, as per earlier disucssion).

This controller extracts key information from the new distribution form including:

  • Partner
  • Storage location
  • Items and Quantitites

And displays these in a modal asking the user if they're sure this is what they intend.

The user can click "No...", which will close the modal and then they're still on the new form where they can continue filling it out, making changes, etc.

Or the user can click "Yes..." in which case the form is submitted, and then the existing server-side flow runs.

Note: There's a previous PR from an earlier attempt which introduced a new pending status for the Distribution model and saved the distribution in this state as part of the confirmation flow: #4341. But after some discussion, it was decided to go with a simpler client-side JS approach as per this PR.

Type of change

New feature (non-breaking change which adds functionality)

How Has This Been Tested?

  • For manual testing, create a new distribution and click Save - the new modal should appear.
  • The existing distribution system tests were maintained to expect the modal display after clicking Save from a new distribution.
  • A new distribution system test was added to verify that user can click "No..." from the confirmation modal, and remain on the new view.

Screenshots

Here is an example of the new distribution confirmation modal, shown after user clicks Save:
image

@cielf
Copy link
Collaborator

cielf commented May 21, 2024

I'm waiting to do any more functional reviews until the issue with bin/setup gets addressed. (Just setting expectations)

</table>

<div class="message fs-5">
<p>Please confirm that the above list is what you meant to distribute.</p>
Copy link
Collaborator

@cielf cielf May 22, 2024

Choose a reason for hiding this comment

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

@danielabar Hrmm. Now that I see it in action, I want a small adjustment here -- let's change "meant" to "want".

Reason: We are displaying this before any error checking. The past tense of "meant" implies you are pretty much finished. "Want" is better for setting expectations of still being in the process. (and it still works if we can do the error checking first)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the wording.

@cielf
Copy link
Collaborator

cielf commented May 22, 2024

@daniealbar Thank you very much for your hard work on this so far! (I really appreciate your thoroughness)

Alas, During the earlier discussions, I failed to catch that the confirmation would be before basic error checking and the combination of multiples, etc.

Does it have to be that way, with this approach? (I'm concerned that it won't meet the need to have useful numbers for their final review)

(also calling @dorner into the discussion)

@danielabar
Copy link
Contributor Author

@cielf Currently the server side part of this performs validations and saves. And this being client side, it runs before all that.

Would a more ideal flow would be, from clicking Save:

  1. Run validations (but do not create new distribution, even if valid)
  2. If invalid, render the form with errors
  3. If valid, render the form with the modal confirmation dialog
  4. From clicking "yes" on modal, again submit to server for validation and creation (should be valid by this point, but just in case since its still a form coming from the client)

It might be possible to introduce a new endpoint for validation only. It may also be possible to cause some Stimulus code to trigger as a result of render (saw something like this in a tutorial, could look into this).

A tricky thing will be the validation though, I ran into this earlier when working on the first attempt pure server side. For basic validation like is partner and storage location filled in, that's straightforward. But for inventory, I it seems to be coupled with actually decrementing the inventory amount in this line in DistributionCreateService: distribution.storage_location.decrease_inventory(distribution.line_item_values).

There's also some validation about this distribution already being fulfilled (if it was created from a request) that also happens together in the creation service, making it difficult to tease apart creation from validation.

There's another complexity with a further inventory check that happens after creation success wrt on-hand minimums. Although that may be only a flash alert after the fact, but doesn't stop creation.

So this might require some server side restructuring of the code to pull out all the distribution validation logic into one place that can be called independently of saving/updating anything.

@danielabar danielabar force-pushed the 3090-v2-distribution-confirmation branch from 1b39ec1 to 09c6b1b Compare May 23, 2024 20:59
@cielf
Copy link
Collaborator

cielf commented May 23, 2024

@danielabar
My gut says that would be better. I had hoped to do a bit of UX research today to see if I'm totally off base on this, but did not get to it.

If I'm understanding correctly, It sounds like this would be a bit of a mess, I mean challenge... g and that it that could make the code harder to understand.

The distribution confirmation is a "nice to have" , rather than core functionality. We'll have to make a judgement call on whether the benefit derived is worth the increased complexity of the code. I'd like to get @dorner 's take on this, see if there's an angle you haven't considered, but also to get his read on maintainablity.

I know you've put a lot of work into this one.

@dorner
Copy link
Collaborator

dorner commented May 24, 2024

I think the idea of separating out validation from creation is a good one. I think it'd make it easier to understand, not harder, because we aren't shoving them all together. I think that should happen in a separate PR though. There also should be a focus on event sourcing, so maybe validating inventory items is not quite as important. (I don't think there's a direct method to say "initialize this event and check if the inventory can handle it", but it's not hard to do - maybe 2-3 lines of code.)

Once we have them separated, the flow becomes a lot easier to work with and we can more easily have that confirmation page.

The fact is that without a way to validate the distribution without creating it, this ticket is pretty much impossible to complete. Whether this is the most important thing for Daniela to spend her time on is a judgment call for @cielf I think.

@cielf
Copy link
Collaborator

cielf commented May 24, 2024

IIUC, the stuff that we'd have to do to make this happen is a good idea anyway?

I am, however, starting a conversation with @danielabar over in slack about the partner profile rework - which is in the important but we're having trouble getting to it bucket.

@dorner
Copy link
Collaborator

dorner commented May 26, 2024

OK - I think the action for this should be to create another issue around separating validation from creation logic, and blocking this issue on that one. Prioritization is a separate question.

@cielf
Copy link
Collaborator

cielf commented May 29, 2024

I'm not sure I know enough about this to write it up -- though one question would be whether we want to separate it just for distributions, or across the board of itemizables? (for consistency's sake, I would think the latter)

@danielabar
Copy link
Contributor Author

danielabar commented May 29, 2024

An attempt at new issue title/description:

Title: Refactor Distribution Creation to Separate Validation from Creation

Label: refactor

Description:
Currently validation and creation of a Distribution are coupled, making it difficult to insert a confirmation feature into the workflow as per issue #3090. (i.e. confirmation should only be displayed for a valid, but not yet created, distribution).

Details:
When a new distribution form is submitted, the create method of app/controllers/distributions_controller.rb is called. This method creates an instance of a Distribution model from the form parameters, but doesn't call the valid? method on this model. Rather, it calls DistributionCreateService which, among other things does:

  • Validate that if this distribution is associated with a request, that the request has not already been fulfilled. This is done via a method in DistributionCreateService rather than with a validate declaration on the Distribution model.
  • Calls save! on the distribution model, which at this point, will run all the declarative validations on the Distribution model. This includes an inventory check line_items_exist_in_inventory, which is defined in the Itemizable concern app/models/concerns/itemizable.rb.
  • Decrease the inventory at the storage location, via the decrease_inventory method of the StorageLocation model. This method also combines updates and validation, and will raise if there are insufficient items.

What we'd like to do is separate out all the validations so that they can be called separately from creation, ideally from a single call to distribution.valid?. This way when DistributionCreateService calls distribution.save! it runs the exact same set of validations as distribution.valid?.

The benefit is this would allow for a future endpoint that only does validation via distribution.valid?, to build the confirmation feature something like described in this comment.

TODO: Are there any significant differences between Itemizable validation line_items_exist_in_inventory and the validation that's embedded in StorageLocation#decrease_inventory?

As for expanding to all the Itemizables, I haven't had a look but it's possible that the business rules and validation implementation could be different on each, so it may keep the PR more manageable to have separate tickets, starting just with Distribution model, since we know it has the issue of validation coupled with creation.

@dorner
Copy link
Collaborator

dorner commented May 30, 2024

Let's keep it to Distribution.

The inventory thing should not be a focus of this. In event world, we're going to get rid of inventory items entirely, so I'd ignore it as much as possible.

@danielabar
Copy link
Contributor Author

Thanks for the clarification, this simplifies things in that it's only the "is there an associated request that's already fulfilled?" validation that isn't part of the distribution validations.

Thinking about this further, if distribution.valid? already does much of the validation, it might be possible to continue on my original stimulus branch, with having it call a new validation only endpoint. I'll attempt this (i.e. may not need that refactor ticket after all).

@danielabar danielabar force-pushed the 3090-v2-distribution-confirmation branch 3 times, most recently from b22fe0c to b48bb23 Compare June 1, 2024 20:51
@danielabar
Copy link
Contributor Author

@cielf I've made some changes so that first it will run some basic validations on the distribution before showing the modal (from user clicking Save on new distribution).

For now, this runs the existing validations defined on the Distribution model via distribution.valid?. I didn't change any of that logic given earlier comment wrt inventory.

And there might still be a case of "request already fulfilled", but I wasn't sure if that validation actually belongs in distribution model (eg: that would also run on update).

Also wanted to see if this is closer to the desired flow wrt validation. Can you try this out whenever you have a chance. For example, if you leave out Partner and try to Save, it won't show the modal, instead it will show the validation error. Then if you fix the error by filling out partner and Save again, then the modal will show.

@cielf
Copy link
Collaborator

cielf commented Jun 1, 2024

You're on my list! -- it's a little long at the moment (managing expectations)

@cielf
Copy link
Collaborator

cielf commented Jun 2, 2024

This is definitely closer! It feels a lot smoother to me at this point.

Is it plausible to handle the case where someone has entered the same item twice before we show the modal?

@danielabar
Copy link
Contributor Author

re: dupe items - How is this handled currently?

I tried on main branch, to create a new distribution with dupe items and it was allowed, didn't see any warning/validation error:
image
image

@danielabar danielabar force-pushed the 3090-v2-distribution-confirmation branch from b48bb23 to 4ce528e Compare June 4, 2024 19:52
@cielf
Copy link
Collaborator

cielf commented Jun 5, 2024

Yeah -- it is allowed. They currently get smooshed together using the distribution's method combine_distribution, which is called before_save.

("No, it' not plausible" may be a valid answer!)

@danielabar
Copy link
Contributor Author

danielabar commented Jun 5, 2024

I had a look at Itemizable#combine!, which is called by Distribution#combine_distribution. I don't think it will be possible to use that from the JS modal as that method is building ActiveRecord models to be associated with the distribution, just before the distribution gets saved. It actually deletes the existing ones it got from the form and builds new one based on the combination, by unique item_id.

But in the confirmation modal, it's client side and only has what it can parse from the DOM. It might be possible to write a similar combine method client side in JS, that would only be used for display purposes in the modal. I can look into this.

@danielabar
Copy link
Contributor Author

Got the combine display working in the confirmation modal, see this commit for details.

@danielabar
Copy link
Contributor Author

Just to clarify, the combining of line items in the modal is display only, it doesn't modify the model because it hasn't gone to the server yet. So if user clicks "No I need to make changes", the line items in the form will be exactly as the user originally entered them.

@cielf
Copy link
Collaborator

cielf commented Jun 6, 2024

It may be tomorrow before I have a chance to try this out, but it sounds workable to me from a functionality pov

@cielf
Copy link
Collaborator

cielf commented Jun 7, 2024

Functionality looks good. Over to @dorner for technical review.

@cielf cielf requested a review from dorner June 7, 2024 15:41
Copy link
Collaborator

@dorner dorner left a comment

Choose a reason for hiding this comment

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

Had a suggestion to tweak the approach. Overall this looks like great work!

.message {
margin-top: 40px;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need a special stylesheet just for one rule? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved into custom.scss

@@ -10,6 +10,7 @@ class DistributionsController < ApplicationController
before_action :enable_turbo!, only: %i[new show]
skip_before_action :authenticate_user!, only: %i(calendar)
skip_before_action :authorize_user, only: %i(calendar)
skip_before_action :verify_authenticity_token, only: :validate
Copy link
Collaborator

Choose a reason for hiding this comment

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

In general it's better not to skip this unless there's a good reason to. If the issue is JavaScript, there is a way to send the CSRF token so this passes: https://bloggie.io/@kinopyo/sending-non-get-requests-with-fetch-javascript-api-in-rails

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a strange one, with the current code that extracts form data, the authenticity_token is indeed being submitted, and validated by Rails successfully, at least, the first time.

But in the scenario where the first time the form is invalid, then user submits again with same/different validation errors, the correct token is being included, but Rails says its invalid.

I tried also with the technique mentioned in that blog post (using header rather than in post body), but the same issue occurs.

Thought it might be due to JS DOM parsing or Stimulus lifecycle keeping a reference to the old token, but that's not the case, it is submitting the latest token.

Requires further investigation if the token verification needs to be there also for the validate endpoint.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did a little more digging into ActionPack (lib/action_controller/metal/request_forgery_protection.rb), where verify_authenticity_token is implemented:

It tries three possible ways to validate the given token:

compare_with_global_token(csrf_token) ||
            compare_with_real_token(csrf_token) ||
            valid_per_form_csrf_token?(csrf_token)

The very first time the form is submitted (and Stimulus controller intercepts and submits POST /distributions/validate.json, compare_with_global_token(csrf_token) returns true.

But on all subsequent attempts, compare_with_global_token(csrf_token) and compare_with_real_token(csrf_token) return false, and so it attempts the last option valid_per_form_csrf_token?(csrf_token).

That last method calculates the correct_token based on the request path and method:

correct_token = per_form_csrf_token(
            session,
            request.path.chomp("/"),
            request.request_method
          )

I put a breakpoint here and compared correct_token when the validate endpoint is submitted vs the create endpoint. Since the request.path is different, /distributions.validate.json vs /distributions, it doesn't match for the validate endpoint.

So I think the token that gets generated in the form upon a render from the first validation error, represents a token for the create endpoint, and so it won't be correct if the validate endpoint is submitted.


openModal(event) {
event.preventDefault();
// this.debugFormData();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove this line?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

.then((response) => response.json())
.then((data) => {
if (data.valid) {
console.log("=== DistributionConfirmationController VALID");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need this in production?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

const partnerName = this.partnerSelectionTarget.selectedOptions[0].text;
const storageName = this.storageSelectionTarget.selectedOptions[0].text;
this.partnerNameTarget.textContent = partnerName;
this.storageNameTarget.textContent = storageName;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not the biggest fan of basically reinventing ERB with this. 😂 The combination of line items is also something that we already have implemented in Ruby and have to do again here.

I'm wondering if we can reuse just the ERB for this. Something like:

  • Send the fetch for validation
  • If valid, it sends the HTML to render, e.g. {"valid": true, "body": "<div ...></div>"}
  • Otherwise you submit the form as usual

You can build the distribution and line items and render the confirmation form with ERB that way. Something like

def validate
  @dist = Distribution.new(distribution_params.merge(organization: current_organization))
  @dist.line_items.combine!
  if @dist.valid?
    body = render_to_string
    render json: {valid: true, body: body}
   else
    render json: {valid: false}
   end 
end

Then you just add validate.html.erb and use it like you would any other view. The Stimulus controller becomes super simple in that case, you just do something like modalTarget.innerHTML = response.body.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implemented, a few notes:

Had to specify the format as html (otherwise was trying to render json) and layout false, to get a focused view of only the modal body:

body = render_to_string(template: 'distributions/validate', formats: [:html], layout: false)

Indeed this greatly simplified the stimulus controller, I was able to remove most of the targets. This could make it easier to re-use for other model confirmations in the future, by passing in a different preCheckPath value.

within "#distributionConfirmationModal" do
expect(page).to have_content("You are about to create a distribution for")
expect(find(:element, "data-testid": "distribution-confirmation-partner")).to have_text(partner.name)
expect(find(:element, "data-testid": "distribution-confirmation-storage")).to have_text(storage_location.name)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we hardcode the names? Faker has bitten us with this before. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the case of spec/system/distribution_system_spec.rb, the storage location and partner names are defined with let and FactoryBot near the top of the file:

let(:storage_location) { create(:storage_location, organization: organization) }
let!(:partner) { create(:partner, organization: organization) }

Then used throughout, for example:

select partner.name, from: "Partner"
select storage_location.name, from: "From storage location"

Did you mean to change the FactoryBot create to pass in specific names and then replace throughout the entire spec file? Something like:

let(:storage_location) { create(:storage_location, organization: organization, name: "Test Storage Location") }
let!(:partner) { create(:partner, organization: organization, name: "Test Partner") }
...

# Later in tests

select "Test Partner", from: "Partner"
select "Test Storage Location", from: "From storage location"
...
expect(find(:element, "data-testid": "distribution-confirmation-partner")).to have_text("Test Partner")

And so on for all occurrences?

btw, the storage location factory already has a hard-coded name:

name { "Smithsonian Conservation Center" }

Although using that in the distribution system spec expectations might create some confusion as to where that value from.

The partner factory on the other hand uses some variability, although with a sequence rather than faker:

sequence(:name) { |n| "Leslie Sue, the #{n}" }

Copy link
Collaborator

Choose a reason for hiding this comment

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

Did you mean to change the FactoryBot create to pass in specific names and then replace throughout the entire spec file?

Yep, exactly! The factory values are IMO something that should only be created but never referenced, because as you say it's really confusing as to where the values come from. If we need to check the value, we should always be providing it ourselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to specific partner and storage names.

…n form submit

and render a bootstrap modal displaying partner name, storage location,
and list of items and quantities. The No button simply closes the modal
so user can return to the new distribution form to make further edits.
The Yes button submits the form and the existing workflow continues.
1. Add validation only endpoint for distribution
2. Update confirmation JS controller to check if form is valid
(using the new validation endpoint) before displaying modal.
Combine same named items & quantities for display in confirmation modal
* Move confirm style into custom
* Remove call to debug form data
* Remove console logs for valid and invalid
@danielabar danielabar force-pushed the 3090-v2-distribution-confirmation branch from 9b5d79c to bddcf4e Compare June 9, 2024 14:28
* Move modal content generation to server side rendering
* Remove unneeded dom targets from stimulus controller
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.

[FEATURE] Allow essential banks to review their distribution before submitting it
3 participants