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

Revoke transaction fee if there is an amount to be credited in order #8689

Conversation

apricot12
Copy link
Contributor

@apricot12 apricot12 commented Jan 10, 2022

What? Why?

Closes #3490

It seems, the revoking of transaction fee for certain situations is handled by :

    def ensure_correct_adjustment
      revoke_adjustment_eligibility if ['failed', 'invalid', 'void'].include?(state) 
      return if adjustment.try(:finalized?)

      if adjustment
        adjustment.originator = payment_method
        adjustment.label = adjustment_label
        adjustment.save
      elsif payment_method.present?
        payment_method.create_adjustment(adjustment_label, self, true)
        adjustment.reload
      end
    end

But I'm facing a a problem :
Once I add || can_credit? to the revoke_adjustment_eligibility if ['failed', 'invalid', 'void'].include?(state) condition, only the first credit is succesful without a transaction fee applied and all subsequent credits have the fee applied.

And even though 'credit' seems to be a valid state, adding it to the state conditions seems to have no effect.

We can ensure that the payment method fee adjustment is not created by adding the condition return if order.payment_state == 'credit_owed to the ensure_correct_adjustment method.

What should we test?

  • There should be no transaction fee/payment method fee applied for crediting an amount.

Release notes

  • Transaction fee is not applied when crediting an order.

Changelog Category: Technical changes

@apricot12
Copy link
Contributor Author

@jibees Since you've worked on this block of code before, maybe you'll have some insight?

@jibees
Copy link
Contributor

jibees commented Jan 10, 2022

Hey @apricot12 !
I've worked on it, and yes, it's tricky. Take everything I said here with a grain of salt.

I don't think this code is good, because of all the failing tests here. What I would do:

  • Create an automatic test, that recreate the described scenario in the linked issue. I think the place to do this is spec/models/spree/payment_spec.rb on line #383 and should look like this (this code isn't right, take it just as a canva) :
        context "if payment method has any adjustments", :debug do
          let!(:payment_method) { create(:payment_method, calculator: ::Calculator::FlatRate.new(preferred_amount: 10)) }

          before do
            payment.update!(payment_method: payment_method)
            payment.save!
          end

          it "should not applied any transaction fees" do
            payment.credit!
            expect(order.all_adjustments.payment_fee.eligible.length).to eq(0)
          end
        end
  • Then, correct the test. I don't think the test (can_credit?) is the right one since it only test that the payment allow some credit. I since it's more around the method credit on file app/models/spree/payment/processing.rb#78.

Sorry, I don't know if it's really helpful, but here's my 2 cents.

@apricot12 apricot12 marked this pull request as ready for review January 12, 2022 12:47
@apricot12 apricot12 force-pushed the Transaction_fee_when_crediting branch from 2ca0447 to 21c2458 Compare January 12, 2022 12:50
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.

Good work on finding the right code part to do this. I had an idea for an improvement (comment below). And I think that this should be covered by a spec. It's always a good idea to write a spec to re-produce a bug and then fix it. In this case, a model spec should be enough.

@@ -141,7 +141,7 @@ def payment_source

def ensure_correct_adjustment
revoke_adjustment_eligibility if ['failed', 'invalid', 'void'].include?(state)
return if adjustment.try(:finalized?)
return if adjustment.try(:finalized?) || order.payment_state == 'credit_owed'
Copy link
Member

Choose a reason for hiding this comment

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

This should work in most cases, but checking the state could go wrong. In theory, we could trigger another payment while credit is owed. Then we would owe even more. It doesn't seem to make sense at first but maybe we want to check if a payment goes through before adding an item.

Anyway, I think that the underlying assumption is that we don't pay transaction fees on refunds, no matter what the state of the order is. I would guess that we can check amount <= 0.

It also looks to me as if revoke_adjustment_eligibility would make sense in this case. So maybe it could look like this:

    def ensure_correct_adjustment
      revoke_adjustment_eligibility if failed_or_refund?
      ...
    end

    def failed_or_refund?
      ['failed', 'invalid', 'void'].include?(state) || amount <= 0
    end

Copy link
Contributor Author

@apricot12 apricot12 Jan 13, 2022

Choose a reason for hiding this comment

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

@mkllnk revoke_adjustment_eligibility looked like the obvious choice here to me too, but it doesnt get executed when we use conditions like amount <=0, order.payment_state == 'credit_owed or order.payment_state == paid.

Copy link
Member

Choose a reason for hiding this comment

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

I don't understand. The revoke_adjustment_eligibility should finalise the adjustment and the next line returns if the adjustment if finalised:

def revoke_adjustment_eligibility
return unless adjustment.try(:reload)
return if adjustment.finalized?
adjustment.update(
eligible: false,
state: "finalized"
)
end

revoke_adjustment_eligibility if ['failed', 'invalid', 'void'].include?(state)
return if adjustment.try(:finalized?)

Can you write a spec first? Then we have an example to experiment with and you can show me what you mean.

@apricot12 apricot12 force-pushed the Transaction_fee_when_crediting branch 2 times, most recently from 9da9265 to 7219384 Compare January 28, 2022 11:01
@apricot12
Copy link
Contributor Author

apricot12 commented Jan 28, 2022

@mkllnk After using debugger, it's at line 175 that the execution of revoke_adjustment_eligibility stops when using argument amount <= 0:

return unless adjustment.try(:reload)
return if adjustment.finalized?

But as you can see from the specs, both of these conditions are supposed to be passing. I'm not sure what exactly is going on in this scenario? Is it because once the adjustment is made when you credit, it go's into the closed state?

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.

Thanks for the spec! That's very helpful. It looks like we don't have an adjustment yet when this code is called. The states 'failed', 'invalid', 'void' refer to a changing payment while a refund payment is a new one. I didn't think of that. So my previous suggestion doesn't work.

I'm wondering if we prevent the adjustment creation instead. We code change CalculatedAdjustments but then it needs to know the difference between payments and other things with adjustments. Not good.

Maybe it's enough to modify this method here further down to avoid the creation of an adjustment:

elsif payment_method.present?

-      elsif payment_method.present?
+      elsif amount.positive? &&  payment_method.present?

I saw in CalculatedAdjustments that some adjustments are mandatory and then we need to add a 0 amount adjustment but we can assume that no transaction fees are mandatory at this stage. Not adding an adjustment is much easier.

Would that work?

@apricot12
Copy link
Contributor Author

@mkllnk Yes not creating the adjustment itself definitely works, and from what I've seen of the related code not adding payment adjustment when refunding shouldn't really affect anything else.

OH so when we're credit some amount back, It's a whole new payment! I didn't think of it that way though it seems so obvious to me now.

I'll update the commits and change up the specs a little bit 👍

@apricot12 apricot12 force-pushed the Transaction_fee_when_crediting branch 2 times, most recently from 247e509 to 97c52f1 Compare January 31, 2022 11:15
@apricot12 apricot12 requested a review from mkllnk January 31, 2022 11:15
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.

Almost there.

app/models/spree/payment.rb Outdated Show resolved Hide resolved
@apricot12 apricot12 force-pushed the Transaction_fee_when_crediting branch from 97c52f1 to 9c7da0e Compare February 1, 2022 08:13
@apricot12 apricot12 requested a review from mkllnk February 1, 2022 08:13
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.

Looks good to me.

apricot12 and others added 6 commits February 15, 2022 11:52
The #stub method is slightly deprecated syntax. Using #expect has the additional benefit here of failing if the object does not receive the expected method call.
#try is useful when the object might be nil or might not respond to the given method. In this case we expect it to exist and respond to #finalized?, so this is a bit more precise.
In this case we actually expect there to be no payment fee adjustments at all on the payment, regardless of their eligibility.
@Matt-Yorkley Matt-Yorkley force-pushed the Transaction_fee_when_crediting branch from 9c7da0e to 711a51f Compare February 15, 2022 12:25
@Matt-Yorkley
Copy link
Contributor

Great! I rebased and added a couple of "review-commits" to tighten up the tests a bit and increase the clarity. I left some notes in the commit messages 👌

What do you think of this change: 8084ad0?

There's an old saying in Software Development:

"Today, only you and god understand your code. In six months, only god will understand your code."

I try to keep this in mind when I'm writing bits of code, and ask myself: "if I come back to this in 6 months when I've completely forgotten all the surrounding context, will it make sense or will I have to find the issue and read all the notes before I can figure out why it's there?" Trying to make my code self-explanatory and idiot-proof is a valuable strategy, especially because the idiot is usually me, in the future. 😅

Copy link
Contributor

@Matt-Yorkley Matt-Yorkley left a comment

Choose a reason for hiding this comment

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

👍

@apricot12
Copy link
Contributor Author

@Matt-Yorkley Haha that change is actually great. It provides all the context one would need from just from the name!

I'll keep it in mind to make everything as idiot proof as possible in future.

@filipefurtad0
Copy link
Contributor

Hey @apricot12 ,

Before this PR:

  • removing an item
  • issuing a partial refund
  • charges a second transaction fee

image

After this PR:

Peek 2022-02-21 20-28

We can see that the second fee is not charged. Great work 💪

@filipefurtad0 filipefurtad0 merged commit dbf5eb7 into openfoodfoundation:master Feb 21, 2022
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.

When crediting an order the transaction fee is applied when it shouldn't
5 participants