From ba43e9d977dae39ebb06213fbd159ea270edee0c Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Fri, 10 Jan 2020 14:24:39 +1100 Subject: [PATCH 01/14] name space tests --- test/javascripts/acceptance/payments-test.js.es6 | 10 +++------- test/javascripts/acceptance/plugin-outlets-test.js.es6 | 2 +- test/javascripts/acceptance/subscribe-test.js.es6 | 2 +- test/javascripts/helpers/product-pretender.js.es6 | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/test/javascripts/acceptance/payments-test.js.es6 b/test/javascripts/acceptance/payments-test.js.es6 index 4978a69..b0391f6 100644 --- a/test/javascripts/acceptance/payments-test.js.es6 +++ b/test/javascripts/acceptance/payments-test.js.es6 @@ -1,18 +1,14 @@ import { acceptance } from "helpers/qunit-helpers"; import { stubStripe } from "discourse/plugins/discourse-subscriptions/helpers/stripe"; -acceptance("Discourse Patrons", { - settings: { - discourse_patrons_amounts: "1.00|2.00" - }, - +acceptance("Discourse Subscriptions", { beforeEach() { stubStripe(); } }); -QUnit.skip("viewing the one-off payment page", async assert => { +QUnit.test("viewing payment page", async assert => { await visit("/s"); - assert.ok($(".donations-page-payment").length, "has payment form class"); + assert.ok($("#product-list").length, "has payment page"); }); diff --git a/test/javascripts/acceptance/plugin-outlets-test.js.es6 b/test/javascripts/acceptance/plugin-outlets-test.js.es6 index 931c8aa..f7c769f 100644 --- a/test/javascripts/acceptance/plugin-outlets-test.js.es6 +++ b/test/javascripts/acceptance/plugin-outlets-test.js.es6 @@ -1,6 +1,6 @@ import { acceptance } from "helpers/qunit-helpers"; -acceptance("Discourse Patrons", { +acceptance("Discourse Subscriptions", { settings: { discourse_subscriptions_extra_nav_subscribe: true } diff --git a/test/javascripts/acceptance/subscribe-test.js.es6 b/test/javascripts/acceptance/subscribe-test.js.es6 index 439059f..211e8b9 100644 --- a/test/javascripts/acceptance/subscribe-test.js.es6 +++ b/test/javascripts/acceptance/subscribe-test.js.es6 @@ -1,6 +1,6 @@ import { acceptance } from "helpers/qunit-helpers"; -acceptance("Discourse Patrons", { +acceptance("Discourse Subscriptions", { settings: { discourse_patrons_subscription_group: "plan-id" }, diff --git a/test/javascripts/helpers/product-pretender.js.es6 b/test/javascripts/helpers/product-pretender.js.es6 index aaa85d6..3e53515 100644 --- a/test/javascripts/helpers/product-pretender.js.es6 +++ b/test/javascripts/helpers/product-pretender.js.es6 @@ -1,7 +1,7 @@ export default function(helpers) { const { response } = helpers; - this.get("/patrons/products", () => { + this.get("/s/products", () => { const products = [ { id: "prod_23o8I7tU4g56", From 7e2d4595caed04199ecb9537c4724c57611bf4f9 Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Fri, 10 Jan 2020 14:41:32 +1100 Subject: [PATCH 02/14] test the payment and subscribe page --- test/javascripts/acceptance/payments-test.js.es6 | 9 ++++++--- test/javascripts/acceptance/subscribe-test.js.es6 | 13 ++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/test/javascripts/acceptance/payments-test.js.es6 b/test/javascripts/acceptance/payments-test.js.es6 index b0391f6..998f69f 100644 --- a/test/javascripts/acceptance/payments-test.js.es6 +++ b/test/javascripts/acceptance/payments-test.js.es6 @@ -4,11 +4,14 @@ import { stubStripe } from "discourse/plugins/discourse-subscriptions/helpers/st acceptance("Discourse Subscriptions", { beforeEach() { stubStripe(); - } + }, + + loggedIn: true }); -QUnit.test("viewing payment page", async assert => { +QUnit.test("viewing product page", async assert => { await visit("/s"); - assert.ok($("#product-list").length, "has payment page"); + assert.ok($("#product-list").length, "has product page"); + assert.ok($(".product:first-child a").length, "has a link"); }); diff --git a/test/javascripts/acceptance/subscribe-test.js.es6 b/test/javascripts/acceptance/subscribe-test.js.es6 index 211e8b9..af50606 100644 --- a/test/javascripts/acceptance/subscribe-test.js.es6 +++ b/test/javascripts/acceptance/subscribe-test.js.es6 @@ -1,14 +1,17 @@ import { acceptance } from "helpers/qunit-helpers"; +import { stubStripe } from "discourse/plugins/discourse-subscriptions/helpers/stripe"; acceptance("Discourse Subscriptions", { - settings: { - discourse_patrons_subscription_group: "plan-id" + beforeEach() { + stubStripe(); }, + loggedIn: true }); -QUnit.skip("subscribing", async assert => { - await visit("/patrons/subscribe"); +QUnit.test("subscribing", async assert => { + await visit("/s"); - assert.ok($("h3").length, "has a heading"); + assert.ok($("#product-list").length, "has product page"); + assert.ok($(".product:first-child a").length, "has a link"); }); From 9902947e99db27aa192ced46e36b70e47df2a1da Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Fri, 10 Jan 2020 16:47:28 +1100 Subject: [PATCH 03/14] a bit more testing --- .../acceptance/subscribe-test.js.es6 | 8 ++++++-- .../helpers/product-pretender.js.es6 | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/test/javascripts/acceptance/subscribe-test.js.es6 b/test/javascripts/acceptance/subscribe-test.js.es6 index af50606..7b9d8bf 100644 --- a/test/javascripts/acceptance/subscribe-test.js.es6 +++ b/test/javascripts/acceptance/subscribe-test.js.es6 @@ -12,6 +12,10 @@ acceptance("Discourse Subscriptions", { QUnit.test("subscribing", async assert => { await visit("/s"); - assert.ok($("#product-list").length, "has product page"); - assert.ok($(".product:first-child a").length, "has a link"); + await click(".product:first-child a"); + + assert.ok( + $(".discourse-subscriptions-section-columns").length, + "has a the sections for billing" + ); }); diff --git a/test/javascripts/helpers/product-pretender.js.es6 b/test/javascripts/helpers/product-pretender.js.es6 index 3e53515..14ccb40 100644 --- a/test/javascripts/helpers/product-pretender.js.es6 +++ b/test/javascripts/helpers/product-pretender.js.es6 @@ -19,4 +19,23 @@ export default function(helpers) { return response(products); }); + + this.get("/s/products/:id", () => { + const product = {}; + + return response(product); + }); + + this.get("/s/plans", () => { + const plans = [ + { + id: "plan_GHGHSHS8654G", + amount: 200, + currency: "usd", + interval: "month" + } + ]; + + return response(plans); + }); } From ae36c00fc0fdcb6acce7394e1a01b4568774442f Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Sun, 12 Jan 2020 09:08:00 +1100 Subject: [PATCH 04/14] test buttons show --- test/javascripts/acceptance/subscribe-test.js.es6 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/javascripts/acceptance/subscribe-test.js.es6 b/test/javascripts/acceptance/subscribe-test.js.es6 index 7b9d8bf..815bb38 100644 --- a/test/javascripts/acceptance/subscribe-test.js.es6 +++ b/test/javascripts/acceptance/subscribe-test.js.es6 @@ -16,6 +16,8 @@ QUnit.test("subscribing", async assert => { assert.ok( $(".discourse-subscriptions-section-columns").length, - "has a the sections for billing" + "has the sections for billing" ); + + assert.ok($(".subscribe-buttons button").length, "has buttons for subscribe"); }); From 0543b3a6a39029b6dd6adece0224c7abe1ea8e05 Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Mon, 13 Jan 2020 11:10:06 +1100 Subject: [PATCH 05/14] basic hook --- app/controllers/hooks_controller.rb | 19 +++++++++++++++++++ config/locales/client.en.yml | 1 + config/settings.yml | 3 +++ spec/requests/hooks_controller_spec.rb | 12 +++++++++++- 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/controllers/hooks_controller.rb b/app/controllers/hooks_controller.rb index 926f01d..9245d11 100644 --- a/app/controllers/hooks_controller.rb +++ b/app/controllers/hooks_controller.rb @@ -3,6 +3,25 @@ module DiscourseSubscriptions class HooksController < ::ApplicationController def create + begin + + # payload, sig_header, endpoint_secret + event = ::Stripe::Webhook.construct_event( + {}, + 'stripe-webhook-signature', + 'endpoint_secret' + ) + + rescue JSON::ParserError => e + # Invalid payload + status 400 + return + rescue Stripe::SignatureVerificationError => e + # Invalid signature + status 400 + return + end + head 200 end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index c23a3a2..bd162e6 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4,6 +4,7 @@ en: discourse_subscriptions_extra_nav_subscribe: Show the subscribe button in the primary navigation discourse_subscriptions_public_key: Stripe Publishable Key discourse_subscriptions_secret_key: Stripe Secret Key + discourse_subscriptions_webhook_secret: Stripe Webhook Secret discourse_subscriptions_currency: Default Currency Code. This can be overridden when creating a subscription plan. discourse_subscriptions_allow_payments: Allow single payments errors: diff --git a/config/settings.yml b/config/settings.yml index 0ed6c2e..fd3975c 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -10,6 +10,9 @@ plugins: discourse_subscriptions_secret_key: default: '' client: false + discourse_subscriptions_webhook_secret: + default: '' + client: false discourse_subscriptions_allow_payments: default: false client: true diff --git a/spec/requests/hooks_controller_spec.rb b/spec/requests/hooks_controller_spec.rb index f7687d8..6cf5083 100644 --- a/spec/requests/hooks_controller_spec.rb +++ b/spec/requests/hooks_controller_spec.rb @@ -4,8 +4,18 @@ require 'rails_helper' module DiscourseSubscriptions RSpec.describe HooksController do - it "responds ok" do + it "contructs a webhook event" do + ::Stripe::Webhook + .expects(:construct_event) + .with({}, 'stripe-webhook-signature', 'endpoint_secret') + .returns(true) + + headers = { + 'HTTP_STRIPE_SIGNATURE' => 'stripe-webhook-signature' + } + post "/s/hooks.json" + expect(response.status).to eq 200 end end From 95413ee161e6b306ed0a6e525b75edc5ea8d0b2a Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Tue, 14 Jan 2020 15:37:53 +1100 Subject: [PATCH 06/14] get the payload, signature and secret for webhook --- app/controllers/hooks_controller.rb | 12 ++++++------ spec/requests/hooks_controller_spec.rb | 16 +++++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/controllers/hooks_controller.rb b/app/controllers/hooks_controller.rb index 9245d11..c87dfe9 100644 --- a/app/controllers/hooks_controller.rb +++ b/app/controllers/hooks_controller.rb @@ -2,15 +2,15 @@ module DiscourseSubscriptions class HooksController < ::ApplicationController + skip_before_action :verify_authenticity_token, only: [:create] + def create begin + payload = request.body.read + sig_header = request.env['HTTP_STRIPE_SIGNATURE'] + webhook_secret = SiteSetting.discourse_subscriptions_webhook_secret - # payload, sig_header, endpoint_secret - event = ::Stripe::Webhook.construct_event( - {}, - 'stripe-webhook-signature', - 'endpoint_secret' - ) + event = ::Stripe::Webhook.construct_event(payload, sig_header, webhook_secret) rescue JSON::ParserError => e # Invalid payload diff --git a/spec/requests/hooks_controller_spec.rb b/spec/requests/hooks_controller_spec.rb index 6cf5083..3cb2303 100644 --- a/spec/requests/hooks_controller_spec.rb +++ b/spec/requests/hooks_controller_spec.rb @@ -4,17 +4,19 @@ require 'rails_helper' module DiscourseSubscriptions RSpec.describe HooksController do + before do + SiteSetting.discourse_subscriptions_webhook_secret = 'zascharoo' + end + it "contructs a webhook event" do + payload = 'we-want-a-shrubbery' + headers = { 'HTTP_STRIPE_SIGNATURE' => 'stripe-webhook-signature' } + ::Stripe::Webhook .expects(:construct_event) - .with({}, 'stripe-webhook-signature', 'endpoint_secret') - .returns(true) + .with('we-want-a-shrubbery', 'stripe-webhook-signature', 'zascharoo') - headers = { - 'HTTP_STRIPE_SIGNATURE' => 'stripe-webhook-signature' - } - - post "/s/hooks.json" + post "/s/hooks.json", params: payload, headers: headers expect(response.status).to eq 200 end From b83da467d03c6922c9e777805e2bbaccd2818785 Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Tue, 14 Jan 2020 18:46:48 +1100 Subject: [PATCH 07/14] respond to incomplete payment --- README.md | 30 ++++++++++++++----- app/controllers/hooks_controller.rb | 6 ++-- .../discourse/controllers/s-show.js.es6 | 11 +++++-- config/locales/client.en.yml | 2 ++ 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 326b7d4..e999c23 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,16 @@ See [Screenshots](#screenshots) below. * Be sure your site is enforcing https. * Follow the install instructions here: https://meta.discourse.org/t/install-a-plugin/19157 -* Add your Stripe public and private keys in settings and set the currency to your local value. + +##Settings: + +You'll need to get some info from your Stripe account to complete the steps below: https://dashboard.stripe.com/ + +* Add your Stripe public and private keys +* Set the currency to your local value. +* Add your Stripe webhook secret. + +See webhook info below. ## What are Subscriptions? @@ -34,6 +43,14 @@ Firstly, you'll need an account with the [Stripe](https://stripe.com) payment ga When you get a moment, take a look at Stripe's documentation. But for now, you can set up an account in test mode and see how it all works without making any real transactions. Then, if you're happy with how everything works, you can start taking real transactions. See below for test credit card numbers. +### Enable Webhooks in your Stripe account + +You'll need to tell Stripe where your end points are. You can enter this in your Stripe dashboard. + +The address for webhooks is: `[your server address]/s/hooks` + +Also: Add the webhook secret in settings (above). + ### Set up your User Groups in Discourse When a user successfully subscribes to your Discourse application, after their credit card transaction has been processed, they are added to a User Group. By assigning users to a User Group, you can manage what your users have access to on your website. User groups are a core functionality of Discourse and this plugin does nothing with them except and and remove users from the group you associated with your Plan. @@ -52,17 +69,14 @@ In the admin, add a new Product. Once you have a product saved, you can add plan If you take a look at your [Stripe Dashboard](https://dashboard.stripe.com), you'll see all those products and plans are listed. Discourse Subscriptions does not create them locally. They are created in Stripe. -## Enable Webhooks - -You'll need to tell Stripe where your end points are. You can enter this in your Stripe dashboard. - -The address for webhooks is: `/s/hooks` - ## Testing Test with these credit card numbers: -* 4111 1111 1111 1111 +* 4111 1111 1111 1111 (no authentication required) +* 4000 0027 6000 3184 (authentication required) + +For more test card numbers: https://stripe.com/docs/testing Visit `/s` and enter a few test transactions. diff --git a/app/controllers/hooks_controller.rb b/app/controllers/hooks_controller.rb index c87dfe9..fcc16e0 100644 --- a/app/controllers/hooks_controller.rb +++ b/app/controllers/hooks_controller.rb @@ -13,12 +13,10 @@ module DiscourseSubscriptions event = ::Stripe::Webhook.construct_event(payload, sig_header, webhook_secret) rescue JSON::ParserError => e - # Invalid payload - status 400 + render_json_error e.message return rescue Stripe::SignatureVerificationError => e - # Invalid signature - status 400 + render_json_error e.message return end diff --git a/assets/javascripts/discourse/controllers/s-show.js.es6 b/assets/javascripts/discourse/controllers/s-show.js.es6 index d0ea851..86a0c54 100644 --- a/assets/javascripts/discourse/controllers/s-show.js.es6 +++ b/assets/javascripts/discourse/controllers/s-show.js.es6 @@ -49,7 +49,7 @@ export default Ember.Controller.extend({ }); }, - createSubsciption(plan) { + createSubscription(plan) { return this.stripe.createToken(this.get("cardElement")).then(result => { if (result.error) { return result; @@ -85,7 +85,7 @@ export default Ember.Controller.extend({ let transaction; if (this.planTypeIsSelected) { - transaction = this.createSubsciption(plan); + transaction = this.createSubscription(plan); } else { transaction = this.createPayment(plan); } @@ -95,7 +95,12 @@ export default Ember.Controller.extend({ if (result.error) { bootbox.alert(result.error.message || result.error); } else { - this.alert(`${type}.success`); + if(result.status === "incomplete") { + this.alert(`${type}.incomplete`); + } + else { + this.alert(`${type}.success`); + } const success_route = this.planTypeIsSelected ? "user.billing.subscriptions" diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index bd162e6..020b917 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -32,6 +32,7 @@ en: payment_button: Subscribe success: Thank you! Your subscription has been created. + incomplete: The payment is incomplete. Your subscription will be updated when payment is complete. validate: payment_options: required: Please select a subscription plan. @@ -43,6 +44,7 @@ en: payment_button: Pay Once success: Thank you! + incomplete: Payment is incomplete. validate: payment_options: required: Please select a payment option. From 071e1d6cb5a29b164c37f7dd2cc8966a67a68144 Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Tue, 14 Jan 2020 20:15:54 +1100 Subject: [PATCH 08/14] whoops prettier complaint --- assets/javascripts/discourse/controllers/s-show.js.es6 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/javascripts/discourse/controllers/s-show.js.es6 b/assets/javascripts/discourse/controllers/s-show.js.es6 index 86a0c54..d481024 100644 --- a/assets/javascripts/discourse/controllers/s-show.js.es6 +++ b/assets/javascripts/discourse/controllers/s-show.js.es6 @@ -95,10 +95,9 @@ export default Ember.Controller.extend({ if (result.error) { bootbox.alert(result.error.message || result.error); } else { - if(result.status === "incomplete") { + if (result.status === "incomplete") { this.alert(`${type}.incomplete`); - } - else { + } else { this.alert(`${type}.success`); } From 190ca2808940a8f7682a602b62b50e456e4896f5 Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Tue, 14 Jan 2020 20:58:34 +1100 Subject: [PATCH 09/14] rm subscription event --- app/controllers/hooks_controller.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/controllers/hooks_controller.rb b/app/controllers/hooks_controller.rb index fcc16e0..d536a35 100644 --- a/app/controllers/hooks_controller.rb +++ b/app/controllers/hooks_controller.rb @@ -2,6 +2,7 @@ module DiscourseSubscriptions class HooksController < ::ApplicationController + include DiscourseSubscriptions::Group skip_before_action :verify_authenticity_token, only: [:create] def create @@ -20,6 +21,29 @@ module DiscourseSubscriptions return end + # Handle the event + case event[:type] + when 'customer.subscription.deleted' + + customer = Customer.find_by( + customer_id: event[:customer], + product_id: event[:plan][:product] + ) + + if customer + customer.delete + + user = ::User.find(customer.user_id) + group = plan_group(event[:plan]) + group.remove(user) if group + end + + else + # Unexpected event type + status 400 + return + end + head 200 end end From dca6c7ddc9562d1ee6c35b7972a757503f9a3d74 Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Tue, 14 Jan 2020 23:38:26 +1100 Subject: [PATCH 10/14] deletes the customer on subscription cancel --- app/controllers/hooks_controller.rb | 16 +++++-------- spec/requests/hooks_controller_spec.rb | 31 +++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/app/controllers/hooks_controller.rb b/app/controllers/hooks_controller.rb index d536a35..442e9eb 100644 --- a/app/controllers/hooks_controller.rb +++ b/app/controllers/hooks_controller.rb @@ -21,7 +21,6 @@ module DiscourseSubscriptions return end - # Handle the event case event[:type] when 'customer.subscription.deleted' @@ -32,16 +31,13 @@ module DiscourseSubscriptions if customer customer.delete - - user = ::User.find(customer.user_id) - group = plan_group(event[:plan]) - group.remove(user) if group + # + # binding.pry + # + # user = ::User.find(customer.user_id) + # group = plan_group(event[:plan]) + # group.remove(user) if group end - - else - # Unexpected event type - status 400 - return end head 200 diff --git a/spec/requests/hooks_controller_spec.rb b/spec/requests/hooks_controller_spec.rb index 3cb2303..345b0d3 100644 --- a/spec/requests/hooks_controller_spec.rb +++ b/spec/requests/hooks_controller_spec.rb @@ -10,15 +10,44 @@ module DiscourseSubscriptions it "contructs a webhook event" do payload = 'we-want-a-shrubbery' - headers = { 'HTTP_STRIPE_SIGNATURE' => 'stripe-webhook-signature' } + headers = { HTTP_STRIPE_SIGNATURE: 'stripe-webhook-signature' } ::Stripe::Webhook .expects(:construct_event) .with('we-want-a-shrubbery', 'stripe-webhook-signature', 'zascharoo') + .returns(type: 'something') post "/s/hooks.json", params: payload, headers: headers expect(response.status).to eq 200 end + + it "cancels a subscription" do + user = Fabricate(:user) + group = Fabricate(:group, name: 'subscribers-group') + + customer = Fabricate( + :customer, + customer_id: 'c_575768', + product_id: 'p_8654', + user_id: user.id + ) + + event = { + type: 'customer.subscription.deleted', + customer: customer.customer_id, + plan: { product: customer.product_id, metadata: { group_name: group.name } } + } + + ::Stripe::Webhook + .expects(:construct_event) + .returns(event) + + expect { + post "/s/hooks.json" + }.to change { DiscourseSubscriptions::Customer.count }.by(-1) + + expect(response.status).to eq 200 + end end end From 83e7ae2711d406ed1bd41deb0207f4734bcd0008 Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Wed, 15 Jan 2020 10:20:21 +1100 Subject: [PATCH 11/14] spec unsubscribe webhook --- app/controllers/hooks_controller.rb | 10 +++--- spec/requests/hooks_controller_spec.rb | 50 +++++++++++++++----------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/app/controllers/hooks_controller.rb b/app/controllers/hooks_controller.rb index 442e9eb..babfa31 100644 --- a/app/controllers/hooks_controller.rb +++ b/app/controllers/hooks_controller.rb @@ -31,12 +31,10 @@ module DiscourseSubscriptions if customer customer.delete - # - # binding.pry - # - # user = ::User.find(customer.user_id) - # group = plan_group(event[:plan]) - # group.remove(user) if group + + user = ::User.find(customer.user_id) + group = plan_group(event[:plan]) + group.remove(user) if group end end diff --git a/spec/requests/hooks_controller_spec.rb b/spec/requests/hooks_controller_spec.rb index 345b0d3..50d27a6 100644 --- a/spec/requests/hooks_controller_spec.rb +++ b/spec/requests/hooks_controller_spec.rb @@ -22,32 +22,40 @@ module DiscourseSubscriptions expect(response.status).to eq 200 end - it "cancels a subscription" do - user = Fabricate(:user) - group = Fabricate(:group, name: 'subscribers-group') + describe "canceling a subscription" do + let(:user) { Fabricate(:user) } + let(:group) { Fabricate(:group, name: 'subscribers-group') } + let(:customer) { Fabricate(:customer, customer_id: 'c_575768', product_id: 'p_8654', user_id: user.id) } - customer = Fabricate( - :customer, - customer_id: 'c_575768', - product_id: 'p_8654', - user_id: user.id - ) + before do + event = { + type: 'customer.subscription.deleted', + customer: customer.customer_id, + plan: { product: customer.product_id, metadata: { group_name: group.name } } + } - event = { - type: 'customer.subscription.deleted', - customer: customer.customer_id, - plan: { product: customer.product_id, metadata: { group_name: group.name } } - } + ::Stripe::Webhook + .stubs(:construct_event) + .returns(event) - ::Stripe::Webhook - .expects(:construct_event) - .returns(event) + group.add(user) + end - expect { - post "/s/hooks.json" - }.to change { DiscourseSubscriptions::Customer.count }.by(-1) + it "deletes the customer" do + expect { + post "/s/hooks.json" + }.to change { DiscourseSubscriptions::Customer.count }.by(-1) - expect(response.status).to eq 200 + expect(response.status).to eq 200 + end + + it "removes the user from the group" do + expect { + post "/s/hooks.json" + }.to change { user.groups.count }.by(-1) + + expect(response.status).to eq 200 + end end end end From 3ec1907c1c0ce8b58fe41333cd806fb659a6c178 Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Wed, 15 Jan 2020 10:33:33 +1100 Subject: [PATCH 12/14] upgrade stripe gem --- plugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.rb b/plugin.rb index 67d8cf9..843726b 100644 --- a/plugin.rb +++ b/plugin.rb @@ -8,7 +8,7 @@ enabled_site_setting :discourse_subscriptions_enabled -gem 'stripe', '5.13.0' +gem 'stripe', '5.14.0' register_asset "stylesheets/common/main.scss" register_asset "stylesheets/common/layout.scss" From ebe891c09b6b84734f4479304420fe69d6d44cec Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Sat, 25 Jan 2020 15:06:05 +1100 Subject: [PATCH 13/14] fix event hash for subscription --- README.md | 6 ++- app/controllers/hooks_controller.rb | 7 +-- spec/requests/hooks_controller_spec.rb | 73 ++++++++++++++++++-------- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index e999c23..6f36b20 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,14 @@ When you get a moment, take a look at Stripe's documentation. But for now, you c ### Enable Webhooks in your Stripe account You'll need to tell Stripe where your end points are. You can enter this in your Stripe dashboard. +Also: Add the webhook secret in settings (above). The address for webhooks is: `[your server address]/s/hooks` -Also: Add the webhook secret in settings (above). +Discourse Subscriptions responds to the following events: + +* `customer.subscription.deleted` +* `customer.subscription.updated` ### Set up your User Groups in Discourse diff --git a/app/controllers/hooks_controller.rb b/app/controllers/hooks_controller.rb index babfa31..c9a9ad2 100644 --- a/app/controllers/hooks_controller.rb +++ b/app/controllers/hooks_controller.rb @@ -22,18 +22,19 @@ module DiscourseSubscriptions end case event[:type] + when 'customer.subscription.updated' when 'customer.subscription.deleted' customer = Customer.find_by( - customer_id: event[:customer], - product_id: event[:plan][:product] + customer_id: event[:data][:object][:customer], + product_id: event[:data][:object][:plan][:product] ) if customer customer.delete user = ::User.find(customer.user_id) - group = plan_group(event[:plan]) + group = plan_group(event[:data][:object][:plan]) group.remove(user) if group end end diff --git a/spec/requests/hooks_controller_spec.rb b/spec/requests/hooks_controller_spec.rb index 50d27a6..c0c25ed 100644 --- a/spec/requests/hooks_controller_spec.rb +++ b/spec/requests/hooks_controller_spec.rb @@ -22,39 +22,66 @@ module DiscourseSubscriptions expect(response.status).to eq 200 end - describe "canceling a subscription" do + describe "event types" do let(:user) { Fabricate(:user) } - let(:group) { Fabricate(:group, name: 'subscribers-group') } let(:customer) { Fabricate(:customer, customer_id: 'c_575768', product_id: 'p_8654', user_id: user.id) } - before do - event = { - type: 'customer.subscription.deleted', - customer: customer.customer_id, - plan: { product: customer.product_id, metadata: { group_name: group.name } } - } + describe "customer.subscription.updated" do + before do + event = { + type: 'customer.subscription.updated', + data: { + object: {} + } + } - ::Stripe::Webhook - .stubs(:construct_event) - .returns(event) + ::Stripe::Webhook + .stubs(:construct_event) + .returns(event) + end - group.add(user) + it 'is successfull' do + post "/s/hooks.json" + expect(response.status).to eq 200 + end end - it "deletes the customer" do - expect { - post "/s/hooks.json" - }.to change { DiscourseSubscriptions::Customer.count }.by(-1) + describe "customer.subscription.deleted" do + let(:group) { Fabricate(:group, name: 'subscribers-group') } - expect(response.status).to eq 200 - end + before do + event = { + type: 'customer.subscription.deleted', + data: { + object: { + customer: customer.customer_id, + plan: { product: customer.product_id, metadata: { group_name: group.name } } + } + } + } - it "removes the user from the group" do - expect { - post "/s/hooks.json" - }.to change { user.groups.count }.by(-1) + ::Stripe::Webhook + .stubs(:construct_event) + .returns(event) - expect(response.status).to eq 200 + group.add(user) + end + + it "deletes the customer" do + expect { + post "/s/hooks.json" + }.to change { DiscourseSubscriptions::Customer.count }.by(-1) + + expect(response.status).to eq 200 + end + + it "removes the user from the group" do + expect { + post "/s/hooks.json" + }.to change { user.groups.count }.by(-1) + + expect(response.status).to eq 200 + end end end end From cc389d2423fedfb45951395677762f915be358b5 Mon Sep 17 00:00:00 2001 From: Rimian Perkins Date: Sun, 26 Jan 2020 10:49:51 +1100 Subject: [PATCH 14/14] add the user to the group if subscription is completing --- app/controllers/hooks_controller.rb | 25 +++++++++++ spec/requests/hooks_controller_spec.rb | 58 +++++++++++++++++++++----- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/app/controllers/hooks_controller.rb b/app/controllers/hooks_controller.rb index c9a9ad2..1c3e2c1 100644 --- a/app/controllers/hooks_controller.rb +++ b/app/controllers/hooks_controller.rb @@ -23,6 +23,17 @@ module DiscourseSubscriptions case event[:type] when 'customer.subscription.updated' + customer = Customer.find_by( + customer_id: event[:data][:object][:customer], + product_id: event[:data][:object][:plan][:product] + ) + + if customer && subscription_completion?(event) + user = ::User.find(customer.user_id) + group = plan_group(event[:data][:object][:plan]) + group.add(user) if group + end + when 'customer.subscription.deleted' customer = Customer.find_by( @@ -41,5 +52,19 @@ module DiscourseSubscriptions head 200 end + + private + + def subscription_completion?(event) + subscription_complete?(event) && previously_incomplete?(event) + end + + def subscription_complete?(event) + event.dig(:data, :object, :status) == 'complete' + end + + def previously_incomplete?(event) + event.dig(:data, :previous_attributes, :status) == 'incomplete' + end end end diff --git a/spec/requests/hooks_controller_spec.rb b/spec/requests/hooks_controller_spec.rb index c0c25ed..6398464 100644 --- a/spec/requests/hooks_controller_spec.rb +++ b/spec/requests/hooks_controller_spec.rb @@ -25,14 +25,22 @@ module DiscourseSubscriptions describe "event types" do let(:user) { Fabricate(:user) } let(:customer) { Fabricate(:customer, customer_id: 'c_575768', product_id: 'p_8654', user_id: user.id) } + let(:group) { Fabricate(:group, name: 'subscribers-group') } + + let(:event_data) do + { + object: { + customer: customer.customer_id, + plan: { product: customer.product_id, metadata: { group_name: group.name } } + } + } + end describe "customer.subscription.updated" do before do event = { type: 'customer.subscription.updated', - data: { - object: {} - } + data: event_data } ::Stripe::Webhook @@ -44,20 +52,48 @@ module DiscourseSubscriptions post "/s/hooks.json" expect(response.status).to eq 200 end + + describe 'completing the subscription' do + it 'does not add the user to the group' do + event_data[:object][:status] = 'incomplete' + event_data[:previous_attributes] = { status: 'incomplete' } + + expect { + post "/s/hooks.json" + }.not_to change { user.groups.count } + + expect(response.status).to eq 200 + end + + it 'does not add the user to the group' do + event_data[:object][:status] = 'incomplete' + event_data[:previous_attributes] = { status: 'something-else' } + + expect { + post "/s/hooks.json" + }.not_to change { user.groups.count } + + expect(response.status).to eq 200 + end + + it 'adds the user to the group when completing the transaction' do + event_data[:object][:status] = 'complete' + event_data[:previous_attributes] = { status: 'incomplete' } + + expect { + post "/s/hooks.json" + }.to change { user.groups.count }.by(1) + + expect(response.status).to eq 200 + end + end end describe "customer.subscription.deleted" do - let(:group) { Fabricate(:group, name: 'subscribers-group') } - before do event = { type: 'customer.subscription.deleted', - data: { - object: { - customer: customer.customer_id, - plan: { product: customer.product_id, metadata: { group_name: group.name } } - } - } + data: event_data } ::Stripe::Webhook