Set up Stripe Payment Gateway in ProCourse Memberships

gateways

#1

Get Stripe API Keys

If you plan on testing ProCourse Memberships with Stripe, make sure to set View test data in the Stripe Dashboard to get your sandbox API keys.

  1. Sign into https://stripe.com/dashboard
  2. Under Developers > API Keys, find your Publishable key and Secret key.

Set up the Stripe Gateway

Configure the following settings under /admin/site_settings/category/procourse_memberships .

  • memberships go live — Check this box only if you are ready to take payments. Requires Live API Publishable and Secret keys when checked.
  • memberships gateway — Set to Stripe
  • memberships stripe publishable key — Enter the sandbox/live Publishable key here.
  • memberships stripe secret key — Enter the sandbox/live Secret key here.

Configure Webhooks (Subscriptions Only)

If you plan on using recurring levels (subscriptions), you must configure a webhook endpoint to send notifications to your site in order for customers to receive receipts upon recurring transaction success or failure.

To enable:

  1. Log into the Stripe Dashboard
  2. Under Developers > Webhooks, click Add endpoint
  3. Enter in the following URL: https://<your Discourse site>/memberships/webhook/stripe
  4. Set the Filter event sector to Select types to send
  5. Check the following event types to send:
    • customer.subscription.deleted
    • invoice.payment_failed
    • invoice.payment_succeeded
  6. Click Add endpoint
  7. Click on the newly created endpoint.
  8. Under Signing secret, obtain the webhook verification key and copy it to the memberships stripe webhook secret setting field under /admin/site_settings/category/procourse_memberships

Creating Levels

Level creation under the Stripe gateway works as normal except in one instance.

When a level is set to recur, the plugin automatically creates the needed product and plan in Stripe. The first time the level is set to Enabled, the billing plan is activated via Stripe’s API.

At this time, Stripe does not allow changes to active billing plans via the API, therefore, once a recurring level is enabled, no further changes are allowed by the plugin. A new level needs to be created with the new settings instead. Levels can still be enabled/disabled and removed.


#2

Hi,

Thanks for this how-t,o, particularly the webhooks part, really helpful to check step by step.

Sadly, I have an error when I test with a recurring payment, in test or live mode.

On the client side, I can’t pass the payment page “Sorry, an error occured”.

If it helps, here some additionnal informations

On the discourse logs, I have this :

NoMethodError (undefined method `pry' for #&lt;Binding:0x00007f3f624b3838&gt; Did you mean? try) /var/www/discourse/plugins/procourse-memberships/lib/procourse_memberships/gateways/stripe.rb:62:in `rescue in subscribe'


/var/www/discourse/plugins/procourse-memberships/lib/procourse_memberships/gateways/stripe.rb:62:in `rescue in subscribe'
/var/www/discourse/plugins/procourse-memberships/lib/procourse_memberships/gateways/stripe.rb:38:in `subscribe'
/var/www/discourse/plugins/procourse-memberships/app/controllers/procourse_memberships/checkout_controller.rb:65:in `submit_payment'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/abstract_controller/base.rb:194:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_controller/metal/rendering.rb:30:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/abstract_controller/callbacks.rb:42:in `block in process_action'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/activesupport-5.2.0/lib/active_support/callbacks.rb:132:in `run_callbacks'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/abstract_controller/callbacks.rb:41:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_controller/metal/rescue.rb:22:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_controller/metal/instrumentation.rb:34:in `block in process_action'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/activesupport-5.2.0/lib/active_support/notifications.rb:168:in `block in instrument'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/activesupport-5.2.0/lib/active_support/notifications/instrumenter.rb:23:in `instrument'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/activesupport-5.2.0/lib/active_support/notifications.rb:168:in `instrument'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_controller/metal/instrumentation.rb:32:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_controller/metal/params_wrapper.rb:256:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/activerecord-5.2.0/lib/active_record/railties/controller_runtime.rb:24:in `process_action'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/abstract_controller/base.rb:134:in `process'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionview-5.2.0/lib/action_view/rendering.rb:32:in `process'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-mini-profiler-1.0.0/lib/mini_profiler/profiling_methods.rb:104:in `block in profile_method'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_controller/metal.rb:191:in `dispatch'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_controller/metal.rb:252:in `dispatch'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:52:in `dispatch'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:34:in `serve'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/mapper.rb:18:in `block in <class:Constraints>'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/mapper.rb:48:in `serve'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/journey/router.rb:52:in `block in serve'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/journey/router.rb:35:in `each'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/journey/router.rb:35:in `serve'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:840:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.0/lib/rails/engine.rb:524:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.0/lib/rails/railtie.rb:190:in `public_send'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.0/lib/rails/railtie.rb:190:in `method_missing'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/mapper.rb:19:in `block in <class:Constraints>'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/mapper.rb:48:in `serve'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/journey/router.rb:52:in `block in serve'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/journey/router.rb:35:in `each'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/journey/router.rb:35:in `serve'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:840:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-protection-2.0.3/lib/rack/protection/frame_options.rb:31:in `call'
/var/www/discourse/lib/middleware/omniauth_bypass_middleware.rb:24:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/tempfile_reaper.rb:15:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/conditional_get.rb:38:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/head.rb:12:in `call'
/var/www/discourse/lib/content_security_policy.rb:14:in `call'
/var/www/discourse/lib/middleware/anonymous_cache.rb:216:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/session/abstract/id.rb:232:in `context'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/session/abstract/id.rb:226:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/middleware/cookies.rb:670:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/middleware/callbacks.rb:28:in `block in call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/activesupport-5.2.0/lib/active_support/callbacks.rb:98:in `run_callbacks'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/middleware/callbacks.rb:26:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/middleware/debug_exceptions.rb:61:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/logster-1.3.1/lib/logster/middleware/reporter.rb:31:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.0/lib/rails/rack/logger.rb:38:in `call_app'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.0/lib/rails/rack/logger.rb:28:in `call'
/var/www/discourse/config/initializers/100-quiet_logger.rb:16:in `call'
/var/www/discourse/config/initializers/100-silence_logger.rb:29:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/middleware/request_id.rb:27:in `call'
/var/www/discourse/lib/middleware/enforce_hostname.rb:17:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/method_override.rb:22:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.0/lib/action_dispatch/middleware/executor.rb:14:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/sendfile.rb:111:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-mini-profiler-1.0.0/lib/mini_profiler/profiler.rb:285:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/message_bus-2.1.6/lib/message_bus/rack/middleware.rb:63:in `call'
/var/www/discourse/lib/middleware/request_tracker.rb:180:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.0/lib/rails/engine.rb:524:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.0/lib/rails/railtie.rb:190:in `public_send'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/railties-5.2.0/lib/rails/railtie.rb:190:in `method_missing'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/urlmap.rb:68:in `block in call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/urlmap.rb:53:in `each'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/urlmap.rb:53:in `call'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/unicorn-5.4.0/lib/unicorn/http_server.rb:606:in `process_client'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/unicorn-5.4.0/lib/unicorn/http_server.rb:701:in `worker_loop'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/unicorn-5.4.0/lib/unicorn/http_server.rb:549:in `spawn_missing_workers'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/unicorn-5.4.0/lib/unicorn/http_server.rb:142:in `start'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/unicorn-5.4.0/bin/unicorn:126:in `<top (required)>'
/var/www/discourse/vendor/bundle/ruby/2.5.0/bin/unicorn:23:in `load'
/var/www/discourse/vendor/bundle/ruby/2.5.0/bin/unicorn:23:in `<main>'
hostname vps575005-app
process_id 458
application_version ed400a90fe1f5e1622e7f28aaddfa5a1aa1afdf4
HTTP_HOST hub.arborersens.fr
REQUEST_URI /memberships/checkout/submit-payment
REQUEST_METHOD POST
HTTP_USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0
HTTP_ACCEPT application/json, text/javascript, /; q=0.01
HTTP_REFERER https://hub.arborersens.fr/memberships/l/8239
HTTP_X_FORWARDED_FOR 86.000.00.00
HTTP_X_REAL_IP 86.000.00.00
username Steven

And the logs on Stripe :

Summary
ID : req_75xTdXXX
Time : 2018/11/27 17:26:51
Method : POST
URL : /v1/subscriptions
Status : 400
IP address : 51.68.000.000
Version : [2017-08-15](https://stripe.com/docs/upgrades#2017-08-15)
Source : Stripe/v1 RubyBindings/3.28.0

#3

Good catch, @Steven. That’s a debugging line that should have been removed. I just pulled that out so you’ll need to update the plugin.

That said, the location of that debugger is in an error catch. Which means there’s a likely error with the Stripe configuration. But with the debugger gone, it should display an error message that actually means something.

Would you be willing to give it another go and let us know what you see?


#4

Thanks for the reply

An error occured: the server responded with status 400

I double checked the webhook on Stripe, I think I did it well.

I wonder if it isn’t because the currency is different on the initial payment (euros) and the reccuring one (dollars) :

https://talk.procourse.co/uploads/default/original/1X/b568879583d57206fad8e50e2ad40c13466bff88.png

I thought it was just a text that didn’t have any incidence.

On Stripe, I have this lines in the logs

https://talk.procourse.co/uploads/default/original/1X/b568879583d57206fad8e50e2ad40c13466bff88.png

It’s missing a parameter but I can’t see which one

I checked the customer.subscription.deleted and these two

https://talk.procourse.co/uploads/default/original/1X/e185e03330334b6d0a2bb337253e4497d89b32e9.png

:thinking:


#5

This may be the source of the issue. Stripe is pretty picky with currencies. I’d try doing some configuration changes to make sure currencies are the same the entirety of the process around with that and give it a shot again.

Keep us posted with your findings!


#6

Little update : I did a test with the USD currency, thinking it would be ok. I have the same issue with the recurring membership. So I think it’s not really the currency that was the issue

Is it possible to mess something up in the stripe configuration (a setting I may have forgotten on the sripe dashboard), when I’m sure these are ok?

The webhook url

I put this : https://hub.arborersens.fr/memberships/webhook/stripe, and it should be ok. Unless the url changed since then?

  • Check the following event types to send:
    • customer.subscription.deleted
    • invoice.payment_failed
    • invoice.payment_succeeded

I checked and double check, the webhook is correctly configured

  • memberships gateway — Set to Stripe
  • memberships stripe publishable key — Enter the sandbox/live Publishable key here.
  • memberships stripe secret key — Enter the sandbox/live Secret key here.

I deleted, re-created the keys and they are correctly configured in the settings. For an immediat payment, the tests are all ok, it’s just the webhook. I still have the error 400

The logs on Stripe show this:

image

In the err400

Request POST body

{
"customer": "cus_E8d***********",
"trial_from_plan": "false"
}

Response body

{
  "error": {
    "code": "parameter_missing",
    "doc_url": "https://stripe.com/docs/error-codes/parameter-missing",
    "message": "Missing required param: items.",
    "param": "items",
    "type": "invalid_request_error"
  }
}

I think I should have shown the message before. Missing required param: items. But I don’t see something useful on the stripe website

There’s nothing urgent at all by the way, I’m just sharing this little updates, hoping we can find a solution. I will contact the client if we can try another gateway if it’s only a bug on my part


#7

Hey @Steven, I tried to replicate on our demo server and can’t get the error to throw. Do you have all fields filled out in the new level when you’re trying to create it? Those need to be completed before saving, as all the info is sent to the Stripe API then.


#8

Ah this is it, I recreated it entirely, fill every option before saving it and it passed, even in euros. I think I did it well the first time, but maybe something was changed at the time.

I’m a little dumb with this plugin, it’s pretty rare usually :sweat:

Sorry about that, and thanks for the solution

You can clean the topic if you want, my posts won’t be useful for others :grin:


#9

Oh no worries! Glad it’s working. :hugs: