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}} +

+ + {{#if planFieldDisabled}} + {{input disabled=true value=selectedInterval}} + {{else}} + {{combo-box + valueProperty="name" + content=availableIntervals + value=selectedInterval + onChange=(action (mut selectedInterval)) + }} + {{/if}} +

+

+ + {{input type="text" name="trial" value=model.plan.trial_period_days}} +

+ {{i18n 'discourse_subscriptions.admin.plans.plan.trial_help'}} +
+

+ {{/if}}

- {{#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"}}

{{#each plans as |plan|}} - {{#ds-button - action="clickPlan" - actionParam=plan - selected=plan.selected - class="btn-discourse-subscriptions-subscribe" - }} -
- {{#if planTypeIsSelected}} - {{i18n (concat "discourse_subscriptions.plans.interval.adverb." plan.recurring.interval)}} - {{else}} - {{i18n "discourse_subscriptions.payment.interval"}} - {{/if}} -
- - {{format-currency plan.currency plan.amountDollars}} - - {{/ds-button}} + {{payment-plan plan=plan selectedPlan=selectedPlan clickPlan=(action "clickPlan")}} {{/each}}
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: [] }); - }); -}