spookylukey / django-paypal

A pluggable Django application for integrating PayPal Payments Standard or Payments Pro
MIT License
726 stars 209 forks source link

Received 3 IPNs for the same transaction, only one flagged as duplicate #243

Open newearthmartin opened 2 years ago

newearthmartin commented 2 years ago

Hi! I received 3 IPNs for the same transaction with the same data, but only one was flagged as duplicate. They have all the same transaction id and all the same payment status, and are all separated by one minute (3:00 am 3:01 am 3:02 am).

Only one got marked as duplicate so my system ended up processing the same payment twice.

Screen Shot 2021-10-18 at 12 42 06

newearthmartin commented 2 years ago

I manually executed the verify() method on the unflagged, second IPN and now it is correctly marked as duplicate txn_id. I also manually executed that on the original IPN and it correctly remains unflagged.

So a workaround would be to call verify() again on every IPN received and check if it remains unflagged.

spookylukey commented 2 years ago

This sounds like a valid issue. The code here looks buggy for the case when you have out-of-order notification of IPNs:

https://github.com/spookylukey/django-paypal/blob/24d914ba7f72c7bcab47445506f54558023c0a0b/paypal/standard/helpers.py#L17-L38

Could you check for your case that there were other IPNs with the same txn_id but different payment_status? Otherwise, we have to look further for the cause.

We should probably be instead checking for duplicate (txn_id, payment_status). I say "probably*, because that would involve trusting that PayPal are doing something sensible with how they send IPNs, or at least documented. Based on experience it's possible that neither are true...

newearthmartin commented 2 years ago

Clearly, the code above checks only if the latest similar IPN has the same payment status. If the latest has a different status but the previous has the same, it doesn't recognize it as duplicate.

So the question is why we make this difference and not just this?:

duplicates = (ipn_obj.__class__._default_manager 
    .filter(txn_id=ipn_obj.txn_id) 
    .exclude(id=ipn_obj.id) 
    .exclude(flag=True)
    .exclude(payment_status=ipn_obj.payment_status)                 
return duplicates.exists()

Still, it doesn't seem that that's what's going on here. If you look at the picture above, those are the only IPNs with that txn_id and all have the same payment_status. There are no other IPNs. After I ran verify() again (which calls duplicate_txn_id()) one that was unflagged (3:01 am) became flagged as duplicate.

So maybe it happened that the first two (3 am and 3:01 am) arrived concurrently?

Here is the picture after running verify() on them again:

Screen Shot 2021-11-04 at 13 17 09