DEV Community

Phil Schalm
Phil Schalm

Posted on

ActiveMerchant and PayPal Express Checkout

So that I don't forget how to do this in the future (I spent 3+ hours today smashing my head into my desk because good documentation on this is hard to find).

PayPal Express is a multi-step process.

  1. Craft an initial request to PayPal, indicating that you wish to send a customer to them to purchase an item. It includes various fields such as a list of items, order total, taxes, currency, return URLs, etc. PayPal will respond with a token that you need to use in future requests.
  2. Send the user to PayPal along with the process token from the prior request.
  3. The user completes their purchase and are redirected back to your site (along with the process token).
  4. Look up the details for the token (another PayPal request) and verify that the PayPal process happened successfully.
  5. Run the actual purchase request to PayPal, again verifying that it succeeded.

In terms of Rails and ActiveMerchant, this would look something like the following:

def checkout
  # ...
  if @order.valid?
    # Here's step 1
    response = paypal_gateway.setup_purchase(
      @order.total_in_cents,
      order_paypal_params(@order).merge(
        return_url: order_receipt_url, # Remember to use _url here and not _path (must be absolute)
        cancel_return_url: order_cancel_url,
      )
    )

    if response.success?
      @order.update(
        paypal_correlation_id: response.params["CorrelationID"],
        paypal_token: response.token
      )

      # Here's step 2
      return redirect_to paypal_gateway.redirect_url_for(response.token)
    end
  end
  # ...
end

def receipt
  # Here we're returning from step 3
  @order = Order.find_by(paypal_token: params[:token])

  catch(:failure) do
    if @order.payment_processed
      @error_message = "This order has already been paid for, no further action is necessary"
      throw :failure
    end

    # Here's step 4
    purchase_details = paypal_gateway.details_for(params[:token])
    unless purchase_details.success?
      @error_message = purchase_details.message
      throw :failure
    end

    # Finally, step 5
    purchase_response = paypal_gateway.purchase(
      @order.total_in_cents,
      order_paypal_params(@order).merge(
        token: params[:token],
        payer_id: purchase_details.payer_id,
      )
    )

    unless purchase_response.success?
      @error_message = purchase_details.message
      throw :failure
    end

    @order.update(
      payment_processed: Time.zone.now,
      amount_paid: @pack_order.total,
    )
  end
  # ...
end

private

def order_paypal_params(order)
  {
    items: [{
      name: "An Awesome Shirt",
      quantity: 1,
      amount: order.total_in_cents,
      description: "This is the coolest shirt ever, woo!",
    }],
    ip: request.remote_ip,
    allow_guest_checkout: true,
    currency: "CAD",
    invoice_id: order.id, # To make correlating easier later, if debugging
  }
end

def paypal_gateway
  @paypal_gateway ||= ActiveMerchant::Billing::PaypalExpressGateway.new(
    login: Configuration.paypal_username,
    password: Configuration.paypal_password,
    signature: Configuration.paypal_signature,
  )
end

Some things to note:

  1. All monetary values passed to PayPal are in cents (for USD and CAD. I'm unsure of cases in currencies where the common monetary unit is non-decimal).
  2. The total that is passed in as the first parameter to both setup_purchase and purchase needs to match the array of items, plus taxes, shipping, etc. If it doesn't match up perfectly you're in for a lot of headache.
  3. The response object that you get back from ActiveMerchant - to be exact, an instance of ActiveMerchant::Billing::PaypalExpressResponse - has a error_code field that (at the time of writing) appears to always filled out, even if you have a successful request. You're going to want to check response.success?.
  4. Especially as you initially put something like this into production, make sure to log, log, log. Don't log anything sensitive, but having lots of information, such as a full trace of the PayPal requests, can be very helpful if you're trying to figure out why a certain purchase isn't going through.
  5. This is condensed into a single post as much as possible so I don't forget this if I ever need it in the future. Don't need to waste 3 hours again :)

Top comments (3)

Collapse
 
noleftsignal profile image
noleftsignal

Thank you, Thank you. I was missing the whole Step 5 purchase block, purchases were succeeding, but no money was ever sent...because my code was never asking for it. Your example breaks down the mystery of the "gateway" in a way that my feeble mind can comprehend. Thanks again for putting this together.

Collapse
 
bashar3a profile image
Bashar Abdullah

Mind if I ask a question, since you already did the integration. I'm testing in my project, and I noticed when the user goes to PayPal page to enter details, there is a message saying

“You’ll be able to review your order before you complete your purchase”

However user does not get a chance to review the order, and is charged instantly. This seems to be a common issue with other merchants as well

paypal-community.com/t5/Payments/q...

Any idea? Are we supposed to show another Confirm You Want To Pay after Receipt page? This API Flow Page does seem to suggest it as well:

developer.paypal.com/docs/archive/...

Though I do recall often paying directly without another trip to the Merchant site. Frankly that extra step seems unnecessary, and likely to be an annoyance.

Love to hear your thoughts.

Collapse
 
bashar3a profile image
Bashar Abdullah

Thanks a lot. Really helpful!