1

I am using Paypal Smart Buttons (Javascript SDK) to create recurring subscriptions. My issue is that (very infrequently) a user manages to create a duplicate subscription and recurring payments.

When I look at my logs I can see the transactions are happening within 60 seconds so I assume it's an accidental double click on the Paypal pay button (or something).

I read the docs and saw that for standard payments you can add:

A unique invoice_id that hasn't been used for a previously-completed transaction to identify the order for accounting purposes and to prevent duplicate payments.

However there doesn't seem to be a way to stop duplicate subscriptions/recurring payments because the subscription API only supports custom_id.

See documentation here and another similar question here.

Here is my code:

    paypal.Buttons({
        style: {
            shape: 'rect',
            color: 'silver',
            label:  'subscribe'
        },
        onError: function (err) {
            // show error message
        },
        onClick: function(data)  {
            // do something when the button is clicked
        },
        // Create the subscription
        createSubscription: function(data, actions) {
            return actions.subscription.create({
                plan_id: "P-12345355RK523772MMDZIKUA",
                start_time: "2022-01-17T09:00:00Z",
                custom_id: order_reference,
                plan: {
                    billing_cycles: [
                    {
                        frequency: {
                            interval_unit: "MONTH",
                            interval_count: 1
                        },
                        tenure_type: "REGULAR",
                        sequence: 1,
                        pricing_scheme: {
                            fixed_price: {
                                value: 149,
                                currency_code: "USD"
                            }
                        }
                    }]
                },
                application_context:  { 
                    shipping_preference: "NO_SHIPPING"
                }
            });
        },

        // Finalize the transaction
        onApprove: function(details, actions) {
            console.log("subscription_id: "+details.subscriptionID)
            $('#thanks').load("/paypal/thanks?reference="+order_reference, details,
              function(responseText, textStatus, request) {
                if (textStatus == "error") {
                    // show error message
                }
            });
        }
    }).render('#paypal-button-container');

My current solution is to check in the paypal/thanks controller whether the order has already been associated with a subscription. If that happens I sent myself an error alert via email and have to manually fix the mess

Someone else suggested I automate that process, but I am hoping there's a better solution.

Thanks

Dagmar
  • 2,968
  • 23
  • 27

1 Answers1

1

You are creating the subscription on the client side with actions.subscription.create, which means two independent clicks of the button will create separate subscription intents, which can be approved independently.

The better solution is to have createSubscription fetch a subscription ID from your server (which it will create via the PayPal API) . If the order_reference is a duplicate, you can return the subscription id you already created. It can only be approved once!


Example of such a createSubscription function that calls a server that would return an id with idempotence (if the same call is repeated, return the same id)

      createSubscription: function(data, actions) {
          return fetch('/path/on/your/server/paypal/subscription/create/' + unique_invoice_id, {
              method: 'post'
          }).then(function(res) {
              return res.json();
          }).then(function(serverData) {
              console.log(serverData);
              return serverData.id;
          });
      },
Preston PHX
  • 27,642
  • 4
  • 24
  • 44
  • 1
    In a multi-threaded application what is stopping two independent clicks of the button within a couple milliseconds creating two subscriptions using your solution? I think it is better (and thanks for clarifying with an example) and will probably reduce the number of duplicate subscriptions because the subscription identifier is being set earlier in the process, however I don't think it's a foolproof solution. But happy to be proven wrong. – Dagmar Feb 11 '22 at 06:33
  • 1
    To get idempotence in Paypal you need to use the `PayPal-Request-Id` header using the unique identifier. That's the missing trick. THanks – Dagmar Feb 11 '22 at 06:41
  • 1
    https://developer.paypal.com/api/rest/reference/idempotency/ – Dagmar Feb 11 '22 at 06:43
  • 1
    That's idempotency at the PayPal API level, yes, which is useful if your own server operations for creating or returning a cached subscription based on your own unique id aren't atomic enough for some reason -- but first you have to make your requests be identifiable as the same operation, which isn't possible with actions.subscription.create on the client side – Preston PHX Feb 11 '22 at 08:08
  • 1
    How do you think PayPal implements idempotency? If they can do it on a huge server farm, surely you can with what's probably something orders of magnitude smaller. Just lock the attempt/invoice id as something you are creating a subscription for, so that no others can be created. – Preston PHX Feb 11 '22 at 08:32
  • 1
    The difference is that Paypal isn't using a web framework to code their APIs :P I'm pretty sure they are using a language like Java where it would be easy to synchronize on an id. However, I do wonder why they left invoice_id out of the parameters for `actions.subscription.create` when it's there for `actions.order.create` ... maybe it was too hard to implement ... and they justified this saying people could switch to the API and use `PayPal-Request-Id` (and swept the issues with their Javascript SDK under the carpet) – Dagmar Feb 11 '22 at 08:47
  • 1
    The server side of a "web framework" (be it node, php, ruby, or whatever) can do everything something like Java could do. So that's the point of my answer, you can't do it on the client side -- fetch a resource from a server, and do it there. – Preston PHX Feb 11 '22 at 08:52