webhook updates subscription

This commit is contained in:
Rimian Perkins 2020-01-26 11:53:56 +11:00
commit 272f3eb998
11 changed files with 256 additions and 30 deletions

View File

@ -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,18 @@ 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.
Also: Add the webhook secret in settings (above).
The address for webhooks is: `[your server address]/s/hooks`
Discourse Subscriptions responds to the following events:
* `customer.subscription.deleted`
* `customer.subscription.updated`
### 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 +73,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.

View File

@ -2,10 +2,69 @@
module DiscourseSubscriptions
class HooksController < ::ApplicationController
include DiscourseSubscriptions::Group
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
event = ::Stripe::Webhook.construct_event(payload, sig_header, webhook_secret)
rescue JSON::ParserError => e
render_json_error e.message
return
rescue Stripe::SignatureVerificationError => e
render_json_error e.message
return
end
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(
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[:data][:object][:plan])
group.remove(user) if group
end
end
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

View File

@ -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,11 @@ 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"

View File

@ -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:
@ -31,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.
@ -42,6 +44,7 @@ en:
payment_button:
Pay Once
success: Thank you!
incomplete: Payment is incomplete.
validate:
payment_options:
required: Please select a payment option.

View File

@ -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

View File

@ -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"

View File

@ -4,9 +4,121 @@ require 'rails_helper'
module DiscourseSubscriptions
RSpec.describe HooksController do
it "responds ok" do
post "/s/hooks.json"
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('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
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: event_data
}
::Stripe::Webhook
.stubs(:construct_event)
.returns(event)
end
it 'is successfull' do
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
before do
event = {
type: 'customer.subscription.deleted',
data: event_data
}
::Stripe::Webhook
.stubs(:construct_event)
.returns(event)
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
end

View File

@ -1,18 +1,17 @@
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();
}
},
loggedIn: true
});
QUnit.skip("viewing the one-off payment page", async assert => {
QUnit.test("viewing product page", async assert => {
await visit("/s");
assert.ok($(".donations-page-payment").length, "has payment form class");
assert.ok($("#product-list").length, "has product page");
assert.ok($(".product:first-child a").length, "has a link");
});

View File

@ -1,6 +1,6 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Discourse Patrons", {
acceptance("Discourse Subscriptions", {
settings: {
discourse_subscriptions_extra_nav_subscribe: true
}

View File

@ -1,14 +1,23 @@
import { acceptance } from "helpers/qunit-helpers";
import { stubStripe } from "discourse/plugins/discourse-subscriptions/helpers/stripe";
acceptance("Discourse Patrons", {
settings: {
discourse_patrons_subscription_group: "plan-id"
acceptance("Discourse Subscriptions", {
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");
await click(".product:first-child a");
assert.ok(
$(".discourse-subscriptions-section-columns").length,
"has the sections for billing"
);
assert.ok($(".subscribe-buttons button").length, "has buttons for subscribe");
});

View File

@ -1,7 +1,7 @@
export default function(helpers) {
const { response } = helpers;
this.get("/patrons/products", () => {
this.get("/s/products", () => {
const products = [
{
id: "prod_23o8I7tU4g56",
@ -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);
});
}