webhook updates subscription
This commit is contained in:
commit
272f3eb998
34
README.md
34
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,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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { acceptance } from "helpers/qunit-helpers";
|
||||
|
||||
acceptance("Discourse Patrons", {
|
||||
acceptance("Discourse Subscriptions", {
|
||||
settings: {
|
||||
discourse_subscriptions_extra_nav_subscribe: true
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue