Author SHA1 Message Date
Nat b1f303a1c8 UX: Surface stripe AuthenticationError on the Admin Dashboard 2024-01-04 15:52:24 +08:00
217 changed files with 7216 additions and 10200 deletions
-5
View File
@@ -1,7 +1,2 @@
< 3.5.0.beta5-dev: 7309c937dddb1933ac66aa2d3e13d071060d30ff
< 3.5.0.beta1-dev: b4f7be4274ada9400238f8692e705669034c1709
< 3.4.0.beta2-dev: 692f410ab59e329d0aaf537cd772698f83c32a8a
< 3.4.0.beta1-dev: bb96d77056a9ef842e01e46fd980320d427073ad
< 3.3.0.beta1-dev: 0dcf17de59bdf82c0233bb630dad0ce407d1ae76
3.1.999: c4f8220a5572ef9a42a9a71500e001d0df206c43 3.1.999: c4f8220a5572ef9a42a9a71500e001d0df206c43
2.9.0.beta9: 39b7253c082270e33839f4b0821fbf75abfdd826 2.9.0.beta9: 39b7253c082270e33839f4b0821fbf75abfdd826
+6
View File
@@ -0,0 +1,6 @@
{
"globals": {
"Stripe": true
},
"extends": "eslint-config-discourse"
}
-2
View File
@@ -1,2 +0,0 @@
engine-strict = true
auto-install-peers = false
+1
View File
@@ -0,0 +1 @@
{}
-1
View File
@@ -1 +0,0 @@
module.exports = require("@discourse/lint-configs/prettier");
-1
View File
@@ -1 +0,0 @@
module.exports = require("@discourse/lint-configs/template-lint");
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
plugins: ["ember-template-lint-plugin-discourse"],
extends: "discourse:recommended",
};
+24 -83
View File
@@ -7,95 +7,36 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
activesupport (8.0.2) ast (2.4.2)
base64 json (2.6.2)
benchmark (>= 0.3) parallel (1.22.1)
bigdecimal parser (3.1.2.1)
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
ast (2.4.3)
base64 (0.3.0)
benchmark (0.4.1)
bigdecimal (3.2.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
drb (2.2.3)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
json (2.12.2)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
minitest (5.25.5)
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc prettier_print (1.2.0)
prettier_print (1.2.1)
prism (1.4.0)
racc (1.8.1)
rack (3.1.16)
rainbow (3.1.1) rainbow (3.1.1)
regexp_parser (2.10.0) regexp_parser (2.6.0)
rubocop (1.75.8) rexml (3.2.5)
rubocop (1.36.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.1.2.1)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 1.8, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.20.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.44.1) rubocop-ast (1.21.0)
parser (>= 3.3.7.2) parser (>= 3.1.1.0)
prism (~> 1.4) rubocop-discourse (3.0)
rubocop-capybara (2.22.1) rubocop (>= 1.1.0)
lint_roller (~> 1.1) rubocop-rspec (>= 2.0.0)
rubocop (~> 1.72, >= 1.72.1) rubocop-rspec (2.13.2)
rubocop-discourse (3.12.1) rubocop (~> 1.33)
activesupport (>= 6.1) ruby-progressbar (1.11.0)
lint_roller (>= 1.1.0) syntax_tree (5.1.0)
rubocop (>= 1.73.2)
rubocop-capybara (>= 2.22.0)
rubocop-factory_bot (>= 2.27.0)
rubocop-rails (>= 2.30.3)
rubocop-rspec (>= 3.0.1)
rubocop-rspec_rails (>= 2.31.0)
rubocop-factory_bot (2.27.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rails (2.32.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.6.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
syntax_tree (6.2.0)
prettier_print (>= 1.2.0) prettier_print (>= 1.2.0)
tzinfo (2.0.6) unicode-display_width (2.3.0)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
PLATFORMS PLATFORMS
ruby ruby
@@ -106,4 +47,4 @@ DEPENDENCIES
translations-manager! translations-manager!
BUNDLED WITH BUNDLED WITH
2.6.9 2.3.4
-3
View File
@@ -1,8 +1,5 @@
# Discourse Subscriptions Plugin # Discourse Subscriptions Plugin
> [!IMPORTANT]
> This plugin has now been bundled into Discourse core. See: https://meta.discourse.org/t/bundling-more-popular-plugins-with-discourse-core/373574
Discourse Subscriptions allows site owners to sell recurring and one-time purchase subscriptions that grant access to a group on a Discourse instance. Discourse Subscriptions allows site owners to sell recurring and one-time purchase subscriptions that grant access to a group on a Discourse instance.
For more information, please see: https://meta.discourse.org/t/discourse-subscriptions-plugin/140818 For more information, please see: https://meta.discourse.org/t/discourse-subscriptions-plugin/140818
+2
View File
@@ -4,6 +4,8 @@ module DiscourseSubscriptions
module Stripe module Stripe
extend ActiveSupport::Concern extend ActiveSupport::Concern
AUTH_PROBLEM_IDENTIFIER = "#{DiscourseSubscriptions::PLUGIN_NAME}.stripe_auth_error"
def set_api_key def set_api_key
::Stripe.api_key = SiteSetting.discourse_subscriptions_secret_key ::Stripe.api_key = SiteSetting.discourse_subscriptions_secret_key
end end
@@ -5,9 +5,6 @@ module DiscourseSubscriptions
class CouponsController < ::Admin::AdminController class CouponsController < ::Admin::AdminController
include DiscourseSubscriptions::Stripe include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group include DiscourseSubscriptions::Group
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key before_action :set_api_key
def index def index
@@ -5,8 +5,6 @@ module DiscourseSubscriptions
class PlansController < ::Admin::AdminController class PlansController < ::Admin::AdminController
include DiscourseSubscriptions::Stripe include DiscourseSubscriptions::Stripe
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key before_action :set_api_key
def index def index
@@ -5,8 +5,6 @@ module DiscourseSubscriptions
class ProductsController < ::Admin::AdminController class ProductsController < ::Admin::AdminController
include DiscourseSubscriptions::Stripe include DiscourseSubscriptions::Stripe
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key before_action :set_api_key
def index def index
@@ -3,8 +3,6 @@
module DiscourseSubscriptions module DiscourseSubscriptions
module Admin module Admin
class SubscriptionsController < ::Admin::AdminController class SubscriptionsController < ::Admin::AdminController
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
include DiscourseSubscriptions::Stripe include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group include DiscourseSubscriptions::Group
before_action :set_api_key before_action :set_api_key
@@ -53,7 +51,7 @@ module DiscourseSubscriptions
params.require(:id) params.require(:id)
begin begin
refund_subscription(params[:id]) if params[:refund] refund_subscription(params[:id]) if params[:refund]
subscription = ::Stripe::Subscription.cancel(params[:id]) subscription = ::Stripe::Subscription.delete(params[:id])
customer = customer =
Customer.find_by( Customer.find_by(
@@ -61,8 +59,11 @@ module DiscourseSubscriptions
customer_id: subscription[:customer], customer_id: subscription[:customer],
) )
Subscription.delete_by(external_id: params[:id])
if customer if customer
user = ::User.find(customer.user_id) user = ::User.find(customer.user_id)
customer.delete
group = plan_group(subscription[:plan]) group = plan_group(subscription[:plan])
group.remove(user) if group group.remove(user) if group
end end
@@ -80,7 +81,6 @@ module DiscourseSubscriptions
expand: ["data.plan.product"], expand: ["data.plan.product"],
limit: PAGE_LIMIT, limit: PAGE_LIMIT,
starting_after: start, starting_after: start,
status: "all",
) )
end end
@@ -2,8 +2,6 @@
module DiscourseSubscriptions module DiscourseSubscriptions
class AdminController < ::Admin::AdminController class AdminController < ::Admin::AdminController
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
def index def index
head 200 head 200
end end
@@ -5,11 +5,8 @@ module DiscourseSubscriptions
include DiscourseSubscriptions::Group include DiscourseSubscriptions::Group
include DiscourseSubscriptions::Stripe include DiscourseSubscriptions::Stripe
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
layout false layout false
before_action :set_api_key
skip_before_action :check_xhr skip_before_action :check_xhr
skip_before_action :redirect_to_login_if_required skip_before_action :redirect_to_login_if_required
skip_before_action :verify_authenticity_token, only: [:create] skip_before_action :verify_authenticity_token, only: [:create]
@@ -28,121 +25,48 @@ module DiscourseSubscriptions
end end
case event[:type] case event[:type]
when "checkout.session.completed"
checkout_session = event[:data][:object]
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("checkout.session.completed data: #{checkout_session}")
end
email = checkout_session[:customer_email]
return head 200 if checkout_session[:status] != "complete"
return render_json_error "email not found" if !email
if checkout_session[:customer].nil?
customer = ::Stripe::Customer.create({ email: email })
customer_id = customer[:id]
else
customer_id = checkout_session[:customer]
end
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("Looking up user with email: #{email}")
end
user = ::User.find_by_username_or_email(email)
return render_json_error "user not found" if !user
discourse_customer = Customer.create(user_id: user.id, customer_id: customer_id)
subscription = checkout_session[:subscription]
if subscription.present?
Subscription.create(customer_id: discourse_customer.id, external_id: subscription)
end
line_items =
::Stripe::Checkout::Session.list_line_items(checkout_session[:id], { limit: 1 })
item = line_items[:data].first
group = plan_group(item[:price])
group.add(user) unless group.nil?
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("Line item with group name meta data: #{item[:price]}")
if group.nil?
Rails.logger.warn("Group not found or not listed in metadata!")
else
Rails.logger.warn("Group: #{group.name}")
end
end
discourse_customer.product_id = item[:price][:product]
discourse_customer.save!
if !subscription.nil?
::Stripe::Subscription.update(
subscription,
{ metadata: { user_id: user.id, username: user.username } },
)
end
when "customer.subscription.created" when "customer.subscription.created"
when "customer.subscription.updated" when "customer.subscription.updated"
subscription = event[:data][:object] customer =
status = subscription[:status] Customer.find_by(
return head 200 if !%w[complete active].include?(status) customer_id: event[:data][:object][:customer],
product_id: event[:data][:object][:plan][:product],
customer = find_active_customer(subscription[:customer], subscription[: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"
update_status(customer.id, subscription[:id], status)
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(subscription[: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"
subscription = event[:data][:object] customer =
Customer.find_by(
customer = find_active_customer(subscription[:customer], subscription[:plan][:product]) customer_id: event[:data][:object][:customer],
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
update_status(customer.id, subscription[:id], subscription[:status]) Subscription.find_by(
customer_id: customer.id,
external_id: event[:data][:object][:id],
)&.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(subscription[:plan]) if group = plan_group(event[:data][:object][:plan])
group.remove(user) group.remove(user)
end end
customer.destroy!
end end
head 200 head 200
end end
private
def update_status(customer_id, subscription_id, status)
discourse_subscription =
Subscription.find_by(customer_id: customer_id, external_id: subscription_id)
discourse_subscription.update(status: status) if discourse_subscription
end
def find_active_customer(customer_id, product_id)
Customer
.joins(:subscriptions)
.where(customer_id: customer_id, product_id: product_id)
.where(
Subscription.arel_table[:status].eq(nil).or(
Subscription.arel_table[:status].not_eq("canceled"),
),
)
.first
end
end end
end end
@@ -1,11 +0,0 @@
# frozen_string_literal: true
module DiscourseSubscriptions
class PricingtableController < ::ApplicationController
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
def index
head 200
end
end
end
@@ -4,9 +4,6 @@ module DiscourseSubscriptions
class SubscribeController < ::ApplicationController class SubscribeController < ::ApplicationController
include DiscourseSubscriptions::Stripe include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group include DiscourseSubscriptions::Group
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key before_action :set_api_key
requires_login except: %i[index contributors show] requires_login except: %i[index contributors show]
@@ -85,19 +82,14 @@ module DiscourseSubscriptions
promo_code_id = promo_code[:id] if promo_code promo_code_id = promo_code[:id] if promo_code
subscription_params = { transaction =
customer: customer[:id], ::Stripe::Subscription.create(
items: [{ price: params[:plan] }], customer: customer[:id],
metadata: metadata_user, items: [{ price: params[:plan] }],
trial_period_days: trial_days, metadata: metadata_user,
promotion_code: promo_code_id, trial_period_days: trial_days,
} promotion_code: promo_code_id,
)
if SiteSetting.discourse_subscriptions_enable_automatic_tax
subscription_params[:automatic_tax] = { enabled: true }
end
transaction = ::Stripe::Subscription.create(subscription_params)
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[ payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[
:status :status
@@ -105,28 +97,16 @@ module DiscourseSubscriptions
else else
coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] && coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] &&
promo_code[:coupon][:id] promo_code[:coupon][:id]
invoice_params = { customer: customer[:id] }
if SiteSetting.discourse_subscriptions_enable_automatic_tax
invoice_params[:automatic_tax] = { enabled: true }
end
invoice = ::Stripe::Invoice.create(invoice_params)
invoice_item = invoice_item =
::Stripe::InvoiceItem.create( ::Stripe::InvoiceItem.create(
customer: customer[:id], customer: customer[:id],
price: params[:plan], price: params[:plan],
discounts: [{ coupon: coupon_id }], discounts: [{ coupon: coupon_id }],
invoice: invoice[: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] == payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] ==
"open" "open"
if payment_intent.nil?
return(
render_json_error I18n.t("js.discourse_subscriptions.subscribe.transaction_error")
)
end
transaction = ::Stripe::Invoice.pay(invoice[:id]) if payment_intent[:status] == transaction = ::Stripe::Invoice.pay(invoice[:id]) if payment_intent[:status] ==
"successful" "successful"
end end
@@ -167,11 +147,7 @@ module DiscourseSubscriptions
) )
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],
status: transaction[:status],
)
end end
end end
@@ -190,17 +166,7 @@ module DiscourseSubscriptions
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
.joins(:subscriptions)
.where(user_id: current_user.id)
.where(
Subscription.arel_table[:status].eq(nil).or(
Subscription.arel_table[:status].not_eq("canceled"),
),
)
.select(:product_id)
.distinct
.pluck(:product_id)
end end
def serialize_plans(plans) def serialize_plans(plans)
@@ -4,9 +4,6 @@ module DiscourseSubscriptions
module User module User
class PaymentsController < ::ApplicationController class PaymentsController < ::ApplicationController
include DiscourseSubscriptions::Stripe include DiscourseSubscriptions::Stripe
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key before_action :set_api_key
requires_login requires_login
@@ -27,31 +24,10 @@ module DiscourseSubscriptions
payments = ::Stripe::PaymentIntent.list(customer: customer_id) payments = ::Stripe::PaymentIntent.list(customer: customer_id)
payments_from_invoices = payments_from_invoices =
payments[:data].select { |payment| invoice_ids.include?(payment[:invoice]) } payments[:data].select { |payment| invoice_ids.include?(payment[:invoice]) }
data = data | payments_from_invoices
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("Payments from invoices: #{payments_from_invoices}")
end
# Pricing table one-off purchases do not have invoices
payments_without_invoices =
payments[:data].select { |payment| payment[:invoice].nil? }
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("Payments without invoices: #{payments_without_invoices}")
end
data = data | payments_from_invoices | payments_without_invoices
end end
end end
if SiteSetting.discourse_subscriptions_pricing_table_enabled && current_user.email
related_guest_payments = fetch_guest_payments(current_user.email)
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("Related guest payments: #{related_guest_payments}")
end
data = data | related_guest_payments
end
data = data.sort_by { |pmt| pmt[:created] }.reverse data = data.sort_by { |pmt| pmt[:created] }.reverse
render_json_dump data render_json_dump data
@@ -63,13 +39,12 @@ module DiscourseSubscriptions
private private
def parse_invoices(all_invoices, product_ids) def parse_invoices(all_invoices, product_ids)
all_invoices[:data].select do |invoice| invoices_with_products =
invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] all_invoices[:data].select do |invoice|
if invoice_lines 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)
end end
end
end end
def parse_invoice_lines(invoice_lines) def parse_invoice_lines(invoice_lines)
@@ -79,40 +54,6 @@ module DiscourseSubscriptions
invoice_lines[:plan][:product] invoice_lines[:plan][:product]
invoice_product_id invoice_product_id
end end
def fetch_guest_payments(email)
guest_payments = []
starting_after = nil
begin
loop do
# Fetch charges in batches of 100, using pagination with starting_after
charges =
::Stripe::Charge.list(
limit: 100,
starting_after: starting_after,
expand: ["data.payment_intent"],
)
charges[:data].each do |charge|
# Check if the charge is associated with the given email and has no customer ID
if charge[:billing_details][:email] == email && charge[:customer].nil?
guest_payments << charge
end
end
# Check if there are more charges to fetch
break if charges[:data].count < 100
# Set starting_after to the last charge's ID for the next batch
starting_after = charges[:data].last[:id]
end
rescue ::Stripe::StripeError => e
render_json_error e.message
end
guest_payments
end
end end
end end
end end
@@ -5,9 +5,6 @@ module DiscourseSubscriptions
class SubscriptionsController < ::ApplicationController class SubscriptionsController < ::ApplicationController
include DiscourseSubscriptions::Stripe include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group include DiscourseSubscriptions::Group
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
before_action :set_api_key before_action :set_api_key
requires_login requires_login
@@ -15,7 +12,6 @@ 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
stripe_customer_ids = customer.map { |c| c.customer_id }.uniq if customer
subscription_ids = subscription_ids =
Subscription.where("customer_id in (?)", customer_ids).pluck( Subscription.where("customer_id in (?)", customer_ids).pluck(
:external_id, :external_id,
@@ -24,25 +20,18 @@ module DiscourseSubscriptions
subscriptions = [] subscriptions = []
if subscription_ids if subscription_ids
prices = [] plans = ::Stripe::Price.list(expand: ["data.product"], limit: 100)
price_params = { limit: 100, expand: ["data.product"] }
loop do
response = ::Stripe::Price.list(price_params)
prices.concat(response[:data])
break unless response[:has_more]
price_params[:starting_after] = response[:data].last.id
end
all_subscriptions = []
stripe_customer_ids.each do |stripe_customer_id| customers =
customer_subscriptions = ::Stripe::Customer.list(email: current_user.email, expand: ["data.subscriptions"])
::Stripe::Subscription.list(customer: stripe_customer_id, status: "all")
all_subscriptions.concat(customer_subscriptions[:data]) subscriptions =
end customers[:data].map { |sub_customer| sub_customer[:subscriptions][:data] }.flatten(1)
subscriptions = subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
subscriptions = all_subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
subscriptions.map! do |subscription| subscriptions.map! do |subscription|
plan = prices.find { |p| p[:id] == subscription[:items][:data][0][:price][:id] } plan = plans[:data].find { |p| p[:id] == subscription[:items][:data][0][:price][:id] }
subscription.to_h.except!(:plan) subscription.to_h.except!(:plan)
subscription.to_h.merge(plan: plan, product: plan[:product].to_h.slice(:id, :name)) subscription.to_h.merge(plan: plan, product: plan[:product].to_h.slice(:id, :name))
end end
@@ -13,20 +13,3 @@ module DiscourseSubscriptions
end end
end end
end end
# == Schema Information
#
# Table name: discourse_subscriptions_customers
#
# id :bigint not null, primary key
# customer_id :string not null
# product_id :string
# user_id :bigint
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_discourse_subscriptions_customers_on_customer_id (customer_id)
# index_discourse_subscriptions_customers_on_user_id (user_id)
#
@@ -4,17 +4,3 @@ module DiscourseSubscriptions
class Product < ActiveRecord::Base class Product < ActiveRecord::Base
end end
end end
# == Schema Information
#
# Table name: discourse_subscriptions_products
#
# id :bigint not null, primary key
# external_id :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_discourse_subscriptions_products_on_external_id (external_id)
#
@@ -5,20 +5,3 @@ module DiscourseSubscriptions
belongs_to :customer belongs_to :customer
end end
end end
# == Schema Information
#
# Table name: discourse_subscriptions_subscriptions
#
# id :bigint not null, primary key
# customer_id :bigint not null
# external_id :string not null
# created_at :datetime not null
# updated_at :datetime not null
# status :string
#
# Indexes
#
# index_discourse_subscriptions_subscriptions_on_customer_id (customer_id)
# index_discourse_subscriptions_subscriptions_on_external_id (external_id)
#
@@ -51,16 +51,6 @@ module DiscourseSubscriptions
"KR" "KR"
when "sgd" when "sgd"
"S$" "S$"
when "zar"
"R"
when "chf"
"CHF"
when "pln"
"zł"
when "czk"
"Kč"
when "sek"
"kr"
else else
"$" "$"
end end
@@ -17,26 +17,39 @@ module DiscourseSubscriptions
product_ids = product_ids.include?(campaign_product) ? [campaign_product] : [] product_ids = product_ids.include?(campaign_product) ? [campaign_product] : []
end end
amount = 0.00 begin
subscriptions = get_subscription_data amount = 0.00
subscriptions = filter_to_subscriptions_products(subscriptions, product_ids) subscriptions = get_subscription_data
subscriptions = filter_to_subscriptions_products(subscriptions, product_ids)
# 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 { |c| amount += c[:price].to_f / 100.00 } one_time_payments.each { |c| amount += c[:price].to_f / 100.00 }
# 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
# calculate amount raised # calculate amount raised
subscriptions&.each do |sub| subscriptions&.each do |sub|
sub_amount = calculate_monthly_amount(sub) sub_amount = calculate_monthly_amount(sub)
amount += sub_amount / 100.00 amount += sub_amount / 100.00
end
SiteSetting.discourse_subscriptions_campaign_amount_raised = amount.round(2)
check_goal_status
rescue ::Stripe::AuthenticationError
message =
I18n.t(
"discourse_subscriptions.auth.invalid",
settingsUrl: "admin/site_settings/category/discourse_subscriptions",
)
AdminDashboardData.add_found_scheduled_check_problem(
AdminDashboardData::Problem.new(message, identifier: AUTH_PROBLEM_IDENTIFIER),
)
else
AdminDashboardData.clear_found_problem(AUTH_PROBLEM_IDENTIFIER)
end end
SiteSetting.discourse_subscriptions_campaign_amount_raised = amount.round(2)
check_goal_status
end end
def create_campaign def create_campaign
@@ -91,7 +104,7 @@ module DiscourseSubscriptions
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: "circle-dollar-to-slot", flair_icon: "donate",
} }
group.update(params) group.update(params)
@@ -154,30 +167,26 @@ module DiscourseSubscriptions
all_invoices = all_invoices =
::Stripe::Invoice.list(limit: 100, starting_after: current_set[:last_record]) ::Stripe::Invoice.list(limit: 100, starting_after: current_set[:last_record])
if all_invoices[:data].present? current_set[:last_record] = all_invoices[:data].last[:id] if all_invoices[:data].present?
current_set[:last_record] = all_invoices[:data].last[:id] current_set[:has_more] = all_invoices[:has_more]
current_set[:has_more] = all_invoices[:has_more]
all_invoices[:data].each do |invoice| all_invoices[:data].each do |invoice|
customer_id = invoice[:customer] customer_id = invoice[:customer]
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 && line_item[:plan] == nil && line_item[:price] && if line_item[:plan] == nil && 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 = {
customer_id: customer_id, customer_id: customer_id,
product_id: product_id, product_id: product_id,
price: line_item[:price][:unit_amount], price: line_item[:price][:unit_amount],
} }
one_time_payments << line_data one_time_payments << line_data
end
end end
end end
else
current_set[:has_more] = false
end end
end end
end end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,173 @@
{{#if shouldShow}}
<div
class="campaign-banner"
style={{html-safe (concat "box-shadow: 5px 5px #" dropShadowColor)}}
>
<DButton @icon="times" @action={{this.dismissBanner}} class="close" />
<div class="campaign-banner-info" style={{html-safe this.bannerInfoStyle}}>
{{#if isGoalMet}}
<h2 class="campaign-banner-info-header">
{{i18n "discourse_subscriptions.campaign.success_title"}}
</h2>
<p class="campaign-banner-info-description">
{{i18n "discourse_subscriptions.campaign.success_body"}}
</p>
{{else}}
<h2 class="campaign-banner-info-header">
{{i18n "discourse_subscriptions.campaign.title"}}
</h2>
<p class="campaign-banner-info-description">
{{i18n "discourse_subscriptions.campaign.body"}}
</p>
{{#if product}}
<LinkTo
@route="subscribe.show"
@model={{product}}
@disabled={{product.subscribed}}
class="btn btn-primary campaign-banner-info-button"
>
{{d-icon "far-heart"}}
{{d-icon "heart" class="hover-heart"}}
{{i18n "discourse_subscriptions.campaign.button"}}
</LinkTo>
{{else}}
<LinkTo
@route="subscribe"
class="btn btn-primary campaign-banner-info-button"
>
{{d-icon "far-heart"}}
{{d-icon "heart" class="hover-heart"}}
{{i18n "discourse_subscriptions.campaign.button"}}
</LinkTo>
{{/if}}
{{/if}}
</div>
<div class="campaign-banner-progress">
{{#if isGoalMet}}
<div class="fireworks">
<div class="before"></div>
<div class="after"></div>
</div>
<div class="campaign-banner-progress-success"></div>
{{#if subscriberGoal}}
<p class="campaign-banner-progress-description">
{{html-safe
(i18n
"discourse_subscriptions.campaign.goal_comparison"
current=subscribers
goal=goalTarget
)
}}
{{i18n "discourse_subscriptions.campaign.subscribers"}}
</p>
{{else}}
<p class="campaign-banner-progress-description">
{{html-safe
(i18n
"discourse_subscriptions.campaign.goal_comparison"
current=(format-currency currency amountRaised)
goal=(format-currency currency goalTarget)
)
}}
{{i18n "discourse_subscriptions.campaign.raised"}}
</p>
{{#if showContributors}}
<ConditionalLoadingSpinner @condition={{loading}} @size="small">
<div class="campaign-banner-progress-users">
<p class="campaign-banner-progress-users-title">
<strong>
{{i18n
"discourse_subscriptions.campaign.recent_contributors"
}}
</strong>
</p>
<div class="campaign-banner-progress-users-avatars">
{{#each contributors as |contributor|}}
{{avatar
contributor
avatarTemplatePath="avatar_template"
usernamePath="username"
namePath="name"
imageSize="small"
}}
{{/each}}
</div>
</div>
</ConditionalLoadingSpinner>
{{/if}}
{{/if}}
{{else}}
{{#if subscriberGoal}}
<progress
class="campaign-banner-progress-bar"
value={{subscribers}}
max={{siteSettings.discourse_subscriptions_campaign_goal}}
></progress>
<p class="campaign-banner-progress-description">
{{html-safe
(i18n
"discourse_subscriptions.campaign.goal_comparison"
current=subscribers
goal=goalTarget
)
}}
{{i18n "discourse_subscriptions.campaign.subscribers"}}
</p>
{{else}}
<progress
class="campaign-banner-progress-bar"
value={{amountRaised}}
max={{siteSettings.discourse_subscriptions_campaign_goal}}
></progress>
<p class="campaign-banner-progress-description">
{{html-safe
(i18n
"discourse_subscriptions.campaign.goal_comparison"
current=(format-currency currency amountRaised)
goal=(format-currency currency goalTarget)
)
}}
{{i18n "discourse_subscriptions.campaign.raised"}}
</p>
{{/if}}
{{#if showContributors}}
<ConditionalLoadingSpinner @condition={{loading}} @size="small">
<div class="campaign-banner-progress-users">
<p class="campaign-banner-progress-users-title">
<strong>
{{i18n
"discourse_subscriptions.campaign.recent_contributors"
}}
</strong>
</p>
<div class="campaign-banner-progress-users-avatars">
{{#each contributors as |contributor|}}
{{avatar
contributor
avatarTemplatePath="avatar_template"
usernamePath="username"
namePath="name"
imageSize="small"
}}
{{/each}}
</div>
</div>
</ConditionalLoadingSpinner>
{{/if}}
{{/if}}
</div>
</div>
{{/if}}
@@ -0,0 +1,184 @@
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { equal } from "@ember/object/computed";
import { setting } from "discourse/lib/computed";
import Component from "@ember/component";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { later } from "@ember/runloop";
import { inject as service } from "@ember/service";
const SIDEBAR_BODY_CLASS = "subscription-campaign-sidebar";
export default Component.extend({
router: service(),
dismissed: false,
loading: false,
dropShadowColor: setting(
"discourse_subscriptions_campaign_banner_shadow_color"
),
backgroundImageUrl: setting(
"discourse_subscriptions_campaign_banner_bg_image"
),
isSidebar: equal(
"siteSettings.discourse_subscriptions_campaign_banner_location",
"Sidebar"
),
subscribers: setting("discourse_subscriptions_campaign_subscribers"),
subscriberGoal: equal(
"siteSettings.discourse_subscriptions_campaign_type",
"Subscribers"
),
currency: setting("discourse_subscriptions_currency"),
amountRaised: setting("discourse_subscriptions_campaign_amount_raised"),
goalTarget: setting("discourse_subscriptions_campaign_goal"),
product: setting("discourse_subscriptions_campaign_product"),
showContributors: setting(
"discourse_subscriptions_campaign_show_contributors"
),
classNameBindings: ["isGoalMet:goal-met"],
init() {
this._super(...arguments);
this.set("contributors", []);
// add background-image url to stylesheet
if (this.backgroundImageUrl) {
const backgroundUrl = `url(${this.backgroundImageUrl}`.replace(/\\/g, "");
if (
document.documentElement.style.getPropertyValue(
"--campaign-background-image"
) !== backgroundUrl
) {
document.documentElement.style.setProperty(
"--campaign-background-image",
backgroundUrl
);
}
}
if (this.currentUser && this.showContributors) {
return ajax("/s/contributors", { method: "get" }).then((result) => {
this.setProperties({
contributors: result,
loading: false,
});
});
}
},
didInsertElement() {
this._super(...arguments);
if (this.isSidebar && this.shouldShow && !this.site.mobileView) {
document.body.classList.add(SIDEBAR_BODY_CLASS);
} else {
document.body.classList.remove(SIDEBAR_BODY_CLASS);
}
// makes sure to only play animation once, & not repeat on reload
if (this.isGoalMet) {
const successAnimationKey = this.keyValueStore.get(
"campaign_success_animation"
);
if (!successAnimationKey) {
later(() => {
this.keyValueStore.set({
key: "campaign_success_animation",
value: Date.now(),
});
document.body.classList.add("success-animation-off");
}, 7000);
} else {
document.body.classList.add("success-animation-off");
}
}
},
willDestroyElement() {
document.body.classList.remove(SIDEBAR_BODY_CLASS);
},
@discourseComputed("backgroundImageUrl")
bannerInfoStyle(backgroundImageUrl) {
if (!backgroundImageUrl) {
return "";
}
return `background-image: linear-gradient(
0deg,
rgba(var(--secondary-rgb), 0.75) 0%,
rgba(var(--secondary-rgb), 0.75) 100%),
var(--campaign-background-image);
background-size: cover;
background-repeat: no-repeat;`;
},
@discourseComputed(
"router.currentRouteName",
"currentUser",
"siteSettings.discourse_subscriptions_campaign_enabled",
"visible"
)
shouldShow(currentRoute, currentUser, enabled, visible) {
// do not show on admin or subscriptions pages
const showOnRoute =
currentRoute !== "discovery.s" &&
!currentRoute.split(".")[0].includes("admin") &&
currentRoute.split(".")[0] !== "subscribe";
if (!this.site.show_campaign_banner) {
return false;
}
// make sure not to render above main container when inside a topic
if (
this.connectorName === "above-main-container" &&
currentRoute.includes("topic")
) {
return false;
}
return showOnRoute && currentUser && enabled && visible;
},
@observes("dismissed")
_updateBodyClasses() {
if (this.dismissed) {
document.body.classList.remove(SIDEBAR_BODY_CLASS);
}
},
@discourseComputed("dismissed")
visible(dismissed) {
const dismissedBannerKey = this.keyValueStore.get(
"dismissed_campaign_banner"
);
const threeMonths = 2628000000 * 3;
const bannerDismissedTime = new Date(dismissedBannerKey);
const now = Date.now();
return (
(!dismissedBannerKey || now - bannerDismissedTime > threeMonths) &&
!dismissed
);
},
@discourseComputed
isGoalMet() {
const currentVolume = this.subscriberGoal
? this.subscribers
: this.amountRaised;
return currentVolume >= this.goalTarget;
},
@action
dismissBanner() {
this.set("dismissed", true);
this.keyValueStore.set({
key: "dismissed_campaign_banner",
value: Date.now(),
});
},
});
@@ -1,92 +0,0 @@
import Component, { Input } from "@ember/component";
import { fn } from "@ember/helper";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
import discourseComputed from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
export default class CreateCouponForm extends Component {
discountType = "amount";
discount = null;
promoCode = null;
active = false;
@discourseComputed
discountTypes() {
return [
{ id: "amount", name: "Amount" },
{ id: "percent", name: "Percent" },
];
}
@action
createNewCoupon() {
const createParams = {
promo: this.promoCode,
discount_type: this.discountType,
discount: this.discount,
active: this.active,
};
this.create(createParams);
}
@action
cancelCreate() {
this.cancel();
}
<template>
<div class="create-coupon-form">
<form class="form-horizontal">
<p>
<label for="promo_code">
{{i18n "discourse_subscriptions.admin.coupons.promo_code"}}
</label>
<Input @type="text" name="promo_code" @value={{this.promoCode}} />
</p>
<p>
<label for="amount">
{{i18n "discourse_subscriptions.admin.coupons.discount"}}
</label>
<ComboBox
@content={{this.discountTypes}}
@value={{this.discountType}}
@onChange={{fn (mut this.discountType)}}
/>
<Input
class="discount-amount"
@type="text"
name="amount"
@value={{this.discount}}
/>
</p>
<p>
<label for="active">
{{i18n "discourse_subscriptions.admin.coupons.active"}}
</label>
<Input @type="checkbox" name="active" @checked={{this.active}} />
</p>
</form>
<DButton
@action={{this.createNewCoupon}}
@label="discourse_subscriptions.admin.coupons.create"
@title="discourse_subscriptions.admin.coupons.create"
@icon="plus"
class="btn-primary btn btn-icon"
/>
<DButton
@action={{this.cancelCreate}}
label="cancel"
@title="cancel"
@icon="xmark"
class="btn btn-icon"
/>
</div>
</template>
}
@@ -0,0 +1,50 @@
<div class="create-coupon-form">
<form class="form-horizontal">
<p>
<label for="promo_code">
{{i18n "discourse_subscriptions.admin.coupons.promo_code"}}
</label>
<Input @type="text" name="promo_code" @value={{promoCode}} />
</p>
<p>
<label for="amount">
{{i18n "discourse_subscriptions.admin.coupons.discount"}}
</label>
<ComboBox
@content={{discountTypes}}
@value={{discountType}}
@onChange={{action (mut discountType)}}
/>
<Input
class="discount-amount"
@type="text"
name="amount"
@value={{discount}}
/>
</p>
<p>
<label for="active">
{{i18n "discourse_subscriptions.admin.coupons.active"}}
</label>
<Input @type="checkbox" name="active" @checked={{active}} />
</p>
</form>
<DButton
@action={{action "createNewCoupon"}}
@label="discourse_subscriptions.admin.coupons.create"
@title="discourse_subscriptions.admin.coupons.create"
@icon="plus"
class="btn-primary btn btn-icon"
/>
<DButton
@action={{action "cancelCreate"}}
label="cancel"
@title="cancel"
@icon="times"
class="btn btn-icon"
/>
</div>

Some files were not shown because too many files have changed in this diff Show More