DEV: Introduce syntax_tree for ruby formatting (#144)

This commit is contained in:
David Taylor 2022-12-29 12:35:06 +00:00 committed by GitHub
parent cbbc23fd5a
commit 4e1a17c40c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 980 additions and 844 deletions

View File

@ -55,3 +55,12 @@ jobs:
- name: Rubocop - name: Rubocop
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
run: bundle exec rubocop . run: bundle exec rubocop .
- name: Syntax Tree
if: ${{ !cancelled() }}
run: |
if test -f .streerc; then
bundle exec stree check Gemfile $(git ls-files '*.rb') $(git ls-files '*.rake')
else
echo "Stree config not detected for this repository. Skipping."
fi

View File

@ -80,7 +80,7 @@ jobs:
- name: Get yarn cache directory - name: Get yarn cache directory
id: yarn-cache-dir id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache - name: Yarn cache
uses: actions/cache@v3 uses: actions/cache@v3
@ -130,7 +130,7 @@ jobs:
shell: bash shell: bash
run: | run: |
if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/spec -type f -name "*.rb" 2> /dev/null | wc -l) ]; then if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/spec -type f -name "*.rb" 2> /dev/null | wc -l) ]; then
echo "::set-output name=files_exist::true" echo "files_exist=true" >> $GITHUB_OUTPUT
fi fi
- name: Plugin RSpec - name: Plugin RSpec
@ -142,7 +142,7 @@ jobs:
shell: bash shell: bash
run: | run: |
if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/test/javascripts -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then if [ 0 -lt $(find plugins/${{ github.event.repository.name }}/test/javascripts -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then
echo "::set-output name=files_exist::true" echo "files_exist=true" >> $GITHUB_OUTPUT
fi fi
- name: Plugin QUnit - name: Plugin QUnit

View File

@ -1,5 +1,5 @@
inherit_gem: inherit_gem:
rubocop-discourse: default.yml rubocop-discourse: stree-compat.yml
AllCops: AllCops:
Exclude: Exclude:
- "gems/**/*" - "gems/**/*"

2
.streerc Normal file
View File

@ -0,0 +1,2 @@
--print-width=100
--plugins=plugin/trailing_comma

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
source 'https://rubygems.org' source "https://rubygems.org"
group :development do group :development do
gem 'translations-manager', git: 'https://github.com/discourse/translations-manager.git' gem "translations-manager", git: "https://github.com/discourse/translations-manager.git"
gem 'rubocop-discourse' gem "rubocop-discourse"
gem "syntax_tree"
end end

View File

@ -12,6 +12,7 @@ GEM
parallel (1.22.1) parallel (1.22.1)
parser (3.1.2.1) parser (3.1.2.1)
ast (~> 2.4.1) ast (~> 2.4.1)
prettier_print (1.2.0)
rainbow (3.1.1) rainbow (3.1.1)
regexp_parser (2.6.0) regexp_parser (2.6.0)
rexml (3.2.5) rexml (3.2.5)
@ -33,6 +34,8 @@ GEM
rubocop-rspec (2.13.2) rubocop-rspec (2.13.2)
rubocop (~> 1.33) rubocop (~> 1.33)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
syntax_tree (5.1.0)
prettier_print (>= 1.2.0)
unicode-display_width (2.3.0) unicode-display_width (2.3.0)
PLATFORMS PLATFORMS
@ -40,6 +43,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
rubocop-discourse rubocop-discourse
syntax_tree
translations-manager! translations-manager!
BUNDLED WITH BUNDLED WITH

View File

@ -9,7 +9,8 @@ module DiscourseSubscriptions
end end
def is_stripe_configured? def is_stripe_configured?
SiteSetting.discourse_subscriptions_public_key.present? && SiteSetting.discourse_subscriptions_secret_key.present? SiteSetting.discourse_subscriptions_public_key.present? &&
SiteSetting.discourse_subscriptions_secret_key.present?
end end
end end
end end

View File

@ -18,23 +18,24 @@ module DiscourseSubscriptions
end end
def create def create
params.require([:promo, :discount_type, :discount, :active]) params.require(%i[promo discount_type discount active])
begin begin
coupon_params = { coupon_params = { duration: "forever" }
duration: 'forever',
}
case params[:discount_type] case params[:discount_type]
when 'amount' when "amount"
coupon_params[:amount_off] = params[:discount].to_i * 100 coupon_params[:amount_off] = params[:discount].to_i * 100
coupon_params[:currency] = SiteSetting.discourse_subscriptions_currency coupon_params[:currency] = SiteSetting.discourse_subscriptions_currency
when 'percent' when "percent"
coupon_params[:percent_off] = params[:discount] coupon_params[:percent_off] = params[:discount]
end end
coupon = ::Stripe::Coupon.create(coupon_params) coupon = ::Stripe::Coupon.create(coupon_params)
promo_code = ::Stripe::PromotionCode.create({ coupon: coupon[:id], code: params[:promo] }) if coupon.present? promo_code =
::Stripe::PromotionCode.create(
{ coupon: coupon[:id], code: params[:promo] },
) if coupon.present?
render_json_dump promo_code render_json_dump promo_code
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
@ -43,14 +44,9 @@ module DiscourseSubscriptions
end end
def update def update
params.require([:id, :active]) params.require(%i[id active])
begin begin
promo_code = ::Stripe::PromotionCode.update( promo_code = ::Stripe::PromotionCode.update(params[:id], { active: params[:active] })
params[:id],
{
active: params[:active]
}
)
render_json_dump promo_code render_json_dump promo_code
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e

View File

@ -27,15 +27,11 @@ module DiscourseSubscriptions
active: params[:active], active: params[:active],
metadata: { metadata: {
group_name: params[:metadata][:group_name], group_name: params[:metadata][:group_name],
trial_period_days: params[:trial_period_days] trial_period_days: params[:trial_period_days],
} },
} }
if params[:type] == 'recurring' price_object[:recurring] = { interval: params[:interval] } if params[:type] == "recurring"
price_object[:recurring] = {
interval: params[:interval]
}
end
plan = ::Stripe::Price.create(price_object) plan = ::Stripe::Price.create(price_object)
@ -56,14 +52,16 @@ module DiscourseSubscriptions
end end
interval = nil interval = nil
if plan[:recurring] && plan[:recurring][:interval] interval = plan[:recurring][:interval] if plan[:recurring] && plan[:recurring][:interval]
interval = plan[:recurring][:interval]
end
serialized = plan.to_h.merge(trial_period_days: trial_days, currency: plan[:currency].upcase, interval: interval) serialized =
plan.to_h.merge(
trial_period_days: trial_days,
currency: plan[:currency].upcase,
interval: interval,
)
render_json_dump serialized render_json_dump serialized
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
render_json_error e.message render_json_error e.message
end end
@ -71,14 +69,15 @@ module DiscourseSubscriptions
def update def update
begin begin
plan = ::Stripe::Price.update( plan =
::Stripe::Price.update(
params[:id], params[:id],
nickname: params[:nickname], nickname: params[:nickname],
active: params[:active], active: params[:active],
metadata: { metadata: {
group_name: params[:metadata][:group_name], group_name: params[:metadata][:group_name],
trial_period_days: params[:trial_period_days] trial_period_days: params[:trial_period_days],
} },
) )
render_json_dump plan render_json_dump plan

View File

@ -27,20 +27,15 @@ module DiscourseSubscriptions
def create def create
begin begin
create_params = product_params.merge!(type: 'service') create_params = product_params.merge!(type: "service")
if params[:statement_descriptor].blank? create_params.except!(:statement_descriptor) if params[:statement_descriptor].blank?
create_params.except!(:statement_descriptor)
end
product = ::Stripe::Product.create(create_params) product = ::Stripe::Product.create(create_params)
Product.create( Product.create(external_id: product[:id])
external_id: product[:id]
)
render_json_dump product render_json_dump product
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
render_json_error e.message render_json_error e.message
end end
@ -51,7 +46,6 @@ module DiscourseSubscriptions
product = ::Stripe::Product.retrieve(params[:id]) product = ::Stripe::Product.retrieve(params[:id])
render_json_dump product render_json_dump product
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
render_json_error e.message render_json_error e.message
end end
@ -59,13 +53,9 @@ module DiscourseSubscriptions
def update def update
begin begin
product = ::Stripe::Product.update( product = ::Stripe::Product.update(params[:id], product_params)
params[:id],
product_params
)
render_json_dump product render_json_dump product
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
render_json_error e.message render_json_error e.message
end end
@ -78,7 +68,6 @@ module DiscourseSubscriptions
Product.delete_by(external_id: params[:id]) Product.delete_by(external_id: params[:id])
render_json_dump product render_json_dump product
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
render_json_error e.message render_json_error e.message
end end
@ -95,8 +84,8 @@ module DiscourseSubscriptions
statement_descriptor: params[:statement_descriptor], statement_descriptor: params[:statement_descriptor],
metadata: { metadata: {
description: params.dig(:metadata, :description), description: params.dig(:metadata, :description),
repurchaseable: params.dig(:metadata, :repurchaseable) repurchaseable: params.dig(:metadata, :repurchaseable),
} },
} }
end end
end end

View File

@ -16,20 +16,23 @@ module DiscourseSubscriptions
has_more: false, has_more: false,
data: [], data: [],
length: 0, length: 0,
last_record: params[:last_record] last_record: params[:last_record],
} }
if subscription_ids.present? && is_stripe_configured? if subscription_ids.present? && is_stripe_configured?
while subscriptions[:length] < PAGE_LIMIT while subscriptions[:length] < PAGE_LIMIT
current_set = get_subscriptions(subscriptions[:last_record]) current_set = get_subscriptions(subscriptions[:last_record])
until valid_subscriptions = find_valid_subscriptions(current_set[:data], subscription_ids) do until valid_subscriptions =
find_valid_subscriptions(current_set[:data], subscription_ids)
current_set = get_subscriptions(current_set[:data].last) current_set = get_subscriptions(current_set[:data].last)
break if current_set[:has_more] == false break if current_set[:has_more] == false
end end
subscriptions[:data] = subscriptions[:data].concat(valid_subscriptions.to_a) subscriptions[:data] = subscriptions[:data].concat(valid_subscriptions.to_a)
subscriptions[:last_record] = current_set[:data].last[:id] if current_set[:data].present? subscriptions[:last_record] = current_set[:data].last[:id] if current_set[
:data
].present?
subscriptions[:length] = subscriptions[:data].length subscriptions[:length] = subscriptions[:data].length
subscriptions[:has_more] = current_set[:has_more] subscriptions[:has_more] = current_set[:has_more]
break if subscriptions[:has_more] == false break if subscriptions[:has_more] == false
@ -50,9 +53,10 @@ module DiscourseSubscriptions
refund_subscription(params[:id]) if params[:refund] refund_subscription(params[:id]) if params[:refund]
subscription = ::Stripe::Subscription.delete(params[:id]) subscription = ::Stripe::Subscription.delete(params[:id])
customer = Customer.find_by( customer =
Customer.find_by(
product_id: subscription[:plan][:product], product_id: subscription[:plan][:product],
customer_id: subscription[:customer] customer_id: subscription[:customer],
) )
Subscription.delete_by(external_id: params[:id]) Subscription.delete_by(external_id: params[:id])
@ -65,7 +69,6 @@ module DiscourseSubscriptions
end end
render_json_dump subscription render_json_dump subscription
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
render_json_error e.message render_json_error e.message
end end
@ -74,7 +77,11 @@ module DiscourseSubscriptions
private private
def get_subscriptions(start) def get_subscriptions(start)
::Stripe::Subscription.list(expand: ['data.plan.product'], limit: PAGE_LIMIT, starting_after: start) ::Stripe::Subscription.list(
expand: ["data.plan.product"],
limit: PAGE_LIMIT,
starting_after: start,
)
end end
def find_valid_subscriptions(data, ids) def find_valid_subscriptions(data, ids)
@ -85,11 +92,11 @@ module DiscourseSubscriptions
# this will only refund the most recent subscription payment # this will only refund the most recent subscription payment
def refund_subscription(subscription_id) def refund_subscription(subscription_id)
subscription = ::Stripe::Subscription.retrieve(subscription_id) subscription = ::Stripe::Subscription.retrieve(subscription_id)
invoice = ::Stripe::Invoice.retrieve(subscription[:latest_invoice]) if subscription[:latest_invoice] invoice = ::Stripe::Invoice.retrieve(subscription[:latest_invoice]) if subscription[
:latest_invoice
]
payment_intent = invoice[:payment_intent] if invoice[:payment_intent] payment_intent = invoice[:payment_intent] if invoice[:payment_intent]
refund = ::Stripe::Refund.create({ refund = ::Stripe::Refund.create({ payment_intent: payment_intent })
payment_intent: payment_intent,
})
end end
end end
end end

View File

@ -14,7 +14,7 @@ module DiscourseSubscriptions
def create def create
begin begin
payload = request.body.read payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE'] sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
webhook_secret = SiteSetting.discourse_subscriptions_webhook_secret webhook_secret = SiteSetting.discourse_subscriptions_webhook_secret
event = ::Stripe::Webhook.construct_event(payload, sig_header, webhook_secret) event = ::Stripe::Webhook.construct_event(payload, sig_header, webhook_secret)
@ -25,37 +25,39 @@ module DiscourseSubscriptions
end end
case event[:type] case event[:type]
when 'customer.subscription.created' when "customer.subscription.created"
when 'customer.subscription.updated' when "customer.subscription.updated"
customer = Customer.find_by( customer =
Customer.find_by(
customer_id: event[:data][:object][:customer], customer_id: event[:data][:object][:customer],
product_id: event[:data][:object][:plan][:product], product_id: event[:data][:object][:plan][:product],
) )
return render_json_error 'customer not found' if !customer return render_json_error "customer not found" if !customer
return head 200 if event[:data][:object][:status] != 'complete' return head 200 if event[:data][:object][:status] != "complete"
user = ::User.find_by(id: customer.user_id) user = ::User.find_by(id: customer.user_id)
return render_json_error 'user not found' if !user return render_json_error "user not found" if !user
if group = plan_group(event[:data][:object][:plan]) if group = plan_group(event[:data][:object][:plan])
group.add(user) group.add(user)
end end
when 'customer.subscription.deleted' when "customer.subscription.deleted"
customer = Customer.find_by( customer =
Customer.find_by(
customer_id: event[:data][:object][:customer], customer_id: event[:data][:object][:customer],
product_id: event[:data][:object][:plan][:product], product_id: event[:data][:object][:plan][:product],
) )
return render_json_error 'customer not found' if !customer return render_json_error "customer not found" if !customer
Subscription.find_by( Subscription.find_by(
customer_id: customer.id, customer_id: customer.id,
external_id: event[:data][:object][:id] external_id: event[:data][:object][:id],
)&.destroy! )&.destroy!
user = ::User.find(customer.user_id) user = ::User.find(customer.user_id)
return render_json_error 'user not found' if !user return render_json_error "user not found" if !user
if group = plan_group(event[:data][:object][:plan]) if group = plan_group(event[:data][:object][:plan])
group.remove(user) group.remove(user)

View File

@ -5,7 +5,7 @@ module DiscourseSubscriptions
include DiscourseSubscriptions::Stripe include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group include DiscourseSubscriptions::Group
before_action :set_api_key before_action :set_api_key
requires_login except: [:index, :contributors, :show] requires_login except: %i[index contributors show]
def index def index
begin begin
@ -13,19 +13,12 @@ module DiscourseSubscriptions
products = [] products = []
if product_ids.present? && is_stripe_configured? if product_ids.present? && is_stripe_configured?
response = ::Stripe::Product.list({ response = ::Stripe::Product.list({ ids: product_ids, active: true })
ids: product_ids,
active: true
})
products = response[:data].map do |p|
serialize_product(p)
end
products = response[:data].map { |p| serialize_product(p) }
end end
render_json_dump products render_json_dump products
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
render_json_error e.message render_json_error e.message
end end
@ -36,7 +29,11 @@ module DiscourseSubscriptions
contributor_ids = Set.new contributor_ids = Set.new
campaign_product = SiteSetting.discourse_subscriptions_campaign_product campaign_product = SiteSetting.discourse_subscriptions_campaign_product
campaign_product.present? ? contributor_ids.merge(Customer.where(product_id: campaign_product).last(5).pluck(:user_id)) : contributor_ids.merge(Customer.last(5).pluck(:user_id)) if campaign_product.present?
contributor_ids.merge(Customer.where(product_id: campaign_product).last(5).pluck(:user_id))
else
contributor_ids.merge(Customer.last(5).pluck(:user_id))
end
contributors = ::User.where(id: contributor_ids) contributors = ::User.where(id: contributor_ids)
@ -49,10 +46,7 @@ module DiscourseSubscriptions
product = ::Stripe::Product.retrieve(params[:id]) product = ::Stripe::Product.retrieve(params[:id])
plans = ::Stripe::Price.list(active: true, product: params[:id]) plans = ::Stripe::Price.list(active: true, product: params[:id])
response = { response = { product: serialize_product(product), plans: serialize_plans(plans) }
product: serialize_product(product),
plans: serialize_plans(plans)
}
render_json_dump response render_json_dump response
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
@ -61,7 +55,7 @@ module DiscourseSubscriptions
end end
def create def create
params.require([:source, :plan]) params.require(%i[source plan])
begin begin
customer = find_or_create_customer(params[:source]) customer = find_or_create_customer(params[:source])
plan = ::Stripe::Price.retrieve(params[:plan]) plan = ::Stripe::Price.retrieve(params[:plan])
@ -70,38 +64,46 @@ module DiscourseSubscriptions
promo_code = ::Stripe::PromotionCode.list({ code: params[:promo] }) promo_code = ::Stripe::PromotionCode.list({ code: params[:promo] })
promo_code = promo_code[:data][0] # we assume promo codes have a unique name promo_code = promo_code[:data][0] # we assume promo codes have a unique name
return render_json_error I18n.t("js.discourse_subscriptions.subscribe.invalid_coupon") if promo_code.blank? if promo_code.blank?
return render_json_error I18n.t("js.discourse_subscriptions.subscribe.invalid_coupon")
end
end end
recurring_plan = plan[:type] == 'recurring' recurring_plan = plan[:type] == "recurring"
if recurring_plan if recurring_plan
trial_days = plan[:metadata][:trial_period_days] if plan[:metadata] && plan[:metadata][:trial_period_days] trial_days = plan[:metadata][:trial_period_days] if plan[:metadata] &&
plan[:metadata][:trial_period_days]
promo_code_id = promo_code[:id] if promo_code promo_code_id = promo_code[:id] if promo_code
transaction = ::Stripe::Subscription.create( transaction =
::Stripe::Subscription.create(
customer: customer[:id], customer: customer[:id],
items: [{ price: params[:plan] }], items: [{ price: params[:plan] }],
metadata: metadata_user, metadata: metadata_user,
trial_period_days: trial_days, trial_period_days: trial_days,
promotion_code: promo_code_id promotion_code: promo_code_id,
) )
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[:status] == 'incomplete' payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[
:status
] == "incomplete"
else else
coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] && promo_code[:coupon][:id] coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] &&
invoice_item = ::Stripe::InvoiceItem.create( promo_code[:coupon][:id]
invoice_item =
::Stripe::InvoiceItem.create(
customer: customer[:id], customer: customer[:id],
price: params[:plan], price: params[:plan],
discounts: [{ coupon: coupon_id }] discounts: [{ coupon: coupon_id }],
)
invoice = ::Stripe::Invoice.create(
customer: customer[:id]
) )
invoice = ::Stripe::Invoice.create(customer: customer[:id])
transaction = ::Stripe::Invoice.finalize_invoice(invoice[:id]) transaction = ::Stripe::Invoice.finalize_invoice(invoice[:id])
payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] == 'open' payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] ==
transaction = ::Stripe::Invoice.pay(invoice[:id]) if payment_intent[:status] == 'successful' "open"
transaction = ::Stripe::Invoice.pay(invoice[:id]) if payment_intent[:status] ==
"successful"
end end
finalize_transaction(transaction, plan) if transaction_ok(transaction) finalize_transaction(transaction, plan) if transaction_ok(transaction)
@ -115,7 +117,7 @@ module DiscourseSubscriptions
end end
def finalize def finalize
params.require([:plan, :transaction]) params.require(%i[plan transaction])
begin begin
price = ::Stripe::Price.retrieve(params[:plan]) price = ::Stripe::Price.retrieve(params[:plan])
transaction = retrieve_transaction(params[:transaction]) transaction = retrieve_transaction(params[:transaction])
@ -132,17 +134,15 @@ module DiscourseSubscriptions
group.add(current_user) if group group.add(current_user) if group
customer = Customer.create( customer =
Customer.create(
user_id: current_user.id, user_id: current_user.id,
customer_id: transaction[:customer], customer_id: transaction[:customer],
product_id: plan[:product] product_id: plan[:product],
) )
if transaction[:object] == 'subscription' if transaction[:object] == "subscription"
Subscription.create( Subscription.create(customer_id: customer.id, external_id: transaction[:id])
customer_id: customer.id,
external_id: transaction[:id]
)
end end
end end
@ -154,23 +154,20 @@ module DiscourseSubscriptions
name: product[:name], name: product[:name],
description: PrettyText.cook(product[:metadata][:description]), description: PrettyText.cook(product[:metadata][:description]),
subscribed: current_user_products.include?(product[:id]), subscribed: current_user_products.include?(product[:id]),
repurchaseable: product[:metadata][:repurchaseable] repurchaseable: product[:metadata][:repurchaseable],
} }
end end
def current_user_products def current_user_products
return [] if current_user.nil? return [] if current_user.nil?
Customer Customer.select(:product_id).where(user_id: current_user.id).map { |c| c.product_id }.compact
.select(:product_id)
.where(user_id: current_user.id)
.map { |c| c.product_id }.compact
end end
def serialize_plans(plans) def serialize_plans(plans)
plans[:data].map do |plan| plans[:data]
plan.to_h.slice(:id, :unit_amount, :currency, :type, :recurring) .map { |plan| plan.to_h.slice(:id, :unit_amount, :currency, :type, :recurring) }
end.sort_by { |plan| plan[:amount] } .sort_by { |plan| plan[:amount] }
end end
def find_or_create_customer(source) def find_or_create_customer(source)
@ -179,10 +176,7 @@ module DiscourseSubscriptions
if customer.present? if customer.present?
::Stripe::Customer.retrieve(customer.customer_id) ::Stripe::Customer.retrieve(customer.customer_id)
else else
::Stripe::Customer.create( ::Stripe::Customer.create(email: current_user.email, source: source)
email: current_user.email,
source: source
)
end end
end end

View File

@ -22,7 +22,8 @@ module DiscourseSubscriptions
invoices_with_products = parse_invoices(all_invoices, product_ids) invoices_with_products = parse_invoices(all_invoices, product_ids)
invoice_ids = invoices_with_products.map { |invoice| invoice[:id] } invoice_ids = invoices_with_products.map { |invoice| invoice[:id] }
payments = ::Stripe::PaymentIntent.list(customer: customer_id) payments = ::Stripe::PaymentIntent.list(customer: customer_id)
payments_from_invoices = payments[:data].select { |payment| invoice_ids.include?(payment[:invoice]) } payments_from_invoices =
payments[:data].select { |payment| invoice_ids.include?(payment[:invoice]) }
data = data | payments_from_invoices data = data | payments_from_invoices
end end
end end
@ -30,7 +31,6 @@ module DiscourseSubscriptions
data = data.sort_by { |pmt| pmt[:created] }.reverse data = data.sort_by { |pmt| pmt[:created] }.reverse
render_json_dump data render_json_dump data
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
render_json_error e.message render_json_error e.message
end end
@ -39,7 +39,8 @@ module DiscourseSubscriptions
private private
def parse_invoices(all_invoices, product_ids) def parse_invoices(all_invoices, product_ids)
invoices_with_products = all_invoices[:data].select do |invoice| invoices_with_products =
all_invoices[:data].select do |invoice|
invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data]
invoice_product_id = parse_invoice_lines(invoice_lines) invoice_product_id = parse_invoice_lines(invoice_lines)
product_ids.include?(invoice_product_id) product_ids.include?(invoice_product_id)
@ -47,8 +48,10 @@ module DiscourseSubscriptions
end end
def parse_invoice_lines(invoice_lines) def parse_invoice_lines(invoice_lines)
invoice_product_id = invoice_lines[:price][:product] if invoice_lines[:price] && invoice_lines[:price][:product] invoice_product_id = invoice_lines[:price][:product] if invoice_lines[:price] &&
invoice_product_id = invoice_lines[:plan][:product] if invoice_lines[:plan] && invoice_lines[:plan][:product] invoice_lines[:price][:product]
invoice_product_id = invoice_lines[:plan][:product] if invoice_lines[:plan] &&
invoice_lines[:plan][:product]
invoice_product_id invoice_product_id
end end
end end

View File

@ -12,24 +12,21 @@ module DiscourseSubscriptions
begin begin
customer = Customer.where(user_id: current_user.id) customer = Customer.where(user_id: current_user.id)
customer_ids = customer.map { |c| c.id } if customer customer_ids = customer.map { |c| c.id } if customer
subscription_ids = Subscription.where("customer_id in (?)", customer_ids).pluck(:external_id) if customer_ids subscription_ids =
Subscription.where("customer_id in (?)", customer_ids).pluck(
:external_id,
) if customer_ids
subscriptions = [] subscriptions = []
if subscription_ids if subscription_ids
plans = ::Stripe::Price.list( plans = ::Stripe::Price.list(expand: ["data.product"], limit: 100)
expand: ['data.product'],
limit: 100
)
customers = ::Stripe::Customer.list( customers =
email: current_user.email, ::Stripe::Customer.list(email: current_user.email, expand: ["data.subscriptions"])
expand: ['data.subscriptions']
)
subscriptions = customers[:data].map do |sub_customer| subscriptions =
sub_customer[:subscriptions][:data] customers[:data].map { |sub_customer| sub_customer[:subscriptions][:data] }.flatten(1)
end.flatten(1)
subscriptions = subscriptions.select { |sub| subscription_ids.include?(sub[:id]) } subscriptions = subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
@ -41,7 +38,6 @@ module DiscourseSubscriptions
end end
render_json_dump subscriptions render_json_dump subscriptions
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
render_json_error e.message render_json_error e.message
end end
@ -51,14 +47,13 @@ module DiscourseSubscriptions
# we cancel but don't remove until the end of the period # we cancel but don't remove until the end of the period
# full removal is done via webhooks # full removal is done via webhooks
begin begin
subscription = ::Stripe::Subscription.update(params[:id], { cancel_at_period_end: true, }) subscription = ::Stripe::Subscription.update(params[:id], { cancel_at_period_end: true })
if subscription if subscription
render_json_dump subscription render_json_dump subscription
else else
render_json_error I18n.t('discourse_subscriptions.customer_not_found') render_json_error I18n.t("discourse_subscriptions.customer_not_found")
end end
rescue ::Stripe::InvalidRequestError => e rescue ::Stripe::InvalidRequestError => e
render_json_error e.message render_json_error e.message
end end
@ -70,7 +65,11 @@ module DiscourseSubscriptions
subscription = Subscription.where(external_id: params[:id]).first subscription = Subscription.where(external_id: params[:id]).first
begin begin
attach_method_to_customer(subscription.customer_id, params[:payment_method]) attach_method_to_customer(subscription.customer_id, params[:payment_method])
subscription = ::Stripe::Subscription.update(params[:id], { default_payment_method: params[:payment_method] }) subscription =
::Stripe::Subscription.update(
params[:id],
{ default_payment_method: params[:payment_method] },
)
render json: success_json render json: success_json
rescue ::Stripe::InvalidRequestError rescue ::Stripe::InvalidRequestError
render_json_error I18n.t("discourse_subscriptions.card.invalid") render_json_error I18n.t("discourse_subscriptions.card.invalid")
@ -81,12 +80,7 @@ module DiscourseSubscriptions
def attach_method_to_customer(customer_id, method) def attach_method_to_customer(customer_id, method)
customer = Customer.find(customer_id) customer = Customer.find(customer_id)
::Stripe::PaymentMethod.attach( ::Stripe::PaymentMethod.attach(method, { customer: customer.customer_id })
method,
{
customer: customer.customer_id
}
)
end end
end end
end end

View File

@ -2,7 +2,6 @@
module ::Jobs module ::Jobs
class ManuallyUpdateCampaignData < ::Jobs::Base class ManuallyUpdateCampaignData < ::Jobs::Base
def execute(args) def execute(args)
return unless SiteSetting.discourse_subscriptions_campaign_enabled return unless SiteSetting.discourse_subscriptions_campaign_enabled
DiscourseSubscriptions::Campaign.new.refresh_data DiscourseSubscriptions::Campaign.new.refresh_data

View File

@ -19,7 +19,7 @@ module DiscourseSubscriptions
ActiveSupport::NumberHelper.number_to_currency( ActiveSupport::NumberHelper.number_to_currency(
object.amount / 100, object.amount / 100,
precision: 2, precision: 2,
unit: currency_unit unit: currency_unit,
) )
end end
@ -32,7 +32,7 @@ module DiscourseSubscriptions
def user def user
begin begin
User.find(object.user_id) User.find(object.user_id)
rescue rescue StandardError
nil nil
end end
end end

View File

@ -23,9 +23,7 @@ module DiscourseSubscriptions
# Fetch product purchases # Fetch product purchases
one_time_payments = get_one_time_payments(product_ids) one_time_payments = get_one_time_payments(product_ids)
one_time_payments.each do |c| one_time_payments.each { |c| amount += c[:price].to_f / 100.00 }
amount += c[:price].to_f / 100.00
end
# get number of subscribers # get number of subscribers
SiteSetting.discourse_subscriptions_campaign_subscribers = subscriptions&.length.to_i SiteSetting.discourse_subscriptions_campaign_subscribers = subscriptions&.length.to_i
@ -57,7 +55,7 @@ module DiscourseSubscriptions
protected protected
def goal_met_date_key def goal_met_date_key
'subscriptions_goal_met_date' "subscriptions_goal_met_date"
end end
def check_goal_status def check_goal_status
@ -91,9 +89,9 @@ module DiscourseSubscriptions
SiteSetting.discourse_subscriptions_campaign_group = group[:id] SiteSetting.discourse_subscriptions_campaign_group = group[:id]
params = { params = {
full_name: I18n.t('js.discourse_subscriptions.campaign.supporters'), full_name: I18n.t("js.discourse_subscriptions.campaign.supporters"),
title: I18n.t('js.discourse_subscriptions.campaign.supporter'), title: I18n.t("js.discourse_subscriptions.campaign.supporter"),
flair_icon: "donate" flair_icon: "donate",
} }
group.update(params) group.update(params)
@ -104,11 +102,11 @@ module DiscourseSubscriptions
def create_campaign_product def create_campaign_product
product_params = { product_params = {
name: I18n.t('js.discourse_subscriptions.campaign.title'), name: I18n.t("js.discourse_subscriptions.campaign.title"),
active: true, active: true,
metadata: { metadata: {
description: I18n.t('js.discourse_subscriptions.campaign.body'), description: I18n.t("js.discourse_subscriptions.campaign.body"),
} },
} }
product = ::Stripe::Product.create(product_params) product = ::Stripe::Product.create(product_params)
@ -123,13 +121,9 @@ module DiscourseSubscriptions
monthly_prices = [3, 5, 10, 25] monthly_prices = [3, 5, 10, 25]
yearly_prices = [50, 100] yearly_prices = [50, 100]
monthly_prices.each do |price| monthly_prices.each { |price| create_price(product[:id], group, price, "month") }
create_price(product[:id], group, price, "month")
end
yearly_prices.each do |price| yearly_prices.each { |price| create_price(product[:id], group, price, "year") }
create_price(product[:id], group, price, "year")
end
end end
def create_price(product_id, group_name, amount, recurrence) def create_price(product_id, group_name, amount, recurrence)
@ -140,11 +134,11 @@ module DiscourseSubscriptions
currency: SiteSetting.discourse_subscriptions_currency, currency: SiteSetting.discourse_subscriptions_currency,
active: true, active: true,
recurring: { recurring: {
interval: recurrence interval: recurrence,
}, },
metadata: { metadata: {
group_name: group_name group_name: group_name,
} },
} }
plan = ::Stripe::Price.create(price_object) plan = ::Stripe::Price.create(price_object)
@ -152,18 +146,13 @@ module DiscourseSubscriptions
def get_one_time_payments(product_ids) def get_one_time_payments(product_ids)
one_time_payments = [] one_time_payments = []
current_set = { current_set = { has_more: true, last_record: nil }
has_more: true,
last_record: nil
}
if product_ids.present? if product_ids.present?
# lots of matching because the Stripe API doesn't make it easy to match products => payments except from invoices # lots of matching because the Stripe API doesn't make it easy to match products => payments except from invoices
until current_set[:has_more] == false until current_set[:has_more] == false
all_invoices = ::Stripe::Invoice.list( all_invoices =
limit: 100, ::Stripe::Invoice.list(limit: 100, starting_after: current_set[:last_record])
starting_after: current_set[:last_record]
)
current_set[:last_record] = all_invoices[:data].last[:id] if all_invoices[:data].present? current_set[:last_record] = all_invoices[:data].last[:id] if all_invoices[:data].present?
current_set[:has_more] = all_invoices[:has_more] current_set[:has_more] = all_invoices[:has_more]
@ -173,11 +162,8 @@ module DiscourseSubscriptions
next if invoice[:paid] != true next if invoice[:paid] != true
line_item = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] # Discourse only makes single-line item charges line_item = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] # Discourse only makes single-line item charges
# check if non-subscription and that the plan is active # check if non-subscription and that the plan is active
if line_item[:plan] == nil && if line_item[:plan] == nil && line_item[:price] &&
line_item[:price] && line_item[:price][:recurring] == nil && line_item[:price][:active] == true
line_item[:price][:recurring] == nil &&
line_item[:price][:active] == true
product_id = line_item[:price][:product] product_id = line_item[:price][:product]
if product_ids.include? product_id if product_ids.include? product_id
line_data = { line_data = {
@ -197,16 +183,14 @@ module DiscourseSubscriptions
def get_subscription_data def get_subscription_data
subscriptions = [] subscriptions = []
current_set = { current_set = { has_more: true, last_record: nil }
has_more: true,
last_record: nil
}
until current_set[:has_more] == false until current_set[:has_more] == false
current_set = ::Stripe::Subscription.list( current_set =
expand: ['data.plan.product'], ::Stripe::Subscription.list(
expand: ["data.plan.product"],
limit: 100, limit: 100,
starting_after: current_set[:last_record] starting_after: current_set[:last_record],
) )
current_set[:last_record] = current_set[:data].last[:id] if current_set[:data].present? current_set[:last_record] = current_set[:data].last[:id] if current_set[:data].present?
@ -217,7 +201,8 @@ module DiscourseSubscriptions
end end
def filter_to_subscriptions_products(data, ids) def filter_to_subscriptions_products(data, ids)
valid = data.select do |sub| valid =
data.select do |sub|
# cannot .dig stripe objects # cannot .dig stripe objects
items = sub[:items][:data][0] if sub[:items] && sub[:items][:data] items = sub[:items][:data][0] if sub[:items] && sub[:items][:data]
product = items[:price][:product] if items[:price] && items[:price][:product] product = items[:price][:product] if items[:price] && items[:price][:product]

View File

@ -2,31 +2,31 @@
require_dependency "subscriptions_user_constraint" require_dependency "subscriptions_user_constraint"
DiscourseSubscriptions::Engine.routes.draw do DiscourseSubscriptions::Engine.routes.draw do
scope 'admin' do scope "admin" do
get '/' => 'admin#index' get "/" => "admin#index"
post '/refresh' => 'admin#refresh_campaign' post "/refresh" => "admin#refresh_campaign"
post '/create-campaign' => 'admin#create_campaign' post "/create-campaign" => "admin#create_campaign"
end end
namespace :admin, constraints: AdminConstraint.new do namespace :admin, constraints: AdminConstraint.new do
resources :plans resources :plans
resources :subscriptions, only: [:index, :destroy] resources :subscriptions, only: %i[index destroy]
resources :products resources :products
resources :coupons, only: [:index, :create] resources :coupons, only: %i[index create]
resource :coupons, only: [:destroy, :update] resource :coupons, only: %i[destroy update]
end end
namespace :user do namespace :user do
resources :payments, only: [:index] resources :payments, only: [:index]
resources :subscriptions, only: [:index, :update, :destroy] resources :subscriptions, only: %i[index update destroy]
end end
get '/' => 'subscribe#index' get "/" => "subscribe#index"
get '.json' => 'subscribe#index' get ".json" => "subscribe#index"
get '/contributors' => 'subscribe#contributors' get "/contributors" => "subscribe#contributors"
get '/:id' => 'subscribe#show' get "/:id" => "subscribe#show"
post '/create' => 'subscribe#create' post "/create" => "subscribe#create"
post '/finalize' => 'subscribe#finalize' post "/finalize" => "subscribe#finalize"
post '/hooks' => 'hooks#create' post "/hooks" => "hooks#create"
end end

View File

@ -2,7 +2,7 @@
module ::DiscourseSubscriptions module ::DiscourseSubscriptions
class Engine < ::Rails::Engine class Engine < ::Rails::Engine
engine_name 'discourse-subscriptions' engine_name "discourse-subscriptions"
isolate_namespace DiscourseSubscriptions isolate_namespace DiscourseSubscriptions
end end
end end

View File

@ -1,23 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'stripe' require "stripe"
require 'highline/import' require "highline/import"
desc 'Import subscriptions from Stripe' desc "Import subscriptions from Stripe"
task 'subscriptions:subscriptions_import' => :environment do task "subscriptions:subscriptions_import" => :environment do
setup_api setup_api
products = get_stripe_products products = get_stripe_products
strip_products_to_import = [] strip_products_to_import = []
procourse_import = false procourse_import = false
procourse_import_response = ask("Were the subscriptions you are importing created in Procourse Memberships?: (y/N)") procourse_import_response =
if procourse_import_response.downcase == 'y' ask("Were the subscriptions you are importing created in Procourse Memberships?: (y/N)")
procourse_import = true procourse_import = true if procourse_import_response.downcase == "y"
end
products.each do |product| products.each do |product|
confirm_import = ask("Do you wish to import product #{product[:name]} (id: #{product[:id]}): (y/N)") confirm_import =
next if confirm_import.downcase != 'y' ask("Do you wish to import product #{product[:name]} (id: #{product[:id]}): (y/N)")
next if confirm_import.downcase != "y"
strip_products_to_import << product strip_products_to_import << product
end end
@ -26,28 +26,28 @@ task 'subscriptions:subscriptions_import' => :environment do
end end
def get_stripe_products(starting_after: nil) def get_stripe_products(starting_after: nil)
puts 'Getting products from Stripe API' puts "Getting products from Stripe API"
all_products = [] all_products = []
loop do loop do
products = Stripe::Product.list({ type: 'service', starting_after: starting_after, active: true }) products =
Stripe::Product.list({ type: "service", starting_after: starting_after, active: true })
all_products += products[:data] all_products += products[:data]
break if products[:has_more] == false break if products[:has_more] == false
starting_after = products[:data].last["id"] starting_after = products[:data].last["id"]
end end
all_products all_products
end end
def get_stripe_subscriptions(starting_after: nil) def get_stripe_subscriptions(starting_after: nil)
puts 'Getting Subscriptions from Stripe API' puts "Getting Subscriptions from Stripe API"
all_subscriptions = [] all_subscriptions = []
loop do loop do
subscriptions = Stripe::Subscription.list({ starting_after: starting_after, status: 'active' }) subscriptions = Stripe::Subscription.list({ starting_after: starting_after, status: "active" })
all_subscriptions += subscriptions[:data] all_subscriptions += subscriptions[:data]
break if subscriptions[:has_more] == false break if subscriptions[:has_more] == false
starting_after = subscriptions[:data].last["id"] starting_after = subscriptions[:data].last["id"]
@ -57,7 +57,7 @@ def get_stripe_subscriptions(starting_after: nil)
end end
def get_stripe_customers(starting_after: nil) def get_stripe_customers(starting_after: nil)
puts 'Getting Customers from Stripe API' puts "Getting Customers from Stripe API"
all_customers = [] all_customers = []
@ -72,7 +72,7 @@ def get_stripe_customers(starting_after: nil)
end end
def import_products(products) def import_products(products)
puts 'Importing products:' puts "Importing products:"
products.each do |product| products.each do |product|
puts "Looking for external_id #{product[:id]} ..." puts "Looking for external_id #{product[:id]} ..."
@ -86,7 +86,7 @@ def import_products(products)
end end
def import_subscriptions(procourse_import) def import_subscriptions(procourse_import)
puts 'Importing subscriptions' puts "Importing subscriptions"
product_ids = DiscourseSubscriptions::Product.all.pluck(:external_id) product_ids = DiscourseSubscriptions::Product.all.pluck(:external_id)
all_customers = get_stripe_customers all_customers = get_stripe_customers
@ -95,7 +95,8 @@ def import_subscriptions(procourse_import)
subscriptions = get_stripe_subscriptions subscriptions = get_stripe_subscriptions
puts "Total Active Subscriptions available: #{subscriptions.length.to_s}" puts "Total Active Subscriptions available: #{subscriptions.length.to_s}"
subscriptions_for_products = subscriptions.select { |sub| product_ids.include?(sub[:items][:data][0][:price][:product]) } subscriptions_for_products =
subscriptions.select { |sub| product_ids.include?(sub[:items][:data][0][:price][:product]) }
puts "Total Subscriptions matching Products to Import: #{subscriptions_for_products.length.to_s}" puts "Total Subscriptions matching Products to Import: #{subscriptions_for_products.length.to_s}"
subscriptions_for_products.each do |subscription| subscriptions_for_products.each do |subscription|
@ -113,17 +114,23 @@ def import_subscriptions(procourse_import)
end end
if product_id && customer_id && subscription_id if product_id && customer_id && subscription_id
subscriptions_customer = DiscourseSubscriptions::Customer.find_by(user_id: user_id, customer_id: customer_id, product_id: product_id) subscriptions_customer =
DiscourseSubscriptions::Customer.find_by(
user_id: user_id,
customer_id: customer_id,
product_id: product_id,
)
if subscriptions_customer.nil? && user_id && user_id > 0 if subscriptions_customer.nil? && user_id && user_id > 0
# create the customer record if doesn't exist only if the user_id and username match, which # create the customer record if doesn't exist only if the user_id and username match, which
# prevents issues if multiple sites use the same Stripe account. Does not apply to a Procourse import. # prevents issues if multiple sites use the same Stripe account. Does not apply to a Procourse import.
user = User.find(user_id) user = User.find(user_id)
if procourse_import || (user && (user.username == username)) if procourse_import || (user && (user.username == username))
subscriptions_customer = DiscourseSubscriptions::Customer.create( subscriptions_customer =
DiscourseSubscriptions::Customer.create(
user_id: user_id, user_id: user_id,
customer_id: customer_id, customer_id: customer_id,
product_id: product_id product_id: product_id,
) )
puts "Subscriptions Customer user_id: #{user_id}, customer_id: #{customer_id}, product_id: #{product_id}) CREATED" puts "Subscriptions Customer user_id: #{user_id}, customer_id: #{customer_id}, product_id: #{product_id}) CREATED"
end end
@ -132,10 +139,13 @@ def import_subscriptions(procourse_import)
end end
if subscriptions_customer if subscriptions_customer
if DiscourseSubscriptions::Subscription.find_by(customer_id: subscriptions_customer.id, external_id: subscription_id).blank? if DiscourseSubscriptions::Subscription.find_by(
customer_id: subscriptions_customer.id,
external_id: subscription_id,
).blank?
DiscourseSubscriptions::Subscription.create( DiscourseSubscriptions::Subscription.create(
customer_id: subscriptions_customer.id, customer_id: subscriptions_customer.id,
external_id: subscription_id external_id: subscription_id,
) )
puts "Discourse Subscription customer_id: #{subscriptions_customer.id}, external_id: #{subscription_id}) CREATED" puts "Discourse Subscription customer_id: #{subscriptions_customer.id}, external_id: #{subscription_id}) CREATED"
else else
@ -147,9 +157,11 @@ def import_subscriptions(procourse_import)
discourse_user = User.find(user_id) discourse_user = User.find(user_id)
puts "Discourse User: #{discourse_user.username_lower} found for Strip metadata update ..." puts "Discourse User: #{discourse_user.username_lower} found for Strip metadata update ..."
updated_subscription = Stripe::Subscription.update(subscription_id, updated_subscription =
{ metadata: { user_id: user_id, Stripe::Subscription.update(
username: discourse_user.username_lower } }) subscription_id,
{ metadata: { user_id: user_id, username: discourse_user.username_lower } },
)
puts "Stripe Subscription: #{updated_subscription[:id]}, metadata: #{updated_subscription[:metadata]} UPDATED" puts "Stripe Subscription: #{updated_subscription[:id]}, metadata: #{updated_subscription[:metadata]} UPDATED"
updated_customer = Stripe::Customer.update(customer_id, { email: discourse_user.email }) updated_customer = Stripe::Customer.update(customer_id, { email: discourse_user.email })
@ -163,6 +175,6 @@ end
private private
def setup_api def setup_api
api_key = SiteSetting.discourse_subscriptions_secret_key || ask('Input Stripe secret key') api_key = SiteSetting.discourse_subscriptions_secret_key || ask("Input Stripe secret key")
Stripe.api_key = api_key Stripe.api_key = api_key
end end

View File

@ -9,7 +9,7 @@
enabled_site_setting :discourse_subscriptions_enabled enabled_site_setting :discourse_subscriptions_enabled
gem 'stripe', '5.29.0' gem "stripe", "5.29.0"
register_asset "stylesheets/common/main.scss" register_asset "stylesheets/common/main.scss"
register_asset "stylesheets/common/layout.scss" register_asset "stylesheets/common/layout.scss"
@ -18,47 +18,56 @@ register_asset "stylesheets/common/campaign.scss"
register_asset "stylesheets/mobile/main.scss" register_asset "stylesheets/mobile/main.scss"
register_svg_icon "far-credit-card" if respond_to?(:register_svg_icon) register_svg_icon "far-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>"
end end
extend_content_security_policy( extend_content_security_policy(script_src: %w[https://js.stripe.com/v3/ https://hooks.stripe.com])
script_src: ['https://js.stripe.com/v3/', 'https://hooks.stripe.com']
)
add_admin_route 'discourse_subscriptions.admin_navigation', 'discourse-subscriptions.products' add_admin_route "discourse_subscriptions.admin_navigation", "discourse-subscriptions.products"
Discourse::Application.routes.append do Discourse::Application.routes.append do
get '/admin/plugins/discourse-subscriptions' => 'admin/plugins#index', constraints: AdminConstraint.new get "/admin/plugins/discourse-subscriptions" => "admin/plugins#index",
get '/admin/plugins/discourse-subscriptions/products' => 'admin/plugins#index', constraints: AdminConstraint.new :constraints => AdminConstraint.new
get '/admin/plugins/discourse-subscriptions/products/:product_id' => 'admin/plugins#index', constraints: AdminConstraint.new get "/admin/plugins/discourse-subscriptions/products" => "admin/plugins#index",
get '/admin/plugins/discourse-subscriptions/products/:product_id/plans' => 'admin/plugins#index', constraints: AdminConstraint.new :constraints => AdminConstraint.new
get '/admin/plugins/discourse-subscriptions/products/:product_id/plans/:plan_id' => 'admin/plugins#index', constraints: AdminConstraint.new get "/admin/plugins/discourse-subscriptions/products/:product_id" => "admin/plugins#index",
get '/admin/plugins/discourse-subscriptions/subscriptions' => 'admin/plugins#index', constraints: AdminConstraint.new :constraints => AdminConstraint.new
get '/admin/plugins/discourse-subscriptions/plans' => 'admin/plugins#index', constraints: AdminConstraint.new get "/admin/plugins/discourse-subscriptions/products/:product_id/plans" => "admin/plugins#index",
get '/admin/plugins/discourse-subscriptions/plans/:plan_id' => 'admin/plugins#index', constraints: AdminConstraint.new :constraints => AdminConstraint.new
get '/admin/plugins/discourse-subscriptions/coupons' => 'admin/plugins#index', constraints: AdminConstraint.new get "/admin/plugins/discourse-subscriptions/products/:product_id/plans/:plan_id" =>
get 'u/:username/billing' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT } "admin/plugins#index",
get 'u/:username/billing/:id' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT } :constraints => AdminConstraint.new
get 'u/:username/billing/subscriptions/card/:subscription_id' => 'users#show', constraints: { username: USERNAME_ROUTE_FORMAT } get "/admin/plugins/discourse-subscriptions/subscriptions" => "admin/plugins#index",
:constraints => AdminConstraint.new
get "/admin/plugins/discourse-subscriptions/plans" => "admin/plugins#index",
:constraints => AdminConstraint.new
get "/admin/plugins/discourse-subscriptions/plans/:plan_id" => "admin/plugins#index",
:constraints => AdminConstraint.new
get "/admin/plugins/discourse-subscriptions/coupons" => "admin/plugins#index",
:constraints => AdminConstraint.new
get "u/:username/billing" => "users#show", :constraints => { username: USERNAME_ROUTE_FORMAT }
get "u/:username/billing/:id" => "users#show", :constraints => { username: USERNAME_ROUTE_FORMAT }
get "u/:username/billing/subscriptions/card/:subscription_id" => "users#show",
:constraints => {
username: USERNAME_ROUTE_FORMAT,
}
end end
load File.expand_path('lib/discourse_subscriptions/engine.rb', __dir__) load File.expand_path("lib/discourse_subscriptions/engine.rb", __dir__)
load File.expand_path('app/controllers/concerns/stripe.rb', __dir__) load File.expand_path("app/controllers/concerns/stripe.rb", __dir__)
load File.expand_path('app/controllers/concerns/group.rb', __dir__) load File.expand_path("app/controllers/concerns/group.rb", __dir__)
after_initialize do after_initialize do
::Stripe.api_version = "2020-08-27" ::Stripe.api_version = "2020-08-27"
::Stripe.set_app_info( ::Stripe.set_app_info(
'Discourse Subscriptions', "Discourse Subscriptions",
version: '2.8.1', version: "2.8.1",
url: 'https://github.com/discourse/discourse-subscriptions' url: "https://github.com/discourse/discourse-subscriptions",
) )
Discourse::Application.routes.append do Discourse::Application.routes.append { mount ::DiscourseSubscriptions::Engine, at: "s" }
mount ::DiscourseSubscriptions::Engine, at: 's'
end
add_to_serializer(:site, :show_campaign_banner) do add_to_serializer(:site, :show_campaign_banner) do
begin begin
@ -67,7 +76,7 @@ after_initialize do
goal_met = Discourse.redis.get("subscriptions_goal_met_date") goal_met = Discourse.redis.get("subscriptions_goal_met_date")
enabled && campaign_enabled && (!goal_met || 7.days.ago <= Date.parse(goal_met)) enabled && campaign_enabled && (!goal_met || 7.days.ago <= Date.parse(goal_met))
rescue rescue StandardError
false false
end end
end end

View File

@ -1,14 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
RSpec.describe Jobs::RefreshSubscriptionsCampaignData do RSpec.describe Jobs::RefreshSubscriptionsCampaignData do
before { SiteSetting.discourse_subscriptions_campaign_enabled = true }
before do it "should execute the job only if stripe is configured" do
SiteSetting.discourse_subscriptions_campaign_enabled = true
end
it 'should execute the job only if stripe is configured' do
DiscourseSubscriptions::Campaign.any_instance.expects(:refresh_data).once DiscourseSubscriptions::Campaign.any_instance.expects(:refresh_data).once
described_class.new.execute({}) described_class.new.execute({})
@ -16,5 +13,4 @@ RSpec.describe Jobs::RefreshSubscriptionsCampaignData do
SiteSetting.discourse_subscriptions_secret_key = "SECRET_KEY" SiteSetting.discourse_subscriptions_secret_key = "SECRET_KEY"
described_class.new.execute({}) described_class.new.execute({})
end end
end end

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
module DiscourseSubscriptions module DiscourseSubscriptions
RSpec.describe Customer do RSpec.describe Customer do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:stripe_customer) { { id: 'cus_id4567' } } let(:stripe_customer) { { id: "cus_id4567" } }
it "has a table name" do it "has a table name" do
expect(described_class.table_name).to eq "discourse_subscriptions_customers" expect(described_class.table_name).to eq "discourse_subscriptions_customers"
@ -13,14 +13,14 @@ module DiscourseSubscriptions
it "creates" do it "creates" do
customer = described_class.create_customer(user, stripe_customer) customer = described_class.create_customer(user, stripe_customer)
expect(customer.customer_id).to eq 'cus_id4567' expect(customer.customer_id).to eq "cus_id4567"
expect(customer.user_id).to eq user.id expect(customer.user_id).to eq user.id
end end
it "has a user scope" do it "has a user scope" do
described_class.create_customer(user, stripe_customer) described_class.create_customer(user, stripe_customer)
customer = described_class.find_user(user) customer = described_class.find_user(user)
expect(customer.customer_id).to eq 'cus_id4567' expect(customer.customer_id).to eq "cus_id4567"
end end
end end
end end

View File

@ -1,14 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
module DiscourseSubscriptions module DiscourseSubscriptions
RSpec.describe Admin::CouponsController do RSpec.describe Admin::CouponsController do
it 'is a subclass of AdminController' do it "is a subclass of AdminController" do
expect(DiscourseSubscriptions::Admin::CouponsController < ::Admin::AdminController).to eq(true) expect(DiscourseSubscriptions::Admin::CouponsController < ::Admin::AdminController).to eq(
true,
)
end end
context 'when unauthenticated' do context "when unauthenticated" do
it "does nothing" do it "does nothing" do
::Stripe::PromotionCode.expects(:list).never ::Stripe::PromotionCode.expects(:list).never
get "/s/admin/coupons.json" get "/s/admin/coupons.json"
@ -16,36 +18,28 @@ module DiscourseSubscriptions
end end
end end
context 'when authenticated' do context "when authenticated" do
let(:admin) { Fabricate(:admin) } let(:admin) { Fabricate(:admin) }
before { sign_in(admin) } before { sign_in(admin) }
describe "#index" do describe "#index" do
it "returns a list of promo codes" do it "returns a list of promo codes" do
::Stripe::PromotionCode.expects(:list).with({ limit: 100 }).returns({ ::Stripe::PromotionCode
data: [{ .expects(:list)
id: 'promo_123', .with({ limit: 100 })
coupon: { .returns({ data: [{ id: "promo_123", coupon: { valid: true } }] })
valid: true
}
}]
})
get "/s/admin/coupons.json" get "/s/admin/coupons.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body[0]['id']).to eq('promo_123') expect(response.parsed_body[0]["id"]).to eq("promo_123")
end end
it "only returns valid promo codes" do it "only returns valid promo codes" do
::Stripe::PromotionCode.expects(:list).with({ limit: 100 }).returns({ ::Stripe::PromotionCode
data: [{ .expects(:list)
id: 'promo_123', .with({ limit: 100 })
coupon: { .returns({ data: [{ id: "promo_123", coupon: { valid: false } }] })
valid: false
}
}]
})
get "/s/admin/coupons.json" get "/s/admin/coupons.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -55,43 +49,39 @@ module DiscourseSubscriptions
describe "#create" do describe "#create" do
it "creates a coupon with an amount off" do it "creates a coupon with an amount off" do
::Stripe::Coupon.expects(:create).returns(id: 'coup_123') ::Stripe::Coupon.expects(:create).returns(id: "coup_123")
::Stripe::PromotionCode.expects(:create).returns({ ::Stripe::PromotionCode.expects(:create).returns(
code: 'p123', { code: "p123", coupon: { amount_off: 2000 } },
coupon: { )
amount_off: 2000
}
})
post "/s/admin/coupons.json", params: { post "/s/admin/coupons.json",
promo: 'p123', params: {
discount_type: 'amount', promo: "p123",
discount: '2000', discount_type: "amount",
discount: "2000",
active: true, active: true,
} }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body['code']).to eq('p123') expect(response.parsed_body["code"]).to eq("p123")
expect(response.parsed_body['coupon']['amount_off']).to eq(2000) expect(response.parsed_body["coupon"]["amount_off"]).to eq(2000)
end end
it "creates a coupon with a percent off" do it "creates a coupon with a percent off" do
::Stripe::Coupon.expects(:create).returns(id: 'coup_123') ::Stripe::Coupon.expects(:create).returns(id: "coup_123")
::Stripe::PromotionCode.expects(:create).returns({ ::Stripe::PromotionCode.expects(:create).returns(
code: 'p123', { code: "p123", coupon: { percent_off: 20 } },
coupon: { )
percent_off: 20
}
})
post "/s/admin/coupons.json", params: { post "/s/admin/coupons.json",
promo: 'p123', params: {
discount_type: 'percent', promo: "p123",
discount: '20', discount_type: "percent",
discount: "20",
active: true, active: true,
} }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body['code']).to eq('p123') expect(response.parsed_body["code"]).to eq("p123")
expect(response.parsed_body['coupon']['percent_off']).to eq(20) expect(response.parsed_body["coupon"]["percent_off"]).to eq(20)
end end
end end
end end

View File

@ -1,15 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
module DiscourseSubscriptions module DiscourseSubscriptions
module Admin module Admin
RSpec.describe PlansController do RSpec.describe PlansController do
it 'is a subclass of AdminController' do it "is a subclass of AdminController" do
expect(DiscourseSubscriptions::Admin::PlansController < ::Admin::AdminController).to eq(true) expect(DiscourseSubscriptions::Admin::PlansController < ::Admin::AdminController).to eq(
true,
)
end end
context 'when not authenticated' do context "when not authenticated" do
describe "index" do describe "index" do
it "does not get the plans" do it "does not get the plans" do
::Stripe::Price.expects(:list).never ::Stripe::Price.expects(:list).never
@ -25,11 +27,11 @@ module DiscourseSubscriptions
describe "create" do describe "create" do
it "does not create a plan" do it "does not create a plan" do
::Stripe::Price.expects(:create).never ::Stripe::Price.expects(:create).never
post "/s/admin/plans.json", params: { name: 'Rick Astley', amount: 1, interval: 'week' } post "/s/admin/plans.json", params: { name: "Rick Astley", amount: 1, interval: "week" }
end end
it "is not ok" do it "is not ok" do
post "/s/admin/plans.json", params: { name: 'Rick Astley', amount: 1, interval: 'week' } post "/s/admin/plans.json", params: { name: "Rick Astley", amount: 1, interval: "week" }
expect(response.status).to eq 404 expect(response.status).to eq 404
end end
end end
@ -54,7 +56,7 @@ module DiscourseSubscriptions
end end
end end
context 'when authenticated' do context "when authenticated" do
let(:admin) { Fabricate(:admin) } let(:admin) { Fabricate(:admin) }
before { sign_in(admin) } before { sign_in(admin) }
@ -66,62 +68,78 @@ module DiscourseSubscriptions
end end
it "lists the plans for the product" do it "lists the plans for the product" do
::Stripe::Price.expects(:list).with({ product: 'prod_id123' }) ::Stripe::Price.expects(:list).with({ product: "prod_id123" })
get "/s/admin/plans.json", params: { product_id: 'prod_id123' } get "/s/admin/plans.json", params: { product_id: "prod_id123" }
end end
end end
describe "show" do describe "show" do
it "shows a plan" do it "shows a plan" do
::Stripe::Price.expects(:retrieve).with('plan_12345').returns(currency: 'aud') ::Stripe::Price.expects(:retrieve).with("plan_12345").returns(currency: "aud")
get "/s/admin/plans/plan_12345.json" get "/s/admin/plans/plan_12345.json"
expect(response.status).to eq 200 expect(response.status).to eq 200
end end
it "upcases the currency" do it "upcases the currency" do
::Stripe::Price.expects(:retrieve).with('plan_12345').returns(currency: 'aud', recurring: { interval: 'year' }) ::Stripe::Price
.expects(:retrieve)
.with("plan_12345")
.returns(currency: "aud", recurring: { interval: "year" })
get "/s/admin/plans/plan_12345.json" get "/s/admin/plans/plan_12345.json"
plan = response.parsed_body plan = response.parsed_body
expect(plan["currency"]).to eq 'AUD' expect(plan["currency"]).to eq "AUD"
expect(plan["interval"]).to eq 'year' expect(plan["interval"]).to eq "year"
end end
end end
describe "create" do describe "create" do
it "creates a plan with a nickname" do it "creates a plan with a nickname" do
::Stripe::Price.expects(:create).with(has_entry(:nickname, 'Veg')) ::Stripe::Price.expects(:create).with(has_entry(:nickname, "Veg"))
post "/s/admin/plans.json", params: { nickname: 'Veg', metadata: { group_name: '' } } post "/s/admin/plans.json", params: { nickname: "Veg", metadata: { group_name: "" } }
end end
it "creates a plan with a currency" do it "creates a plan with a currency" do
::Stripe::Price.expects(:create).with(has_entry(:currency, 'AUD')) ::Stripe::Price.expects(:create).with(has_entry(:currency, "AUD"))
post "/s/admin/plans.json", params: { currency: 'AUD', metadata: { group_name: '' } } post "/s/admin/plans.json", params: { currency: "AUD", metadata: { group_name: "" } }
end end
it "creates a plan with an interval" do it "creates a plan with an interval" do
::Stripe::Price.expects(:create).with(has_entry(recurring: { interval: 'week' })) ::Stripe::Price.expects(:create).with(has_entry(recurring: { interval: "week" }))
post "/s/admin/plans.json", params: { type: 'recurring', interval: 'week', metadata: { group_name: '' } } post "/s/admin/plans.json",
params: {
type: "recurring",
interval: "week",
metadata: {
group_name: "",
},
}
end end
it "creates a plan as a one-time purchase" do it "creates a plan as a one-time purchase" do
::Stripe::Price.expects(:create).with(Not(has_key(:recurring))) ::Stripe::Price.expects(:create).with(Not(has_key(:recurring)))
post "/s/admin/plans.json", params: { metadata: { group_name: '' } } post "/s/admin/plans.json", params: { metadata: { group_name: "" } }
end end
it "creates a plan with an amount" do it "creates a plan with an amount" do
::Stripe::Price.expects(:create).with(has_entry(:unit_amount, '102')) ::Stripe::Price.expects(:create).with(has_entry(:unit_amount, "102"))
post "/s/admin/plans.json", params: { amount: '102', metadata: { group_name: '' } } post "/s/admin/plans.json", params: { amount: "102", metadata: { group_name: "" } }
end end
it "creates a plan with a product" do it "creates a plan with a product" do
::Stripe::Price.expects(:create).with(has_entry(product: 'prod_walterwhite')) ::Stripe::Price.expects(:create).with(has_entry(product: "prod_walterwhite"))
post "/s/admin/plans.json", params: { product: 'prod_walterwhite', metadata: { group_name: '' } } post "/s/admin/plans.json",
params: {
product: "prod_walterwhite",
metadata: {
group_name: "",
},
}
end end
it "creates a plan with an active status" do it "creates a plan with an active status" do
::Stripe::Price.expects(:create).with(has_entry(:active, 'false')) ::Stripe::Price.expects(:create).with(has_entry(:active, "false"))
post "/s/admin/plans.json", params: { active: 'false', metadata: { group_name: '' } } post "/s/admin/plans.json", params: { active: "false", metadata: { group_name: "" } }
end end
# TODO: Need to fix the metadata tests # TODO: Need to fix the metadata tests
@ -141,7 +159,13 @@ module DiscourseSubscriptions
describe "update" do describe "update" do
it "updates a plan" do it "updates a plan" do
::Stripe::Price.expects(:update) ::Stripe::Price.expects(:update)
patch "/s/admin/plans/plan_12345.json", params: { trial_period_days: '14', metadata: { group_name: 'discourse-user-group-name' } } patch "/s/admin/plans/plan_12345.json",
params: {
trial_period_days: "14",
metadata: {
group_name: "discourse-user-group-name",
},
}
end end
end end
end end

View File

@ -1,15 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
module DiscourseSubscriptions module DiscourseSubscriptions
module Admin module Admin
RSpec.describe ProductsController do RSpec.describe ProductsController do
it 'is a subclass of AdminController' do it "is a subclass of AdminController" do
expect(DiscourseSubscriptions::Admin::ProductsController < ::Admin::AdminController).to eq(true) expect(DiscourseSubscriptions::Admin::ProductsController < ::Admin::AdminController).to eq(
true,
)
end end
context 'when unauthenticated' do context "when unauthenticated" do
it "does not list the products" do it "does not list the products" do
::Stripe::Product.expects(:list).never ::Stripe::Product.expects(:list).never
get "/s/admin/products.json" get "/s/admin/products.json"
@ -41,12 +43,12 @@ module DiscourseSubscriptions
end end
end end
context 'when authenticated' do context "when authenticated" do
let(:admin) { Fabricate(:admin) } let(:admin) { Fabricate(:admin) }
before { sign_in(admin) } before { sign_in(admin) }
describe 'index' do describe "index" do
it "gets the empty products" do it "gets the empty products" do
SiteSetting.discourse_subscriptions_public_key = "public-key" SiteSetting.discourse_subscriptions_public_key = "public-key"
SiteSetting.discourse_subscriptions_secret_key = "secret-key" SiteSetting.discourse_subscriptions_secret_key = "secret-key"
@ -55,61 +57,74 @@ module DiscourseSubscriptions
end end
end end
describe 'create' do describe "create" do
it 'is of product type service' do it "is of product type service" do
::Stripe::Product.expects(:create).with(has_entry(:type, 'service')) ::Stripe::Product.expects(:create).with(has_entry(:type, "service"))
post "/s/admin/products.json", params: {} post "/s/admin/products.json", params: {}
end end
it 'has a name' do it "has a name" do
::Stripe::Product.expects(:create).with(has_entry(:name, 'Jesse Pinkman')) ::Stripe::Product.expects(:create).with(has_entry(:name, "Jesse Pinkman"))
post "/s/admin/products.json", params: { name: 'Jesse Pinkman' } post "/s/admin/products.json", params: { name: "Jesse Pinkman" }
end end
it 'has an active attribute' do it "has an active attribute" do
::Stripe::Product.expects(:create).with(has_entry(active: 'false')) ::Stripe::Product.expects(:create).with(has_entry(active: "false"))
post "/s/admin/products.json", params: { active: 'false' } post "/s/admin/products.json", params: { active: "false" }
end end
it 'has a statement descriptor' do it "has a statement descriptor" do
::Stripe::Product.expects(:create).with(has_entry(statement_descriptor: 'Blessed are the cheesemakers')) ::Stripe::Product.expects(:create).with(
post "/s/admin/products.json", params: { statement_descriptor: 'Blessed are the cheesemakers' } has_entry(statement_descriptor: "Blessed are the cheesemakers"),
)
post "/s/admin/products.json",
params: {
statement_descriptor: "Blessed are the cheesemakers",
}
end end
it 'has no statement descriptor if empty' do it "has no statement descriptor if empty" do
::Stripe::Product.expects(:create).with(has_key(:statement_descriptor)).never ::Stripe::Product.expects(:create).with(has_key(:statement_descriptor)).never
post "/s/admin/products.json", params: { statement_descriptor: '' } post "/s/admin/products.json", params: { statement_descriptor: "" }
end end
it 'has metadata' do it "has metadata" do
::Stripe::Product.expects(:create).with(has_entry(metadata: { description: 'Oi, I think he just said bless be all the bignoses!', repurchaseable: 'false' })) ::Stripe::Product.expects(:create).with(
has_entry(
post "/s/admin/products.json", params: {
metadata: { metadata: {
description: 'Oi, I think he just said bless be all the bignoses!', description: "Oi, I think he just said bless be all the bignoses!",
repurchaseable: 'false' repurchaseable: "false",
} },
),
)
post "/s/admin/products.json",
params: {
metadata: {
description: "Oi, I think he just said bless be all the bignoses!",
repurchaseable: "false",
},
} }
end end
end end
describe 'show' do describe "show" do
it 'retrieves the product' do it "retrieves the product" do
::Stripe::Product.expects(:retrieve).with('prod_walterwhite') ::Stripe::Product.expects(:retrieve).with("prod_walterwhite")
get "/s/admin/products/prod_walterwhite.json" get "/s/admin/products/prod_walterwhite.json"
end end
end end
describe 'update' do describe "update" do
it 'updates the product' do it "updates the product" do
::Stripe::Product.expects(:update) ::Stripe::Product.expects(:update)
patch "/s/admin/products/prod_walterwhite.json", params: {} patch "/s/admin/products/prod_walterwhite.json", params: {}
end end
end end
describe 'delete' do describe "delete" do
it 'deletes the product' do it "deletes the product" do
::Stripe::Product.expects(:delete).with('prod_walterwhite') ::Stripe::Product.expects(:delete).with("prod_walterwhite")
delete "/s/admin/products/prod_walterwhite.json" delete "/s/admin/products/prod_walterwhite.json"
end end
end end

View File

@ -1,22 +1,26 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
module DiscourseSubscriptions module DiscourseSubscriptions
RSpec.describe Admin::SubscriptionsController do RSpec.describe Admin::SubscriptionsController do
it 'is a subclass of AdminController' do it "is a subclass of AdminController" do
expect(DiscourseSubscriptions::Admin::SubscriptionsController < ::Admin::AdminController).to eq(true) expect(
DiscourseSubscriptions::Admin::SubscriptionsController < ::Admin::AdminController,
).to eq(true)
end end
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:customer) { Fabricate(:customer, user_id: user.id, customer_id: 'c_123', product_id: 'pr_34578') } let(:customer) do
Fabricate(:customer, user_id: user.id, customer_id: "c_123", product_id: "pr_34578")
end
before do before do
Fabricate(:subscription, external_id: "sub_12345", customer_id: customer.id) Fabricate(:subscription, external_id: "sub_12345", customer_id: customer.id)
Fabricate(:subscription, external_id: "sub_77777", customer_id: customer.id) Fabricate(:subscription, external_id: "sub_77777", customer_id: customer.id)
end end
context 'when unauthenticated' do context "when unauthenticated" do
it "does nothing" do it "does nothing" do
::Stripe::Subscription.expects(:list).never ::Stripe::Subscription.expects(:list).never
get "/s/admin/subscriptions.json" get "/s/admin/subscriptions.json"
@ -29,7 +33,7 @@ module DiscourseSubscriptions
end end
end end
context 'when authenticated' do context "when authenticated" do
let(:admin) { Fabricate(:admin) } let(:admin) { Fabricate(:admin) }
before { sign_in(admin) } before { sign_in(admin) }
@ -41,15 +45,10 @@ module DiscourseSubscriptions
end end
it "gets the subscriptions and products" do it "gets the subscriptions and products" do
::Stripe::Subscription.expects(:list) ::Stripe::Subscription
.with(expand: ['data.plan.product'], limit: 10, starting_after: nil) .expects(:list)
.returns( .with(expand: ["data.plan.product"], limit: 10, starting_after: nil)
has_more: false, .returns(has_more: false, data: [{ id: "sub_12345" }, { id: "sub_nope" }])
data: [
{ id: "sub_12345" },
{ id: "sub_nope" }
]
)
get "/s/admin/subscriptions.json" get "/s/admin/subscriptions.json"
subscriptions = response.parsed_body["data"][0]["id"] subscriptions = response.parsed_body["data"][0]["id"]
@ -58,16 +57,11 @@ module DiscourseSubscriptions
end end
it "handles starting at a different point in the set" do it "handles starting at a different point in the set" do
::Stripe::Subscription.expects(:list) ::Stripe::Subscription
.with(expand: ['data.plan.product'], limit: 10, starting_after: 'sub_nope') .expects(:list)
.returns( .with(expand: ["data.plan.product"], limit: 10, starting_after: "sub_nope")
has_more: false, .returns(has_more: false, data: [{ id: "sub_77777" }, { id: "sub_yepnoep" }])
data: [ get "/s/admin/subscriptions.json", params: { last_record: "sub_nope" }
{ id: "sub_77777" },
{ id: "sub_yepnoep" }
]
)
get "/s/admin/subscriptions.json", params: { last_record: 'sub_nope' }
subscriptions = response.parsed_body["data"][0]["id"] subscriptions = response.parsed_body["data"][0]["id"]
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -76,65 +70,70 @@ module DiscourseSubscriptions
end end
describe "destroy" do describe "destroy" do
let(:group) { Fabricate(:group, name: 'subscribers') } let(:group) { Fabricate(:group, name: "subscribers") }
before do before { group.add(user) }
group.add(user)
end
it "deletes a customer" do it "deletes a customer" do
::Stripe::Subscription ::Stripe::Subscription
.expects(:delete) .expects(:delete)
.with('sub_12345') .with("sub_12345")
.returns( .returns(plan: { product: "pr_34578" }, customer: "c_123")
plan: { product: 'pr_34578' },
customer: 'c_123'
)
expect { expect { delete "/s/admin/subscriptions/sub_12345.json" }.to change {
delete "/s/admin/subscriptions/sub_12345.json" DiscourseSubscriptions::Customer.count
}.to change { DiscourseSubscriptions::Customer.count }.by(-1) }.by(-1)
end end
it "removes the user from the group" do it "removes the user from the group" do
::Stripe::Subscription ::Stripe::Subscription
.expects(:delete) .expects(:delete)
.with('sub_12345') .with("sub_12345")
.returns( .returns(
plan: { product: 'pr_34578', metadata: { group_name: 'subscribers' } }, plan: {
customer: 'c_123' product: "pr_34578",
metadata: {
group_name: "subscribers",
},
},
customer: "c_123",
) )
expect { expect { delete "/s/admin/subscriptions/sub_12345.json" }.to change {
delete "/s/admin/subscriptions/sub_12345.json" user.groups.count
}.to change { user.groups.count }.by(-1) }.by(-1)
end end
it "does not remove the user from the group" do it "does not remove the user from the group" do
::Stripe::Subscription ::Stripe::Subscription
.expects(:delete) .expects(:delete)
.with('sub_12345') .with("sub_12345")
.returns( .returns(
plan: { product: 'pr_34578', metadata: { group_name: 'group_does_not_exist' } }, plan: {
customer: 'c_123' product: "pr_34578",
metadata: {
group_name: "group_does_not_exist",
},
},
customer: "c_123",
) )
expect { expect { delete "/s/admin/subscriptions/sub_12345.json" }.not_to change {
delete "/s/admin/subscriptions/sub_12345.json" user.groups.count
}.not_to change { user.groups.count } }
end end
it "refunds if params[:refund] present" do it "refunds if params[:refund] present" do
::Stripe::Subscription ::Stripe::Subscription
.expects(:delete) .expects(:delete)
.with('sub_12345') .with("sub_12345")
.returns( .returns(plan: { product: "pr_34578" }, customer: "c_123")
plan: { product: 'pr_34578' }, ::Stripe::Subscription
customer: 'c_123' .expects(:retrieve)
) .with("sub_12345")
::Stripe::Subscription.expects(:retrieve).with('sub_12345').returns(latest_invoice: 'in_123') .returns(latest_invoice: "in_123")
::Stripe::Invoice.expects(:retrieve).with('in_123').returns(payment_intent: 'pi_123') ::Stripe::Invoice.expects(:retrieve).with("in_123").returns(payment_intent: "pi_123")
::Stripe::Refund.expects(:create).with({ payment_intent: 'pi_123' }) ::Stripe::Refund.expects(:create).with({ payment_intent: "pi_123" })
delete "/s/admin/subscriptions/sub_12345.json", params: { refund: true } delete "/s/admin/subscriptions/sub_12345.json", params: { refund: true }
end end

View File

@ -1,15 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
module DiscourseSubscriptions module DiscourseSubscriptions
RSpec.describe AdminController do RSpec.describe AdminController do
let(:admin) { Fabricate(:admin) } let(:admin) { Fabricate(:admin) }
before { sign_in(admin) } before { sign_in(admin) }
it 'is a subclass of AdminController' do it "is a subclass of AdminController" do
expect(DiscourseSubscriptions::AdminController < ::Admin::AdminController).to eq(true) expect(DiscourseSubscriptions::AdminController < ::Admin::AdminController).to eq(true)
end end

View File

@ -1,21 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
module DiscourseSubscriptions module DiscourseSubscriptions
RSpec.describe HooksController do RSpec.describe HooksController do
before do before { SiteSetting.discourse_subscriptions_webhook_secret = "zascharoo" }
SiteSetting.discourse_subscriptions_webhook_secret = 'zascharoo'
end
it "contructs a webhook event" do it "contructs a webhook event" do
payload = 'we-want-a-shrubbery' payload = "we-want-a-shrubbery"
headers = { HTTP_STRIPE_SIGNATURE: 'stripe-webhook-signature' } headers = { HTTP_STRIPE_SIGNATURE: "stripe-webhook-signature" }
::Stripe::Webhook ::Stripe::Webhook
.expects(:construct_event) .expects(:construct_event)
.with('we-want-a-shrubbery', 'stripe-webhook-signature', 'zascharoo') .with("we-want-a-shrubbery", "stripe-webhook-signature", "zascharoo")
.returns(type: 'something') .returns(type: "something")
post "/s/hooks.json", params: payload, headers: headers post "/s/hooks.json", params: payload, headers: headers
@ -24,65 +22,61 @@ module DiscourseSubscriptions
describe "event types" do describe "event types" do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:customer) { Fabricate(:customer, customer_id: 'c_575768', product_id: 'p_8654', user_id: user.id) } let(:customer) do
let(:group) { Fabricate(:group, name: 'subscribers-group') } Fabricate(:customer, customer_id: "c_575768", product_id: "p_8654", user_id: user.id)
end
let(:group) { Fabricate(:group, name: "subscribers-group") }
let(:event_data) do let(:event_data) do
{ {
object: { object: {
customer: customer.customer_id, customer: customer.customer_id,
plan: { product: customer.product_id, metadata: { group_name: group.name } } plan: {
} product: customer.product_id,
metadata: {
group_name: group.name,
},
},
},
} }
end end
describe "customer.subscription.updated" do describe "customer.subscription.updated" do
before do before do
event = { event = { type: "customer.subscription.updated", data: event_data }
type: 'customer.subscription.updated',
data: event_data
}
::Stripe::Webhook ::Stripe::Webhook.stubs(:construct_event).returns(event)
.stubs(:construct_event)
.returns(event)
end end
it 'is successfull' do it "is successfull" do
post "/s/hooks.json" post "/s/hooks.json"
expect(response.status).to eq 200 expect(response.status).to eq 200
end end
describe 'completing the subscription' do describe "completing the subscription" do
it 'does not add the user to the group' do it "does not add the user to the group" do
event_data[:object][:status] = 'incomplete' event_data[:object][:status] = "incomplete"
event_data[:previous_attributes] = { status: 'incomplete' } event_data[:previous_attributes] = { status: "incomplete" }
expect { expect { post "/s/hooks.json" }.not_to change { user.groups.count }
post "/s/hooks.json"
}.not_to change { user.groups.count }
expect(response.status).to eq 200 expect(response.status).to eq 200
end end
it 'does not add the user to the group' do it "does not add the user to the group" do
event_data[:object][:status] = 'incomplete' event_data[:object][:status] = "incomplete"
event_data[:previous_attributes] = { status: 'something-else' } event_data[:previous_attributes] = { status: "something-else" }
expect { expect { post "/s/hooks.json" }.not_to change { user.groups.count }
post "/s/hooks.json"
}.not_to change { user.groups.count }
expect(response.status).to eq 200 expect(response.status).to eq 200
end end
it 'adds the user to the group when completing the transaction' do it "adds the user to the group when completing the transaction" do
event_data[:object][:status] = 'complete' event_data[:object][:status] = "complete"
event_data[:previous_attributes] = { status: 'incomplete' } event_data[:previous_attributes] = { status: "incomplete" }
expect { expect { post "/s/hooks.json" }.to change { user.groups.count }.by(1)
post "/s/hooks.json"
}.to change { user.groups.count }.by(1)
expect(response.status).to eq 200 expect(response.status).to eq 200
end end
@ -91,30 +85,23 @@ module DiscourseSubscriptions
describe "customer.subscription.deleted" do describe "customer.subscription.deleted" do
before do before do
event = { event = { type: "customer.subscription.deleted", data: event_data }
type: 'customer.subscription.deleted',
data: event_data
}
::Stripe::Webhook ::Stripe::Webhook.stubs(:construct_event).returns(event)
.stubs(:construct_event)
.returns(event)
group.add(user) group.add(user)
end end
it "deletes the customer" do it "deletes the customer" do
expect { expect { post "/s/hooks.json" }.to change { DiscourseSubscriptions::Customer.count }.by(
post "/s/hooks.json" -1,
}.to change { DiscourseSubscriptions::Customer.count }.by(-1) )
expect(response.status).to eq 200 expect(response.status).to eq 200
end end
it "removes the user from the group" do it "removes the user from the group" do
expect { expect { post "/s/hooks.json" }.to change { user.groups.count }.by(-1)
post "/s/hooks.json"
}.to change { user.groups.count }.by(-1)
expect(response.status).to eq 200 expect(response.status).to eq 200
end end

View File

@ -1,11 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
module DiscourseSubscriptions module DiscourseSubscriptions
RSpec.describe SubscribeController do RSpec.describe SubscribeController do
let (:user) { Fabricate(:user) } let (:user) {
let (:campaign_user) { Fabricate(:user) } Fabricate(:user)
}
let (:campaign_user) {
Fabricate(:user)
}
context "when showing products" do context "when showing products" do
let(:product) do let(:product) do
@ -13,8 +17,9 @@ module DiscourseSubscriptions
id: "prodct_23456", id: "prodct_23456",
name: "Very Special Product", name: "Very Special Product",
metadata: { metadata: {
description: "Many people listened to my phone call with the Ukrainian President while it was being made", description:
repurchaseable: false "Many people listened to my phone call with the Ukrainian President while it was being made",
repurchaseable: false,
}, },
otherstuff: true, otherstuff: true,
} }
@ -23,10 +28,37 @@ module DiscourseSubscriptions
let(:prices) do let(:prices) do
{ {
data: [ data: [
{ id: 'plan_id123', unit_amount: 1220, currency: 'aud', recurring: { interval: 'year' }, metadata: {} }, {
{ id: 'plan_id234', unit_amount: 1399, currency: 'usd', recurring: { interval: 'year' }, metadata: {} }, id: "plan_id123",
{ id: 'plan_id678', unit_amount: 1000, currency: 'aud', recurring: { interval: 'week' }, metadata: {} } unit_amount: 1220,
] currency: "aud",
recurring: {
interval: "year",
},
metadata: {
},
},
{
id: "plan_id234",
unit_amount: 1399,
currency: "usd",
recurring: {
interval: "year",
},
metadata: {
},
},
{
id: "plan_id678",
unit_amount: 1000,
currency: "aud",
recurring: {
interval: "week",
},
metadata: {
},
},
],
} }
end end
@ -40,24 +72,36 @@ module DiscourseSubscriptions
end end
describe "#index" do describe "#index" do
it "gets products" do it "gets products" do
::Stripe::Product.expects(:list).with({ ids: product_ids, active: true }).returns(data: [product]) ::Stripe::Product
.expects(:list)
.with({ ids: product_ids, active: true })
.returns(data: [product])
get "/s.json" get "/s.json"
expect(response.parsed_body).to eq([{ expect(response.parsed_body).to eq(
[
{
"id" => "prodct_23456", "id" => "prodct_23456",
"name" => "Very Special Product", "name" => "Very Special Product",
"description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), "description" =>
PrettyText.cook(
"Many people listened to my phone call with the Ukrainian President while it was being made",
),
"subscribed" => false, "subscribed" => false,
"repurchaseable" => false, "repurchaseable" => false,
}]) },
],
)
end end
it "is subscribed" do it "is subscribed" do
Fabricate(:customer, product_id: product[:id], user_id: user.id, customer_id: 'x') Fabricate(:customer, product_id: product[:id], user_id: user.id, customer_id: "x")
::Stripe::Product.expects(:list).with({ ids: product_ids, active: true }).returns(data: [product]) ::Stripe::Product
.expects(:list)
.with({ ids: product_ids, active: true })
.returns(data: [product])
get "/s.json" get "/s.json"
data = response.parsed_body data = response.parsed_body
@ -66,7 +110,10 @@ module DiscourseSubscriptions
it "is not subscribed" do it "is not subscribed" do
::DiscourseSubscriptions::Customer.delete_all ::DiscourseSubscriptions::Customer.delete_all
::Stripe::Product.expects(:list).with({ ids: product_ids, active: true }).returns(data: [product]) ::Stripe::Product
.expects(:list)
.with({ ids: product_ids, active: true })
.returns(data: [product])
get "/s.json" get "/s.json"
data = response.parsed_body data = response.parsed_body
@ -77,11 +124,16 @@ module DiscourseSubscriptions
describe "#get_contributors" do describe "#get_contributors" do
before do before do
Fabricate(:product, external_id: "prod_campaign") Fabricate(:product, external_id: "prod_campaign")
Fabricate(:customer, product_id: "prodct_23456", user_id: user.id, customer_id: 'x') Fabricate(:customer, product_id: "prodct_23456", user_id: user.id, customer_id: "x")
Fabricate(:customer, product_id: "prod_campaign", user_id: campaign_user.id, customer_id: 'y') Fabricate(
:customer,
product_id: "prod_campaign",
user_id: campaign_user.id,
customer_id: "y",
)
end end
context 'when not showing contributors' do context "when not showing contributors" do
it 'returns nothing if not set to show contributors' do it "returns nothing if not set to show contributors" do
SiteSetting.discourse_subscriptions_campaign_show_contributors = false SiteSetting.discourse_subscriptions_campaign_show_contributors = false
get "/s/contributors.json" get "/s/contributors.json"
@ -90,12 +142,10 @@ module DiscourseSubscriptions
end end
end end
context 'when showing contributors' do context "when showing contributors" do
before do before { SiteSetting.discourse_subscriptions_campaign_show_contributors = true }
SiteSetting.discourse_subscriptions_campaign_show_contributors = true
end
it 'filters users by campaign product if set' do it "filters users by campaign product if set" do
SiteSetting.discourse_subscriptions_campaign_product = "prod_campaign" SiteSetting.discourse_subscriptions_campaign_product = "prod_campaign"
get "/s/contributors.json" get "/s/contributors.json"
@ -105,7 +155,7 @@ module DiscourseSubscriptions
expect(data.length).to eq 1 expect(data.length).to eq 1
end end
it 'shows all purchases if campaign product not set' do it "shows all purchases if campaign product not set" do
SiteSetting.discourse_subscriptions_campaign_product = nil SiteSetting.discourse_subscriptions_campaign_product = nil
get "/s/contributors.json" get "/s/contributors.json"
@ -117,25 +167,54 @@ module DiscourseSubscriptions
end end
describe "#show" do describe "#show" do
it 'retrieves the product' do it "retrieves the product" do
::Stripe::Product.expects(:retrieve).with('prod_walterwhite').returns(product) ::Stripe::Product.expects(:retrieve).with("prod_walterwhite").returns(product)
::Stripe::Price.expects(:list).with(active: true, product: 'prod_walterwhite').returns(prices) ::Stripe::Price
.expects(:list)
.with(active: true, product: "prod_walterwhite")
.returns(prices)
get "/s/prod_walterwhite.json" get "/s/prod_walterwhite.json"
expect(response.parsed_body).to eq({ expect(response.parsed_body).to eq(
{
"product" => { "product" => {
"id" => "prodct_23456", "id" => "prodct_23456",
"name" => "Very Special Product", "name" => "Very Special Product",
"description" => PrettyText.cook("Many people listened to my phone call with the Ukrainian President while it was being made"), "description" =>
PrettyText.cook(
"Many people listened to my phone call with the Ukrainian President while it was being made",
),
"subscribed" => false, "subscribed" => false,
"repurchaseable" => false "repurchaseable" => false,
}, },
"plans" => [ "plans" => [
{ "currency" => "aud", "id" => "plan_id123", "recurring" => { "interval" => "year" }, "unit_amount" => 1220 }, {
{ "currency" => "usd", "id" => "plan_id234", "recurring" => { "interval" => "year" }, "unit_amount" => 1399 }, "currency" => "aud",
{ "currency" => "aud", "id" => "plan_id678", "recurring" => { "interval" => "week" }, "unit_amount" => 1000 } "id" => "plan_id123",
] "recurring" => {
}) "interval" => "year",
},
"unit_amount" => 1220,
},
{
"currency" => "usd",
"id" => "plan_id234",
"recurring" => {
"interval" => "year",
},
"unit_amount" => 1399,
},
{
"currency" => "aud",
"id" => "plan_id678",
"recurring" => {
"interval" => "week",
},
"unit_amount" => 1000,
},
],
},
)
end end
end end
end end
@ -146,81 +225,91 @@ module DiscourseSubscriptions
::Stripe::Customer.expects(:create).never ::Stripe::Customer.expects(:create).never
::Stripe::Price.expects(:retrieve).never ::Stripe::Price.expects(:retrieve).never
::Stripe::Subscription.expects(:create).never ::Stripe::Subscription.expects(:create).never
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
end end
end end
context "when authenticated" do context "when authenticated" do
before do before { sign_in(user) }
sign_in(user)
end
describe "#create" do describe "#create" do
before do before { ::Stripe::Customer.expects(:create).returns(id: "cus_1234") }
::Stripe::Customer.expects(:create).returns(id: 'cus_1234')
end
it "creates a subscription" do it "creates a subscription" do
::Stripe::Price.expects(:retrieve).returns( ::Stripe::Price.expects(:retrieve).returns(
type: 'recurring', type: "recurring",
product: 'product_12345', product: "product_12345",
metadata: { metadata: {
group_name: 'awesome', group_name: "awesome",
trial_period_days: 0 trial_period_days: 0,
} },
) )
::Stripe::Subscription.expects(:create).with( ::Stripe::Subscription
customer: 'cus_1234', .expects(:create)
items: [ price: 'plan_1234' ], .with(
metadata: { user_id: user.id, username: user.username_lower }, customer: "cus_1234",
items: [price: "plan_1234"],
metadata: {
user_id: user.id,
username: user.username_lower,
},
trial_period_days: 0, trial_period_days: 0,
promotion_code: nil promotion_code: nil,
).returns(status: 'active', customer: 'cus_1234') )
.returns(status: "active", customer: "cus_1234")
expect { expect {
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { DiscourseSubscriptions::Customer.count } }.to change { DiscourseSubscriptions::Customer.count }
end end
it "creates a one time payment subscription" do it "creates a one time payment subscription" do
::Stripe::Price.expects(:retrieve).returns( ::Stripe::Price.expects(:retrieve).returns(
type: 'one_time', type: "one_time",
product: 'product_12345', product: "product_12345",
metadata: { metadata: {
group_name: 'awesome' group_name: "awesome",
} },
) )
::Stripe::InvoiceItem.expects(:create) ::Stripe::InvoiceItem.expects(:create)
::Stripe::Invoice.expects(:create).returns(status: 'open', id: 'in_123') ::Stripe::Invoice.expects(:create).returns(status: "open", id: "in_123")
::Stripe::Invoice.expects(:finalize_invoice).returns(id: 'in_123', status: 'open', payment_intent: 'pi_123') ::Stripe::Invoice.expects(:finalize_invoice).returns(
id: "in_123",
status: "open",
payment_intent: "pi_123",
)
::Stripe::Invoice.expects(:retrieve).returns(id: 'in_123', status: 'open', payment_intent: 'pi_123') ::Stripe::Invoice.expects(:retrieve).returns(
id: "in_123",
status: "open",
payment_intent: "pi_123",
)
::Stripe::PaymentIntent.expects(:retrieve).returns(status: 'successful') ::Stripe::PaymentIntent.expects(:retrieve).returns(status: "successful")
::Stripe::Invoice.expects(:pay).returns(status: 'paid', customer: 'cus_1234') ::Stripe::Invoice.expects(:pay).returns(status: "paid", customer: "cus_1234")
expect { expect {
post '/s/create.json', params: { plan: 'plan_1234', source: 'tok_1234' } post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { DiscourseSubscriptions::Customer.count } }.to change { DiscourseSubscriptions::Customer.count }
end end
it "creates a customer model" do it "creates a customer model" do
::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: {}).twice ::Stripe::Price.expects(:retrieve).returns(type: "recurring", metadata: {}).twice
::Stripe::Subscription.expects(:create).returns(status: 'active', customer: 'cus_1234') ::Stripe::Subscription.expects(:create).returns(status: "active", customer: "cus_1234")
expect { expect {
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { DiscourseSubscriptions::Customer.count } }.to change { DiscourseSubscriptions::Customer.count }
::Stripe::Customer.expects(:retrieve).with('cus_1234') ::Stripe::Customer.expects(:retrieve).with("cus_1234")
expect { expect {
post "/s/create.json", params: { plan: 'plan_5678', source: 'tok_5678' } post "/s/create.json", params: { plan: "plan_5678", source: "tok_5678" }
}.not_to change { DiscourseSubscriptions::Customer.count } }.not_to change { DiscourseSubscriptions::Customer.count }
end end
@ -228,19 +317,22 @@ module DiscourseSubscriptions
context "with invalid code" do context "with invalid code" do
it "prevents use of invalid coupon codes" do it "prevents use of invalid coupon codes" do
::Stripe::Price.expects(:retrieve).returns( ::Stripe::Price.expects(:retrieve).returns(
type: 'recurring', type: "recurring",
product: 'product_12345', product: "product_12345",
metadata: { metadata: {
group_name: 'awesome', group_name: "awesome",
trial_period_days: 0 trial_period_days: 0,
},
)
::Stripe::PromotionCode.expects(:list).with({ code: "invalid" }).returns(data: [])
post "/s/create.json",
params: {
plan: "plan_1234",
source: "tok_1234",
promo: "invalid",
} }
)
::Stripe::PromotionCode.expects(:list).with({ code: 'invalid' }).returns(
data: []
)
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234', promo: 'invalid' }
data = response.parsed_body data = response.parsed_body
expect(data["errors"]).not_to be_blank expect(data["errors"]).not_to be_blank
@ -249,61 +341,86 @@ module DiscourseSubscriptions
context "with valid code" do context "with valid code" do
before do before do
::Stripe::PromotionCode.expects(:list).with({ code: '123' }).returns( ::Stripe::PromotionCode
data: [{ .expects(:list)
id: 'promo123', .with({ code: "123" })
coupon: { id: 'c123' } .returns(data: [{ id: "promo123", coupon: { id: "c123" } }])
}]
)
end end
it "applies promo code to recurring subscription" do it "applies promo code to recurring subscription" do
::Stripe::Price.expects(:retrieve).returns( ::Stripe::Price.expects(:retrieve).returns(
type: 'recurring', type: "recurring",
product: 'product_12345', product: "product_12345",
metadata: { metadata: {
group_name: 'awesome', group_name: "awesome",
trial_period_days: 0 trial_period_days: 0,
} },
) )
::Stripe::Subscription.expects(:create).with( ::Stripe::Subscription
customer: 'cus_1234', .expects(:create)
items: [ price: 'plan_1234' ], .with(
metadata: { user_id: user.id, username: user.username_lower }, customer: "cus_1234",
items: [price: "plan_1234"],
metadata: {
user_id: user.id,
username: user.username_lower,
},
trial_period_days: 0, trial_period_days: 0,
promotion_code: 'promo123' promotion_code: "promo123",
).returns(status: 'active', customer: 'cus_1234') )
.returns(status: "active", customer: "cus_1234")
expect { expect {
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234', promo: '123' } post "/s/create.json",
params: {
plan: "plan_1234",
source: "tok_1234",
promo: "123",
}
}.to change { DiscourseSubscriptions::Customer.count } }.to change { DiscourseSubscriptions::Customer.count }
end end
it "applies promo code to one time purchase" do it "applies promo code to one time purchase" do
::Stripe::Price.expects(:retrieve).returns( ::Stripe::Price.expects(:retrieve).returns(
type: 'one_time', type: "one_time",
product: 'product_12345', product: "product_12345",
metadata: { metadata: {
group_name: 'awesome' group_name: "awesome",
} },
) )
::Stripe::InvoiceItem.expects(:create).with(customer: 'cus_1234', price: 'plan_1234', discounts: [{ coupon: 'c123' }]) ::Stripe::InvoiceItem.expects(:create).with(
customer: "cus_1234",
price: "plan_1234",
discounts: [{ coupon: "c123" }],
)
::Stripe::Invoice.expects(:create).returns(status: 'open', id: 'in_123') ::Stripe::Invoice.expects(:create).returns(status: "open", id: "in_123")
::Stripe::Invoice.expects(:finalize_invoice).returns(id: 'in_123', status: 'open', payment_intent: 'pi_123') ::Stripe::Invoice.expects(:finalize_invoice).returns(
id: "in_123",
status: "open",
payment_intent: "pi_123",
)
::Stripe::Invoice.expects(:retrieve).returns(id: 'in_123', status: 'open', payment_intent: 'pi_123') ::Stripe::Invoice.expects(:retrieve).returns(
id: "in_123",
status: "open",
payment_intent: "pi_123",
)
::Stripe::PaymentIntent.expects(:retrieve).returns(status: 'successful') ::Stripe::PaymentIntent.expects(:retrieve).returns(status: "successful")
::Stripe::Invoice.expects(:pay).returns(status: 'paid', customer: 'cus_1234') ::Stripe::Invoice.expects(:pay).returns(status: "paid", customer: "cus_1234")
expect { expect {
post '/s/create.json', params: { plan: 'plan_1234', source: 'tok_1234', promo: '123' } post "/s/create.json",
params: {
plan: "plan_1234",
source: "tok_1234",
promo: "123",
}
}.to change { DiscourseSubscriptions::Customer.count } }.to change { DiscourseSubscriptions::Customer.count }
end end
end end
@ -313,81 +430,114 @@ module DiscourseSubscriptions
describe "#finalize strong customer authenticated transaction" do describe "#finalize strong customer authenticated transaction" do
context "with subscription" do context "with subscription" do
it "finalizes the subscription" do it "finalizes the subscription" do
::Stripe::Price.expects(:retrieve).returns(id: "plan_1234", product: "prod_1234", metadata: {}) ::Stripe::Price.expects(:retrieve).returns(
::Stripe::Subscription.expects(:retrieve).returns(id: "sub_123", customer: 'cus_1234', status: "active") id: "plan_1234",
product: "prod_1234",
metadata: {
},
)
::Stripe::Subscription.expects(:retrieve).returns(
id: "sub_123",
customer: "cus_1234",
status: "active",
)
expect { expect {
post "/s/finalize.json", params: { plan: 'plan_1234', transaction: 'sub_1234' } post "/s/finalize.json", params: { plan: "plan_1234", transaction: "sub_1234" }
}.to change { DiscourseSubscriptions::Customer.count } }.to change { DiscourseSubscriptions::Customer.count }
end end
end end
context "with one-time payment" do context "with one-time payment" do
it "finalizes the one-time payment" do it "finalizes the one-time payment" do
::Stripe::Price.expects(:retrieve).returns(id: "plan_1234", product: "prod_1234", metadata: {}) ::Stripe::Price.expects(:retrieve).returns(
::Stripe::Invoice.expects(:retrieve).returns(id: "in_123", customer: 'cus_1234', status: "paid") id: "plan_1234",
product: "prod_1234",
metadata: {
},
)
::Stripe::Invoice.expects(:retrieve).returns(
id: "in_123",
customer: "cus_1234",
status: "paid",
)
expect { expect {
post "/s/finalize.json", params: { plan: 'plan_1234', transaction: 'in_1234' } post "/s/finalize.json", params: { plan: "plan_1234", transaction: "in_1234" }
}.to change { DiscourseSubscriptions::Customer.count } }.to change { DiscourseSubscriptions::Customer.count }
end end
end end
end end
describe "user groups" do describe "user groups" do
let(:group_name) { 'group-123' } let(:group_name) { "group-123" }
let(:group) { Fabricate(:group, name: group_name) } let(:group) { Fabricate(:group, name: group_name) }
context "with unauthorized group" do context "with unauthorized group" do
before do before do
::Stripe::Customer.expects(:create).returns(id: 'cus_1234') ::Stripe::Customer.expects(:create).returns(id: "cus_1234")
::Stripe::Subscription.expects(:create).returns(status: 'active') ::Stripe::Subscription.expects(:create).returns(status: "active")
end end
it "does not add the user to the admins group" do it "does not add the user to the admins group" do
::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: { group_name: 'admins' }) ::Stripe::Price.expects(:retrieve).returns(
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } type: "recurring",
metadata: {
group_name: "admins",
},
)
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
expect(user.admin).to eq false expect(user.admin).to eq false
end end
it "does not add the user to other group" do it "does not add the user to other group" do
::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: { group_name: 'other' }) ::Stripe::Price.expects(:retrieve).returns(
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } type: "recurring",
metadata: {
group_name: "other",
},
)
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
expect(user.groups).to be_empty expect(user.groups).to be_empty
end end
end end
context "when plan has group in metadata" do context "when plan has group in metadata" do
before do before do
::Stripe::Customer.expects(:create).returns(id: 'cus_1234') ::Stripe::Customer.expects(:create).returns(id: "cus_1234")
::Stripe::Price.expects(:retrieve).returns(type: 'recurring', metadata: { group_name: group_name }) ::Stripe::Price.expects(:retrieve).returns(
type: "recurring",
metadata: {
group_name: group_name,
},
)
end end
it "does not add the user to the group when subscription fails" do it "does not add the user to the group when subscription fails" do
::Stripe::Subscription.expects(:create).returns(status: 'failed') ::Stripe::Subscription.expects(:create).returns(status: "failed")
expect { expect {
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.not_to change { group.users.count } }.not_to change { group.users.count }
expect(user.groups).to be_empty expect(user.groups).to be_empty
end end
it "adds the user to the group when the subscription is active" do it "adds the user to the group when the subscription is active" do
::Stripe::Subscription.expects(:create).returns(status: 'active') ::Stripe::Subscription.expects(:create).returns(status: "active")
expect { expect {
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { group.users.count } }.to change { group.users.count }
expect(user.groups).not_to be_empty expect(user.groups).not_to be_empty
end end
it "adds the user to the group when the subscription is trialing" do it "adds the user to the group when the subscription is trialing" do
::Stripe::Subscription.expects(:create).returns(status: 'trialing') ::Stripe::Subscription.expects(:create).returns(status: "trialing")
expect { expect {
post "/s/create.json", params: { plan: 'plan_1234', source: 'tok_1234' } post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { group.users.count } }.to change { group.users.count }
expect(user.groups).not_to be_empty expect(user.groups).not_to be_empty

View File

@ -1,10 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
module DiscourseSubscriptions module DiscourseSubscriptions
RSpec.describe User::PaymentsController do RSpec.describe User::PaymentsController do
it 'is a subclass of ApplicationController' do it "is a subclass of ApplicationController" do
expect(DiscourseSubscriptions::User::PaymentsController < ::ApplicationController).to eq(true) expect(DiscourseSubscriptions::User::PaymentsController < ::ApplicationController).to eq(true)
end end
@ -17,79 +17,38 @@ module DiscourseSubscriptions
end end
context "when authenticated" do context "when authenticated" do
let(:user) { Fabricate(:user, email: 'zasch@example.com') } let(:user) { Fabricate(:user, email: "zasch@example.com") }
before do before do
sign_in(user) sign_in(user)
Fabricate(:customer, customer_id: 'c_345678', user_id: user.id) Fabricate(:customer, customer_id: "c_345678", user_id: user.id)
Fabricate(:product, external_id: 'prod_8675309') Fabricate(:product, external_id: "prod_8675309")
Fabricate(:product, external_id: 'prod_8675310') Fabricate(:product, external_id: "prod_8675310")
end end
it "gets payment intents" do it "gets payment intents" do
created_time = Time.now created_time = Time.now
::Stripe::Invoice.expects(:list).with( ::Stripe::Invoice
customer: 'c_345678' .expects(:list)
).returns( .with(customer: "c_345678")
.returns(
data: [ data: [
{ { id: "inv_900007", lines: { data: [plan: { product: "prod_8675309" }] } },
id: "inv_900007", { id: "inv_900008", lines: { data: [plan: { product: "prod_8675310" }] } },
lines: { { id: "inv_900008", lines: { data: [plan: { product: "prod_8675310" }] } },
data: [ ],
plan: {
product: "prod_8675309"
}
]
}
},
{
id: "inv_900008",
lines: {
data: [
plan: {
product: "prod_8675310"
}
]
}
},
{
id: "inv_900008",
lines: {
data: [
plan: {
product: "prod_8675310"
}
]
}
},
]
) )
::Stripe::PaymentIntent.expects(:list).with( ::Stripe::PaymentIntent
customer: 'c_345678', .expects(:list)
).returns( .with(customer: "c_345678")
.returns(
data: [ data: [
{ { id: "pi_900008", invoice: "inv_900008", created: created_time },
id: "pi_900008", { id: "pi_900008", invoice: "inv_900008", created: created_time },
invoice: "inv_900008", { id: "pi_900007", invoice: "inv_900007", created: Time.now },
created: created_time { id: "pi_007", invoice: "inv_007", created: Time.now },
}, ],
{
id: "pi_900008",
invoice: "inv_900008",
created: created_time
},
{
id: "pi_900007",
invoice: "inv_900007",
created: Time.now
},
{
id: "pi_007",
invoice: "inv_007",
created: Time.now
}
]
) )
get "/s/user/payments.json" get "/s/user/payments.json"
@ -99,9 +58,7 @@ module DiscourseSubscriptions
expect(invoice).to eq("inv_900007") expect(invoice).to eq("inv_900007")
expect(parsed_body.count).to eq(2) expect(parsed_body.count).to eq(2)
end end
end end
end end
end end

View File

@ -1,11 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
module DiscourseSubscriptions module DiscourseSubscriptions
RSpec.describe User::SubscriptionsController do RSpec.describe User::SubscriptionsController do
it 'is a subclass of ApplicationController' do it "is a subclass of ApplicationController" do
expect(DiscourseSubscriptions::User::SubscriptionsController < ::ApplicationController).to eq(true) expect(DiscourseSubscriptions::User::SubscriptionsController < ::ApplicationController).to eq(
true,
)
end end
context "when not authenticated" do context "when not authenticated" do
@ -27,8 +29,10 @@ module DiscourseSubscriptions
end end
context "when authenticated" do context "when authenticated" do
let(:user) { Fabricate(:user, email: 'beanie@example.com') } let(:user) { Fabricate(:user, email: "beanie@example.com") }
let(:customer) { Fabricate(:customer, user_id: user.id, customer_id: "cus_23456", product_id: "prod_123") } let(:customer) do
Fabricate(:customer, user_id: user.id, customer_id: "cus_23456", product_id: "prod_123")
end
before do before do
sign_in(user) sign_in(user)
@ -39,42 +43,35 @@ module DiscourseSubscriptions
let(:plans) do let(:plans) do
{ {
data: [ data: [
{ { id: "plan_1", product: { name: "ACME Subscriptions" } },
id: "plan_1", { id: "plan_2", product: { name: "ACME Other Subscriptions" } },
product: { name: 'ACME Subscriptions' }, ],
},
{
id: "plan_2",
product: { name: 'ACME Other Subscriptions' },
}
]
} }
end end
let(:customers) do let(:customers) do
{ {
data: [{ data: [
{
id: "cus_23456", id: "cus_23456",
subscriptions: { subscriptions: {
data: [ data: [
{ id: "sub_1234", items: { data: [price: { id: "plan_1" }] } }, { id: "sub_1234", items: { data: [price: { id: "plan_1" }] } },
{ id: "sub_4567", items: { data: [price: { id: "plan_2" }] } } { id: "sub_4567", items: { data: [price: { id: "plan_2" }] } },
] ],
}, },
}] },
],
} }
end end
it "gets subscriptions" do it "gets subscriptions" do
::Stripe::Price.expects(:list).with( ::Stripe::Price.expects(:list).with(expand: ["data.product"], limit: 100).returns(plans)
expand: ['data.product'],
limit: 100
).returns(plans)
::Stripe::Customer.expects(:list).with( ::Stripe::Customer
email: user.email, .expects(:list)
expand: ['data.subscriptions'] .with(email: user.email, expand: ["data.subscriptions"])
).returns(customers) .returns(customers)
get "/s/user/subscriptions.json" get "/s/user/subscriptions.json"
@ -82,9 +79,18 @@ module DiscourseSubscriptions
expect(subscription).to eq( expect(subscription).to eq(
"id" => "sub_1234", "id" => "sub_1234",
"items" => { "data" => [{ "price" => { "id" => "plan_1" } }] }, "items" => {
"plan" => { "id" => "plan_1", "product" => { "name" => "ACME Subscriptions" } }, "data" => [{ "price" => { "id" => "plan_1" } }],
"product" => { "name" => "ACME Subscriptions" } },
"plan" => {
"id" => "plan_1",
"product" => {
"name" => "ACME Subscriptions",
},
},
"product" => {
"name" => "ACME Subscriptions",
},
) )
end end
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
describe SiteSerializer do describe SiteSerializer do
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
@ -11,22 +11,22 @@ describe SiteSerializer do
SiteSetting.discourse_subscriptions_enabled = true SiteSetting.discourse_subscriptions_enabled = true
SiteSetting.discourse_subscriptions_campaign_enabled = true SiteSetting.discourse_subscriptions_campaign_enabled = true
end end
it 'is false if the goal_met date is < 7 days old' do it "is false if the goal_met date is < 7 days old" do
Discourse.redis.set('subscriptions_goal_met_date', 10.days.ago) Discourse.redis.set("subscriptions_goal_met_date", 10.days.ago)
data = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json data = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
expect(data[:show_campaign_banner]).to be false expect(data[:show_campaign_banner]).to be false
end end
it 'is true if the goal_met date is > 7 days old' do it "is true if the goal_met date is > 7 days old" do
Discourse.redis.set('subscriptions_goal_met_date', 1.days.ago) Discourse.redis.set("subscriptions_goal_met_date", 1.days.ago)
data = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json data = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
expect(data[:show_campaign_banner]).to be true expect(data[:show_campaign_banner]).to be true
end end
it 'fails gracefully if the goal_met date is invalid' do it "fails gracefully if the goal_met date is invalid" do
Discourse.redis.set('subscriptions_goal_met_date', 'bananas') Discourse.redis.set("subscriptions_goal_met_date", "bananas")
data = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json data = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
expect(data[:show_campaign_banner]).to be false expect(data[:show_campaign_banner]).to be false
end end

View File

@ -1,11 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require "rails_helper"
describe DiscourseSubscriptions::Campaign do describe DiscourseSubscriptions::Campaign do
describe 'campaign data is refreshed' do describe "campaign data is refreshed" do
let (:user) { Fabricate(:user) } let (:user) {
let (:user2) { Fabricate(:user) } Fabricate(:user)
}
let (:user2) {
Fabricate(:user)
}
let(:subscription) do let(:subscription) do
{ {
id: "sub_1234", id: "sub_1234",
@ -16,12 +20,12 @@ describe DiscourseSubscriptions::Campaign do
product: "prodct_23456", product: "prodct_23456",
unit_amount: 1000, unit_amount: 1000,
recurring: { recurring: {
interval: "month" interval: "month",
} },
} },
} },
] ],
} },
} }
end end
let(:invoice) do let(:invoice) do
@ -37,10 +41,10 @@ describe DiscourseSubscriptions::Campaign do
active: true, active: true,
unit_amount: 1000, unit_amount: 1000,
recurring: nil, recurring: nil,
} },
} },
] ],
} },
} }
end end
let(:invoice2) do let(:invoice2) do
@ -56,20 +60,20 @@ describe DiscourseSubscriptions::Campaign do
active: true, active: true,
unit_amount: 600, unit_amount: 600,
recurring: nil, recurring: nil,
} },
} },
] ],
} },
} }
end end
before do before do
Fabricate(:product, external_id: "prodct_23456") Fabricate(:product, external_id: "prodct_23456")
Fabricate(:customer, product_id: "prodct_23456", user_id: user.id, customer_id: 'x') Fabricate(:customer, product_id: "prodct_23456", user_id: user.id, customer_id: "x")
Fabricate(:product, external_id: "prodct_65432") Fabricate(:product, external_id: "prodct_65432")
Fabricate(:customer, product_id: "prodct_65432", user_id: user2.id, customer_id: 'y') Fabricate(:customer, product_id: "prodct_65432", user_id: user2.id, customer_id: "y")
Fabricate(:product, external_id: "prodct_65433") Fabricate(:product, external_id: "prodct_65433")
Fabricate(:customer, product_id: "prodct_65433", user_id: user2.id, customer_id: 'y') Fabricate(:customer, product_id: "prodct_65433", user_id: user2.id, customer_id: "y")
SiteSetting.discourse_subscriptions_public_key = "public-key" SiteSetting.discourse_subscriptions_public_key = "public-key"
SiteSetting.discourse_subscriptions_secret_key = "secret-key" SiteSetting.discourse_subscriptions_secret_key = "secret-key"
end end
@ -92,17 +96,17 @@ describe DiscourseSubscriptions::Campaign do
::Stripe::Invoice.expects(:list).returns(data: [invoice], has_more: false) ::Stripe::Invoice.expects(:list).returns(data: [invoice], has_more: false)
DiscourseSubscriptions::Campaign.new.refresh_data DiscourseSubscriptions::Campaign.new.refresh_data
expect(Discourse.redis.get('subscriptions_goal_met_date')).to be_present expect(Discourse.redis.get("subscriptions_goal_met_date")).to be_present
end end
it "checks if goal is < 90% met after being met" do it "checks if goal is < 90% met after being met" do
SiteSetting.discourse_subscriptions_campaign_goal = 25 SiteSetting.discourse_subscriptions_campaign_goal = 25
Discourse.redis.set('subscriptions_goal_met_date', 10.days.ago) Discourse.redis.set("subscriptions_goal_met_date", 10.days.ago)
::Stripe::Subscription.expects(:list).returns(data: [subscription], has_more: false) ::Stripe::Subscription.expects(:list).returns(data: [subscription], has_more: false)
::Stripe::Invoice.expects(:list).returns(data: [invoice], has_more: false) ::Stripe::Invoice.expects(:list).returns(data: [invoice], has_more: false)
DiscourseSubscriptions::Campaign.new.refresh_data DiscourseSubscriptions::Campaign.new.refresh_data
expect(Discourse.redis.get('subscriptions_goal_met_date')).to be_blank expect(Discourse.redis.get("subscriptions_goal_met_date")).to be_blank
end end
end end
@ -116,25 +120,28 @@ describe DiscourseSubscriptions::Campaign do
{ {
price: { price: {
product: "prod_use", product: "prod_use",
unit_amount: 10000, unit_amount: 10_000,
recurring: { recurring: {
interval: "year" interval: "year",
} },
} },
} },
] ],
} },
} }
end end
before do before do
Fabricate(:product, external_id: "prod_use") Fabricate(:product, external_id: "prod_use")
Fabricate(:customer, product_id: "prod_use", user_id: user2.id, customer_id: 'y') Fabricate(:customer, product_id: "prod_use", user_id: user2.id, customer_id: "y")
SiteSetting.discourse_subscriptions_campaign_product = "prod_use" SiteSetting.discourse_subscriptions_campaign_product = "prod_use"
end end
it "refreshes campaign data with only the campaign product/subscriptions" do it "refreshes campaign data with only the campaign product/subscriptions" do
::Stripe::Subscription.expects(:list).returns(data: [subscription, campaign_subscription], has_more: false) ::Stripe::Subscription.expects(:list).returns(
data: [subscription, campaign_subscription],
has_more: false,
)
::Stripe::Invoice.expects(:list).returns(data: [invoice], has_more: false) ::Stripe::Invoice.expects(:list).returns(data: [invoice], has_more: false)
DiscourseSubscriptions::Campaign.new.refresh_data DiscourseSubscriptions::Campaign.new.refresh_data