mirror of
https://github.com/discourse/discourse-subscriptions.git
synced 2025-02-07 20:28:09 +00:00
Merge pull request #2 from rimian/feature/subscriptions
Feature/subscriptions
This commit is contained in:
commit
d26a60a3e8
@ -11,13 +11,10 @@ This is a newer version of https://github.com/rimian/discourse-donations.
|
|||||||
* Be sure your site is enforcing https.
|
* Be sure your site is enforcing https.
|
||||||
* Follow the install instructions here: https://meta.discourse.org/t/install-a-plugin/19157
|
* Follow the install instructions here: https://meta.discourse.org/t/install-a-plugin/19157
|
||||||
* Add your Stripe public and private keys in settings and set the currency to your local value.
|
* Add your Stripe public and private keys in settings and set the currency to your local value.
|
||||||
* Enable the plugin and wait for people to donate money.
|
|
||||||
|
|
||||||
## Usage
|
## Creating Subscription Plans
|
||||||
|
|
||||||
Enable the plugin and enter your Stripe API keys in the settings. You can also configure amounts and the default currency.
|
When users subscribe to your Discourse application, they are added to a user group. You can create new user groups or use existing ones. Of course, you should be careful what permissions you apply to the user group.
|
||||||
|
|
||||||
Visit `/patrons`
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
87
app/controllers/admin/plans_controller.rb
Normal file
87
app/controllers/admin/plans_controller.rb
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
module Admin
|
||||||
|
class PlansController < ::Admin::AdminController
|
||||||
|
include DiscoursePatrons::Stripe
|
||||||
|
|
||||||
|
before_action :set_api_key
|
||||||
|
|
||||||
|
def index
|
||||||
|
begin
|
||||||
|
plans = ::Stripe::Plan.list(product_params)
|
||||||
|
|
||||||
|
render_json_dump plans.data
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
begin
|
||||||
|
plan = ::Stripe::Plan.create(
|
||||||
|
nickname: params[:nickname],
|
||||||
|
amount: params[:amount],
|
||||||
|
interval: params[:interval],
|
||||||
|
product: params[:product],
|
||||||
|
trial_period_days: params[:trial_period_days],
|
||||||
|
currency: SiteSetting.discourse_patrons_currency,
|
||||||
|
active: params[:active],
|
||||||
|
metadata: { group_name: params[:metadata][:group_name] }
|
||||||
|
)
|
||||||
|
|
||||||
|
render_json_dump plan
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
begin
|
||||||
|
plan = ::Stripe::Plan.retrieve(params[:id])
|
||||||
|
|
||||||
|
render_json_dump plan
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
begin
|
||||||
|
plan = ::Stripe::Plan.update(
|
||||||
|
params[:id],
|
||||||
|
nickname: params[:nickname],
|
||||||
|
trial_period_days: params[:trial_period_days],
|
||||||
|
active: params[:active],
|
||||||
|
metadata: { group_name: params[:metadata][:group_name] }
|
||||||
|
)
|
||||||
|
|
||||||
|
render_json_dump plan
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
begin
|
||||||
|
plan = ::Stripe::Plan.delete(params[:id])
|
||||||
|
|
||||||
|
render_json_dump plan
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def product_params
|
||||||
|
{ product: params[:product_id] } if params[:product_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
87
app/controllers/admin/products_controller.rb
Normal file
87
app/controllers/admin/products_controller.rb
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
module Admin
|
||||||
|
class ProductsController < ::Admin::AdminController
|
||||||
|
include DiscoursePatrons::Stripe
|
||||||
|
|
||||||
|
before_action :set_api_key
|
||||||
|
|
||||||
|
def index
|
||||||
|
begin
|
||||||
|
products = ::Stripe::Product.list
|
||||||
|
|
||||||
|
render_json_dump products.data
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
begin
|
||||||
|
create_params = product_params.merge!(type: 'service')
|
||||||
|
|
||||||
|
if params[:statement_descriptor].blank?
|
||||||
|
create_params.except!(:statement_descriptor)
|
||||||
|
end
|
||||||
|
|
||||||
|
product = ::Stripe::Product.create(create_params)
|
||||||
|
|
||||||
|
render_json_dump product
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
begin
|
||||||
|
product = ::Stripe::Product.retrieve(params[:id])
|
||||||
|
|
||||||
|
render_json_dump product
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
begin
|
||||||
|
product = ::Stripe::Product.update(
|
||||||
|
params[:id],
|
||||||
|
product_params
|
||||||
|
)
|
||||||
|
|
||||||
|
render_json_dump product
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
begin
|
||||||
|
product = ::Stripe::Product.delete(params[:id])
|
||||||
|
|
||||||
|
render_json_dump product
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def product_params
|
||||||
|
params.permit!
|
||||||
|
|
||||||
|
{
|
||||||
|
name: params[:name],
|
||||||
|
active: params[:active],
|
||||||
|
statement_descriptor: params[:statement_descriptor],
|
||||||
|
metadata: { description: params.dig(:metadata, :description) }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
app/controllers/admin/subscriptions_controller.rb
Normal file
21
app/controllers/admin/subscriptions_controller.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
module Admin
|
||||||
|
class SubscriptionsController < ::Admin::AdminController
|
||||||
|
include DiscoursePatrons::Stripe
|
||||||
|
|
||||||
|
before_action :set_api_key
|
||||||
|
|
||||||
|
def index
|
||||||
|
begin
|
||||||
|
subscriptions = ::Stripe::Subscription.list
|
||||||
|
|
||||||
|
render_json_dump subscriptions
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -3,27 +3,7 @@
|
|||||||
module DiscoursePatrons
|
module DiscoursePatrons
|
||||||
class AdminController < ::Admin::AdminController
|
class AdminController < ::Admin::AdminController
|
||||||
def index
|
def index
|
||||||
payments = Payment.all.order(payments_order)
|
head 200
|
||||||
|
|
||||||
render_serialized(payments, PaymentSerializer)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def payments_order
|
|
||||||
if %w(created_at amount).include?(params[:order])
|
|
||||||
{ params[:order] => ascending }
|
|
||||||
else
|
|
||||||
{ created_at: :desc }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def ascending
|
|
||||||
if params[:descending] == 'false'
|
|
||||||
:desc
|
|
||||||
else
|
|
||||||
:asc
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
11
app/controllers/concerns/stripe.rb
Normal file
11
app/controllers/concerns/stripe.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
module Stripe
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def set_api_key
|
||||||
|
::Stripe.api_key = SiteSetting.discourse_patrons_secret_key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
28
app/controllers/customers_controller.rb
Normal file
28
app/controllers/customers_controller.rb
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
class CustomersController < ::ApplicationController
|
||||||
|
include DiscoursePatrons::Stripe
|
||||||
|
|
||||||
|
before_action :set_api_key
|
||||||
|
|
||||||
|
def create
|
||||||
|
begin
|
||||||
|
customer = ::Stripe::Customer.create(
|
||||||
|
email: current_user.email,
|
||||||
|
source: params[:source]
|
||||||
|
)
|
||||||
|
|
||||||
|
DiscoursePatrons::Customer.create_customer(
|
||||||
|
current_user,
|
||||||
|
customer
|
||||||
|
)
|
||||||
|
|
||||||
|
render_json_dump customer
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
35
app/controllers/invoices_controller.rb
Normal file
35
app/controllers/invoices_controller.rb
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
class InvoicesController < ::ApplicationController
|
||||||
|
include DiscoursePatrons::Stripe
|
||||||
|
before_action :set_api_key
|
||||||
|
requires_login
|
||||||
|
|
||||||
|
def index
|
||||||
|
begin
|
||||||
|
customer = find_customer
|
||||||
|
|
||||||
|
if viewing_own_invoices && customer.present?
|
||||||
|
invoices = ::Stripe::Invoice.list(customer: customer.customer_id)
|
||||||
|
|
||||||
|
render_json_dump invoices.data
|
||||||
|
else
|
||||||
|
render_json_dump []
|
||||||
|
end
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def viewing_own_invoices
|
||||||
|
current_user.id == params[:user_id].to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_customer
|
||||||
|
DiscoursePatrons::Customer.find_user(current_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
module DiscoursePatrons
|
module DiscoursePatrons
|
||||||
class PatronsController < ::ApplicationController
|
class PatronsController < ::ApplicationController
|
||||||
|
include DiscoursePatrons::Stripe
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token, only: [:create]
|
skip_before_action :verify_authenticity_token, only: [:create]
|
||||||
before_action :set_api_key
|
before_action :set_api_key
|
||||||
|
|
||||||
@ -15,18 +17,6 @@ module DiscoursePatrons
|
|||||||
render json: result
|
render json: result
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
|
||||||
payment_intent = Stripe::PaymentIntent.retrieve(params[:pid])
|
|
||||||
|
|
||||||
if current_user && (current_user.admin || payment_intent[:customer] == current_user.id)
|
|
||||||
result = payment_intent
|
|
||||||
else
|
|
||||||
result = { error: 'Not found' }
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: result
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
begin
|
begin
|
||||||
|
|
||||||
@ -41,15 +31,6 @@ module DiscoursePatrons
|
|||||||
metadata: { user_id: user_id }
|
metadata: { user_id: user_id }
|
||||||
)
|
)
|
||||||
|
|
||||||
Payment.create(
|
|
||||||
user_id: response[:metadata][:user_id],
|
|
||||||
payment_intent_id: response[:id],
|
|
||||||
receipt_email: response[:receipt_email],
|
|
||||||
url: response[:charges][:url],
|
|
||||||
amount: response[:amount],
|
|
||||||
currency: response[:currency]
|
|
||||||
)
|
|
||||||
|
|
||||||
rescue ::Stripe::InvalidRequestError => e
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
response = { error: e }
|
response = { error: e }
|
||||||
rescue ::Stripe::CardError => e
|
rescue ::Stripe::CardError => e
|
||||||
@ -61,10 +42,6 @@ module DiscoursePatrons
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_api_key
|
|
||||||
::Stripe.api_key = SiteSetting.discourse_patrons_secret_key
|
|
||||||
end
|
|
||||||
|
|
||||||
def param_currency_to_number
|
def param_currency_to_number
|
||||||
params[:amount].to_s.sub('.', '').to_i
|
params[:amount].to_s.sub('.', '').to_i
|
||||||
end
|
end
|
||||||
|
28
app/controllers/plans_controller.rb
Normal file
28
app/controllers/plans_controller.rb
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
class PlansController < ::ApplicationController
|
||||||
|
include DiscoursePatrons::Stripe
|
||||||
|
|
||||||
|
before_action :set_api_key
|
||||||
|
|
||||||
|
def index
|
||||||
|
begin
|
||||||
|
if params[:product_id].present?
|
||||||
|
plans = ::Stripe::Plan.list(active: true, product: params[:product_id])
|
||||||
|
else
|
||||||
|
plans = ::Stripe::Plan.list(active: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
serialized = plans[:data].map do |plan|
|
||||||
|
plan.to_h.slice(:id, :amount, :currency, :interval)
|
||||||
|
end.sort_by { |plan| plan[:amount] }
|
||||||
|
|
||||||
|
render_json_dump serialized
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
45
app/controllers/products_controller.rb
Normal file
45
app/controllers/products_controller.rb
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
class ProductsController < ::ApplicationController
|
||||||
|
include DiscoursePatrons::Stripe
|
||||||
|
|
||||||
|
before_action :set_api_key
|
||||||
|
|
||||||
|
def index
|
||||||
|
begin
|
||||||
|
response = ::Stripe::Product.list(active: true)
|
||||||
|
|
||||||
|
products = response[:data].map do |p|
|
||||||
|
serialize(p)
|
||||||
|
end
|
||||||
|
|
||||||
|
render_json_dump products
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return 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
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def serialize(product)
|
||||||
|
{
|
||||||
|
id: product[:id],
|
||||||
|
name: product[:name],
|
||||||
|
description: product[:metadata][:description]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
63
app/controllers/subscriptions_controller.rb
Normal file
63
app/controllers/subscriptions_controller.rb
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
class SubscriptionsController < ::ApplicationController
|
||||||
|
include DiscoursePatrons::Stripe
|
||||||
|
before_action :set_api_key
|
||||||
|
requires_login
|
||||||
|
|
||||||
|
def index
|
||||||
|
begin
|
||||||
|
products = ::Stripe::Product.list(active: true)
|
||||||
|
|
||||||
|
subscriptions = products[:data].map do |p|
|
||||||
|
{
|
||||||
|
id: p[:id],
|
||||||
|
description: p.dig(:metadata, :description)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
render_json_dump subscriptions
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
begin
|
||||||
|
plan = ::Stripe::Plan.retrieve(params[:plan])
|
||||||
|
|
||||||
|
@subscription = ::Stripe::Subscription.create(
|
||||||
|
customer: params[:customer],
|
||||||
|
items: [ { plan: params[:plan] } ]
|
||||||
|
)
|
||||||
|
|
||||||
|
group = plan_group(plan)
|
||||||
|
|
||||||
|
if subscription_ok && group
|
||||||
|
group.add(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless DiscoursePatrons::Customer.exists?(user_id: current_user.id)
|
||||||
|
DiscoursePatrons::Customer.create(user_id: current_user.id, customer_id: params[:customer])
|
||||||
|
end
|
||||||
|
|
||||||
|
render_json_dump @subscription
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def plan_group(plan)
|
||||||
|
Group.find_by_name(plan[:metadata][:group_name])
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscription_ok
|
||||||
|
['active', 'trialing'].include?(@subscription[:status])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
41
app/controllers/user/subscriptions_controller.rb
Normal file
41
app/controllers/user/subscriptions_controller.rb
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
module User
|
||||||
|
class SubscriptionsController < ::ApplicationController
|
||||||
|
include DiscoursePatrons::Stripe
|
||||||
|
before_action :set_api_key
|
||||||
|
requires_login
|
||||||
|
|
||||||
|
def index
|
||||||
|
begin
|
||||||
|
customers = ::Stripe::Customer.list(
|
||||||
|
email: current_user.email,
|
||||||
|
expand: ['data.subscriptions']
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Serialize and remove stuff
|
||||||
|
subscriptions = customers[:data].map do |customer|
|
||||||
|
customer[:subscriptions][:data]
|
||||||
|
end.flatten(1)
|
||||||
|
|
||||||
|
render_json_dump subscriptions
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
begin
|
||||||
|
subscription = ::Stripe::Subscription.delete(params[:id])
|
||||||
|
|
||||||
|
render_json_dump subscription
|
||||||
|
|
||||||
|
rescue ::Stripe::InvalidRequestError => e
|
||||||
|
return render_json_error e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
app/models/customer.rb
Normal file
15
app/models/customer.rb
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
class Customer < ActiveRecord::Base
|
||||||
|
scope :find_user, ->(user) { find_by_user_id(user.id) }
|
||||||
|
|
||||||
|
class << self
|
||||||
|
table_name = "discourse_patrons_customers"
|
||||||
|
|
||||||
|
def create_customer(user, customer)
|
||||||
|
create(customer_id: customer[:id], user_id: user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,4 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Payment < ActiveRecord::Base
|
|
||||||
end
|
|
@ -10,7 +10,7 @@ export default Ember.Component.extend({
|
|||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
const settings = Discourse.SiteSettings;
|
const settings = Discourse.SiteSettings;
|
||||||
const amounts = Discourse.SiteSettings.discourse_patrons_amounts.split("|");
|
const amounts = settings.discourse_patrons_amounts.split("|");
|
||||||
|
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
confirmation: false,
|
confirmation: false,
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
export default Ember.Component.extend({
|
||||||
|
didInsertElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.cardElement.mount("#card-element");
|
||||||
|
},
|
||||||
|
didDestroyElement() {}
|
||||||
|
});
|
@ -0,0 +1,11 @@
|
|||||||
|
import DiscourseURL from "discourse/lib/url";
|
||||||
|
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
actions: {
|
||||||
|
editPlan(id) {
|
||||||
|
return DiscourseURL.redirectTo(
|
||||||
|
`/admin/plugins/discourse-patrons/plans/${id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,19 @@
|
|||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
actions: {
|
||||||
|
createPlan() {
|
||||||
|
if (this.get("model.plan.product_id") === undefined) {
|
||||||
|
const productID = this.get("model.products.firstObject.id");
|
||||||
|
this.set("model.plan.product_id", productID);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.get("model.plan")
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
this.transitionToRoute("adminPlugins.discourse-patrons.plans");
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export default Ember.Controller.extend({});
|
@ -0,0 +1 @@
|
|||||||
|
export default Ember.Controller.extend({});
|
@ -0,0 +1,51 @@
|
|||||||
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
import DiscourseURL from "discourse/lib/url";
|
||||||
|
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
@computed("model.plan.isNew")
|
||||||
|
planFieldDisabled(isNew) {
|
||||||
|
return !isNew;
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("model.product.id")
|
||||||
|
productId(id) {
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
|
||||||
|
redirect(product_id) {
|
||||||
|
DiscourseURL.redirectTo(
|
||||||
|
`/admin/plugins/discourse-patrons/products/${product_id}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
cancelPlan(product_id) {
|
||||||
|
this.redirect(product_id);
|
||||||
|
},
|
||||||
|
|
||||||
|
createPlan() {
|
||||||
|
// TODO: set default group name beforehand
|
||||||
|
if (this.get("model.plan.metadata.group_name") === undefined) {
|
||||||
|
this.set("model.plan.metadata", {
|
||||||
|
group_name: this.get("model.groups.firstObject.name")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.get("model.plan")
|
||||||
|
.save()
|
||||||
|
.then(() => this.redirect(this.productId))
|
||||||
|
.catch(data =>
|
||||||
|
bootbox.alert(data.jqXHR.responseJSON.errors.join("\n"))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePlan() {
|
||||||
|
this.get("model.plan")
|
||||||
|
.update()
|
||||||
|
.then(() => this.redirect(this.productId))
|
||||||
|
.catch(data =>
|
||||||
|
bootbox.alert(data.jqXHR.responseJSON.errors.join("\n"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,30 @@
|
|||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
actions: {
|
||||||
|
cancelProduct() {
|
||||||
|
this.transitionToRoute("adminPlugins.discourse-patrons.products");
|
||||||
|
},
|
||||||
|
|
||||||
|
createProduct() {
|
||||||
|
this.get("model.product")
|
||||||
|
.save()
|
||||||
|
.then(product => {
|
||||||
|
this.transitionToRoute(
|
||||||
|
"adminPlugins.discourse-patrons.products.show",
|
||||||
|
product.id
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateProduct() {
|
||||||
|
this.get("model.product")
|
||||||
|
.update()
|
||||||
|
.then(() => {
|
||||||
|
this.transitionToRoute("adminPlugins.discourse-patrons.products");
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export default Ember.Controller.extend({});
|
@ -1,4 +1,3 @@
|
|||||||
import DiscourseURL from "discourse/lib/url";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
@ -12,8 +11,12 @@ export default Ember.Controller.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
paymentSuccessHandler(paymentIntentId) {
|
paymentSuccessHandler(/* paymentIntentId */) {
|
||||||
DiscourseURL.redirectTo(`patrons/${paymentIntentId}`);
|
bootbox.alert(I18n.t("discourse_patrons.transactions.payment.success"));
|
||||||
|
this.transitionToRoute(
|
||||||
|
"user.billing",
|
||||||
|
Discourse.User.current().username.toLowerCase()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import DiscourseURL from "discourse/lib/url";
|
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
|
||||||
actions: {
|
|
||||||
goBack() {
|
|
||||||
return DiscourseURL.redirectTo("/patrons");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -0,0 +1,50 @@
|
|||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.set(
|
||||||
|
"stripe",
|
||||||
|
Stripe(Discourse.SiteSettings.discourse_patrons_public_key)
|
||||||
|
);
|
||||||
|
const elements = this.get("stripe").elements();
|
||||||
|
this.set("cardElement", elements.create("card", { hidePostalCode: true }));
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
stripePaymentHandler() {
|
||||||
|
this.stripe.createToken(this.get("cardElement")).then(result => {
|
||||||
|
if (result.error) {
|
||||||
|
bootbox.alert(result.error.message);
|
||||||
|
} else {
|
||||||
|
const customerData = {
|
||||||
|
source: result.token.id
|
||||||
|
};
|
||||||
|
|
||||||
|
return ajax("/patrons/customers", {
|
||||||
|
method: "post",
|
||||||
|
data: customerData
|
||||||
|
}).then(customer => {
|
||||||
|
const subscription = this.get("model.subscription");
|
||||||
|
|
||||||
|
subscription.set("customer", customer.id);
|
||||||
|
|
||||||
|
if (subscription.get("plan") === undefined) {
|
||||||
|
subscription.set("plan", this.get("model.plans.firstObject.id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.save().then(() => {
|
||||||
|
bootbox.alert(
|
||||||
|
I18n.t("discourse_patrons.transactions.payment.success")
|
||||||
|
);
|
||||||
|
this.transitionToRoute(
|
||||||
|
"user.subscriptions",
|
||||||
|
Discourse.User.current().username.toLowerCase()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -1,7 +1,20 @@
|
|||||||
export default {
|
export default {
|
||||||
resource: "admin.adminPlugins",
|
resource: "admin.adminPlugins",
|
||||||
path: "/plugins",
|
path: "/plugins",
|
||||||
|
|
||||||
map() {
|
map() {
|
||||||
this.route("discourse-patrons");
|
this.route("discourse-patrons", function() {
|
||||||
|
this.route("dashboard");
|
||||||
|
|
||||||
|
this.route("products", function() {
|
||||||
|
this.route("show", { path: "/:product-id" }, function() {
|
||||||
|
this.route("plans", function() {
|
||||||
|
this.route("show", { path: "/:plan-id" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route("subscriptions");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
resource: "user",
|
||||||
|
path: "users/:username",
|
||||||
|
map() {
|
||||||
|
this.route("billing");
|
||||||
|
this.route("subscriptions");
|
||||||
|
}
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
|
// TODO: typo in this helper name: currency not curency.
|
||||||
export default Ember.Helper.helper(function(params) {
|
export default Ember.Helper.helper(function(params) {
|
||||||
let currencySign;
|
let currencySign;
|
||||||
|
|
||||||
|
16
assets/javascripts/discourse/helpers/format-currency.js.es6
Normal file
16
assets/javascripts/discourse/helpers/format-currency.js.es6
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export default Ember.Helper.helper(function(params) {
|
||||||
|
let currencySign;
|
||||||
|
|
||||||
|
switch (Discourse.SiteSettings.discourse_patrons_currency) {
|
||||||
|
case "EUR":
|
||||||
|
currencySign = "€";
|
||||||
|
break;
|
||||||
|
case "GBP":
|
||||||
|
currencySign = "£";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
currencySign = "$";
|
||||||
|
}
|
||||||
|
|
||||||
|
return currencySign + params.map(p => p.toUpperCase()).join(" ");
|
||||||
|
});
|
16
assets/javascripts/discourse/helpers/format-unix-date.js.es6
Normal file
16
assets/javascripts/discourse/helpers/format-unix-date.js.es6
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||||
|
import { autoUpdatingRelativeAge } from "discourse/lib/formatter";
|
||||||
|
|
||||||
|
registerUnbound("format-unix-date", function(timestamp) {
|
||||||
|
if (timestamp) {
|
||||||
|
const date = new Date(moment.unix(timestamp).format());
|
||||||
|
|
||||||
|
return new Handlebars.SafeString(
|
||||||
|
autoUpdatingRelativeAge(date, {
|
||||||
|
format: "medium",
|
||||||
|
title: true,
|
||||||
|
leaveAgo: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,5 @@
|
|||||||
|
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||||
|
|
||||||
|
export default registerUnbound("show-extra-nav", function() {
|
||||||
|
return Discourse.SiteSettings.discourse_patrons_extra_nav_subscribe;
|
||||||
|
});
|
@ -0,0 +1,12 @@
|
|||||||
|
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||||
|
|
||||||
|
export default registerUnbound("user-viewing-self", function(model) {
|
||||||
|
if (Discourse.User.current()) {
|
||||||
|
return (
|
||||||
|
Discourse.User.current().username.toLowerCase() ===
|
||||||
|
model.username.toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
66
assets/javascripts/discourse/models/admin-plan.js.es6
Normal file
66
assets/javascripts/discourse/models/admin-plan.js.es6
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import Plan from "discourse/plugins/discourse-patrons/discourse/models/plan";
|
||||||
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
const AdminPlan = Plan.extend({
|
||||||
|
isNew: false,
|
||||||
|
name: "",
|
||||||
|
interval: "month",
|
||||||
|
amount: 0,
|
||||||
|
intervals: ["day", "week", "month", "year"],
|
||||||
|
metadata: {},
|
||||||
|
|
||||||
|
@computed("trial_period_days")
|
||||||
|
parseTrialPeriodDays(trial_period_days) {
|
||||||
|
if (trial_period_days) {
|
||||||
|
return parseInt(0 + trial_period_days);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
return ajax(`/patrons/admin/plans/${this.id}`, { method: "delete" });
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const data = {
|
||||||
|
nickname: this.nickname,
|
||||||
|
interval: this.interval,
|
||||||
|
amount: this.amount,
|
||||||
|
trial_period_days: this.parseTrialPeriodDays,
|
||||||
|
product: this.product,
|
||||||
|
metadata: this.metadata,
|
||||||
|
active: this.active
|
||||||
|
};
|
||||||
|
|
||||||
|
return ajax("/patrons/admin/plans", { method: "post", data });
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const data = {
|
||||||
|
nickname: this.nickname,
|
||||||
|
trial_period_days: this.parseTrialPeriodDays,
|
||||||
|
metadata: this.metadata,
|
||||||
|
active: this.active
|
||||||
|
};
|
||||||
|
|
||||||
|
return ajax(`/patrons/admin/plans/${this.id}`, { method: "patch", data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AdminPlan.reopenClass({
|
||||||
|
findAll(data) {
|
||||||
|
return ajax("/patrons/admin/plans", { method: "get", data }).then(result =>
|
||||||
|
result.map(plan => AdminPlan.create(plan))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
find(id) {
|
||||||
|
return ajax(`/patrons/admin/plans/${id}`, { method: "get" }).then(plan =>
|
||||||
|
AdminPlan.create(plan)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AdminPlan;
|
53
assets/javascripts/discourse/models/admin-product.js.es6
Normal file
53
assets/javascripts/discourse/models/admin-product.js.es6
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
const AdminProduct = Discourse.Model.extend({
|
||||||
|
isNew: false,
|
||||||
|
metadata: {},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
return ajax(`/patrons/admin/products/${this.id}`, { method: "delete" });
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const data = {
|
||||||
|
name: this.name,
|
||||||
|
statement_descriptor: this.statement_descriptor,
|
||||||
|
metadata: this.metadata,
|
||||||
|
active: this.active
|
||||||
|
};
|
||||||
|
|
||||||
|
return ajax("/patrons/admin/products", { method: "post", data }).then(
|
||||||
|
product => AdminProduct.create(product)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const data = {
|
||||||
|
name: this.name,
|
||||||
|
statement_descriptor: this.statement_descriptor,
|
||||||
|
metadata: this.metadata,
|
||||||
|
active: this.active
|
||||||
|
};
|
||||||
|
|
||||||
|
return ajax(`/patrons/admin/products/${this.id}`, {
|
||||||
|
method: "patch",
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AdminProduct.reopenClass({
|
||||||
|
findAll() {
|
||||||
|
return ajax("/patrons/admin/products", { method: "get" }).then(result =>
|
||||||
|
result.map(product => AdminProduct.create(product))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
find(id) {
|
||||||
|
return ajax(`/patrons/admin/products/${id}`, { method: "get" }).then(
|
||||||
|
product => AdminProduct.create(product)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AdminProduct;
|
@ -0,0 +1,14 @@
|
|||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
const AdminSubscription = Discourse.Model.extend({});
|
||||||
|
|
||||||
|
AdminSubscription.reopenClass({
|
||||||
|
find() {
|
||||||
|
return ajax("/patrons/admin/subscriptions", { method: "get" }).then(
|
||||||
|
result =>
|
||||||
|
result.data.map(subscription => AdminSubscription.create(subscription))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AdminSubscription;
|
18
assets/javascripts/discourse/models/group.js.es6
Normal file
18
assets/javascripts/discourse/models/group.js.es6
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
const Group = Discourse.Model.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;
|
13
assets/javascripts/discourse/models/invoice.js.es6
Normal file
13
assets/javascripts/discourse/models/invoice.js.es6
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
const Invoice = Discourse.Model.extend({});
|
||||||
|
|
||||||
|
Invoice.reopenClass({
|
||||||
|
findAll() {
|
||||||
|
return ajax("/patrons/invoices", { method: "get" }).then(result =>
|
||||||
|
result.map(invoice => Invoice.create(invoice))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Invoice;
|
30
assets/javascripts/discourse/models/plan.js.es6
Normal file
30
assets/javascripts/discourse/models/plan.js.es6
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
const Plan = Discourse.Model.extend({
|
||||||
|
amountDollars: Ember.computed("amount", {
|
||||||
|
get() {
|
||||||
|
return parseFloat(this.get("amount") / 100).toFixed(2);
|
||||||
|
},
|
||||||
|
set(key, value) {
|
||||||
|
const decimal = parseFloat(value) * 100;
|
||||||
|
this.set("amount", decimal);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
@computed("amountDollars", "currency", "interval")
|
||||||
|
subscriptionRate(amountDollars, currency, interval) {
|
||||||
|
return `$${amountDollars} ${currency.toUpperCase()} / ${interval}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Plan.reopenClass({
|
||||||
|
findAll(data) {
|
||||||
|
return ajax("/patrons/plans", { method: "get", data }).then(result =>
|
||||||
|
result.map(plan => Plan.create(plan))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Plan;
|
19
assets/javascripts/discourse/models/product.js.es6
Normal file
19
assets/javascripts/discourse/models/product.js.es6
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
const Product = Discourse.Model.extend({});
|
||||||
|
|
||||||
|
Product.reopenClass({
|
||||||
|
findAll() {
|
||||||
|
return ajax("/patrons/products", { method: "get" }).then(result =>
|
||||||
|
result.map(product => Product.create(product))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
find(id) {
|
||||||
|
return ajax(`/patrons/products/${id}`, { method: "get" }).then(product =>
|
||||||
|
Product.create(product)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Product;
|
28
assets/javascripts/discourse/models/subscription.js.es6
Normal file
28
assets/javascripts/discourse/models/subscription.js.es6
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
const Subscription = Discourse.Model.extend({
|
||||||
|
@computed("status")
|
||||||
|
canceled(status) {
|
||||||
|
return status === "canceled";
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const data = {
|
||||||
|
customer: this.customer,
|
||||||
|
plan: this.plan
|
||||||
|
};
|
||||||
|
|
||||||
|
return ajax("/patrons/subscriptions", { method: "post", data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Subscription.reopenClass({
|
||||||
|
findAll() {
|
||||||
|
return ajax("/patrons/subscriptions", { method: "get" }).then(result =>
|
||||||
|
result.map(subscription => Subscription.create(subscription))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Subscription;
|
29
assets/javascripts/discourse/models/user-subscription.js.es6
Normal file
29
assets/javascripts/discourse/models/user-subscription.js.es6
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import Plan from "discourse/plugins/discourse-patrons/discourse/models/plan";
|
||||||
|
|
||||||
|
const UserSubscription = Discourse.Model.extend({
|
||||||
|
@computed("status")
|
||||||
|
canceled(status) {
|
||||||
|
return status === "canceled";
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
return ajax(`/patrons/user/subscriptions/${this.id}`, {
|
||||||
|
method: "delete"
|
||||||
|
}).then(result => UserSubscription.create(result));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
UserSubscription.reopenClass({
|
||||||
|
findAll() {
|
||||||
|
return ajax("/patrons/user/subscriptions", { method: "get" }).then(result =>
|
||||||
|
result.map(subscription => {
|
||||||
|
subscription.plan = Plan.create(subscription.plan);
|
||||||
|
return UserSubscription.create(subscription);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UserSubscription;
|
@ -1,5 +1,7 @@
|
|||||||
export default function() {
|
export default function() {
|
||||||
this.route("patrons", function() {
|
this.route("patrons", function() {
|
||||||
this.route("show", { path: ":pid" });
|
this.route("subscribe", function() {
|
||||||
|
this.route("show", { path: "/:subscription-id" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
|
export default Discourse.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);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,7 @@
|
|||||||
|
import AdminPlan from "discourse/plugins/discourse-patrons/discourse/models/admin-plan";
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model() {
|
||||||
|
return AdminPlan.findAll();
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export default Discourse.Route.extend({});
|
@ -0,0 +1,31 @@
|
|||||||
|
import AdminProduct from "discourse/plugins/discourse-patrons/discourse/models/admin-product";
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model() {
|
||||||
|
return AdminProduct.findAll();
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
destroyProduct(product) {
|
||||||
|
bootbox.confirm(
|
||||||
|
I18n.t("discourse_patrons.admin.products.operations.destroy.confirm"),
|
||||||
|
I18n.t("no_value"),
|
||||||
|
I18n.t("yes_value"),
|
||||||
|
confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
product
|
||||||
|
.destroy()
|
||||||
|
.then(() => {
|
||||||
|
this.controllerFor("adminPluginsDiscoursePatronsProductsIndex")
|
||||||
|
.get("model")
|
||||||
|
.removeObject(product);
|
||||||
|
})
|
||||||
|
.catch(data =>
|
||||||
|
bootbox.alert(data.jqXHR.responseJSON.errors.join("\n"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,34 @@
|
|||||||
|
import AdminPlan from "discourse/plugins/discourse-patrons/discourse/models/admin-plan";
|
||||||
|
import Group from "discourse/models/group";
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model(params) {
|
||||||
|
const id = params["plan-id"];
|
||||||
|
const product = this.modelFor(
|
||||||
|
"adminPlugins.discourse-patrons.products.show"
|
||||||
|
).product;
|
||||||
|
let plan;
|
||||||
|
|
||||||
|
if (id === "new") {
|
||||||
|
plan = AdminPlan.create({
|
||||||
|
active: true,
|
||||||
|
isNew: true,
|
||||||
|
product: product.get("id")
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
plan = AdminPlan.find(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = Group.findAll({ ignore_automatic: true });
|
||||||
|
|
||||||
|
return Ember.RSVP.hash({ plan, product, groups });
|
||||||
|
},
|
||||||
|
|
||||||
|
renderTemplate() {
|
||||||
|
this.render("adminPlugins.discourse-patrons.products.show.plans.show", {
|
||||||
|
into: "adminPlugins.discourse-patrons.products",
|
||||||
|
outlet: "main",
|
||||||
|
controller: "adminPlugins.discourse-patrons.products.show.plans.show"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,43 @@
|
|||||||
|
import AdminProduct from "discourse/plugins/discourse-patrons/discourse/models/admin-product";
|
||||||
|
import AdminPlan from "discourse/plugins/discourse-patrons/discourse/models/admin-plan";
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model(params) {
|
||||||
|
const product_id = params["product-id"];
|
||||||
|
let product;
|
||||||
|
let plans = [];
|
||||||
|
|
||||||
|
if (product_id === "new") {
|
||||||
|
product = AdminProduct.create({ active: true, isNew: true });
|
||||||
|
} else {
|
||||||
|
product = AdminProduct.find(product_id);
|
||||||
|
plans = AdminPlan.findAll({ product_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ember.RSVP.hash({ plans, product });
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
destroyPlan(plan) {
|
||||||
|
bootbox.confirm(
|
||||||
|
I18n.t("discourse_patrons.admin.plans.operations.destroy.confirm"),
|
||||||
|
I18n.t("no_value"),
|
||||||
|
I18n.t("yes_value"),
|
||||||
|
confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
plan
|
||||||
|
.destroy()
|
||||||
|
.then(() => {
|
||||||
|
this.controllerFor("adminPluginsDiscoursePatronsProductsShow")
|
||||||
|
.get("model.plans")
|
||||||
|
.removeObject(plan);
|
||||||
|
})
|
||||||
|
.catch(data =>
|
||||||
|
bootbox.alert(data.jqXHR.responseJSON.errors.join("\n"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export default Discourse.Route.extend({});
|
@ -0,0 +1,7 @@
|
|||||||
|
import AdminSubscription from "discourse/plugins/discourse-patrons/discourse/models/admin-subscription";
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model() {
|
||||||
|
return AdminSubscription.find();
|
||||||
|
}
|
||||||
|
});
|
@ -1,22 +1 @@
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
export default Discourse.Route.extend({});
|
||||||
|
|
||||||
export default Discourse.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,7 +0,0 @@
|
|||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
|
|
||||||
export default Discourse.Route.extend({
|
|
||||||
model(params) {
|
|
||||||
return ajax(`/patrons/${params.pid}`, { method: "get" });
|
|
||||||
}
|
|
||||||
});
|
|
@ -0,0 +1,16 @@
|
|||||||
|
import Product from "discourse/plugins/discourse-patrons/discourse/models/product";
|
||||||
|
import Plan from "discourse/plugins/discourse-patrons/discourse/models/plan";
|
||||||
|
import Subscription from "discourse/plugins/discourse-patrons/discourse/models/subscription";
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model(params) {
|
||||||
|
const product_id = params["subscription-id"];
|
||||||
|
const product = Product.find(product_id);
|
||||||
|
const subscription = Subscription.create();
|
||||||
|
const plans = Plan.findAll({ product_id: product_id }).then(results =>
|
||||||
|
results.map(p => ({ id: p.id, name: p.subscriptionRate }))
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ember.RSVP.hash({ plans, product, subscription });
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,7 @@
|
|||||||
|
import Product from "discourse/plugins/discourse-patrons/discourse/models/product";
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model() {
|
||||||
|
return Product.findAll();
|
||||||
|
}
|
||||||
|
});
|
15
assets/javascripts/discourse/routes/user-billing.js.es6
Normal file
15
assets/javascripts/discourse/routes/user-billing.js.es6
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import Invoice from "discourse/plugins/discourse-patrons/discourse/models/invoice";
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model() {
|
||||||
|
return Invoice.findAll();
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
if (this.currentUser.id !== this.modelFor("user").id) {
|
||||||
|
this.replaceWith("userActivity");
|
||||||
|
} else {
|
||||||
|
controller.setProperties({ model });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,37 @@
|
|||||||
|
import UserSubscription from "discourse/plugins/discourse-patrons/discourse/models/user-subscription";
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model() {
|
||||||
|
return UserSubscription.findAll();
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
if (this.currentUser.id !== this.modelFor("user").id) {
|
||||||
|
this.replaceWith("userActivity");
|
||||||
|
} else {
|
||||||
|
controller.setProperties({ model });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
cancelSubscription(subscription) {
|
||||||
|
bootbox.confirm(
|
||||||
|
I18n.t(
|
||||||
|
"discourse_patrons.user.subscriptions.operations.destroy.confirm"
|
||||||
|
),
|
||||||
|
I18n.t("no_value"),
|
||||||
|
I18n.t("yes_value"),
|
||||||
|
confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
subscription
|
||||||
|
.destroy()
|
||||||
|
.then(result => subscription.set("status", result.status))
|
||||||
|
.catch(data =>
|
||||||
|
bootbox.alert(data.jqXHR.responseJSON.errors.join("\n"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
<h3>{{i18n 'discourse_patrons.admin.dashboard.title'}}</h3>
|
||||||
|
|
||||||
|
{{#load-more selector=".discourse-patrons-table tr" action=(action "loadMore")}}
|
||||||
|
{{#if model}}
|
||||||
|
<table class="table discourse-patrons-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.dashboard.table.head.user'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.dashboard.table.head.payment_intent'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.dashboard.table.head.receipt_email'}}</th>
|
||||||
|
<th onclick={{action "orderPayments" "created_at"}} class="sortable">{{i18n 'created'}}</th>
|
||||||
|
<th class="amount" onclick={{action "orderPayments" "amount"}} class="sortable amount">{{i18n 'discourse_patrons.admin.dashboard.table.head.amount'}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{{#each model as |payment|}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{#link-to "adminUser.index" payment.user_id payment.username}}
|
||||||
|
{{payment.username}}
|
||||||
|
{{/link-to}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{#link-to "patrons.show" payment.payment_intent_id}}
|
||||||
|
{{{payment.payment_intent_id}}}
|
||||||
|
{{/link-to}}
|
||||||
|
</td>
|
||||||
|
<td>{{payment.receipt_email}}</td>
|
||||||
|
<td>{{{format-duration payment.created_at_age}}}</td>
|
||||||
|
<td class="amount">{{payment.amount_currency}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
{{/if}}
|
||||||
|
{{/load-more}}
|
@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
<table class="table discourse-patrons-table">
|
||||||
|
<thead>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.plans.plan.plan_id'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.plans.plan.nickname.title'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.plans.plan.interval'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.plans.plan.amount'}}</th>
|
||||||
|
<th></th>
|
||||||
|
</thead>
|
||||||
|
{{#each model as |plan|}}
|
||||||
|
<tr>
|
||||||
|
<td>{{plan.id}}</td>
|
||||||
|
<td>{{plan.nickname}}</td>
|
||||||
|
<td>{{plan.interval}}</td>
|
||||||
|
<td>{{plan.amount}}</td>
|
||||||
|
<td class="td-right">
|
||||||
|
{{d-button
|
||||||
|
action=(action "editPlan" plan.id)
|
||||||
|
icon="far-edit"
|
||||||
|
class="btn no-text btn-icon"}}
|
||||||
|
{{d-button
|
||||||
|
action=(route-action "destroyPlan")
|
||||||
|
actionParam=plan
|
||||||
|
icon="trash-alt"
|
||||||
|
class="btn-danger btn no-text btn-icon"}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
<p class="btn-right">
|
||||||
|
{{#link-to 'adminPlugins.discourse-patrons.products.show' 'new' class="btn btn-primary"}}
|
||||||
|
{{d-icon "plus"}}
|
||||||
|
<span>{{i18n 'discourse_patrons.admin.products.operations.new'}}</span>
|
||||||
|
{{/link-to}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{#if model}}
|
||||||
|
<table class="table discourse-patrons-table">
|
||||||
|
<thead>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.products.product.name'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.products.product.created_at'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.products.product.updated_at'}}</th>
|
||||||
|
<th class="td-right">{{i18n 'discourse_patrons.admin.products.product.active'}}</th>
|
||||||
|
<th></th>
|
||||||
|
</thead>
|
||||||
|
{{#each model as |product|}}
|
||||||
|
<tr>
|
||||||
|
<td>{{product.name}}</td>
|
||||||
|
<td>{{format-unix-date product.created}}</td>
|
||||||
|
<td>{{format-unix-date product.updated}}</td>
|
||||||
|
<td class="td-right">{{product.active}}</td>
|
||||||
|
<td class="td-right">
|
||||||
|
{{#link-to "adminPlugins.discourse-patrons.products.show" product.id class="btn no-text btn-icon"}}
|
||||||
|
{{d-icon "far-edit"}}
|
||||||
|
{{/link-to}}
|
||||||
|
{{d-button
|
||||||
|
action=(route-action "destroyProduct")
|
||||||
|
actionParam=product
|
||||||
|
icon="trash-alt"
|
||||||
|
class="btn-danger btn no-text btn-icon"}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<p>
|
||||||
|
{{i18n 'discourse_patrons.admin.products.product_help'}}
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
@ -0,0 +1,68 @@
|
|||||||
|
|
||||||
|
<h4>{{i18n 'discourse_patrons.admin.plans.title'}}</h4>
|
||||||
|
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<p>
|
||||||
|
<label for="product">{{i18n 'discourse_patrons.admin.products.product.name'}}</label>
|
||||||
|
{{input type="text" name="product_name" value=model.product.name disabled=true}}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="name">{{i18n 'discourse_patrons.admin.plans.plan.nickname'}}</label>
|
||||||
|
{{input type="text" name="name" value=model.plan.nickname}}
|
||||||
|
<div class="control-instructions">
|
||||||
|
{{i18n 'discourse_patrons.admin.plans.plan.nickname_help'}}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="interval">{{i18n 'discourse_patrons.admin.plans.plan.group'}}</label>
|
||||||
|
{{combo-box valueAttribute="name" content=model.groups value=model.plan.metadata.group_name}}
|
||||||
|
<div class="control-instructions">
|
||||||
|
{{i18n 'discourse_patrons.admin.plans.plan.group_help'}}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="amount">{{i18n 'discourse_patrons.admin.plans.plan.amount'}}</label>
|
||||||
|
{{input type="text" name="name" value=model.plan.amountDollars disabled=planFieldDisabled}}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="trial">
|
||||||
|
{{i18n 'discourse_patrons.admin.plans.plan.trial'}}
|
||||||
|
({{i18n 'discourse_patrons.optional'}})
|
||||||
|
</label>
|
||||||
|
{{input type="text" name="trial" value=model.plan.trial_period_days}}
|
||||||
|
<div class="control-instructions">
|
||||||
|
{{i18n 'discourse_patrons.admin.plans.plan.trial_help'}}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="interval">
|
||||||
|
{{i18n 'discourse_patrons.admin.plans.plan.interval'}}
|
||||||
|
</label>
|
||||||
|
{{combo-box valueAttribute="value" content=model.plan.intervals value=model.plan.interval}}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="active">
|
||||||
|
{{i18n 'discourse_patrons.admin.plans.plan.active'}}
|
||||||
|
</label>
|
||||||
|
{{input type="checkbox" name="active" checked=model.plan.active}}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p class="control-instructions">
|
||||||
|
{{i18n 'discourse_patrons.admin.plans.operations.create_help'}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="pull-right">
|
||||||
|
{{d-button label="cancel" action=(action "cancelPlan" model.plan.product) icon="times"}}
|
||||||
|
|
||||||
|
{{#if model.plan.isNew}}
|
||||||
|
{{d-button label="discourse_patrons.admin.plans.operations.create" action="createPlan" icon="plus" class="btn btn-primary"}}
|
||||||
|
{{else}}
|
||||||
|
{{d-button label="discourse_patrons.admin.plans.operations.update" action="updatePlan" icon="check" class="btn btn-primary"}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
@ -0,0 +1,97 @@
|
|||||||
|
<h4>{{i18n 'discourse_patrons.admin.products.title'}}</h4>
|
||||||
|
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<p>
|
||||||
|
<label for="name">{{i18n 'discourse_patrons.admin.products.product.name'}}</label>
|
||||||
|
{{input type="text" name="name" value=model.product.name}}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="description">
|
||||||
|
{{i18n 'discourse_patrons.admin.products.product.description'}}
|
||||||
|
</label>
|
||||||
|
{{textarea name="description" value=model.product.metadata.description class="discourse-patrons-admin-textarea"}}
|
||||||
|
<div class="control-instructions">
|
||||||
|
{{i18n 'discourse_patrons.admin.products.product.description_help'}}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="statement_descriptor">
|
||||||
|
{{i18n 'discourse_patrons.admin.products.product.statement_descriptor'}}
|
||||||
|
</label>
|
||||||
|
{{input type="text" name="statement_descriptor" value=model.product.statement_descriptor}}
|
||||||
|
<div class="control-instructions">
|
||||||
|
{{i18n 'discourse_patrons.admin.products.product.statement_descriptor_help'}}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="active">{{i18n 'discourse_patrons.admin.products.product.active'}}</label>
|
||||||
|
{{input type="checkbox" name="active" checked=model.product.active}}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{#unless model.product.isNew}}
|
||||||
|
<h4>{{i18n 'discourse_patrons.admin.plans.title'}}</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<table class="table discourse-patrons-table">
|
||||||
|
<thead>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.plans.plan.nickname'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.plans.plan.interval'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.plans.plan.created_at'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.plans.plan.group'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.plans.plan.active'}}</th>
|
||||||
|
<th class="td-right">{{i18n 'discourse_patrons.admin.plans.plan.amount'}}</th>
|
||||||
|
<th class="td-right">
|
||||||
|
{{#link-to "adminPlugins.discourse-patrons.products.show.plans.show" model.product.id "new" class="btn"}}
|
||||||
|
{{i18n 'discourse_patrons.admin.plans.operations.add'}}
|
||||||
|
{{/link-to}}
|
||||||
|
</th>
|
||||||
|
</thead>
|
||||||
|
{{#each model.plans as |plan|}}
|
||||||
|
<tr>
|
||||||
|
<td>{{plan.nickname}}</td>
|
||||||
|
<td>{{plan.interval}}</td>
|
||||||
|
<td>{{format-unix-date plan.created}}</td>
|
||||||
|
<td>{{plan.metadata.group_name}}</td>
|
||||||
|
<td>{{plan.active}}</td>
|
||||||
|
<td class="td-right">{{format-currency plan.currency plan.amountDollars}}</td>
|
||||||
|
<td class="td-right">
|
||||||
|
{{#link-to "adminPlugins.discourse-patrons.products.show.plans.show" model.product.id plan.id class="btn no-text btn-icon"}}
|
||||||
|
{{d-icon "far-edit"}}
|
||||||
|
{{/link-to}}
|
||||||
|
{{d-button
|
||||||
|
action=(route-action "destroyPlan")
|
||||||
|
actionParam=plan
|
||||||
|
icon="trash-alt"
|
||||||
|
class="btn-danger btn no-text btn-icon"}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="7">
|
||||||
|
{{#unless model.plans}}
|
||||||
|
<hr>
|
||||||
|
{{i18n 'discourse_patrons.admin.products.product.plan_help'}}
|
||||||
|
{{/unless}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</p>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="pull-right">
|
||||||
|
{{d-button label="cancel" action=(action "cancelProduct") icon="times"}}
|
||||||
|
{{#if model.product.isNew}}
|
||||||
|
{{d-button label="discourse_patrons.admin.products.operations.create" action="createProduct" icon="plus" class="btn btn-primary"}}
|
||||||
|
{{else}}
|
||||||
|
{{d-button label="discourse_patrons.admin.products.operations.update" action="updateProduct" icon="check" class="btn btn-primary"}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{outlet}}
|
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
{{outlet}}
|
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
<table class="table discourse-patrons-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.subscriptions.subscription.customer'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.subscriptions.subscription.plan'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.admin.subscriptions.subscription.status'}}</th>
|
||||||
|
<th class="td-right">{{i18n 'discourse_patrons.admin.subscriptions.subscription.created_at'}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{{#each model as |subscription|}}
|
||||||
|
<tr>
|
||||||
|
<td>{{subscription.customer}}</td>
|
||||||
|
<td>{{subscription.plan.id}}</td>
|
||||||
|
<td>{{subscription.status}}</td>
|
||||||
|
<td class="td-right">{{format-unix-date subscription.created}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
@ -1,35 +1,14 @@
|
|||||||
|
|
||||||
<h2>{{i18n 'discourse_patrons.title' site_name=siteSettings.title}}</h2>
|
<h2>{{i18n 'discourse_patrons.title' site_name=siteSettings.title}}</h2>
|
||||||
|
|
||||||
{{#load-more selector=".discourse-patrons-admin tr" action=(action "loadMore")}}
|
<ul class="nav nav-pills">
|
||||||
{{#if model}}
|
{{!-- {{nav-item route='adminPlugins.discourse-patrons.dashboard' label='discourse_patrons.admin.dashboard.title'}} --}}
|
||||||
<table class="table discourse-patrons-admin">
|
{{nav-item route='adminPlugins.discourse-patrons.products' label='discourse_patrons.admin.products.title'}}
|
||||||
<thead>
|
{{nav-item route='adminPlugins.discourse-patrons.subscriptions' label='discourse_patrons.admin.subscriptions.title'}}
|
||||||
<tr>
|
</ul>
|
||||||
<th>{{i18n 'discourse_patrons.admin.payment_history.table.head.user'}}</th>
|
|
||||||
<th>{{i18n 'discourse_patrons.admin.payment_history.table.head.payment_intent'}}</th>
|
<hr>
|
||||||
<th>{{i18n 'discourse_patrons.admin.payment_history.table.head.receipt_email'}}</th>
|
|
||||||
<th onclick={{action "orderPayments" "created_at"}} class="sortable">{{i18n 'created'}}</th>
|
<div id="discourse-patrons-admin">
|
||||||
<th class="amount" onclick={{action "orderPayments" "amount"}} class="sortable amount">{{i18n 'discourse_patrons.admin.payment_history.table.head.amount'}}</th>
|
{{outlet}}
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
|
||||||
{{#each model as |payment|}}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{{#link-to "adminUser.index" payment.user_id payment.username}}
|
|
||||||
{{payment.username}}
|
|
||||||
{{/link-to}}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{#link-to "patrons.show" payment.payment_intent_id}}
|
|
||||||
{{{payment.payment_intent_id}}}
|
|
||||||
{{/link-to}}
|
|
||||||
</td>
|
|
||||||
<td>{{payment.receipt_email}}</td>
|
|
||||||
<td>{{{format-duration payment.created_at_age}}}</td>
|
|
||||||
<td class="amount">{{payment.amount_currency}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</table>
|
|
||||||
{{/if}}
|
|
||||||
{{/load-more}}
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
{{#if confirmation}}
|
{{#if confirmation}}
|
||||||
{{#d-modal closeModal=(action "closeModal") modalStyle="inline-modal" title=(i18n "discourse_patrons.payment.payment_confirmation")}}
|
{{#d-modal closeModal=(action "closeModal") modalStyle="inline-modal" title=(i18n "discourse_patrons.one_time.payment.payment_confirmation")}}
|
||||||
{{#d-modal-body}}
|
{{#d-modal-body}}
|
||||||
<div class="discourse-patrons-section-columns">
|
<div class="discourse-patrons-section-columns">
|
||||||
<div class="section-column discourse-patrons-confirmation-billing">
|
<div class="section-column discourse-patrons-confirmation-billing">
|
||||||
@ -51,7 +51,7 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<div class="discourse-patrons-section-columns discourse-patrons-payment-details">
|
<div class="discourse-patrons-section-columns discourse-patrons-payment-details">
|
||||||
<div class="section-column">
|
<div class="section-column">
|
||||||
<h3>{{i18n 'discourse_patrons.payment.your_information'}}</h3>
|
<h3>{{i18n 'discourse_patrons.one_time.payment.your_information'}}</h3>
|
||||||
|
|
||||||
<div class="user-controls discourse-patrons-fields discourse-patrons-billing">
|
<div class="user-controls discourse-patrons-fields discourse-patrons-billing">
|
||||||
<div class="display-row">
|
<div class="display-row">
|
||||||
@ -60,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
{{input value=billing.name}}
|
{{input value=billing.name}}
|
||||||
<div class="desc">{{i18n 'discourse_patrons.payment.optional'}}</div>
|
<div class="desc">{{i18n 'discourse_patrons.one_time.payment.optional'}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-row">
|
<div class="display-row">
|
||||||
@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
{{input type="email" value=billing.email}}
|
{{input type="email" value=billing.email}}
|
||||||
<div class="desc">{{i18n 'discourse_patrons.payment.receipt_info'}}</div>
|
<div class="desc">{{i18n 'discourse_patrons.one_time.payment.receipt_info'}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-row">
|
<div class="display-row">
|
||||||
@ -78,19 +78,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
{{input value=billing.phone}}
|
{{input value=billing.phone}}
|
||||||
<div class="desc">{{i18n 'discourse_patrons.payment.optional'}}</div>
|
<div class="desc">{{i18n 'discourse_patrons.one_time.payment.optional'}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="section-column">
|
<div class="section-column">
|
||||||
<h3>{{i18n 'discourse_patrons.payment.payment_information'}}</h3>
|
<h3>{{i18n 'discourse_patrons.one_time.payment.payment_information'}}</h3>
|
||||||
|
|
||||||
<div class="user-controls discourse-patrons-fields">
|
<div class="user-controls discourse-patrons-fields">
|
||||||
<div class="display-row">
|
<div class="display-row">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{i18n 'discourse_patrons.amount'}}
|
{{i18n 'discourse_patrons.one_time.amount'}}
|
||||||
{{siteSettings.discourse_patrons_currency}}
|
{{siteSettings.discourse_patrons_currency}}
|
||||||
</div>
|
</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
<div id="card-element"></div>
|
@ -0,0 +1,5 @@
|
|||||||
|
{{#if (show-extra-nav)}}
|
||||||
|
{{#link-to 'patrons.subscribe' class='discourse-patrons-subscribe'}}
|
||||||
|
{{i18n 'discourse_patrons.navigation.subscribe'}}
|
||||||
|
{{/link-to}}
|
||||||
|
{{/if}}
|
@ -0,0 +1,3 @@
|
|||||||
|
{{#if (user-viewing-self model)}}
|
||||||
|
{{#link-to 'user.subscriptions'}}{{d-icon "credit-card"}}{{I18n 'discourse_patrons.navigation.subscriptions'}}{{/link-to}}
|
||||||
|
{{/if}}
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
<h3>{{i18n 'discourse_patrons.heading.payment' site_name=siteSettings.title}}</h3>
|
<h3>{{i18n 'discourse_patrons.one_time.heading.payment' site_name=siteSettings.title}}</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{cook-text siteSettings.discourse_patrons_payment_page}}
|
{{cook-text siteSettings.discourse_patrons_payment_page}}
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
|
|
||||||
{{#unless model.error}}
|
|
||||||
<h3>{{i18n 'discourse_patrons.heading.success' site_name=siteSettings.title}}</h3>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{cook-text siteSettings.discourse_patrons_success_page}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>{{i18n 'discourse_patrons.payment_intent_id'}}</td>
|
|
||||||
<td>{{model.id}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{{i18n 'discourse_patrons.amount'}}</td>
|
|
||||||
<td>{{model.amount}}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{{/unless}}
|
|
12
assets/javascripts/discourse/templates/patrons/subscribe.hbs
Normal file
12
assets/javascripts/discourse/templates/patrons/subscribe.hbs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="title-wrapper">
|
||||||
|
<h1>
|
||||||
|
{{i18n 'discourse_patrons.subscribe.title'}}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{{outlet}}
|
||||||
|
</div>
|
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
{{#each model as |product|}}
|
||||||
|
<div>
|
||||||
|
<h2>{{product.name}}</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{product.description}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="pull-right">
|
||||||
|
{{#link-to "patrons.subscribe.show" product.id class="btn btn-primary"}}
|
||||||
|
{{i18n 'discourse_patrons.subscribe.title'}}
|
||||||
|
{{/link-to}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
<div class="discourse-patrons-section-columns">
|
||||||
|
<div class="section-column discourse-patrons-confirmation-billing">
|
||||||
|
<h2>
|
||||||
|
{{model.product.name}}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{{model.product.description}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="section-column">
|
||||||
|
{{combo-box valueAttribute="id" content=model.plans value=model.product.plan}}
|
||||||
|
|
||||||
|
{{#d-button
|
||||||
|
action="stripePaymentHandler"
|
||||||
|
class="btn btn-primary btn-payment btn-discourse-patrons"}}
|
||||||
|
{{i18n 'discourse_patrons.subscribe.buttons.subscribe'}}
|
||||||
|
{{/d-button}}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h4>{{i18n 'discourse_patrons.subscribe.card.title'}}</h4>
|
||||||
|
{{subscribe-card cardElement=cardElement}}
|
||||||
|
|
||||||
|
{{!-- <div id="discourse-patrons-subscribe-customer">
|
||||||
|
<h4>{{i18n 'discourse_patrons.subscribe.customer.title'}}</h4>
|
||||||
|
<div class="discourse-patrons-subscribe-customer-empty">
|
||||||
|
{{i18n 'discourse_patrons.subscribe.customer.empty'}}
|
||||||
|
</div>
|
||||||
|
</div> --}}
|
||||||
|
</div>
|
||||||
|
</div>
|
27
assets/javascripts/discourse/templates/user/billing.hbs
Normal file
27
assets/javascripts/discourse/templates/user/billing.hbs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
<h3>{{i18n 'discourse_patrons.user.billing.title'}}</h3>
|
||||||
|
|
||||||
|
{{#if model}}
|
||||||
|
<table class="topic-list">
|
||||||
|
<thead>
|
||||||
|
<th>{{i18n 'discourse_patrons.user.billing.invoices.amount'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.user.billing.invoices.number'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.user.billing.invoices.created_at'}}</th>
|
||||||
|
<th></th>
|
||||||
|
</thead>
|
||||||
|
{{#each model as |invoice|}}
|
||||||
|
<tr>
|
||||||
|
<td>{{invoice.amount_paid}}</td>
|
||||||
|
<td>{{invoice.number}}</td>
|
||||||
|
<td>{{format-unix-date invoice.created}}</td>
|
||||||
|
<td class="td-right">
|
||||||
|
<a href="{{invoice.invoice_pdf}}" class="btn btn-icon">
|
||||||
|
{{d-icon "download"}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<p>{{i18n 'discourse_patrons.user.billing_help'}}</p>
|
||||||
|
{{/if}}
|
27
assets/javascripts/discourse/templates/user/invoices.hbs
Normal file
27
assets/javascripts/discourse/templates/user/invoices.hbs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
<h3>{{i18n 'discourse_patrons.user.billing.title'}}</h3>
|
||||||
|
|
||||||
|
{{#if model}}
|
||||||
|
<table class="topic-list">
|
||||||
|
<thead>
|
||||||
|
<th>{{i18n 'discourse_patrons.user.billing.invoices.amount'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.user.billing.invoices.number'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.user.billing.invoices.created_at'}}</th>
|
||||||
|
<th></th>
|
||||||
|
</thead>
|
||||||
|
{{#each model as |invoice|}}
|
||||||
|
<tr>
|
||||||
|
<td>{{invoice.amount_paid}}</td>
|
||||||
|
<td>{{invoice.number}}</td>
|
||||||
|
<td>{{format-unix-date invoice.created}}</td>
|
||||||
|
<td class="td-right">
|
||||||
|
<a href="{{invoice.invoice_pdf}}" class="btn btn-icon">
|
||||||
|
{{d-icon "download"}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<p>{{i18n 'discourse_patrons.user.billing_help'}}</p>
|
||||||
|
{{/if}}
|
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
{{i18n 'discourse_patrons.user.subscriptions.title'}}
|
||||||
|
|
||||||
|
{{#if model}}
|
||||||
|
<table class="table discourse-patrons-user-table">
|
||||||
|
<thead>
|
||||||
|
<th>{{i18n 'discourse_patrons.user.subscriptions.id'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.user.plans.rate'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.user.subscriptions.status'}}</th>
|
||||||
|
<th>{{i18n 'discourse_patrons.user.subscriptions.created_at'}}</th>
|
||||||
|
<th></th>
|
||||||
|
</thead>
|
||||||
|
{{#each model as |subscription|}}
|
||||||
|
<tr>
|
||||||
|
<td>{{subscription.id}}</td>
|
||||||
|
<td>{{subscription.plan.subscriptionRate}}</td>
|
||||||
|
<td>{{subscription.status}}</td>
|
||||||
|
<td>{{format-unix-date subscription.created}}</td>
|
||||||
|
<td class="td-right">{{d-button disabled=subscription.canceled label="cancel" action=(route-action "cancelSubscription" subscription) icon="times"}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<p>{{i18n 'discourse_patrons.user.subscriptions_help'}}</p>
|
||||||
|
{{/if}}
|
33
assets/stylesheets/common/discourse-patrons-layout.scss
Normal file
33
assets/stylesheets/common/discourse-patrons-layout.scss
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
.discourse-patrons-section-columns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
@include breakpoint(medium) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-column {
|
||||||
|
min-width: calc(50% - 0.5em);
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include breakpoint(medium) {
|
||||||
|
min-width: 100%;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,40 +1,30 @@
|
|||||||
.discourse-patrons-section-columns {
|
// TODO: This gets overridden somewhere. It is defined in common/base/discourse.scss
|
||||||
display: flex;
|
input[disabled],
|
||||||
justify-content: space-between;
|
input[readonly],
|
||||||
|
select[disabled],
|
||||||
|
select[readonly],
|
||||||
|
textarea[disabled],
|
||||||
|
textarea[readonly] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
border-color: #e9e9e9;
|
||||||
|
}
|
||||||
|
|
||||||
@include breakpoint(medium) {
|
#discourse-patrons-admin {
|
||||||
flex-direction: column;
|
.btn-right {
|
||||||
}
|
text-align: right;
|
||||||
|
|
||||||
.section-column {
|
|
||||||
min-width: calc(50% - 0.5em);
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include breakpoint(medium) {
|
|
||||||
min-width: 100%;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.discourse-patrons-admin {
|
.td-right {
|
||||||
.amount {
|
text-align: right;
|
||||||
text-align: right;
|
}
|
||||||
|
|
||||||
|
table.discourse-patrons-user-table {
|
||||||
|
width: 100%;
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +33,10 @@
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.discourse-patrons-admin-textarea {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
#stripe-elements {
|
#stripe-elements {
|
||||||
border: 1px $primary-low-mid solid;
|
border: 1px $primary-low-mid solid;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
.donations-category-header .donations-category-metadata {
|
|
||||||
flex-flow: wrap;
|
|
||||||
padding: 0 10px;
|
|
||||||
|
|
||||||
div {
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +1,62 @@
|
|||||||
en:
|
en:
|
||||||
site_settings:
|
site_settings:
|
||||||
discourse_patrons_enabled: "Enable the Discourse Patrons plugin."
|
discourse_patrons_enabled: Enable the Discourse Patrons plugin.
|
||||||
discourse_patrons_secret_key: "Stripe Secret Key"
|
discourse_patrons_extra_nav_subscribe: Show the subscribe button in the primary navigation
|
||||||
discourse_patrons_public_key: "Stripe Public Key"
|
discourse_patrons_secret_key: Stripe Secret Key
|
||||||
discourse_patrons_currency: "Currency Code"
|
discourse_patrons_public_key: Stripe Public Key
|
||||||
|
discourse_patrons_currency: Default Currency Code. This can be overridden when creating a subscription plan
|
||||||
discourse_patrons_zip_code: "Show Zip Code"
|
discourse_patrons_zip_code: "Show Zip Code"
|
||||||
discourse_patrons_billing_address: "Collect billing address"
|
discourse_patrons_billing_address: "Collect billing address"
|
||||||
discourse_patrons_payment_page: "Text to be added to enter payments page. Markdown is supported."
|
discourse_patrons_payment_page: "Text to be added to enter payments page. Markdown is supported."
|
||||||
discourse_patrons_success_page: "Text to be added to success page. Markdown is supported."
|
discourse_patrons_success_page: "Text to be added to success page. Markdown is supported."
|
||||||
discourse_patrons_payment_description: "This is sent to Stripe and shows in the payment information"
|
discourse_patrons_payment_description: "This is sent to Stripe and shows in the payment information"
|
||||||
|
discourse_patrons_amounts: "Payment amounts a user can select"
|
||||||
errors:
|
errors:
|
||||||
discourse_patrons_amount_must_be_currency: "Currency amounts must be currencies without dollar symbol (eg 1.50)"
|
discourse_patrons_amount_must_be_currency: "Currency amounts must be currencies without dollar symbol (eg 1.50)"
|
||||||
js:
|
js:
|
||||||
discourse_patrons:
|
discourse_patrons:
|
||||||
title: Discourse Patrons
|
title: Discourse Patrons
|
||||||
nav_item: Payment
|
optional: Optional
|
||||||
heading:
|
transactions:
|
||||||
payment: Make a Payment
|
payment:
|
||||||
success: Thank you!
|
success: Your payment was successful
|
||||||
payment:
|
navigation:
|
||||||
optional: Optional
|
subscriptions: Subscriptions
|
||||||
receipt_info: A receipt is sent to this email address
|
subscribe: Subscribe
|
||||||
your_information: Your information
|
billing: Billing
|
||||||
payment_information: Payment information
|
user:
|
||||||
payment_confirmation: Confirm information
|
plans:
|
||||||
amount: Amount
|
rate: Rate
|
||||||
payment_intent_id: Payment ID
|
subscriptions_help: You have no subscriptions.
|
||||||
|
subscriptions:
|
||||||
|
title: Subscriptions
|
||||||
|
id: Subscription ID
|
||||||
|
status: Status
|
||||||
|
created_at: Created
|
||||||
|
operations:
|
||||||
|
destroy:
|
||||||
|
confirm: Are you sure you want to cancel this subscription?
|
||||||
|
subscribe:
|
||||||
|
title: Subscribe
|
||||||
|
card:
|
||||||
|
title: Payment
|
||||||
|
customer:
|
||||||
|
title: Customer Details
|
||||||
|
empty: We couldn't find a customer identifier in our system. A new one will be created for you.
|
||||||
|
buttons:
|
||||||
|
subscribe: Subscribe
|
||||||
|
one_time:
|
||||||
|
heading:
|
||||||
|
payment: Make a Payment
|
||||||
|
success: Thank you!
|
||||||
|
payment:
|
||||||
|
optional: Optional
|
||||||
|
receipt_info: A receipt is sent to this email address
|
||||||
|
your_information: Your information
|
||||||
|
payment_information: Payment information
|
||||||
|
payment_confirmation: Confirm information
|
||||||
|
amount: Amount
|
||||||
|
payment_intent_id: Payment ID
|
||||||
billing:
|
billing:
|
||||||
name: Full name
|
name: Full name
|
||||||
email: Email
|
email: Email
|
||||||
@ -38,10 +69,62 @@ en:
|
|||||||
confirm_payment: Confirm payment
|
confirm_payment: Confirm payment
|
||||||
success: Go back
|
success: Go back
|
||||||
admin:
|
admin:
|
||||||
payment_history:
|
dashboard:
|
||||||
|
title: Dashboard
|
||||||
table:
|
table:
|
||||||
head:
|
head:
|
||||||
user: User
|
user: User
|
||||||
payment_intent: Payment ID
|
payment_intent: Payment ID
|
||||||
receipt_email: Receipt Email
|
receipt_email: Receipt Email
|
||||||
amount: Amount
|
amount: Amount
|
||||||
|
products:
|
||||||
|
title: Products
|
||||||
|
operations:
|
||||||
|
create: Create New Product
|
||||||
|
update: Update Product
|
||||||
|
new: New Product
|
||||||
|
destroy:
|
||||||
|
confirm: Are you sure you want to destroy this product?
|
||||||
|
product:
|
||||||
|
product_id: Product ID
|
||||||
|
name: Product Name
|
||||||
|
statement_descriptor: Statement Descriptor
|
||||||
|
statement_descriptor_help: Extra information about a product which will appear on your customer’s credit card statement.
|
||||||
|
plan_help: Create a pricing plan to subscribe customers to this product
|
||||||
|
description: Description
|
||||||
|
description_help: This describes your subscription product.
|
||||||
|
active: Active
|
||||||
|
created_at: Created
|
||||||
|
updated_at: Updated
|
||||||
|
product_help: Before cutomers can subscribe to your site, you need to create at least one product and an associated plan.
|
||||||
|
plans:
|
||||||
|
title: Pricing Plans
|
||||||
|
operations:
|
||||||
|
add: Add New Plan
|
||||||
|
create: Create Plan
|
||||||
|
update: Update Plan
|
||||||
|
create_help: Once a pricing plan is created, only its nickname, trial period and user group can be updated.
|
||||||
|
new: New Plan
|
||||||
|
destroy:
|
||||||
|
confirm: Are you sure you want to destroy this plan?
|
||||||
|
plan:
|
||||||
|
nickname: Plan Nickname
|
||||||
|
nickname_help: This won't be visible to customers, but will help you find this plan later
|
||||||
|
plan_id: Plan ID
|
||||||
|
product: Product
|
||||||
|
interval: Billing Interval
|
||||||
|
amount: Amount
|
||||||
|
trial: Trial Period Days
|
||||||
|
trial_help: Subscriptions to this plan will automatically start with a free trial of this length
|
||||||
|
group: User Group
|
||||||
|
group_help: This is the discourse user group the customer gets added to when the subscription is created.
|
||||||
|
active: Active
|
||||||
|
created_at: Created
|
||||||
|
subscriptions:
|
||||||
|
title: Subscriptions
|
||||||
|
subscription:
|
||||||
|
subscription_id: Subscription ID
|
||||||
|
customer: Customer
|
||||||
|
plan: Plan
|
||||||
|
status: Status
|
||||||
|
created_at: Created
|
||||||
|
@ -1,9 +1,29 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
DiscoursePatrons::Engine.routes.draw do
|
DiscoursePatrons::Engine.routes.draw do
|
||||||
get '/admin' => 'admin#index'
|
# TODO: namespace this
|
||||||
get '/' => 'patrons#index'
|
scope 'admin' do
|
||||||
get '/:pid' => 'patrons#show'
|
get '/' => 'admin#index'
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace :admin do
|
||||||
|
resources :plans
|
||||||
|
resources :subscriptions, only: [:index]
|
||||||
|
resources :products
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace :user do
|
||||||
|
resources :subscriptions, only: [:index, :destroy]
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :customers, only: [:create]
|
||||||
|
resources :invoices, only: [:index]
|
||||||
resources :patrons, only: [:index, :create]
|
resources :patrons, only: [:index, :create]
|
||||||
|
resources :plans, only: [:index]
|
||||||
|
resources :products, only: [:index, :show]
|
||||||
|
resources :subscriptions, only: [:create]
|
||||||
|
|
||||||
|
get '/' => 'patrons#index'
|
||||||
|
get '/subscribe' => 'patrons#index'
|
||||||
|
get '/subscribe/:id' => 'patrons#index'
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
plugins:
|
plugins:
|
||||||
discourse_patrons_enabled:
|
discourse_patrons_enabled:
|
||||||
default: false
|
default: false
|
||||||
|
discourse_patrons_extra_nav_subscribe:
|
||||||
|
default: false
|
||||||
|
client: true
|
||||||
discourse_patrons_public_key:
|
discourse_patrons_public_key:
|
||||||
default: ''
|
default: ''
|
||||||
client: true
|
client: true
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class CreatePaymentsTable < ActiveRecord::Migration[5.2]
|
|
||||||
def change
|
|
||||||
create_table :payments do |t|
|
|
||||||
t.string :payment_intent_id, null: false
|
|
||||||
t.string :receipt_email, null: false
|
|
||||||
t.string :currency, null: false
|
|
||||||
t.string :url, null: false
|
|
||||||
t.integer :amount, null: false
|
|
||||||
t.references :user, foreign_key: true
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
|
|
||||||
add_index :payments, :payment_intent_id, unique: true
|
|
||||||
end
|
|
||||||
end
|
|
13
db/migrate/20191025031631_create_customers.rb
Normal file
13
db/migrate/20191025031631_create_customers.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateCustomers < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :discourse_patrons_customers do |t|
|
||||||
|
t.string :customer_id, null: false
|
||||||
|
t.references :user, foreign_key: true
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :discourse_patrons_customers, :customer_id, unique: true
|
||||||
|
end
|
||||||
|
end
|
32
plugin.rb
32
plugin.rb
@ -1,8 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# name: discourse-patrons
|
# name: discourse-patrons
|
||||||
# about: Integrates Stripe into Discourse to allow visitors to make payments
|
# about: Integrates Stripe into Discourse to allow visitors to make payments and Subscribe
|
||||||
# version: 1.3.1
|
# version: 2.0.0
|
||||||
# url: https://github.com/rimian/discourse-patrons
|
# url: https://github.com/rimian/discourse-patrons
|
||||||
# authors: Rimian Perkins
|
# authors: Rimian Perkins
|
||||||
|
|
||||||
@ -11,7 +11,9 @@ enabled_site_setting :discourse_patrons_enabled
|
|||||||
gem 'stripe', '5.8.0'
|
gem 'stripe', '5.8.0'
|
||||||
|
|
||||||
register_asset "stylesheets/common/discourse-patrons.scss"
|
register_asset "stylesheets/common/discourse-patrons.scss"
|
||||||
|
register_asset "stylesheets/common/discourse-patrons-layout.scss"
|
||||||
register_asset "stylesheets/mobile/discourse-patrons.scss"
|
register_asset "stylesheets/mobile/discourse-patrons.scss"
|
||||||
|
register_svg_icon "credit-card" if respond_to?(:register_svg_icon)
|
||||||
|
|
||||||
register_html_builder('server:before-head-close') do
|
register_html_builder('server:before-head-close') do
|
||||||
"<script src='https://js.stripe.com/v3/'></script>"
|
"<script src='https://js.stripe.com/v3/'></script>"
|
||||||
@ -21,22 +23,42 @@ extend_content_security_policy(
|
|||||||
script_src: ['https://js.stripe.com/v3/']
|
script_src: ['https://js.stripe.com/v3/']
|
||||||
)
|
)
|
||||||
|
|
||||||
add_admin_route 'discourse_patrons.title', 'discourse-patrons'
|
add_admin_route 'discourse_patrons.title', 'discourse-patrons.products'
|
||||||
|
|
||||||
Discourse::Application.routes.append do
|
Discourse::Application.routes.append do
|
||||||
get '/admin/plugins/discourse-patrons' => 'admin/plugins#index'
|
get '/admin/plugins/discourse-patrons' => 'admin/plugins#index'
|
||||||
|
get '/admin/plugins/discourse-patrons/dashboard' => 'admin/plugins#index'
|
||||||
|
get '/admin/plugins/discourse-patrons/products' => 'admin/plugins#index'
|
||||||
|
get '/admin/plugins/discourse-patrons/products/:product_id' => 'admin/plugins#index'
|
||||||
|
get '/admin/plugins/discourse-patrons/products/:product_id/plans' => 'admin/plugins#index'
|
||||||
|
get '/admin/plugins/discourse-patrons/products/:product_id/plans/:plan_id' => 'admin/plugins#index'
|
||||||
|
get '/admin/plugins/discourse-patrons/subscriptions' => 'admin/plugins#index'
|
||||||
|
get '/admin/plugins/discourse-patrons/plans' => 'admin/plugins#index'
|
||||||
|
get '/admin/plugins/discourse-patrons/plans/:plan_id' => 'admin/plugins#index'
|
||||||
|
get 'u/:username/billing' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT }
|
||||||
|
get 'u/:username/subscriptions' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT }
|
||||||
end
|
end
|
||||||
|
|
||||||
after_initialize do
|
after_initialize do
|
||||||
::Stripe.api_version = "2019-11-05"
|
::Stripe.api_version = "2019-11-05"
|
||||||
::Stripe.set_app_info('Discourse Patrons', version: '1.3.1', url: 'https://github.com/rimian/discourse-patrons')
|
::Stripe.set_app_info('Discourse Patrons', version: '2.0.0', url: 'https://github.com/rimian/discourse-patrons')
|
||||||
|
|
||||||
[
|
[
|
||||||
"../lib/discourse_patrons/engine",
|
"../lib/discourse_patrons/engine",
|
||||||
"../config/routes",
|
"../config/routes",
|
||||||
|
"../app/controllers/concerns/stripe",
|
||||||
"../app/controllers/admin_controller",
|
"../app/controllers/admin_controller",
|
||||||
|
"../app/controllers/admin/plans_controller",
|
||||||
|
"../app/controllers/admin/products_controller",
|
||||||
|
"../app/controllers/admin/subscriptions_controller",
|
||||||
|
"../app/controllers/user/subscriptions_controller",
|
||||||
|
"../app/controllers/customers_controller",
|
||||||
|
"../app/controllers/invoices_controller",
|
||||||
"../app/controllers/patrons_controller",
|
"../app/controllers/patrons_controller",
|
||||||
"../app/models/payment",
|
"../app/controllers/plans_controller",
|
||||||
|
"../app/controllers/products_controller",
|
||||||
|
"../app/controllers/subscriptions_controller",
|
||||||
|
"../app/models/customer",
|
||||||
"../app/serializers/payment_serializer",
|
"../app/serializers/payment_serializer",
|
||||||
].each { |path| require File.expand_path(path, __FILE__) }
|
].each { |path| require File.expand_path(path, __FILE__) }
|
||||||
|
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
module DiscoursePatrons
|
|
||||||
RSpec.describe AdminController, type: :controller do
|
|
||||||
routes { DiscoursePatrons::Engine.routes }
|
|
||||||
|
|
||||||
it 'is a subclass of AdminController' do
|
|
||||||
expect(DiscoursePatrons::AdminController < Admin::AdminController).to eq(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: authenticate to test these
|
|
||||||
it "is ascending"
|
|
||||||
it "is has ordered by"
|
|
||||||
end
|
|
||||||
end
|
|
@ -27,54 +27,6 @@ module DiscoursePatrons
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'show' do
|
|
||||||
let!(:admin) { Fabricate(:admin) }
|
|
||||||
let!(:user) { Fabricate(:user) }
|
|
||||||
let(:payment_intent) { { customer: user.id } }
|
|
||||||
|
|
||||||
before do
|
|
||||||
controller.stubs(:current_user).returns(user)
|
|
||||||
::Stripe::PaymentIntent.stubs(:retrieve).returns(payment_intent)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'responds ok' do
|
|
||||||
get :show, params: { pid: '123' }, format: :json
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'requests the payment intent' do
|
|
||||||
::Stripe::PaymentIntent.expects(:retrieve).with('abc-1234').returns(payment_intent)
|
|
||||||
get :show, params: { pid: 'abc-1234' }, format: :json
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows admin to see receipts' do
|
|
||||||
controller.expects(:current_user).returns(admin)
|
|
||||||
::Stripe::PaymentIntent.expects(:retrieve).returns(metadata: { user_id: user.id })
|
|
||||||
get :show, params: { pid: '123' }, format: :json
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not allow another the user to see receipts' do
|
|
||||||
::Stripe::PaymentIntent.expects(:retrieve).returns(metadata: { user_id: 9999 })
|
|
||||||
get :show, params: { pid: '123' }, format: :json
|
|
||||||
|
|
||||||
aggregate_failures do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
expect(JSON.parse(response.body)).to eq("error" => "Not found")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not allow anon user to see receipts' do
|
|
||||||
controller.stubs(:current_user).returns(nil)
|
|
||||||
get :show, params: { pid: '123' }, format: :json
|
|
||||||
|
|
||||||
aggregate_failures do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
expect(JSON.parse(response.body)).to eq("error" => "Not found")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'create' do
|
describe 'create' do
|
||||||
let!(:current_user) { Fabricate(:user) }
|
let!(:current_user) { Fabricate(:user) }
|
||||||
|
|
||||||
@ -101,14 +53,6 @@ module DiscoursePatrons
|
|||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a payment' do
|
|
||||||
::Stripe::PaymentIntent.expects(:create).returns(payment)
|
|
||||||
|
|
||||||
expect {
|
|
||||||
post :create, params: { receipt_email: 'hello@example.com', amount: '20.00' }, format: :json
|
|
||||||
}.to change { Payment.count }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'has no user' do
|
it 'has no user' do
|
||||||
controller.stubs(:current_user).returns(nil)
|
controller.stubs(:current_user).returns(nil)
|
||||||
::Stripe::PaymentIntent.expects(:create).returns(payment)
|
::Stripe::PaymentIntent.expects(:create).returns(payment)
|
||||||
|
26
spec/models/customer_spec.rb
Normal file
26
spec/models/customer_spec.rb
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
RSpec.describe Customer do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:stripe_customer) { { id: 'cus_id4567' } }
|
||||||
|
|
||||||
|
it "has a table name" do
|
||||||
|
expect(described_class.table_name).to eq "discourse_patrons_customers"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates" do
|
||||||
|
customer = described_class.create_customer(user, stripe_customer)
|
||||||
|
expect(customer.customer_id).to eq 'cus_id4567'
|
||||||
|
expect(customer.user_id).to eq user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
it "has a user scope" do
|
||||||
|
described_class.create_customer(user, stripe_customer)
|
||||||
|
customer = described_class.find_user(user)
|
||||||
|
expect(customer.customer_id).to eq 'cus_id4567'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
150
spec/requests/admin/plans_controller_spec.rb
Normal file
150
spec/requests/admin/plans_controller_spec.rb
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
module Admin
|
||||||
|
RSpec.describe PlansController do
|
||||||
|
it 'is a subclass of AdminController' do
|
||||||
|
expect(DiscoursePatrons::Admin::PlansController < ::Admin::AdminController).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'not authenticated' do
|
||||||
|
describe "index" do
|
||||||
|
it "does not get the plans" do
|
||||||
|
::Stripe::Plan.expects(:list).never
|
||||||
|
get "/patrons/admin/plans.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "not ok" do
|
||||||
|
get "/patrons/admin/plans.json"
|
||||||
|
expect(response.status).to eq 403
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "create" do
|
||||||
|
it "does not create a plan" do
|
||||||
|
::Stripe::Plan.expects(:create).never
|
||||||
|
post "/patrons/admin/plans.json", params: { name: 'Rick Astley', amount: 1, interval: 'week' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is not ok" do
|
||||||
|
post "/patrons/admin/plans.json", params: { name: 'Rick Astley', amount: 1, interval: 'week' }
|
||||||
|
expect(response.status).to eq 403
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "show" do
|
||||||
|
it "does not show the plan" do
|
||||||
|
::Stripe::Plan.expects(:retrieve).never
|
||||||
|
get "/patrons/admin/plans/plan_12345.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is not ok" do
|
||||||
|
get "/patrons/admin/plans/plan_12345.json"
|
||||||
|
expect(response.status).to eq 403
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update" do
|
||||||
|
it "does not update a plan" do
|
||||||
|
::Stripe::Plan.expects(:update).never
|
||||||
|
delete "/patrons/admin/plans/plan_12345.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is not ok" do
|
||||||
|
delete "/patrons/admin/plans/plan_12345.json"
|
||||||
|
expect(response.status).to eq 403
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "delete" do
|
||||||
|
it "does not delete a plan" do
|
||||||
|
::Stripe::Plan.expects(:delete).never
|
||||||
|
patch "/patrons/admin/plans/plan_12345.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is not ok" do
|
||||||
|
patch "/patrons/admin/plans/plan_12345.json"
|
||||||
|
expect(response.status).to eq 403
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'authenticated' do
|
||||||
|
let(:admin) { Fabricate(:admin) }
|
||||||
|
|
||||||
|
before { sign_in(admin) }
|
||||||
|
|
||||||
|
describe "index" do
|
||||||
|
it "lists the plans" do
|
||||||
|
::Stripe::Plan.expects(:list).with(nil)
|
||||||
|
get "/patrons/admin/plans.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "lists the plans for the product" do
|
||||||
|
::Stripe::Plan.expects(:list).with(product: 'prod_id123')
|
||||||
|
get "/patrons/admin/plans.json", params: { product_id: 'prod_id123' }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "create" do
|
||||||
|
it "creates a plan with a nickname" do
|
||||||
|
::Stripe::Plan.expects(:create).with(has_entry(:nickname, 'Veg'))
|
||||||
|
post "/patrons/admin/plans.json", params: { nickname: 'Veg', metadata: { group_name: '' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a plan with a currency" do
|
||||||
|
SiteSetting.stubs(:discourse_patrons_currency).returns('aud')
|
||||||
|
::Stripe::Plan.expects(:create).with(has_entry(:currency, 'aud'))
|
||||||
|
post "/patrons/admin/plans.json", params: { metadata: { group_name: '' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a plan with an interval" do
|
||||||
|
::Stripe::Plan.expects(:create).with(has_entry(:interval, 'week'))
|
||||||
|
post "/patrons/admin/plans.json", params: { interval: 'week', metadata: { group_name: '' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a plan with an amount" do
|
||||||
|
::Stripe::Plan.expects(:create).with(has_entry(:amount, '102'))
|
||||||
|
post "/patrons/admin/plans.json", params: { amount: '102', metadata: { group_name: '' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a plan with a trial period" do
|
||||||
|
::Stripe::Plan.expects(:create).with(has_entry(:trial_period_days, '14'))
|
||||||
|
post "/patrons/admin/plans.json", params: { trial_period_days: '14', metadata: { group_name: '' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a plan with a product" do
|
||||||
|
::Stripe::Plan.expects(:create).with(has_entry(product: 'prod_walterwhite'))
|
||||||
|
post "/patrons/admin/plans.json", params: { product: 'prod_walterwhite', metadata: { group_name: '' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a plan with an active status" do
|
||||||
|
::Stripe::Plan.expects(:create).with(has_entry(:active, 'false'))
|
||||||
|
post "/patrons/admin/plans.json", params: { active: 'false', metadata: { group_name: '' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a metadata' do
|
||||||
|
::Stripe::Plan.expects(:create).with(has_entry(metadata: { group_name: 'discourse-user-group-name' }))
|
||||||
|
post "/patrons/admin/plans.json", params: { metadata: { group_name: 'discourse-user-group-name' } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update" do
|
||||||
|
it "updates a plan" do
|
||||||
|
::Stripe::Plan.expects(:update)
|
||||||
|
patch "/patrons/admin/plans/plan_12345.json", params: { metadata: { group_name: 'discourse-user-group-name' } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "delete" do
|
||||||
|
it "deletes a plan" do
|
||||||
|
::Stripe::Plan.expects(:delete).with('plan_12345')
|
||||||
|
delete "/patrons/admin/plans/plan_12345.json"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
111
spec/requests/admin/products_controller_spec.rb
Normal file
111
spec/requests/admin/products_controller_spec.rb
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
module Admin
|
||||||
|
RSpec.describe ProductsController do
|
||||||
|
it 'is a subclass of AdminController' do
|
||||||
|
expect(DiscoursePatrons::Admin::ProductsController < ::Admin::AdminController).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'unauthenticated' do
|
||||||
|
it "does not list the products" do
|
||||||
|
::Stripe::Product.expects(:list).never
|
||||||
|
get "/patrons/admin/products.json"
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not create the product" do
|
||||||
|
::Stripe::Product.expects(:create).never
|
||||||
|
post "/patrons/admin/products.json"
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not show the product" do
|
||||||
|
::Stripe::Product.expects(:retrieve).never
|
||||||
|
get "/patrons/admin/products/prod_qwerty123.json"
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not update the product" do
|
||||||
|
::Stripe::Product.expects(:update).never
|
||||||
|
put "/patrons/admin/products/prod_qwerty123.json"
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not delete the product" do
|
||||||
|
::Stripe::Product.expects(:delete).never
|
||||||
|
delete "/patrons/admin/products/u2.json"
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'authenticated' do
|
||||||
|
let(:admin) { Fabricate(:admin) }
|
||||||
|
|
||||||
|
before { sign_in(admin) }
|
||||||
|
|
||||||
|
describe 'index' do
|
||||||
|
it "gets the empty products" do
|
||||||
|
::Stripe::Product.expects(:list)
|
||||||
|
get "/patrons/admin/products.json"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'create' do
|
||||||
|
it 'is of product type service' do
|
||||||
|
::Stripe::Product.expects(:create).with(has_entry(:type, 'service'))
|
||||||
|
post "/patrons/admin/products.json", params: {}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a name' do
|
||||||
|
::Stripe::Product.expects(:create).with(has_entry(:name, 'Jesse Pinkman'))
|
||||||
|
post "/patrons/admin/products.json", params: { name: 'Jesse Pinkman' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has an active attribute' do
|
||||||
|
::Stripe::Product.expects(:create).with(has_entry(active: 'false'))
|
||||||
|
post "/patrons/admin/products.json", params: { active: 'false' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a statement descriptor' do
|
||||||
|
::Stripe::Product.expects(:create).with(has_entry(statement_descriptor: 'Blessed are the cheesemakers'))
|
||||||
|
post "/patrons/admin/products.json", params: { statement_descriptor: 'Blessed are the cheesemakers' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has no statement descriptor if empty' do
|
||||||
|
::Stripe::Product.expects(:create).with(has_key(:statement_descriptor)).never
|
||||||
|
post "/patrons/admin/products.json", params: { statement_descriptor: '' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a description' do
|
||||||
|
::Stripe::Product.expects(:create).with(has_entry(metadata: { description: 'Oi, I think he just said bless be all the bignoses!' }))
|
||||||
|
post "/patrons/admin/products.json", params: { metadata: { description: 'Oi, I think he just said bless be all the bignoses!' } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'show' do
|
||||||
|
it 'retrieves the product' do
|
||||||
|
::Stripe::Product.expects(:retrieve).with('prod_walterwhite')
|
||||||
|
get "/patrons/admin/products/prod_walterwhite.json"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'update' do
|
||||||
|
it 'updates the product' do
|
||||||
|
::Stripe::Product.expects(:update)
|
||||||
|
patch "/patrons/admin/products/prod_walterwhite.json", params: {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'delete' do
|
||||||
|
it 'deletes the product' do
|
||||||
|
::Stripe::Product.expects(:delete).with('prod_walterwhite')
|
||||||
|
delete "/patrons/admin/products/prod_walterwhite.json"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
spec/requests/admin/subscriptions_controller_spec.rb
Normal file
31
spec/requests/admin/subscriptions_controller_spec.rb
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
RSpec.describe Admin::SubscriptionsController do
|
||||||
|
it 'is a subclass of AdminController' do
|
||||||
|
expect(DiscoursePatrons::Admin::SubscriptionsController < ::Admin::AdminController).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'unauthenticated' do
|
||||||
|
it "does nothing" do
|
||||||
|
::Stripe::Subscription.expects(:list).never
|
||||||
|
get "/patrons/admin/subscriptions.json"
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'authenticated' do
|
||||||
|
let(:admin) { Fabricate(:admin) }
|
||||||
|
|
||||||
|
before { sign_in(admin) }
|
||||||
|
|
||||||
|
it "gets the empty subscriptions" do
|
||||||
|
::Stripe::Subscription.expects(:list)
|
||||||
|
get "/patrons/admin/subscriptions.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
spec/requests/admin_controller_spec.rb
Normal file
21
spec/requests/admin_controller_spec.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
RSpec.describe AdminController do
|
||||||
|
|
||||||
|
let(:admin) { Fabricate(:admin) }
|
||||||
|
|
||||||
|
before { sign_in(admin) }
|
||||||
|
|
||||||
|
it 'is a subclass of AdminController' do
|
||||||
|
expect(DiscoursePatrons::AdminController < ::Admin::AdminController).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is ok" do
|
||||||
|
get "/patrons/admin.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
34
spec/requests/customers_controller_spec.rb
Normal file
34
spec/requests/customers_controller_spec.rb
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
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 "/patrons/customers.json", params: { source: 'tok_interesting' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "saves the customer" do
|
||||||
|
::Stripe::Customer.expects(:create).returns(id: 'cus_id23456')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
post "/patrons/customers.json", params: { source: 'tok_interesting' }
|
||||||
|
}.to change { DiscoursePatrons::Customer.count }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
54
spec/requests/invoices_controller_spec.rb
Normal file
54
spec/requests/invoices_controller_spec.rb
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
RSpec.describe InvoicesController do
|
||||||
|
describe "index" do
|
||||||
|
describe "not authenticated" do
|
||||||
|
it "does not list the invoices" do
|
||||||
|
::Stripe::Invoice.expects(:list).never
|
||||||
|
get "/patrons/invoices.json"
|
||||||
|
expect(response.status).to eq 403
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "authenticated" do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:stripe_customer) { { id: 'cus_id4567' } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "other user invoices" do
|
||||||
|
it "does not list the invoices" do
|
||||||
|
::Stripe::Invoice.expects(:list).never
|
||||||
|
get "/patrons/invoices.json", params: { user_id: 999999 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "own invoices" do
|
||||||
|
context "stripe customer does not exist" do
|
||||||
|
it "lists empty" do
|
||||||
|
::Stripe::Invoice.expects(:list).never
|
||||||
|
get "/patrons/invoices.json", params: { user_id: user.id }
|
||||||
|
expect(response.body).to eq "[]"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "stripe customer exists" do
|
||||||
|
before do
|
||||||
|
DiscoursePatrons::Customer.create_customer(user, stripe_customer)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "lists the invoices" do
|
||||||
|
::Stripe::Invoice.expects(:list).with(customer: 'cus_id4567')
|
||||||
|
get "/patrons/invoices.json", params: { user_id: user.id }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
37
spec/requests/plans_controller_spec.rb
Normal file
37
spec/requests/plans_controller_spec.rb
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
RSpec.describe PlansController do
|
||||||
|
describe "index" do
|
||||||
|
it "lists the active plans" do
|
||||||
|
::Stripe::Plan.expects(:list).with(active: true)
|
||||||
|
get "/patrons/plans.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "lists the active plans for a product" do
|
||||||
|
::Stripe::Plan.expects(:list).with(active: true, product: 'prod_3765')
|
||||||
|
get "/patrons/plans.json", params: { product_id: 'prod_3765' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "orders and serialises the plans" do
|
||||||
|
::Stripe::Plan.expects(:list).returns(
|
||||||
|
data: [
|
||||||
|
{ id: 'plan_id123', amount: 1220, currency: 'aud', interval: 'year', metadata: {} },
|
||||||
|
{ id: 'plan_id234', amount: 1399, currency: 'usd', interval: 'year', metadata: {} },
|
||||||
|
{ id: 'plan_id678', amount: 1000, currency: 'aud', interval: 'week', metadata: {} }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
get "/patrons/plans.json"
|
||||||
|
|
||||||
|
expect(JSON.parse(response.body)).to eq([
|
||||||
|
{ "amount" => 1000, "currency" => "aud", "id" => "plan_id678", "interval" => "week" },
|
||||||
|
{ "amount" => 1220, "currency" => "aud", "id" => "plan_id123", "interval" => "year" },
|
||||||
|
{ "amount" => 1399, "currency" => "usd", "id" => "plan_id234", "interval" => "year" }
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
45
spec/requests/products_controller_spec.rb
Normal file
45
spec/requests/products_controller_spec.rb
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
RSpec.describe ProductsController 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
|
||||||
|
|
||||||
|
describe "index" do
|
||||||
|
it "gets products" do
|
||||||
|
::Stripe::Product.expects(:list).with(active: true).returns(data: [product])
|
||||||
|
|
||||||
|
get "/patrons/products.json"
|
||||||
|
|
||||||
|
expect(JSON.parse(response.body)).to eq([{
|
||||||
|
"id" => "prodct_23456",
|
||||||
|
"name" => "Very Special Product",
|
||||||
|
"description" => "Many people listened to my phone call with the Ukrainian President while it was being made"
|
||||||
|
}])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'show' do
|
||||||
|
it 'retrieves the product' do
|
||||||
|
::Stripe::Product.expects(:retrieve).with('prod_walterwhite').returns(product)
|
||||||
|
get "/patrons/products/prod_walterwhite.json"
|
||||||
|
|
||||||
|
expect(JSON.parse(response.body)).to eq(
|
||||||
|
"id" => "prodct_23456",
|
||||||
|
"name" => "Very Special Product",
|
||||||
|
"description" => "Many people listened to my phone call with the Ukrainian President while it was being made"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
102
spec/requests/subscriptions_controller_spec.rb
Normal file
102
spec/requests/subscriptions_controller_spec.rb
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module DiscoursePatrons
|
||||||
|
RSpec.describe SubscriptionsController do
|
||||||
|
context "not authenticated" do
|
||||||
|
it "does not create a subscription" do
|
||||||
|
::Stripe::Plan.expects(:retrieve).never
|
||||||
|
::Stripe::Subscription.expects(:create).never
|
||||||
|
post "/patrons/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::Plan.expects(:retrieve).returns(metadata: { group_name: 'awesome' })
|
||||||
|
::Stripe::Subscription.expects(:create).with(
|
||||||
|
customer: 'cus_1234',
|
||||||
|
items: [ plan: 'plan_1234' ]
|
||||||
|
)
|
||||||
|
post "/patrons/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a customer model" do
|
||||||
|
::Stripe::Plan.expects(:retrieve).returns(metadata: {})
|
||||||
|
::Stripe::Subscription.expects(:create).returns(status: 'active')
|
||||||
|
|
||||||
|
expect {
|
||||||
|
post "/patrons/subscriptions.json", params: { plan: 'plan_1234', customer: 'cus_1234' }
|
||||||
|
}.to change { DiscoursePatrons::Customer.count }
|
||||||
|
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::Plan.expects(:retrieve).returns(metadata: { group_name: 'admins' })
|
||||||
|
post "/patrons/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::Plan.expects(:retrieve).returns(metadata: { group_name: 'other' })
|
||||||
|
post "/patrons/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::Plan.expects(:retrieve).returns(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 "/patrons/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 "/patrons/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 "/patrons/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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user