diff --git a/app/controllers/discourse_subscriptions/customers_controller.rb b/app/controllers/discourse_subscriptions/customers_controller.rb deleted file mode 100644 index 3dce949..0000000 --- a/app/controllers/discourse_subscriptions/customers_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module DiscourseSubscriptions - class CustomersController < ::ApplicationController - include DiscourseSubscriptions::Stripe - - before_action :set_api_key - - def create - begin - customer = ::Stripe::Customer.create( - email: current_user.email, - source: params[:source] - ) - - render_json_dump customer - - rescue ::Stripe::InvalidRequestError => e - render_json_error e.message - end - end - end -end diff --git a/app/controllers/discourse_subscriptions/plans_controller.rb b/app/controllers/discourse_subscriptions/plans_controller.rb deleted file mode 100644 index 15a4c8e..0000000 --- a/app/controllers/discourse_subscriptions/plans_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module DiscourseSubscriptions - class PlansController < ::ApplicationController - include DiscourseSubscriptions::Stripe - - before_action :set_api_key - - def index - begin - if params[:product_id].present? - plans = ::Stripe::Price.list(active: true, product: params[:product_id]) - else - plans = ::Stripe::Price.list(active: true) - end - - serialized = plans[:data].map do |plan| - 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 - end - end -end diff --git a/app/controllers/discourse_subscriptions/products_controller.rb b/app/controllers/discourse_subscriptions/products_controller.rb deleted file mode 100644 index 1032ba2..0000000 --- a/app/controllers/discourse_subscriptions/products_controller.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -module DiscourseSubscriptions - class ProductsController < ::ApplicationController - include DiscourseSubscriptions::Stripe - - before_action :set_api_key - - def index - begin - product_ids = Product.all.pluck(:external_id) - products = [] - - if product_ids.present? && is_stripe_configured? - response = ::Stripe::Product.list({ - ids: product_ids, - active: true - }) - - products = response[:data].map do |p| - serialize(p) - end - end - - render_json_dump products - - rescue ::Stripe::InvalidRequestError => e - render_json_error e.message - end - end - - def show - begin - product = ::Stripe::Product.retrieve(params[:id]) - - render_json_dump serialize(product) - - rescue ::Stripe::InvalidRequestError => e - render_json_error e.message - end - end - - private - - def serialize(product) - { - id: product[:id], - name: product[:name], - description: PrettyText.cook(product[:metadata][:description]), - subscribed: current_user_products.include?(product[:id]) - } - end - - def current_user_products - return [] if current_user.nil? - - Customer - .select(:product_id) - .where(user_id: current_user.id) - .map { |c| c.product_id }.compact - end - end -end diff --git a/app/controllers/discourse_subscriptions/subscriptions_controller.rb b/app/controllers/discourse_subscriptions/subscribe_controller.rb similarity index 63% rename from app/controllers/discourse_subscriptions/subscriptions_controller.rb rename to app/controllers/discourse_subscriptions/subscribe_controller.rb index ec94c84..65973b6 100644 --- a/app/controllers/discourse_subscriptions/subscriptions_controller.rb +++ b/app/controllers/discourse_subscriptions/subscribe_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module DiscourseSubscriptions - class SubscriptionsController < ::ApplicationController + class SubscribeController < ::ApplicationController include DiscourseSubscriptions::Stripe include DiscourseSubscriptions::Group before_action :set_api_key @@ -9,23 +9,49 @@ module DiscourseSubscriptions def index begin - products = ::Stripe::Product.list(active: true) + product_ids = Product.all.pluck(:external_id) + products = [] + + if product_ids.present? && is_stripe_configured? + response = ::Stripe::Product.list({ + ids: product_ids, + active: true + }) + + products = response[:data].map do |p| + serialize_product(p) + end - subscriptions = products[:data].map do |p| - { - id: p[:id], - description: p.dig(:metadata, :description) - } end - render_json_dump subscriptions + render_json_dump products + + rescue ::Stripe::InvalidRequestError => e + render_json_error e.message + end + end + + def show + params.require(:id) + begin + product = ::Stripe::Product.retrieve(params[:id]) + plans = ::Stripe::Price.list(active: true, product: params[:id]) + + response = { + product: serialize_product(product), + plans: serialize_plans(plans) + } + + render_json_dump response rescue ::Stripe::InvalidRequestError => e render_json_error e.message end end def create + params.require([:source, :plan]) begin + customer = create_customer(params[:source]) plan = ::Stripe::Price.retrieve(params[:plan]) recurring_plan = plan[:type] == 'recurring' @@ -34,7 +60,7 @@ module DiscourseSubscriptions trial_days = plan[:metadata][:trial_period_days] if plan[:metadata] && plan[:metadata][:trial_period_days] transaction = ::Stripe::Subscription.create( - customer: params[:customer], + customer: customer[:id], items: [{ price: params[:plan] }], metadata: metadata_user, trial_period_days: trial_days @@ -43,11 +69,11 @@ module DiscourseSubscriptions payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[:status] == 'incomplete' else invoice_item = ::Stripe::InvoiceItem.create( - customer: params[:customer], + customer: customer[:id], price: params[:plan] ) invoice = ::Stripe::Invoice.create( - customer: params[:customer] + customer: customer[:id] ) transaction = ::Stripe::Invoice.finalize_invoice(invoice[:id]) payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] == 'open' @@ -65,6 +91,7 @@ module DiscourseSubscriptions end def finalize + params.require([:plan, :transaction]) begin price = ::Stripe::Price.retrieve(params[:plan]) transaction = retrieve_transaction(params[:transaction]) @@ -76,24 +103,6 @@ module DiscourseSubscriptions end end - def retrieve_transaction(transaction) - begin - case transaction - when /^sub_/ - ::Stripe::Subscription.retrieve(transaction) - when /^in_/ - ::Stripe::Invoice.retrieve(transaction) - end - rescue ::Stripe::InvalidRequestError => e - e.message - end - end - - def retrieve_payment_intent(invoice_id) - invoice = ::Stripe::Invoice.retrieve(invoice_id) - ::Stripe::PaymentIntent.retrieve(invoice[:payment_intent]) - end - def finalize_transaction(transaction, plan) group = plan_group(plan) @@ -115,6 +124,55 @@ module DiscourseSubscriptions private + def serialize_product(product) + { + id: product[:id], + name: product[:name], + description: PrettyText.cook(product[:metadata][:description]), + subscribed: current_user_products.include?(product[:id]) + } + end + + def current_user_products + return [] if current_user.nil? + + Customer + .select(:product_id) + .where(user_id: current_user.id) + .map { |c| c.product_id }.compact + end + + def serialize_plans(plans) + plans[:data].map do |plan| + plan.to_h.slice(:id, :unit_amount, :currency, :type, :recurring) + end.sort_by { |plan| plan[:amount] } + end + + def create_customer(source) + ::Stripe::Customer.create( + email: current_user.email, + source: source + ) + end + + def retrieve_payment_intent(invoice_id) + invoice = ::Stripe::Invoice.retrieve(invoice_id) + ::Stripe::PaymentIntent.retrieve(invoice[:payment_intent]) + end + + def retrieve_transaction(transaction) + begin + case transaction + when /^sub_/ + ::Stripe::Subscription.retrieve(transaction) + when /^in_/ + ::Stripe::Invoice.retrieve(transaction) + end + rescue ::Stripe::InvalidRequestError => e + e.message + end + end + def metadata_user { user_id: current_user.id, username: current_user.username_lower } end diff --git a/app/controllers/discourse_subscriptions/user/payments_controller.rb b/app/controllers/discourse_subscriptions/user/payments_controller.rb index 7f870a6..3bc7841 100644 --- a/app/controllers/discourse_subscriptions/user/payments_controller.rb +++ b/app/controllers/discourse_subscriptions/user/payments_controller.rb @@ -19,13 +19,7 @@ module DiscourseSubscriptions customer_ids.each do |customer_id| # lots of matching because the Stripe API doesn't make it easy to match products => payments except from invoices 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 - 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 + invoices_with_products = parse_invoices(all_invoices, product_ids) invoice_ids = invoices_with_products.map { |invoice| invoice[:id] } payments = ::Stripe::PaymentIntent.list(customer: customer_id) payments_from_invoices = payments[:data].select { |payment| invoice_ids.include?(payment[:invoice]) } @@ -41,6 +35,21 @@ module DiscourseSubscriptions render_json_error e.message end end + + private + + def parse_invoices(all_invoices, product_ids) + invoices_with_products = all_invoices[:data].select do |invoice| + invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] + invoice_product_id = parse_invoice_lines(invoice_lines) + product_ids.include?(invoice_product_id) + end + end + + def parse_invoice_lines(invoice_lines) + 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] + end end end end diff --git a/assets/javascripts/discourse/controllers/s-show.js.es6 b/assets/javascripts/discourse/controllers/s-show.js.es6 index 0d1c5f7..315a7f8 100644 --- a/assets/javascripts/discourse/controllers/s-show.js.es6 +++ b/assets/javascripts/discourse/controllers/s-show.js.es6 @@ -1,5 +1,4 @@ import Controller from "@ember/controller"; -import Customer from "discourse/plugins/discourse-subscriptions/discourse/models/customer"; import Subscription from "discourse/plugins/discourse-subscriptions/discourse/models/subscription"; import Transaction from "discourse/plugins/discourse-subscriptions/discourse/models/transaction"; import I18n from "I18n"; @@ -28,16 +27,12 @@ export default Controller.extend({ this.set("loading", false); return result; } else { - const customer = Customer.create({ source: result.token.id }); - - return customer.save().then((c) => { - const subscription = Subscription.create({ - customer: c.id, - plan: plan.get("id"), - }); - - return subscription.save(); + const subscription = Subscription.create({ + source: result.token.id, + plan: plan.get("id"), }); + + return subscription.save(); } }); }, diff --git a/assets/javascripts/discourse/models/customer.js.es6 b/assets/javascripts/discourse/models/customer.js.es6 deleted file mode 100644 index 7aac3df..0000000 --- a/assets/javascripts/discourse/models/customer.js.es6 +++ /dev/null @@ -1,14 +0,0 @@ -import { ajax } from "discourse/lib/ajax"; -import EmberObject from "@ember/object"; - -const Customer = EmberObject.extend({ - save() { - const data = { - source: this.source, - }; - - return ajax("/s/customers", { method: "post", data }); - }, -}); - -export default Customer; diff --git a/assets/javascripts/discourse/models/group.js.es6 b/assets/javascripts/discourse/models/group.js.es6 deleted file mode 100644 index d6fbcb5..0000000 --- a/assets/javascripts/discourse/models/group.js.es6 +++ /dev/null @@ -1,19 +0,0 @@ -import { ajax } from "discourse/lib/ajax"; -import EmberObject from "@ember/object"; - -const Group = EmberObject.extend({}); - -Group.reopenClass({ - subscriptionGroup: - Discourse.SiteSettings.discourse_patrons_subscription_group, - - find() { - return ajax(`/groups/${this.subscriptionGroup}`, { method: "get" }).then( - (result) => { - return Group.create(result.group); - } - ); - }, -}); - -export default Group; diff --git a/assets/javascripts/discourse/models/plan.js.es6 b/assets/javascripts/discourse/models/plan.js.es6 index 65857ed..bcbcfda 100644 --- a/assets/javascripts/discourse/models/plan.js.es6 +++ b/assets/javascripts/discourse/models/plan.js.es6 @@ -1,6 +1,5 @@ import EmberObject from "@ember/object"; import discourseComputed from "discourse-common/utils/decorators"; -import { ajax } from "discourse/lib/ajax"; const Plan = EmberObject.extend({ amountDollars: Ember.computed("unit_amount", { @@ -24,12 +23,4 @@ const Plan = EmberObject.extend({ }, }); -Plan.reopenClass({ - findAll(data) { - return ajax("/s/plans", { method: "get", data }).then((result) => - result.map((plan) => Plan.create(plan)) - ); - }, -}); - export default Plan; diff --git a/assets/javascripts/discourse/models/product.js.es6 b/assets/javascripts/discourse/models/product.js.es6 index 652f03b..7ce4b1c 100644 --- a/assets/javascripts/discourse/models/product.js.es6 +++ b/assets/javascripts/discourse/models/product.js.es6 @@ -5,16 +5,10 @@ const Product = EmberObject.extend({}); Product.reopenClass({ findAll() { - return ajax("/s/products", { method: "get" }).then((result) => + return ajax("/s", { method: "get" }).then((result) => result.map((product) => Product.create(product)) ); }, - - find(id) { - return ajax(`/s/products/${id}`, { method: "get" }).then((product) => - Product.create(product) - ); - }, }); export default Product; diff --git a/assets/javascripts/discourse/models/subscription.js.es6 b/assets/javascripts/discourse/models/subscription.js.es6 index 782a4cd..893ba61 100644 --- a/assets/javascripts/discourse/models/subscription.js.es6 +++ b/assets/javascripts/discourse/models/subscription.js.es6 @@ -10,19 +10,17 @@ const Subscription = EmberObject.extend({ save() { const data = { - customer: this.customer, + source: this.source, plan: this.plan, }; - return ajax("/s/subscriptions", { method: "post", data }); + return ajax("/s/create", { method: "post", data }); }, }); Subscription.reopenClass({ - findAll() { - return ajax("/s/subscriptions", { method: "get" }).then((result) => - result.map((subscription) => Subscription.create(subscription)) - ); + show(id) { + return ajax(`/s/${id}`, { method: "get" }); }, }); diff --git a/assets/javascripts/discourse/models/transaction.js.es6 b/assets/javascripts/discourse/models/transaction.js.es6 index d18d9af..dc52e55 100644 --- a/assets/javascripts/discourse/models/transaction.js.es6 +++ b/assets/javascripts/discourse/models/transaction.js.es6 @@ -7,6 +7,6 @@ export default { plan: plan, }; - return ajax("/s/subscriptions/finalize", { method: "post", data }); + return ajax("/s/finalize", { method: "post", data }); }, }; diff --git a/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-dashboard.js.es6 b/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-dashboard.js.es6 deleted file mode 100644 index 2abc347..0000000 --- a/assets/javascripts/discourse/routes/admin-plugins-discourse-subscriptions-dashboard.js.es6 +++ /dev/null @@ -1,23 +0,0 @@ -import Route from "@ember/routing/route"; -import { ajax } from "discourse/lib/ajax"; - -export default Route.extend({ - queryParams: { - order: { - refreshModel: true, - }, - descending: { - refreshModel: true, - }, - }, - - model(params) { - return ajax("/patrons/admin", { - method: "get", - data: { - order: params.order, - descending: params.descending, - }, - }).then((results) => results); - }, -}); diff --git a/assets/javascripts/discourse/routes/s-show.js.es6 b/assets/javascripts/discourse/routes/s-show.js.es6 index 74d06dd..b371868 100644 --- a/assets/javascripts/discourse/routes/s-show.js.es6 +++ b/assets/javascripts/discourse/routes/s-show.js.es6 @@ -1,15 +1,19 @@ import Route from "@ember/routing/route"; import Product from "discourse/plugins/discourse-subscriptions/discourse/models/product"; import Plan from "discourse/plugins/discourse-subscriptions/discourse/models/plan"; -import { hash } from "rsvp"; +import Subscription from "discourse/plugins/discourse-subscriptions/discourse/models/subscription"; export default Route.extend({ model(params) { const product_id = params["subscription-id"]; - const product = Product.find(product_id); - const plans = Plan.findAll({ product_id }); + return Subscription.show(product_id).then((result) => { + result.product = Product.create(result.product); + result.plans = result.plans.map((plan) => { + return Plan.create(plan); + }); - return hash({ plans, product }); + return result; + }); }, }); diff --git a/config/routes.rb b/config/routes.rb index 758b702..7120370 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,14 +18,11 @@ DiscourseSubscriptions::Engine.routes.draw do resources :subscriptions, only: [:index, :destroy] end - resources :customers, only: [:create] - resources :plans, only: [:index], constraints: SubscriptionsUserConstraint.new - resources :products, only: [:index, :show] - resources :subscriptions, only: [:create] - - post '/subscriptions/finalize' => 'subscriptions#finalize' + get '/' => 'subscribe#index' + get '.json' => 'subscribe#index' + get '/:id' => 'subscribe#show' + post '/create' => 'subscribe#create' + post '/finalize' => 'subscribe#finalize' post '/hooks' => 'hooks#create' - get '/' => 'subscriptions#index', constraints: SubscriptionsUserConstraint.new - get '/:id' => 'subscriptions#index', constraints: SubscriptionsUserConstraint.new end diff --git a/spec/requests/customers_controller_spec.rb b/spec/requests/customers_controller_spec.rb deleted file mode 100644 index 71003ac..0000000 --- a/spec/requests/customers_controller_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -module DiscourseSubscriptions - RSpec.describe CustomersController do - describe "create" do - describe "authenticated" do - let(:user) { Fabricate(:user, email: 'hello.2@example.com') } - - before do - sign_in(user) - end - - it "creates a stripe customer" do - ::Stripe::Customer.expects(:create).with( - email: 'hello.2@example.com', - source: 'tok_interesting' - ) - - post "/s/customers.json", params: { source: 'tok_interesting' } - end - end - end - end -end diff --git a/spec/requests/plans_controller_spec.rb b/spec/requests/plans_controller_spec.rb deleted file mode 100644 index 71c7c36..0000000 --- a/spec/requests/plans_controller_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -module DiscourseSubscriptions - RSpec.describe PlansController do - let(:user) { Fabricate(:user) } - - before do - sign_in(user) - end - - describe "index" do - it "lists the active plans" do - ::Stripe::Price.expects(:list).with(active: true) - get "/s/plans.json" - end - - it "lists the active plans for a product" do - ::Stripe::Price.expects(:list).with(active: true, product: 'prod_3765') - get "/s/plans.json", params: { product_id: 'prod_3765' } - end - - it "orders and serialises the plans" do - ::Stripe::Price.expects(:list).returns( - data: [ - { id: 'plan_id123', unit_amount: 1220, currency: 'aud', recurring: { interval: 'year' }, metadata: {} }, - { id: 'plan_id234', unit_amount: 1399, currency: 'usd', recurring: { interval: 'year' }, metadata: {} }, - { id: 'plan_id678', unit_amount: 1000, currency: 'aud', recurring: { interval: 'week' }, metadata: {} } - ] - ) - - get "/s/plans.json" - - expect(response.parsed_body).to eq([ - { "currency" => "aud", "id" => "plan_id123", "recurring" => { "interval" => "year" }, "unit_amount" => 1220 }, - { "currency" => "usd", "id" => "plan_id234", "recurring" => { "interval" => "year" }, "unit_amount" => 1399 }, - { "currency" => "aud", "id" => "plan_id678", "recurring" => { "interval" => "week" }, "unit_amount" => 1000 } - ]) - end - end - end -end diff --git a/spec/requests/products_controller_spec.rb b/spec/requests/products_controller_spec.rb deleted file mode 100644 index 5921843..0000000 --- a/spec/requests/products_controller_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -module DiscourseSubscriptions - RSpec.describe ProductsController do - describe "products" do - let(:product) do - { - id: "prodct_23456", - name: "Very Special Product", - metadata: { - description: "Many people listened to my phone call with the Ukrainian President while it was being made" - }, - otherstuff: true, - } - end - let(:product_ids) { ["prodct_23456"] } - - before do - Fabricate(:product, external_id: "prodct_23456") - SiteSetting.discourse_subscriptions_public_key = "public-key" - SiteSetting.discourse_subscriptions_secret_key = "secret-key" - end - - context "unauthenticated" do - it "gets products" do - ::Stripe::Product.expects(:list).with(ids: product_ids, active: true).returns(data: [product]) - - get "/s/products.json" - - expect(response.parsed_body).to eq([{ - "id" => "prodct_23456", - "name" => "Very Special Product", - "description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), - "subscribed" => false - }]) - end - end - - context "authenticated" do - let(:user) { Fabricate(:user) } - - before do - sign_in(user) - end - - describe "index" do - it "gets products" do - ::Stripe::Product.expects(:list).with(ids: product_ids, active: true).returns(data: [product]) - - get "/s/products.json" - - expect(response.parsed_body).to eq([{ - "id" => "prodct_23456", - "name" => "Very Special Product", - "description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), - "subscribed" => false - }]) - end - - it "is subscribed" do - Fabricate(:customer, product_id: product[:id], user_id: user.id, customer_id: 'x') - ::Stripe::Product.expects(:list).with(ids: product_ids, active: true).returns(data: [product]) - - get "/s/products.json" - data = response.parsed_body - expect(data.first["subscribed"]).to eq true - end - - it "is not subscribed" do - ::DiscourseSubscriptions::Customer.delete_all - ::Stripe::Product.expects(:list).with(ids: product_ids, active: true).returns(data: [product]) - - get "/s/products.json" - data = response.parsed_body - expect(data.first["subscribed"]).to eq false - end - end - - describe 'show' do - it 'retrieves the product' do - ::Stripe::Product.expects(:retrieve).with('prod_walterwhite').returns(product) - get "/s/products/prod_walterwhite.json" - - expect(response.parsed_body).to eq( - "id" => "prodct_23456", - "name" => "Very Special Product", - "description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), - "subscribed" => false - ) - end - end - end - end - end -end diff --git a/spec/requests/subscribe_controller_spec.rb b/spec/requests/subscribe_controller_spec.rb new file mode 100644 index 0000000..e137353 --- /dev/null +++ b/spec/requests/subscribe_controller_spec.rb @@ -0,0 +1,262 @@ +# frozen_string_literal: true + +require 'rails_helper' + +module DiscourseSubscriptions + RSpec.describe SubscribeController do + let (:user) { Fabricate(:user) } + + context "showing products" do + let(:product) do + { + id: "prodct_23456", + name: "Very Special Product", + metadata: { + description: "Many people listened to my phone call with the Ukrainian President while it was being made" + }, + otherstuff: true, + } + end + + let(:prices) do + { + data: [ + { id: 'plan_id123', unit_amount: 1220, currency: 'aud', recurring: { interval: 'year' }, metadata: {} }, + { id: 'plan_id234', unit_amount: 1399, currency: 'usd', recurring: { interval: 'year' }, metadata: {} }, + { id: 'plan_id678', unit_amount: 1000, currency: 'aud', recurring: { interval: 'week' }, metadata: {} } + ] + } + end + + let(:product_ids) { ["prodct_23456"] } + + before do + sign_in(user) + Fabricate(:product, external_id: "prodct_23456") + SiteSetting.discourse_subscriptions_public_key = "public-key" + SiteSetting.discourse_subscriptions_secret_key = "secret-key" + end + + describe "#index" do + + it "gets products" do + ::Stripe::Product.expects(:list).with(ids: product_ids, active: true).returns(data: [product]) + + get "/s.json" + + expect(response.parsed_body).to eq([{ + "id" => "prodct_23456", + "name" => "Very Special Product", + "description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), + "subscribed" => false + }]) + end + + it "is subscribed" do + Fabricate(:customer, product_id: product[:id], user_id: user.id, customer_id: 'x') + ::Stripe::Product.expects(:list).with(ids: product_ids, active: true).returns(data: [product]) + + get "/s.json" + data = response.parsed_body + expect(data.first["subscribed"]).to eq true + end + + it "is not subscribed" do + ::DiscourseSubscriptions::Customer.delete_all + ::Stripe::Product.expects(:list).with(ids: product_ids, active: true).returns(data: [product]) + + get "/s.json" + data = response.parsed_body + expect(data.first["subscribed"]).to eq false + end + end + + describe "#show" do + it 'retrieves the product' do + ::Stripe::Product.expects(:retrieve).with('prod_walterwhite').returns(product) + ::Stripe::Price.expects(:list).with(active: true, product: 'prod_walterwhite').returns(prices) + get "/s/prod_walterwhite.json" + + expect(response.parsed_body).to eq({ + "product" => { + "id" => "prodct_23456", + "name" => "Very Special Product", + "description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), + "subscribed" => false + }, + "plans" => [ + { "currency" => "aud", "id" => "plan_id123", "recurring" => { "interval" => "year" }, "unit_amount" => 1220 }, + { "currency" => "usd", "id" => "plan_id234", "recurring" => { "interval" => "year" }, "unit_amount" => 1399 }, + { "currency" => "aud", "id" => "plan_id678", "recurring" => { "interval" => "week" }, "unit_amount" => 1000 } + ] + }) + end + end + end + + context "creating subscriptions" do + context "unauthenticated" do + it "does not create a subscription" do + ::Stripe::Customer.expects(:create).never + ::Stripe::Price.expects(:retrieve).never + ::Stripe::Subscription.expects(:create).never + post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } + end + end + + context "authenticated" do + before do + sign_in(user) + end + + describe "#create" do + before do + ::Stripe::Customer.expects(:create).returns(id: 'cus_1234') + end + + it "creates a subscription" do + ::Stripe::Price.expects(:retrieve).returns( + type: 'recurring', + product: 'product_12345', + metadata: { + group_name: 'awesome', + trial_period_days: 0 + } + ) + + ::Stripe::Subscription.expects(:create).with( + customer: 'cus_1234', + items: [ price: 'plan_1234' ], + metadata: { user_id: user.id, username: user.username_lower }, + trial_period_days: 0 + ).returns(status: 'active', customer: 'cus_1234') + + expect { + post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } + }.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(status: 'open', id: 'in_123') + + ::Stripe::Invoice.expects(:finalize_invoice).returns(id: 'in_123', status: 'open', payment_intent: 'pi_123') + + ::Stripe::Invoice.expects(:retrieve).returns(id: 'in_123', status: 'open', payment_intent: 'pi_123') + + ::Stripe::PaymentIntent.expects(:retrieve).returns(status: 'successful') + + ::Stripe::Invoice.expects(:pay).returns(status: 'paid', customer: 'cus_1234') + + expect { + post '/s/create.json', params: { plan: 'plan_1234', source: 'tok_1234' } + }.to change { DiscourseSubscriptions::Customer.count } + end + + it "creates a customer model" do + ::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: {}) + ::Stripe::Subscription.expects(:create).returns(status: 'active', customer: 'cus_1234') + + expect { + post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } + }.to change { DiscourseSubscriptions::Customer.count } + end + end + + describe "#finalize strong customer authenticated transaction" do + context "with subscription" do + it "finalizes the subscription" do + ::Stripe::Price.expects(:retrieve).returns(id: "plan_1234", product: "prod_1234", metadata: {}) + ::Stripe::Subscription.expects(:retrieve).returns(id: "sub_123", customer: 'cus_1234', status: "active") + + expect { + post "/s/finalize.json", params: { plan: 'plan_1234', transaction: 'sub_1234' } + }.to change { DiscourseSubscriptions::Customer.count } + end + end + + context "with one-time payment" do + it "finalizes the one-time payment" do + ::Stripe::Price.expects(:retrieve).returns(id: "plan_1234", product: "prod_1234", metadata: {}) + ::Stripe::Invoice.expects(:retrieve).returns(id: "in_123", customer: 'cus_1234', status: "paid") + + expect { + post "/s/finalize.json", params: { plan: 'plan_1234', transaction: 'in_1234' } + }.to change { DiscourseSubscriptions::Customer.count } + end + end + end + + describe "user groups" do + let(:group_name) { 'group-123' } + let(:group) { Fabricate(:group, name: group_name) } + + context "unauthorized group" do + before do + ::Stripe::Customer.expects(:create).returns(id: 'cus_1234') + ::Stripe::Subscription.expects(:create).returns(status: 'active') + end + + it "does not add the user to the admins group" do + ::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: { group_name: 'admins' }) + post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } + expect(user.admin).to eq false + end + + it "does not add the user to other group" do + ::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: { group_name: 'other' }) + post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } + expect(user.groups).to be_empty + end + end + + context "plan has group in metadata" do + before do + ::Stripe::Customer.expects(:create).returns(id: 'cus_1234') + ::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 + ::Stripe::Subscription.expects(:create).returns(status: 'failed') + + expect { + post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } + }.not_to change { group.users.count } + + expect(user.groups).to be_empty + end + + it "adds the user to the group when the subscription is active" do + ::Stripe::Subscription.expects(:create).returns(status: 'active') + + expect { + post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } + }.to change { group.users.count } + + expect(user.groups).not_to be_empty + end + + it "adds the user to the group when the subscription is trialing" do + ::Stripe::Subscription.expects(:create).returns(status: 'trialing') + + expect { + post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } + }.to change { group.users.count } + + expect(user.groups).not_to be_empty + end + end + end + end + end + end +end diff --git a/spec/requests/subscriptions_controller_spec.rb b/spec/requests/subscriptions_controller_spec.rb deleted file mode 100644 index 6e8f980..0000000 --- a/spec/requests/subscriptions_controller_spec.rb +++ /dev/null @@ -1,166 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -module DiscourseSubscriptions - RSpec.describe SubscriptionsController do - context "not authenticated" do - it "does not create a subscription" do - ::Stripe::Price.expects(:retrieve).never - ::Stripe::Subscription.expects(:create).never - post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' } - end - end - - context "authenticated" do - let(:user) { Fabricate(:user) } - - before do - sign_in(user) - end - - describe "create" do - it "creates a subscription" do - ::Stripe::Price.expects(:retrieve).returns( - type: 'recurring', - product: 'product_12345', - metadata: { - group_name: 'awesome', - trial_period_days: 0 - } - ) - - ::Stripe::Subscription.expects(:create).with( - customer: 'cus_1234', - items: [ price: 'plan_1234' ], - metadata: { user_id: user.id, username: user.username_lower }, - trial_period_days: 0 - ).returns(status: 'active', customer: 'cus_1234') - - expect { - post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' } - }.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(status: 'open', id: 'in_123') - - ::Stripe::Invoice.expects(:finalize_invoice).returns(id: 'in_123', status: 'open', payment_intent: 'pi_123') - - ::Stripe::Invoice.expects(:retrieve).returns(id: 'in_123', status: 'open', payment_intent: 'pi_123') - - ::Stripe::PaymentIntent.expects(:retrieve).returns(status: 'successful') - - ::Stripe::Invoice.expects(:pay).returns(status: 'paid', customer: 'cus_1234') - - 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(type: 'recurring', metadata: {}) - ::Stripe::Subscription.expects(:create).returns(status: 'active', customer: 'cus_1234') - - expect { - post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' } - }.to change { DiscourseSubscriptions::Customer.count } - end - end - - describe "strong customer authenticated transaction" do - context "with subscription" do - it "finalizes the subscription" do - ::Stripe::Price.expects(:retrieve).returns(id: "plan_1234", product: "prod_1234", metadata: {}) - ::Stripe::Subscription.expects(:retrieve).returns(id: "sub_123", customer: 'cus_1234', status: "active") - - expect { - post "/s/subscriptions/finalize.json", params: { plan: 'plan_1234', transaction: 'sub_1234' } - }.to change { DiscourseSubscriptions::Customer.count } - end - end - - context "with one-time payment" do - it "finalizes the one-time payment" do - ::Stripe::Price.expects(:retrieve).returns(id: "plan_1234", product: "prod_1234", metadata: {}) - ::Stripe::Invoice.expects(:retrieve).returns(id: "in_123", customer: 'cus_1234', status: "paid") - - expect { - post "/s/subscriptions/finalize.json", params: { plan: 'plan_1234', transaction: 'in_1234' } - }.to change { DiscourseSubscriptions::Customer.count } - end - end - end - - describe "user groups" do - let(:group_name) { 'group-123' } - let(:group) { Fabricate(:group, name: group_name) } - - context "unauthorized group" do - before do - ::Stripe::Subscription.expects(:create).returns(status: 'active') - end - - it "does not add the user to the admins group" do - ::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(type: 'recurring', metadata: { group_name: 'other' }) - post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' } - expect(user.groups).to be_empty - end - end - - context "plan has group in metadata" do - before do - ::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 - ::Stripe::Subscription.expects(:create).returns(status: 'failed') - - expect { - post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' } - }.not_to change { group.users.count } - - expect(user.groups).to be_empty - end - - it "adds the user to the group when the subscription is active" do - ::Stripe::Subscription.expects(:create).returns(status: 'active') - - expect { - post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' } - }.to change { group.users.count } - - expect(user.groups).not_to be_empty - end - - it "adds the user to the group when the subscription is trialing" do - ::Stripe::Subscription.expects(:create).returns(status: 'trialing') - - expect { - post "/s/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' } - }.to change { group.users.count } - - expect(user.groups).not_to be_empty - end - end - end - end - end -end