REFACTOR: Simplify controller setup (#24)

The code in the plugin needed a dramatic cleanup. This refactor collapses the Plan/Product/Subscription controllers on the backend into one new controller: `SubscribeController`.

This reduces N+1 calls to the back end during the subscription process and simplifies use of the code. 

I've also removed a bunch of dead code and refactored some logic into methods for easier readability. No feature/functionality changes in this commit; only refactoring. However, refactoring will allow for implementation of better anonymous user handling, so this is largely a foundation to enable making that change.
This commit is contained in:
Justin DiRose 2020-10-21 13:36:31 -05:00 committed by GitHub
parent 3428429d77
commit 3a5078ded6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 389 additions and 582 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}
});
},

View File

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

View File

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

View File

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

View File

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

View File

@ -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" });
},
});

View File

@ -7,6 +7,6 @@ export default {
plan: plan,
};
return ajax("/s/subscriptions/finalize", { method: "post", data });
return ajax("/s/finalize", { method: "post", data });
},
};

View File

@ -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);
},
});

View File

@ -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;
});
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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