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:
parent
3428429d77
commit
3a5078ded6
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -7,6 +7,6 @@ export default {
|
|||
plan: plan,
|
||||
};
|
||||
|
||||
return ajax("/s/subscriptions/finalize", { method: "post", data });
|
||||
return ajax("/s/finalize", { method: "post", data });
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue