SumOfUs / Champaign

SumOfUs Online Campaign Platform.
MIT License
49 stars 21 forks source link

GoCardless #454

Closed NealJMD closed 7 years ago

NealJMD commented 8 years ago

I spent a long time today researching GoCardless to gear us up for our integration. I think it's going to be pretty straightforward. Except for the UI, it's really almost identical to Braintree.

Resources

GoCardless has a ruby gem. It's a really minimal wrapper around their API, to the point that I haven't found any actual docs for it, just for their API. Those docs are here. It's tricky cause their are docs for their old API running around (which had some features the new one doesn't have), so make sure that's the one you're looking at.

I also found it very helpful to look at their example Sinatra app. It uses the hosted integration, and pretty much all the relevant logic is in this one file.

User interface

I went in to the research really hoping I would come out confident about doing a full integration without redirecting to their page. Unfortunately, I didn't. It's not that the requirements imposed are particularly onerous (you can read the three different requirement sets here, here, and here). Instead, it became clear after playing around for a while with a GoCardless page that it would take a lot of javascript engineering time to produce a form that's nearly as adaptable as the one provided by GC. Each country has a different format of IBAN number, but also supports local bank details. Those are different fields for every country, and aren't consistent between even UK and Germany (our main target markets). Whether IBAN is the default also depends on the country. I highly encourage you guys to play with the interfaces by switching country after clicking one of the buttons on this demo page.

The big goal on doing our own full integration would be to save users entering their name and email twice. However, I think that potential frustration pales in comparison to the potential frustration of us messing up the validation for one of the many countries, or of not supporting a country to keep our own validation simple. Furthermore, there's no reason that we can't revisit this down the line - none of the work using the hosted fields is work we wouldn't have to do if we did a full integration.

For now, I think that we should just add a GC-green button with the GC logo and a button that says "Direct Debit."

Controller layer

This part actually makes me really happy and proud - the only change that I think we need to make to make Api::BraintreeController work with both providers is to rename it to Api::PaymentsController and change the client method to:

  def client
    if params[:provider] == 'gocardless'
      PaymentProcessor::Clients::GoCardless
    else
      PaymentProcessor::Clients::Braintree
    end
  end

The rest will be handled by the new back end code that mirrors the existing logic for Braintree, but with different fields to shuffle around.

Data persistence

I think our existing tables for Braintree map very nicely to GoCardless:

The fields that we store are different, but the models and the associations between them stay the same. I think the only major code that we need to write is PaymentProcessor::Clients::GoCardless::Subscription and `PaymentProcessor::Clients::GoCardless::Transaction classes to handle creating the subscription and transaction with GC (the stuff handled in app.rb in the example app), and new GC versions of BraintreeCustomerBuilder and the other classes that are in payment.rb to record stuff to our own database.

Webhooks

GoCardless actually has a bunch of webhooks. At the beginning, I imagine we'll mostly just want to support the one to log transactions when they fire from subscriptions.

osahyoun commented 8 years ago

Neal, thank you for doing this! Their sinatra demo app is really helpful.

Are you good to break this up into pivotal tasks for the pride to jump on and devour? Either way, the three of us should meet after the org call to decide how we’re going to work together on this.

NealJMD commented 8 years ago

If you want to make a payment on the sandbox page, you can use

Account number: 55779911
Sort code: 20-00-00

For reference, these are what their API returns for various types:

Customer = {
  "customers": {
    "id": "CU123",
    "created_at": "2014-05-08T17:01:06.000Z",
    "email": "user@example.com",
    "given_name": "Frank",
    "family_name": "Osborne",
    "address_line1": "27 Acer Road",
    "address_line2": "Apt 2",
    "address_line3": null,
    "city": "London",
    "region": null,
    "postal_code": "E8 3GX",
    "country_code": "GB",
    "language": "en",
    "swedish_identity_number": null,
    "metadata": {
      "salesforce_id": "ABCD1234"
    }
  }
}

Mandate = {
  "mandates": {
    "id": "MD123",
    "created_at": "2014-05-08T17:01:06.000Z",
    "reference": "REF-123",
    "status": "pending_submission",
    "scheme": "bacs",
    "next_possible_charge_date": "2014-11-10",
    "metadata": {
      "contract": "ABCD1234"
    },
    "links": {
      "customer_bank_account": "BA123",
      "creditor": "CR123"
    }
  }
}

Payment = {
  "payments": {
    "id": "PM123",
    "created_at": "2014-05-08T17:01:06.000Z",
    "charge_date": "2014-05-21",
    "amount": 100,
    "description": null,
    "currency": "GBP",
    "status": "pending_submission",
    "reference": "WINEBOX001",
    "metadata": {
      "order_dispatch_date": "2014-05-22"
    },
    "amount_refunded": 0,
    "links": {
      "mandate": "MD123",
      "creditor": "CR123"
    }
  }
}

Subscription = {
  "subscriptions": {
    "id": "SU123",
    "created_at": "2014-10-20T17:01:06.000Z",
    "amount": 2500,
    "currency": "GBP",
    "status": "active",
    "name": "Monthly Magazine",
    "start_date": "2014-11-03",
    "end_date": null,
    "interval": 1,
    "interval_unit": "monthly",
    "day_of_month": 1,
    "month": null,
    "payment_reference": null,
    "upcoming_payments": [
      { "charge_date": "2014-11-03", "amount": 2500 },
      { "charge_date": "2014-12-01", "amount": 2500 },
      { "charge_date": "2015-01-02", "amount": 2500 },
      { "charge_date": "2015-02-02", "amount": 2500 },
      { "charge_date": "2015-03-02", "amount": 2500 },
      { "charge_date": "2015-04-01", "amount": 2500 },
      { "charge_date": "2015-05-01", "amount": 2500 },
      { "charge_date": "2015-06-01", "amount": 2500 },
      { "charge_date": "2015-07-01", "amount": 2500 },
      { "charge_date": "2015-08-03", "amount": 2500 }
    ],
    "metadata": {
      "order_no": "ABCD1234"
    },
    "links": {
      "mandate": "MA123"
    }
  }
}
paulaferris commented 8 years ago

Couple of user flow questions from me:

paulaferris commented 8 years ago

One more:

Really nice write-up!

NealJMD commented 8 years ago

OK, got some answers to the outstanding questions -

Can we pass any amount in either GBP or EUR through to the GC form to be processed? (Assume yes: Just asking as with their non-Pro offering you have to use one of a number of "plans" that you create in advance with a particular amount/currency combination)

Yup, this should be no problem. See the amount property here.

If there isn't a process of selecting an amount on the GC form, but instead passing an amount to that form, it might make sense essentially a DD-only country like Germany to have a user flow that involves selecting a donation amount in the email (we already do this with Champaign) but going straight to the GC form to donate that amount, instead of having to click a DD button on Champaign first. This is because in that case, we'd already know the donation amount and the user. How feasible is that? I assume the link might first hit a SOU server and then automatically route that person to GC with the correct details?

I think this is totally feasible, but I think that it makes sense to make this a round of iteration after our first GC integration is released and working.

What do you know (if anything) about the "one click" possibilities with GoCardless? In other words, once a user has donated once (either one-off or recurring) with GC, what do we need to do to create another authorization and/or modify the existing order without getting the user to re-enter their bank account details?

When the user enters their details on GoCardless, it creates a mandate we can charge against that is recorded in the GC database. The GC docs don't talk about how long before it expires, but Tuuli did some digging yesterday and found that SEPA mandates stay good for 36 months after the last charge. Unless there's a catch we didn't realize, it seems like we should be able to do one click against mandates people have created for donations or subscriptions.

If using the hosted GC page, does that mean we are also bound to use their email notifications too, or can we choose to handle that ourselves? I believe we are required to inform a donor (at least for EUR DDs) in advance that a DD will be processed, and possibly also then inform them it has been processed. Wondering what our options are re those emails, basically.

We can either send those emails ourselves, or we can let GoCardless send them. There's quite a bit of logic around when to send what, plus some light compliance requirements that need to get signed off on GC's bank. The standard emails don't even show any GoCardless branding, just the SEPA or DD logo, as shown below. I think that duplicating all that logic just to make the emails look more like SOU emails is pretty unappealing. Is that something you would like us to eventually do?

sepa-email-one-off

paulaferris commented 8 years ago

Slow response again but thanks @NealJMD -- that's all clear and makes sense.

I agree with holding on a direct-to-GC flow for now, and on getting them to do the emails unless there's a good reason not to that emerges in the future. Doesn't change answer at least for now, but curious if there is any ability to customize the text of email or not? (e.g. to make it explicit it's a donation).

Tuuleh commented 8 years ago

Hey!

I started looking at persisting GC customers, transactions and payment methods / mandates locally today, which entailed doing some more research.

So basically the workflow as I understand it is that we create a redirect flow, the customer completes it, and then we get a post back to an endpoint and send out a request to complete the redirect flow.

This is the magic bit in the Sinatra demo app:

get '/payment_complete' do
  package = params[:package]
  redirect_flow_id = params[:redirect_flow_id]
  price = PACKAGE_PRICES.fetch(package)

  # Complete the redirect flow
  completed_redirect_flow = settings.api_client.redirect_flows.
    complete(redirect_flow_id, params: { session_token: session[:token] })

  mandate = settings.api_client.mandates.get(completed_redirect_flow.links.mandate)

  # Create the subscription
  currency = case mandate.scheme
             when "bacs" then "GBP"
             when "sepa_core" then "EUR"
             end

  subscription = settings.api_client.subscriptions.create(params: {
    amount: price[currency] * 100, # Price in pence/cents
    currency: currency,
    name: I18n.t(:package_description, package: package.capitalize),
    interval_unit: "monthly",
    day_of_month:  "1",
    metadata: {
      order_no: SecureRandom.uuid # Could be anything
    },
    links: {
      mandate: mandate.id
    }
  })

  redirect "/thankyou?package=#{package}&subscription_id=#{subscription.id}"
end

What we get back with the completed redirect flow should look something like this per the complete redirect flow api docs:

POST https://api.gocardless.com/redirect_flows/RE123/actions/complete HTTP/1.1

{
  "data": {
    "session_token": "SESS_wSs0uGYMISxzqOBq"
  }
}

HTTP/1.1 200 (OK)
{
  "redirect_flows": {
    "id": "RE123",
    "description": "Wine boxes",
    "session_token": "SESS_wSs0uGYMISxzqOBq",
    "scheme": null,
    "success_redirect_url": "https://example.com/pay/confirm",
    "created_at": "2014-10-22T13:10:06.000Z",
    "links": {
      "creditor": "CR123",
      "mandate": "MD123",
      "customer": "CU123",
      "customer_bank_account": "BA123"
    }
  }
}

Basically, we get back the IDs of the mandate and the customer. If we want to store more information about the customer than their ID and bank account ID, we're going to have to do another request for that. There are already three requests:

  1. Complete the redirect flow
  2. Get the mandate from the service by its ID
  3. Create a subscription on GC.

Since we already feel a bit like we're storing more data than we necessarily need to for the Braintree integration, I think it makes sense to consider what data we should store for GoCardless, especially if it means doing more requests to their API. I mean, it's only one request, but if we don't extract them out and run them separately from the main application, they all happen before the member gets redirected to the post action page.

Also, GC recommends persisting the redirect flow locally, so we should add that. I'm wondering to what degree we should try and make this look similar to our Braintree integration considering that the responses we get are a little different and that it might be important to store different things (like Braintree has no parallel to GC's redirect flow).

Tuuleh commented 8 years ago

@osahyoun I'm writing here just to keep you up-to-date. We want to loop you in on this later when you'll be back to full strength.

We had a call with Neal right now about what we should store in the tables considering that we need to do more requests to get them fully populated. The minimal solution (which we're bending towards) is just to store the IDs we get back from GoCardless upon completing the redirect flow. We saw no use for storing the redirect flow itself and decided not to do that for now. When we'll actually have the member management UI, we will need to add some more data to the tables (e.g. if the user has several mandates, they need to be able to distinguish between them, and so we will need to get some data about the bank accounts and account holder names associated with the mandates). Getting this data could be done periodically on a worker, or we can also just get it from GoCardless when the member requests the UI. We'll talk about this later - but the core idea is that we won't have all the data we have for Braintree by default because that isn't contained in the redirect flow completion response object.

osahyoun commented 8 years ago

Cheers for the update

sjayanna commented 8 years ago

Just curious why not use stripe instead of Gocardless?

Tuuleh commented 8 years ago

We're actually using GoCardless to support direct debit in Europe. We also have a Braintree integration, which we chose over Stripe for a few reasons - our previous stack had a Braintree integration, we had experience with Braintree from previous jobs, Braintree gave us a slightly better offer, and because since Braintree was acquired by PayPal, it comes with a PayPal integration out of the box, which is pretty important for our members. GoCardless is a necessary addition for us because Braintree cannot handle direct debit.