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

[Checkout] Shipping fees update, remove order callback #13023

Merged

Conversation

rioug
Copy link
Collaborator

@rioug rioug commented Dec 10, 2024

What? Why?

Now that checkout workflow is restarted when an order is updated, it fixes the issue with shipping fee calculation. That said, while investigating the issue, I realised shipment fees were recalculated multiple times when completing an order , which seemed unnecessary. I tracked down the issue to a callback ( 😮 surprise! 😮 surprise! ) :

before_save :update_shipping_fees!, if: :complete?

Given that we have https://github.com/openfoodfoundation/openfoodnetwork/blob/master/app/models/spree/order_contents.rb that should be used to managed adding/updating/removing an order's line item. Spree::OrderContents already handle updating shipment and shipment fees, so now the before_save callback becomes redundant.

Drawback : shipment fees won't be updated is someone directly update a line item on an order. There isn't anyway to prevent that (as far as I know), we (as the dev team) need to enforce the use Spree::OrderContents for any line item update.

What should we test?

As an enterprise manager:

  • Create a shipping method with a fee using the flat percent calculator.

As a customer:

  • start an order with that shipping method and proceed to step three (order summary) of the checkout but don't complete the order.
  • Take note of the order total and the shipping fee.
  • Continue shopping, i.e. click on the shop's name next to the cart icon in the top right corner.
  • Add one or more products to your cart.
  • Click checkout again - you should land on step 1, continue checking out using the shipping method set up earlier
  • Take note of the order total and the shipping fee - both should be updated correctly, including your added products.
  • Complete the order.
  • Compare the numbers of order total and shipping fee again.
    --> they should be matching.

Same scenario as above but this time once on step 3 of checkout, use "Edit" link next to "Order Details" to update your order

As an enterprise manager

  • Pick a completed order in the backoffice
  • Update a line item quantity or add a new variant
  • Check the shipping fees have been updated

As an enterprise manager, in the backoffice

  • Create a new order
  • Choose the shipping method with a flat percent calculator created a the start
    --> check the shipping fee is calculated correctly
    -- Add or update a line item in the order
    --> check the shipping fee is updated correctly

Release notes

Changelog Category (reviewers may add a label for the release notes):

  • User facing changes
  • API changes (V0, V1, DFC or Webhook)
  • Technical changes only
  • Feature toggled

The title of the pull request will be included in the release notes.

Dependencies

#12954 needs to be merged first

@rioug rioug marked this pull request as ready for review December 10, 2024 04:27
@rioug rioug added the user facing changes Thes pull requests affect the user experience label Dec 10, 2024
@rioug rioug changed the title 12907 fix checkout shipping fee [Checkout] Shipping fees update, remove order callback Dec 10, 2024
Copy link
Member

@mkllnk mkllnk left a comment

Choose a reason for hiding this comment

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

Cool. That should speed up the app.

Comment on lines 455 to 456
order2.contents.update_item(Spree::LineItem.where(order_id: order2.id).first,
{ quantity: 5 })
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
order2.contents.update_item(Spree::LineItem.where(order_id: order2.id).first,
{ quantity: 5 })
order2.contents.update_item(Spree::LineItem.find_by(order: order2), { quantity: 5 })

Copy link
Member

@dacook dacook left a comment

Choose a reason for hiding this comment

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

Looks good!

It seems more reasonable to be enforcing this at the controller level 👍

spec/models/spree/order_spec.rb Outdated Show resolved Hide resolved
@drummer83 drummer83 force-pushed the 12907-fix-checkout-shipping-fee branch from d7216ae to 14e18f7 Compare January 9, 2025 19:15
rioug added 5 commits January 15, 2025 15:44
It should be handled in the controller, it's currently handled in
`Spree::OrderContents#remove`. As long as we don't manually remove line
item from an order we should be good.
Currently it gets trigerred each time the order is saved, which seems to
happen mutiple time when we finalize an order. It's a bit useless to
recalculated the fees over and over
Context, it was added here : openfoodfoundation@217eda8
And update related specs
User Order::Contents#update_item to update line item on an order, it
ensures the order is properly updated
@filipefurtad0 filipefurtad0 force-pushed the 12907-fix-checkout-shipping-fee branch from 14e18f7 to 1afa7fe Compare January 15, 2025 21:44
@filipefurtad0 filipefurtad0 self-assigned this Jan 15, 2025
@filipefurtad0 filipefurtad0 added pr-staged-fr staging.coopcircuits.fr and removed pr-staged-fr staging.coopcircuits.fr labels Jan 15, 2025
@filipefurtad0
Copy link
Contributor

filipefurtad0 commented Jan 16, 2025

Hey @rioug ,

Thanks for outlining the test scenarios, with the focus on the shipping method fees. I went through them, and if a cash payment method is used, I can confirm that adding items after reaching the /summary page correctly updates the shipping fee values.

However, the PR introduces a regression with Stripe, which only occurs, if the shipping method has a shipping fee.

Below, when the order is setup using a shipping method with:

  1. no fee
  2. a flat percent calculator.

We can see, the payment total is twice the value of the order total, which results in a credit owed (order-)payment state.

image

The same scenarios 1) and 2) in current master:

The Stripe payment total equals the order total, so the (order-)payment state is paid.

image

In the tests above, the payment method was set with a transaction fee. However, this does not seem to be relevant in this case: the issue is observable as well, when no transaction fee is set:

image

What do you think is causing this? It might be useful to note that this only occurs on checkout, and not for orders placed in the backoffice. Please let me know if you feel you need more tests to debug the issue.
Moving back to In progress for now.

@rioug
Copy link
Collaborator Author

rioug commented Jan 20, 2025

Thanks @filipefurtad0 I managed to reproduce the issue on my machine. I think it might be due to "stripe payment intent" not being updated appropriately when we update the order. I'll have a look at it.

rioug added 5 commits January 20, 2025 16:07
For payment that complete during the checkout (Paypal, Stripe) the
amount was recorded twice against `order.payment_total`. This is because
the `payment_total` gets updated in an afer_save Payment callback when a
payment is completed, and then once more when we process payment in
`Spree::Order#process_each_payment`.
This is an existing  issue on master, but it was hidden by the
`update_shipping_fees!` callback, it trigerred an update of the order's
total, which then updated `order.payment_total` with the correct value.
Now that we removed the callback, the bug showed up.

Note, I updated the stripe specs for consistency even though they are
currently disabled.
Use a completed payment, instead of trying to complete a payment
Payment needs to be linked to the order, in order for the payment
callback to update `order.payment_total`
@rioug
Copy link
Collaborator Author

rioug commented Jan 21, 2025

@openfoodfoundation/reviewers Bug fix start at : c71ae35

Comment on lines -676 to -679

if payment.completed?
self.payment_total += payment.amount
end
Copy link
Member

Choose a reason for hiding this comment

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

It's a bit weird to take a payment without updating the total. But I guess that we are just moving the responsibility of updating the total out of the callback. And at some point, we may not want payments in a callback either.

Copy link
Member

Choose a reason for hiding this comment

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

It was cause for me to look it up to make sure I understood. I found that four controllers update the order.payment_total (via OrderManagement::Order::Updater#update_totals_and_states):
checkout_controller.rb, voucher_adjustments_controller.rb, api/v0/shipments_controller.rb, admin/adjustments_controller.rb

It is updated by counting up all payments. That seems like a much safer method than the incrementing above.

https://github.com/openfoodfoundation/openfoodnetwork/blob/master/engines/order_management/app/services/order_management/order/updater.rb#L60

        order.payment_total = payments.completed.sum(:amount)

Very good!

Copy link
Collaborator Author

@rioug rioug Jan 21, 2025

Choose a reason for hiding this comment

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

The issue was actually caused by this :

def update_order
OrderManagement::Order::Updater.new(order).after_payment_update(self)
end

which is triggered by an after_save callback.
So by the time the code reach these deleted lines the payment was already added to order.payment_total. It was hidden by the fact OrderManagement::Order::Updater#update_totals_and_states was triggered further down the line, which now does not always happen.

Copy link
Member

Choose a reason for hiding this comment

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

I see. I can see that this triggers the same code (order.payment_total = payments.completed.sum(:amount))

If we can be sure that update_totals_and_states is always triggered down the line (by a controller), then perhaps we can remove this model callback also! But I'm afraid to suggest it in this PR..

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think that would need more investigation. I believe the payment callback is there to ensure the order.payment_total is updated when a payment is completed outside the checkout. Ie, capturing a cash payment in the backoffice. There might be other scenarios as well, It's not something I would want to do here.

Copy link
Member

@dacook dacook left a comment

Choose a reason for hiding this comment

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

Great, I have a suggestion to potentially simplify the spec, but it's fine either way.

@@ -69,6 +69,7 @@

click_on "Complete order"
expect(page).to have_content "Your order has been processed successfully"
expect(page.find("#amount-paid").text).to have_content "$19.99"
Copy link
Member

Choose a reason for hiding this comment

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

Instead of using an ID, can we test for the associated label instead? (also below)

Suggested change
expect(page.find("#amount-paid").text).to have_content "$19.99"
expect(page).to have_content "Amount Paid\n$19.99"

let(:payment) { build(:payment) }
before { allow(order).to receive_messages pending_payments: [payment], total: 10 }

it "should return false if no pending_payments available" do
it "returns false if no pending_payments available" do
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for tidying these 👍

Comment on lines -676 to -679

if payment.completed?
self.payment_total += payment.amount
end
Copy link
Member

Choose a reason for hiding this comment

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

It was cause for me to look it up to make sure I understood. I found that four controllers update the order.payment_total (via OrderManagement::Order::Updater#update_totals_and_states):
checkout_controller.rb, voucher_adjustments_controller.rb, api/v0/shipments_controller.rb, admin/adjustments_controller.rb

It is updated by counting up all payments. That seems like a much safer method than the incrementing above.

https://github.com/openfoodfoundation/openfoodnetwork/blob/master/engines/order_management/app/services/order_management/order/updater.rb#L60

        order.payment_total = payments.completed.sum(:amount)

Very good!

@filipefurtad0 filipefurtad0 added the pr-staged-fr staging.coopcircuits.fr label Jan 23, 2025
@filipefurtad0
Copy link
Contributor

Hey @rioug,

Nice, the issue with the payment is fixed now 🎉
Going though the test cases you've outlined:

  1. As a customer:

Having an order on step/state summary and updating it:
i) clicking on the shop's name next to the cart icon in the top right corner
ii) using the "Edit" link next to "Order Details"
iii) editing the cart (clicking top right corner)

resulted in correct update of the shipping fee, which included added or removed items.

As an example, with a 10% rate on shipping, changing the line item quantity from 2:
image

to 4:

image

  1. As an enterprise manager

i) editing line items quantity
ii) creating an order

resulted in correct update of the shipping fee, which included added or removed items.

Same example as above, with a 10% rate on shipping, changing the line item quantity from 2:
image

to 4:

image

Looks good, I think we're ready to merge this one. Thanks again 💪

@filipefurtad0 filipefurtad0 merged commit 5d36073 into openfoodfoundation:master Jan 24, 2025
53 checks passed
@filipefurtad0 filipefurtad0 added technical changes only These pull requests do not contain user facing changes and are grouped in release notes and removed pr-staged-fr staging.coopcircuits.fr user facing changes Thes pull requests affect the user experience labels Jan 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
technical changes only These pull requests do not contain user facing changes and are grouped in release notes
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

4 participants