From 587661fafb5419db2e2707d3a471ee252e7d0d96 Mon Sep 17 00:00:00 2001
From: Justin DiRose
Date: Wed, 22 Jul 2020 11:06:34 -0500
Subject: [PATCH] FEATURE: Allow one-time purchases on products (#18)
Building off the foundation of using the Prices API, this PR adds the ability to create a one-time purchase plan for any product, which then can add a user to the specified plan group.
Some things to be aware of:
One-time purchases cannot have trials.
One-time purchases use the Invoice API instead of Subscriptions. Invoices are created then charged immediately.
Users should receive emails for these invoices directly from Stripe just like subscriptions.
---
.../admin/plans_controller.rb | 18 +--
.../hooks_controller.rb | 1 -
.../invoices_controller.rb | 35 ------
.../payments_controller.rb | 40 -------
.../plans_controller.rb | 3 +-
.../subscriptions_controller.rb | 65 ++++++-----
.../user/payments_controller.rb | 7 +-
.../components/payment-options.js.es6 | 15 +--
.../discourse/components/payment-plan.js.es6 | 23 ++++
...scriptions-products-show-plans-show.js.es6 | 9 ++
.../discourse/controllers/s-show.js.es6 | 57 ++-------
.../discourse/models/admin-plan.js.es6 | 1 +
.../discourse/models/invoice.js.es6 | 14 ---
.../discourse/models/payment.js.es6 | 16 ---
...scriptions-products-show-plans-show.js.es6 | 3 +
...subscriptions-products-show-plans-show.hbs | 53 ++++++---
...-discourse-subscriptions-products-show.hbs | 2 +-
.../templates/components/payment-options.hbs | 49 +-------
.../templates/components/payment-plan.hbs | 16 +++
.../discourse/templates/s/show.hbs | 11 +-
config/locales/client.en.yml | 15 +--
config/routes.rb | 2 -
config/settings.yml | 3 -
plugin.rb | 16 +--
spec/requests/admin/plans_controller_spec.rb | 7 +-
spec/requests/invoices_controller_spec.rb | 54 ---------
spec/requests/payments_controller_spec.rb | 50 --------
.../requests/subscriptions_controller_spec.rb | 30 ++++-
.../components/payment-options-test.js.es6 | 109 +-----------------
.../components/payment-plan-test.js.es6 | 68 +++++++++++
.../discourse-patrons-pretender.js.es6 | 13 ---
31 files changed, 272 insertions(+), 533 deletions(-)
delete mode 100644 app/controllers/discourse_subscriptions/invoices_controller.rb
delete mode 100644 app/controllers/discourse_subscriptions/payments_controller.rb
create mode 100644 assets/javascripts/discourse/components/payment-plan.js.es6
delete mode 100644 assets/javascripts/discourse/models/invoice.js.es6
delete mode 100644 assets/javascripts/discourse/models/payment.js.es6
create mode 100644 assets/javascripts/discourse/templates/components/payment-plan.hbs
delete mode 100644 spec/requests/invoices_controller_spec.rb
delete mode 100644 spec/requests/payments_controller_spec.rb
create mode 100644 test/javascripts/components/payment-plan-test.js.es6
delete mode 100644 test/javascripts/helpers/discourse-patrons-pretender.js.es6
diff --git a/app/controllers/discourse_subscriptions/admin/plans_controller.rb b/app/controllers/discourse_subscriptions/admin/plans_controller.rb
index b37f7c2..2326f62 100644
--- a/app/controllers/discourse_subscriptions/admin/plans_controller.rb
+++ b/app/controllers/discourse_subscriptions/admin/plans_controller.rb
@@ -12,7 +12,6 @@ module DiscourseSubscriptions
plans = ::Stripe::Price.list(product_params)
render_json_dump plans.data
-
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -20,12 +19,9 @@ module DiscourseSubscriptions
def create
begin
- plan = ::Stripe::Price.create(
+ price_object = {
nickname: params[:nickname],
unit_amount: params[:amount],
- recurring: {
- interval: params[:interval],
- },
product: params[:product],
currency: params[:currency],
active: params[:active],
@@ -33,10 +29,17 @@ module DiscourseSubscriptions
group_name: params[:metadata][:group_name],
trial_period_days: params[:trial_period_days]
}
- )
+ }
+
+ if params[:type] == 'recurring'
+ price_object[:recurring] = {
+ interval: params[:interval]
+ }
+ end
+
+ plan = ::Stripe::Price.create(price_object)
render_json_dump plan
-
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -74,7 +77,6 @@ module DiscourseSubscriptions
)
render_json_dump plan
-
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
diff --git a/app/controllers/discourse_subscriptions/hooks_controller.rb b/app/controllers/discourse_subscriptions/hooks_controller.rb
index 1c3e2c1..1c64768 100644
--- a/app/controllers/discourse_subscriptions/hooks_controller.rb
+++ b/app/controllers/discourse_subscriptions/hooks_controller.rb
@@ -12,7 +12,6 @@ module DiscourseSubscriptions
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
diff --git a/app/controllers/discourse_subscriptions/invoices_controller.rb b/app/controllers/discourse_subscriptions/invoices_controller.rb
deleted file mode 100644
index fe35d7d..0000000
--- a/app/controllers/discourse_subscriptions/invoices_controller.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseSubscriptions
- class InvoicesController < ::ApplicationController
- include DiscourseSubscriptions::Stripe
- before_action :set_api_key
- requires_login
-
- def index
- begin
- customer = find_customer
-
- if viewing_own_invoices && customer.present?
- invoices = ::Stripe::Invoice.list(customer: customer.customer_id)
-
- render_json_dump invoices.data
- else
- render_json_dump []
- end
- rescue ::Stripe::InvalidRequestError => e
- render_json_error e.message
- end
- end
-
- private
-
- def viewing_own_invoices
- current_user.id == params[:user_id].to_i
- end
-
- def find_customer
- Customer.find_user(current_user)
- end
- end
-end
diff --git a/app/controllers/discourse_subscriptions/payments_controller.rb b/app/controllers/discourse_subscriptions/payments_controller.rb
deleted file mode 100644
index 72f6bc0..0000000
--- a/app/controllers/discourse_subscriptions/payments_controller.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module DiscourseSubscriptions
- class PaymentsController < ::ApplicationController
- include DiscourseSubscriptions::Stripe
-
- skip_before_action :verify_authenticity_token, only: [:create]
- before_action :set_api_key
-
- requires_login
-
- def create
- begin
- customer = Customer.where(user_id: current_user.id, product_id: nil).first_or_create do |c|
- new_customer = ::Stripe::Customer.create(
- email: current_user.email
- )
-
- c.customer_id = new_customer[:id]
- end
-
- payment = ::Stripe::PaymentIntent.create(
- payment_method_types: ['card'],
- payment_method: params[:payment_method],
- amount: params[:amount],
- currency: params[:currency],
- customer: customer[:customer_id],
- confirm: true
- )
-
- render_json_dump payment
-
- rescue ::Stripe::InvalidRequestError => e
- render_json_error e.message
- rescue ::Stripe::CardError => e
- render_json_error I18n.t('discourse_subscriptions.card.declined')
- end
- end
- end
-end
diff --git a/app/controllers/discourse_subscriptions/plans_controller.rb b/app/controllers/discourse_subscriptions/plans_controller.rb
index 0ae171f..15a4c8e 100644
--- a/app/controllers/discourse_subscriptions/plans_controller.rb
+++ b/app/controllers/discourse_subscriptions/plans_controller.rb
@@ -15,11 +15,10 @@ module DiscourseSubscriptions
end
serialized = plans[:data].map do |plan|
- plan.to_h.slice(:id, :unit_amount, :currency, :recurring)
+ plan.to_h.slice(:id, :unit_amount, :currency, :type, :recurring)
end.sort_by { |plan| plan[:amount] }
render_json_dump serialized
-
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
diff --git a/app/controllers/discourse_subscriptions/subscriptions_controller.rb b/app/controllers/discourse_subscriptions/subscriptions_controller.rb
index 55dfaa8..0ad7650 100644
--- a/app/controllers/discourse_subscriptions/subscriptions_controller.rb
+++ b/app/controllers/discourse_subscriptions/subscriptions_controller.rb
@@ -19,7 +19,6 @@ module DiscourseSubscriptions
end
render_json_dump subscriptions
-
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -29,36 +28,48 @@ module DiscourseSubscriptions
begin
plan = ::Stripe::Price.retrieve(params[:plan])
- if plan[:metadata] && plan[:metadata][:trial_period_days]
- trial_days = plan[:metadata][:trial_period_days]
+ recurring_plan = plan[:type] == 'recurring'
+
+ if recurring_plan
+ trial_days = plan[:metadata][:trial_period_days] if plan[:metadata] && plan[:metadata][:trial_period_days]
+
+ transaction = ::Stripe::Subscription.create(
+ customer: params[:customer],
+ items: [{ price: params[:plan] }],
+ metadata: metadata_user,
+ trial_period_days: trial_days
+ )
+ else
+ invoice_item = ::Stripe::InvoiceItem.create(
+ customer: params[:customer],
+ price: params[:plan]
+ )
+ invoice = ::Stripe::Invoice.create(
+ customer: params[:customer]
+ )
+ transaction = ::Stripe::Invoice.pay(invoice[:id])
end
- @subscription = ::Stripe::Subscription.create(
- customer: params[:customer],
- items: [ { price: params[:plan] } ],
- metadata: metadata_user,
- trial_period_days: trial_days
- )
+ if transaction_ok(transaction)
+ group = plan_group(plan)
- group = plan_group(plan)
+ group.add(current_user) if group
- if subscription_ok && group
- group.add(current_user)
+ customer = Customer.create(
+ user_id: current_user.id,
+ customer_id: params[:customer],
+ product_id: plan[:product]
+ )
+
+ if transaction[:object] == 'subscription'
+ Subscription.create(
+ customer_id: customer.id,
+ external_id: transaction[:id]
+ )
+ end
end
- customer = Customer.create(
- user_id: current_user.id,
- customer_id: params[:customer],
- product_id: plan[:product]
- )
-
- Subscription.create(
- customer_id: customer.id,
- external_id: @subscription[:id]
- )
-
- render_json_dump @subscription
-
+ render_json_dump transaction
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
@@ -70,8 +81,8 @@ module DiscourseSubscriptions
{ user_id: current_user.id, username: current_user.username_lower }
end
- def subscription_ok
- ['active', 'trialing'].include?(@subscription[:status])
+ def transaction_ok(transaction)
+ %w[active trialing paid].include?(transaction[:status])
end
end
end
diff --git a/app/controllers/discourse_subscriptions/user/payments_controller.rb b/app/controllers/discourse_subscriptions/user/payments_controller.rb
index 95b7688..7f870a6 100644
--- a/app/controllers/discourse_subscriptions/user/payments_controller.rb
+++ b/app/controllers/discourse_subscriptions/user/payments_controller.rb
@@ -21,9 +21,10 @@ module DiscourseSubscriptions
all_invoices = ::Stripe::Invoice.list(customer: customer_id)
invoices_with_products = all_invoices[:data].select do |invoice|
# i cannot dig it so we must get iffy with it
- if invoice[:lines] && invoice[:lines][:data] && invoice[:lines][:data][0] && invoice[:lines][:data][0][:plan] && invoice[:lines][:data][0][:plan][:product]
- product_ids.include?(invoice[:lines][:data][0][:plan][:product])
- end
+ invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data]
+ invoice_product_id = invoice_lines[:price][:product] if invoice_lines[:price] && invoice_lines[:price][:product]
+ invoice_product_id = invoice_lines[:plan][:product] if invoice_lines[:plan] && invoice_lines[:plan][:product]
+ product_ids.include?(invoice_product_id)
end
invoice_ids = invoices_with_products.map { |invoice| invoice[:id] }
payments = ::Stripe::PaymentIntent.list(customer: customer_id)
diff --git a/assets/javascripts/discourse/components/payment-options.js.es6 b/assets/javascripts/discourse/components/payment-options.js.es6
index f702f1d..c19f52b 100644
--- a/assets/javascripts/discourse/components/payment-options.js.es6
+++ b/assets/javascripts/discourse/components/payment-options.js.es6
@@ -1,22 +1,9 @@
-import { equal } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
- planButtonSelected: equal("planTypeIsSelected", true),
- paymentButtonSelected: equal("planTypeIsSelected", false),
-
actions: {
- selectPlans() {
- this.set("planTypeIsSelected", true);
- },
-
- selectPayments() {
- this.set("planTypeIsSelected", false);
- },
-
clickPlan(plan) {
- this.plans.map(p => p.set("selected", false));
- plan.set("selected", true);
+ this.set("selectedPlan", plan.id);
}
}
});
diff --git a/assets/javascripts/discourse/components/payment-plan.js.es6 b/assets/javascripts/discourse/components/payment-plan.js.es6
new file mode 100644
index 0000000..efb9a16
--- /dev/null
+++ b/assets/javascripts/discourse/components/payment-plan.js.es6
@@ -0,0 +1,23 @@
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
+
+const RECURRING = "recurring";
+
+export default Component.extend({
+ @discourseComputed("selectedPlan")
+ selected(planId) {
+ return planId === this.plan.id;
+ },
+
+ @discourseComputed("plan.type")
+ recurringPlan(type) {
+ return type === RECURRING;
+ },
+
+ actions: {
+ planClick() {
+ this.clickPlan(this.plan);
+ return false;
+ }
+ }
+});
diff --git a/assets/javascripts/discourse/controllers/admin-plugins-discourse-subscriptions-products-show-plans-show.js.es6 b/assets/javascripts/discourse/controllers/admin-plugins-discourse-subscriptions-products-show-plans-show.js.es6
index a34d2a3..c8c8b28 100644
--- a/assets/javascripts/discourse/controllers/admin-plugins-discourse-subscriptions-products-show-plans-show.js.es6
+++ b/assets/javascripts/discourse/controllers/admin-plugins-discourse-subscriptions-products-show-plans-show.js.es6
@@ -2,6 +2,9 @@ import discourseComputed from "discourse-common/utils/decorators";
import DiscourseURL from "discourse/lib/url";
import Controller from "@ember/controller";
+const RECURRING = "recurring";
+const ONE_TIME = "one_time";
+
export default Controller.extend({
// Also defined in settings.
selectedCurrency: Ember.computed.alias("model.plan.currency"),
@@ -47,6 +50,12 @@ export default Controller.extend({
},
actions: {
+ changeRecurring() {
+ const recurring = this.get("model.plan.isRecurring");
+ this.set("model.plan.type", recurring ? ONE_TIME : RECURRING);
+ this.set("model.plan.isRecurring", !recurring);
+ },
+
createPlan() {
// TODO: set default group name beforehand
if (this.get("model.plan.metadata.group_name") === undefined) {
diff --git a/assets/javascripts/discourse/controllers/s-show.js.es6 b/assets/javascripts/discourse/controllers/s-show.js.es6
index 6b01d85..a623572 100644
--- a/assets/javascripts/discourse/controllers/s-show.js.es6
+++ b/assets/javascripts/discourse/controllers/s-show.js.es6
@@ -1,29 +1,13 @@
import Controller from "@ember/controller";
import Customer from "discourse/plugins/discourse-subscriptions/discourse/models/customer";
-import Payment from "discourse/plugins/discourse-subscriptions/discourse/models/payment";
import Subscription from "discourse/plugins/discourse-subscriptions/discourse/models/subscription";
-import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
export default Controller.extend({
- planTypeIsSelected: true,
-
- @discourseComputed("planTypeIsSelected")
- type(planTypeIsSelected) {
- return planTypeIsSelected ? "plans" : "payment";
- },
-
- @discourseComputed("type")
- buttonText(type) {
- return I18n.t(`discourse_subscriptions.${type}.payment_button`);
- },
+ selectedPlan: null,
init() {
this._super(...arguments);
- this.set(
- "paymentsAllowed",
- Discourse.SiteSettings.discourse_subscriptions_allow_payments
- );
this.set(
"stripe",
Stripe(Discourse.SiteSettings.discourse_subscriptions_public_key)
@@ -37,20 +21,6 @@ export default Controller.extend({
bootbox.alert(I18n.t(`discourse_subscriptions.${path}`));
},
- createPayment(plan) {
- return this.stripe
- .createPaymentMethod("card", this.get("cardElement"))
- .then(result => {
- const payment = Payment.create({
- payment_method: result.paymentMethod.id,
- amount: plan.get("amount"),
- currency: plan.get("currency")
- });
-
- return payment.save();
- });
- },
-
createSubscription(plan) {
return this.stripe.createToken(this.get("cardElement")).then(result => {
if (result.error) {
@@ -73,24 +43,17 @@ export default Controller.extend({
actions: {
stripePaymentHandler() {
this.set("loading", true);
- const type = this.get("type");
const plan = this.get("model.plans")
- .filterBy("selected")
+ .filterBy("id", this.selectedPlan)
.get("firstObject");
if (!plan) {
- this.alert(`${type}.validate.payment_options.required`);
+ this.alert("plans.validate.payment_options.required");
this.set("loading", false);
return;
}
- let transaction;
-
- if (this.planTypeIsSelected) {
- transaction = this.createSubscription(plan);
- } else {
- transaction = this.createPayment(plan);
- }
+ let transaction = this.createSubscription(plan);
transaction
.then(result => {
@@ -98,17 +61,15 @@ export default Controller.extend({
bootbox.alert(result.error.message || result.error);
} else {
if (result.status === "incomplete") {
- this.alert(`${type}.incomplete`);
+ this.alert("plans.incomplete");
} else {
- this.alert(`${type}.success`);
+ this.alert("plans.success");
}
- const success_route = this.planTypeIsSelected
- ? "user.billing.subscriptions"
- : "user.billing.payments";
-
this.transitionToRoute(
- success_route,
+ plan.type === "recurring"
+ ? "user.billing.subscriptions"
+ : "user.billing.payments",
Discourse.User.current().username.toLowerCase()
);
}
diff --git a/assets/javascripts/discourse/models/admin-plan.js.es6 b/assets/javascripts/discourse/models/admin-plan.js.es6
index bc72a21..446897a 100644
--- a/assets/javascripts/discourse/models/admin-plan.js.es6
+++ b/assets/javascripts/discourse/models/admin-plan.js.es6
@@ -26,6 +26,7 @@ const AdminPlan = Plan.extend({
amount: this.unit_amount,
currency: this.currency,
trial_period_days: this.parseTrialPeriodDays,
+ type: this.type,
product: this.product,
metadata: this.metadata,
active: this.active
diff --git a/assets/javascripts/discourse/models/invoice.js.es6 b/assets/javascripts/discourse/models/invoice.js.es6
deleted file mode 100644
index 8bbb88d..0000000
--- a/assets/javascripts/discourse/models/invoice.js.es6
+++ /dev/null
@@ -1,14 +0,0 @@
-import { ajax } from "discourse/lib/ajax";
-import EmberObject from "@ember/object";
-
-const Invoice = EmberObject.extend({});
-
-Invoice.reopenClass({
- findAll() {
- return ajax("/s/invoices", { method: "get" }).then(result =>
- result.map(invoice => Invoice.create(invoice))
- );
- }
-});
-
-export default Invoice;
diff --git a/assets/javascripts/discourse/models/payment.js.es6 b/assets/javascripts/discourse/models/payment.js.es6
deleted file mode 100644
index 5a2cd17..0000000
--- a/assets/javascripts/discourse/models/payment.js.es6
+++ /dev/null
@@ -1,16 +0,0 @@
-import EmberObject from "@ember/object";
-import { ajax } from "discourse/lib/ajax";
-
-const Payment = EmberObject.extend({
- save() {
- const data = {
- payment_method: this.payment_method,
- amount: this.amount,
- currency: this.currency
- };
-
- return ajax("/s/payments", { method: "post", data });
- }
-});
-
-export default Payment;
diff --git a/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-products-show-plans-show.js.es6 b/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-products-show-plans-show.js.es6
index a89cafc..dcbb3ab 100644
--- a/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-products-show-plans-show.js.es6
+++ b/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-products-show-plans-show.js.es6
@@ -16,11 +16,14 @@ export default Route.extend({
active: true,
isNew: true,
interval: "month",
+ type: "recurring",
+ isRecurring: true,
currency: Discourse.SiteSettings.discourse_subscriptions_currency,
product: product.get("id")
});
} else {
plan = AdminPlan.find(id);
+ plan.isRecurring = plan.type === "recurring";
}
const groups = Group.findAll({ ignore_automatic: true });
diff --git a/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show-plans-show.hbs b/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show-plans-show.hbs
index fd61f37..5285e76 100644
--- a/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show-plans-show.hbs
+++ b/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show-plans-show.hbs
@@ -40,30 +40,47 @@
{{input class="plan-amount" type="text" name="name" value=model.plan.amountDollars disabled=planFieldDisabled}}
-
- {{input type="text" name="trial" value=model.plan.trial_period_days}}
-
- {{i18n 'discourse_subscriptions.admin.plans.plan.trial_help'}}
-
-
-
-
+ {{#if model.plan.isRecurring}}
+
+
+ {{i18n 'discourse_subscriptions.admin.plans.plan.interval'}}
+
+ {{#if planFieldDisabled}}
+ {{input disabled=true value=selectedInterval}}
+ {{else}}
+ {{combo-box
+ valueProperty="name"
+ content=availableIntervals
+ value=selectedInterval
+ onChange=(action (mut selectedInterval))
+ }}
+ {{/if}}
+
+
+
+ {{i18n 'discourse_subscriptions.admin.plans.plan.trial'}}
+ ({{i18n 'discourse_subscriptions.optional'}})
+
+ {{input type="text" name="trial" value=model.plan.trial_period_days}}
+
+ {{i18n 'discourse_subscriptions.admin.plans.plan.trial_help'}}
+
+
+ {{/if}}
{{i18n 'discourse_subscriptions.admin.plans.plan.active'}}
diff --git a/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs b/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs
index b7a89e3..d01ab69 100644
--- a/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs
+++ b/assets/javascripts/discourse/templates/admin/plugins-discourse-subscriptions-products-show.hbs
@@ -53,7 +53,7 @@
{{#each model.plans as |plan|}}
| {{plan.nickname}} |
- {{plan.interval}} |
+ {{plan.recurring.interval}} |
{{format-unix-date plan.created}} |
{{plan.metadata.group_name}} |
{{plan.active}} |
diff --git a/assets/javascripts/discourse/templates/components/payment-options.hbs b/assets/javascripts/discourse/templates/components/payment-options.hbs
index 0b03f27..62552fa 100644
--- a/assets/javascripts/discourse/templates/components/payment-options.hbs
+++ b/assets/javascripts/discourse/templates/components/payment-options.hbs
@@ -1,54 +1,9 @@
-
-{{#if paymentsAllowed}}
-
- {{#ds-button
- id="discourse-subscriptions-payment-type-plan"
- selected=planButtonSelected
- action="selectPlans"
- class="btn-discourse-subscriptions-payment-type"
- }}
- {{i18n "discourse_subscriptions.plans.purchase"}}
- {{/ds-button}}
-
- {{#ds-button
- id="discourse-subscriptions-payment-type-payment"
- selected=paymentButtonSelected
- action="selectPayments"
- class="btn-discourse-subscriptions-payment-type"
- }}
- {{i18n "discourse_subscriptions.payment.purchase"}}
- {{/ds-button}}
-
-{{/if}}
-
-
-
- {{#if planTypeIsSelected}}
- {{i18n "discourse_subscriptions.plans.select"}}
- {{else}}
- {{i18n "discourse_subscriptions.payment.select"}}
- {{/if}}
+ {{i18n "discourse_subscriptions.plans.select"}}
diff --git a/assets/javascripts/discourse/templates/components/payment-plan.hbs b/assets/javascripts/discourse/templates/components/payment-plan.hbs
new file mode 100644
index 0000000..7d59ed3
--- /dev/null
+++ b/assets/javascripts/discourse/templates/components/payment-plan.hbs
@@ -0,0 +1,16 @@
+{{#ds-button
+ action="planClick"
+ selected=selected
+ class="btn-discourse-subscriptions-subscribe"
+}}
+
+ {{#if recurringPlan}}
+ {{i18n (concat "discourse_subscriptions.plans.interval.adverb." plan.recurring.interval)}}
+ {{else}}
+ {{i18n "discourse_subscriptions.one_time_payment"}}
+ {{/if}}
+
+
+ {{format-currency plan.currency plan.amountDollars}}
+
+{{/ds-button}}
diff --git a/assets/javascripts/discourse/templates/s/show.hbs b/assets/javascripts/discourse/templates/s/show.hbs
index d08c72a..fa214ed 100644
--- a/assets/javascripts/discourse/templates/s/show.hbs
+++ b/assets/javascripts/discourse/templates/s/show.hbs
@@ -21,8 +21,7 @@
{{payment-options
plans=model.plans
- paymentsAllowed=paymentsAllowed
- planTypeIsSelected=planTypeIsSelected
+ selectedPlan=selectedPlan
}}
@@ -32,12 +31,12 @@
{{#if loading}}
{{loading-spinner}}
{{else}}
- {{#d-button
+ {{d-button
disabled=loading
action="stripePaymentHandler"
- class="btn btn-primary btn-payment"}}
- {{buttonText}}
- {{/d-button}}
+ class="btn btn-primary btn-payment"
+ label="discourse_subscriptions.plans.payment_button"
+ }}
{{/if}}
{{/unless}}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index ad1ebd7..e744e60 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -6,7 +6,6 @@ en:
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:
discourse_patrons_amount_must_be_currency: "Currency amounts must be currencies without dollar symbol (eg 1.50)"
js:
@@ -21,6 +20,7 @@ en:
subscribe: Subscribe
user_activity:
payments: Payments
+ one_time_payment: One-Time Payment
plans:
purchase: Purchase a subscription
select: Select subscription plan
@@ -36,18 +36,6 @@ en:
validate:
payment_options:
required: Please select a subscription plan.
- payment:
- purchase: Make just one payment
- select: Select a payment option
- interval:
- One payment
- payment_button:
- Pay Once
- success: Thank you!
- incomplete: Payment is incomplete.
- validate:
- payment_options:
- required: Please select a payment option.
user:
payments:
id: Payment ID
@@ -140,6 +128,7 @@ en:
group_help: This is the discourse user group the customer gets added to when the subscription is created.
active: Active
created_at: Created
+ recurring: Recurring Plan?
subscriptions:
title: Subscriptions
subscription:
diff --git a/config/routes.rb b/config/routes.rb
index e453ea5..56a372a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -20,8 +20,6 @@ DiscourseSubscriptions::Engine.routes.draw do
resources :customers, only: [:create]
resources :hooks, only: [:create]
- resources :invoices, only: [:index]
- resources :payments, only: [:create]
resources :plans, only: [:index], constraints: SubscriptionsUserConstraint.new
resources :products, only: [:index, :show]
resources :subscriptions, only: [:create]
diff --git a/config/settings.yml b/config/settings.yml
index 90a3d25..1e4ce95 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -13,9 +13,6 @@ plugins:
discourse_subscriptions_webhook_secret:
default: ''
client: false
- discourse_subscriptions_allow_payments:
- default: false
- client: true
discourse_subscriptions_currency:
client: true
default: "USD"
diff --git a/plugin.rb b/plugin.rb
index 051ccdd..06b2656 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -27,14 +27,14 @@ extend_content_security_policy(
add_admin_route 'discourse_subscriptions.admin_navigation', 'discourse-subscriptions.products'
Discourse::Application.routes.append do
- get '/admin/plugins/discourse-subscriptions' => 'admin/plugins#index'
- get '/admin/plugins/discourse-subscriptions/products' => 'admin/plugins#index'
- get '/admin/plugins/discourse-subscriptions/products/:product_id' => 'admin/plugins#index'
- get '/admin/plugins/discourse-subscriptions/products/:product_id/plans' => 'admin/plugins#index'
- get '/admin/plugins/discourse-subscriptions/products/:product_id/plans/:plan_id' => 'admin/plugins#index'
- get '/admin/plugins/discourse-subscriptions/subscriptions' => 'admin/plugins#index'
- get '/admin/plugins/discourse-subscriptions/plans' => 'admin/plugins#index'
- get '/admin/plugins/discourse-subscriptions/plans/:plan_id' => 'admin/plugins#index'
+ get '/admin/plugins/discourse-subscriptions' => 'admin/plugins#index', constraints: AdminConstraint.new
+ get '/admin/plugins/discourse-subscriptions/products' => 'admin/plugins#index', constraints: AdminConstraint.new
+ get '/admin/plugins/discourse-subscriptions/products/:product_id' => 'admin/plugins#index', constraints: AdminConstraint.new
+ get '/admin/plugins/discourse-subscriptions/products/:product_id/plans' => 'admin/plugins#index', constraints: AdminConstraint.new
+ get '/admin/plugins/discourse-subscriptions/products/:product_id/plans/:plan_id' => 'admin/plugins#index', constraints: AdminConstraint.new
+ get '/admin/plugins/discourse-subscriptions/subscriptions' => 'admin/plugins#index', constraints: AdminConstraint.new
+ get '/admin/plugins/discourse-subscriptions/plans' => 'admin/plugins#index', constraints: AdminConstraint.new
+ get '/admin/plugins/discourse-subscriptions/plans/:plan_id' => 'admin/plugins#index', constraints: AdminConstraint.new
get 'u/:username/billing' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT }
get 'u/:username/billing/:id' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT }
end
diff --git a/spec/requests/admin/plans_controller_spec.rb b/spec/requests/admin/plans_controller_spec.rb
index 4f628df..6846727 100644
--- a/spec/requests/admin/plans_controller_spec.rb
+++ b/spec/requests/admin/plans_controller_spec.rb
@@ -98,7 +98,12 @@ module DiscourseSubscriptions
it "creates a plan with an interval" do
::Stripe::Price.expects(:create).with(has_entry(recurring: { interval: 'week' }))
- post "/s/admin/plans.json", params: { interval: 'week', metadata: { group_name: '' } }
+ post "/s/admin/plans.json", params: { type: 'recurring', interval: 'week', metadata: { group_name: '' } }
+ end
+
+ it "creates a plan as a one-time purchase" do
+ ::Stripe::Price.expects(:create).with(Not(has_key(:recurring)))
+ post "/s/admin/plans.json", params: { metadata: { group_name: '' } }
end
it "creates a plan with an amount" do
diff --git a/spec/requests/invoices_controller_spec.rb b/spec/requests/invoices_controller_spec.rb
deleted file mode 100644
index 054c56f..0000000
--- a/spec/requests/invoices_controller_spec.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-module DiscourseSubscriptions
- RSpec.describe InvoicesController do
- describe "index" do
- describe "not authenticated" do
- it "does not list the invoices" do
- ::Stripe::Invoice.expects(:list).never
- get "/s/invoices.json"
- expect(response.status).to eq 403
- end
- end
-
- describe "authenticated" do
- let(:user) { Fabricate(:user) }
- let(:stripe_customer) { { id: 'cus_id4567' } }
-
- before do
- sign_in(user)
- end
-
- describe "other user invoices" do
- it "does not list the invoices" do
- ::Stripe::Invoice.expects(:list).never
- get "/s/invoices.json", params: { user_id: 999999 }
- end
- end
-
- describe "own invoices" do
- context "stripe customer does not exist" do
- it "lists empty" do
- ::Stripe::Invoice.expects(:list).never
- get "/s/invoices.json", params: { user_id: user.id }
- expect(response.body).to eq "[]"
- end
- end
-
- context "stripe customer exists" do
- before do
- DiscourseSubscriptions::Customer.create_customer(user, stripe_customer)
- end
-
- it "lists the invoices" do
- ::Stripe::Invoice.expects(:list).with(customer: 'cus_id4567')
- get "/s/invoices.json", params: { user_id: user.id }
- end
- end
- end
- end
- end
- end
-end
diff --git a/spec/requests/payments_controller_spec.rb b/spec/requests/payments_controller_spec.rb
deleted file mode 100644
index f4fdc17..0000000
--- a/spec/requests/payments_controller_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-module DiscourseSubscriptions
- RSpec.describe PaymentsController do
- context "not authenticated" do
- it "does not create a payment intent" do
- ::Stripe::PaymentIntent.expects(:create).never
-
- post "/s/payments.json", params: {
- payment_method: 'pm_123',
- amount: 999,
- currency: 'gdp'
- }
- end
- end
-
- context "authenticated" do
- let(:user) { Fabricate(:user) }
-
- before do
- sign_in(user)
- end
-
- describe "create" do
- it "creates a payment intent" do
- ::Stripe::Customer.expects(:create).with(
- email: user.email
- ).returns(id: 'cus_87653')
-
- ::Stripe::PaymentIntent.expects(:create).with(
- payment_method_types: ['card'],
- payment_method: 'pm_123',
- amount: '999',
- currency: 'gdp',
- confirm: true,
- customer: 'cus_87653'
- )
-
- post "/s/payments.json", params: {
- payment_method: 'pm_123',
- amount: 999,
- currency: 'gdp'
- }
- end
- end
- end
- end
-end
diff --git a/spec/requests/subscriptions_controller_spec.rb b/spec/requests/subscriptions_controller_spec.rb
index e077edf..4660e24 100644
--- a/spec/requests/subscriptions_controller_spec.rb
+++ b/spec/requests/subscriptions_controller_spec.rb
@@ -22,6 +22,7 @@ module DiscourseSubscriptions
describe "create" do
it "creates a subscription" do
::Stripe::Price.expects(:retrieve).returns(
+ type: 'recurring',
product: 'product_12345',
metadata: {
group_name: 'awesome',
@@ -41,8 +42,29 @@ module DiscourseSubscriptions
}.to change { DiscourseSubscriptions::Customer.count }
end
+ it "creates a one time payment subscription" do
+ ::Stripe::Price.expects(:retrieve).returns(
+ type: 'one_time',
+ product: 'product_12345',
+ metadata: {
+ group_name: 'awesome'
+ }
+ )
+
+ ::Stripe::InvoiceItem.expects(:create)
+
+ ::Stripe::Invoice.expects(:create).returns(id: 'in_123')
+
+ ::Stripe::Invoice.expects(:pay).returns(status: 'paid')
+
+ expect {
+ post '/s/subscriptions.json', params: { plan: 'plan_1234', customer: 'cus_1234' }
+ }.to change { DiscourseSubscriptions::Customer.count }
+
+ end
+
it "creates a customer model" do
- ::Stripe::Price.expects(:retrieve).returns(metadata: {})
+ ::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: {})
::Stripe::Subscription.expects(:create).returns(status: 'active')
expect {
@@ -61,13 +83,13 @@ module DiscourseSubscriptions
end
it "does not add the user to the admins group" do
- ::Stripe::Price.expects(:retrieve).returns(metadata: { group_name: 'admins' })
+ ::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: { group_name: 'admins' })
post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' }
expect(user.admin).to eq false
end
it "does not add the user to other group" do
- ::Stripe::Price.expects(:retrieve).returns(metadata: { group_name: 'other' })
+ ::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: { group_name: 'other' })
post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' }
expect(user.groups).to be_empty
end
@@ -75,7 +97,7 @@ module DiscourseSubscriptions
context "plan has group in metadata" do
before do
- ::Stripe::Price.expects(:retrieve).returns(metadata: { group_name: group_name })
+ ::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: { group_name: group_name })
end
it "does not add the user to the group when subscription fails" do
diff --git a/test/javascripts/components/payment-options-test.js.es6 b/test/javascripts/components/payment-options-test.js.es6
index f7bb274..84233ff 100644
--- a/test/javascripts/components/payment-options-test.js.es6
+++ b/test/javascripts/components/payment-options-test.js.es6
@@ -18,9 +18,9 @@ componentTest("Discourse Subscriptions payment options have no plans", {
componentTest("Discourse Subscriptions payment options has content", {
template: `{{payment-options
- paymentsAllowed=paymentsAllowed
- plans=plans
- planTypeIsSelected=planTypeIsSelected}}`,
+ plans=plans
+ selectedPlan=selectedPlan
+ }}`,
beforeEach() {
this.set("plans", [
@@ -35,110 +35,9 @@ componentTest("Discourse Subscriptions payment options has content", {
amountDollars: "9.99"
}
]);
-
- this.set("planTypeIsSelected", true);
- this.set("paymentsAllowed", true);
},
async test(assert) {
- assert.equal(
- find(".btn-discourse-subscriptions-payment-type").length,
- 2,
- "The payment type buttons are shown"
- );
- assert.equal(
- find(".btn-discourse-subscriptions-subscribe").length,
- 2,
- "The plan buttons are shown"
- );
- assert.equal(
- find("#subscribe-buttons .btn-primary").length,
- 0,
- "No plan buttons are selected by default"
- );
- assert.equal(
- find(".btn-discourse-subscriptions-subscribe:first-child .interval")
- .text()
- .trim(),
- "Yearly",
- "The plan interval is shown"
- );
- assert.equal(
- find(".btn-discourse-subscriptions-subscribe:first-child .amount")
- .text()
- .trim(),
- "$AUD 44.99",
- "The plan amount and currency is shown"
- );
- }
-});
-
-componentTest("Discourse Subscriptions payments allowed setting", {
- template: `{{payment-options plans=plans paymentsAllowed=paymentsAllowed}}`,
-
- async test(assert) {
- this.set("paymentsAllowed", true);
-
- assert.ok(
- find("#discourse-subscriptions-payment-type-plan").length,
- "The plan type button displayed"
- );
- assert.ok(
- find("#discourse-subscriptions-payment-type-payment").length,
- "The payment type button displayed"
- );
-
- this.set("paymentsAllowed", false);
-
- assert.notOk(
- find("#discourse-subscriptions-payment-type-plan").length,
- "The plan type button hidden"
- );
- assert.notOk(
- find("#discourse-subscriptions-payment-type-payment").length,
- "The payment type button hidden"
- );
- }
-});
-
-componentTest("Discourse Subscriptions payment type plan", {
- template: `{{payment-options
- paymentsAllowed=paymentsAllowed
- plans=plans
- planTypeIsSelected=planTypeIsSelected}}`,
-
- async test(assert) {
- this.set("plans", [
- { currency: "aud", interval: "year", amountDollars: "44.99" }
- ]);
-
- this.set("paymentsAllowed", true);
- this.set("planTypeIsSelected", true);
-
- assert.equal(
- find("#discourse-subscriptions-payment-type-plan.btn-primary").length,
- 1,
- "The plan type button is selected"
- );
-
- assert.equal(
- find("#discourse-subscriptions-payment-type-payment.btn-primary").length,
- 0,
- "The payment type button is not selected"
- );
-
- await click("#discourse-subscriptions-payment-type-payment");
-
- assert.equal(
- find("#discourse-subscriptions-payment-type-plan.btn-primary").length,
- 0,
- "The plan type button is selected"
- );
-
- assert.equal(
- find("#discourse-subscriptions-payment-type-payment.btn-primary").length,
- 1,
- "The payment type button is not selected"
- );
+ assert.equal(this.selectedPlan, null, "No plans are selected by default");
}
});
diff --git a/test/javascripts/components/payment-plan-test.js.es6 b/test/javascripts/components/payment-plan-test.js.es6
new file mode 100644
index 0000000..e4b5dfa
--- /dev/null
+++ b/test/javascripts/components/payment-plan-test.js.es6
@@ -0,0 +1,68 @@
+import componentTest from "helpers/component-test";
+
+moduleForComponent("payment-plan", { integration: true });
+
+componentTest("Payment plan subscription button rendered", {
+ template: `{{payment-plan
+ plan=plan
+ selectedPlan=selectedPlan
+ }}`,
+
+ beforeEach() {
+ this.set("plan", {
+ type: "recurring",
+ currency: "aud",
+ recurring: { interval: "year" },
+ amountDollars: "44.99"
+ });
+ },
+
+ async test(assert) {
+ assert.equal(
+ find(".btn-discourse-subscriptions-subscribe").length,
+ 1,
+ "The payment button is shown"
+ );
+
+ assert.equal(
+ find(".btn-discourse-subscriptions-subscribe:first-child .interval")
+ .text()
+ .trim(),
+ "Yearly",
+ "The plan interval is shown -- Yearly"
+ );
+
+ assert.equal(
+ find(".btn-discourse-subscriptions-subscribe:first-child .amount")
+ .text()
+ .trim(),
+ "$AUD 44.99",
+ "The plan amount and currency is shown"
+ );
+ }
+});
+
+componentTest("Payment plan one-time-payment button rendered", {
+ template: `{{payment-plan
+ plan=plan
+ selectedPlan=selectedPlan
+ }}`,
+
+ beforeEach() {
+ this.set("plan", {
+ type: "one_time",
+ currency: "USD",
+ amountDollars: "3.99"
+ });
+ },
+
+ async test(assert) {
+ assert.equal(
+ find(".btn-discourse-subscriptions-subscribe:first-child .interval")
+ .text()
+ .trim(),
+ "One-Time Payment",
+ "Shown as one time payment"
+ );
+ }
+});
diff --git a/test/javascripts/helpers/discourse-patrons-pretender.js.es6 b/test/javascripts/helpers/discourse-patrons-pretender.js.es6
deleted file mode 100644
index 8944550..0000000
--- a/test/javascripts/helpers/discourse-patrons-pretender.js.es6
+++ /dev/null
@@ -1,13 +0,0 @@
-export default function(helpers) {
- const { response } = helpers;
-
- this.get("/patrons", () => response({ email: "hello@example.com" }));
-
- this.get("/groups/:plan", () => {
- return response({ full_name: "Saboo", bio_cooked: "This is the plan" });
- });
-
- this.get("/patrons/plans", () => {
- return response({ plans: [] });
- });
-}