diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..4dc8879 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,50 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + json (2.6.3) + language_server-protocol (3.17.0.3) + parallel (1.23.0) + parser (3.2.2.4) + ast (~> 2.4.1) + racc + racc (1.7.3) + rainbow (3.1.1) + regexp_parser (2.8.2) + rexml (3.2.6) + rubocop (1.57.2) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.2.2.4) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.19.0) + rubocop (~> 1.41) + rubocop-discourse (3.4.1) + rubocop (>= 1.1.0) + rubocop-rspec (>= 2.0.0) + rubocop-factory_bot (2.24.0) + rubocop (~> 1.33) + rubocop-rspec (2.25.0) + rubocop (~> 1.40) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + ruby-progressbar (1.13.0) + unicode-display_width (2.5.0) + +PLATFORMS + arm64-darwin-22 + x86_64-linux + +DEPENDENCIES + rubocop-discourse + +BUNDLED WITH + 2.4.13 diff --git a/app/controllers/subscription_server/user_authorizations_controller.rb b/app/controllers/subscription_server/user_authorizations_controller.rb new file mode 100644 index 0000000..0baed79 --- /dev/null +++ b/app/controllers/subscription_server/user_authorizations_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class SubscriptionServer::UserAuthorizationsController < ApplicationController + before_action :ensure_logged_in + + def destroy + params.require(:domain) + current_user.remove_subscription_domain(params[:domain]) + render json: success_json + end +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml deleted file mode 100644 index 4334ed8..0000000 --- a/config/locales/client.en.yml +++ /dev/null @@ -1,8 +0,0 @@ -en: - js: - discourse_subscriptions: - admin: - products: - product: - plugin_name: Plugin Name - plugin_name_help: Name of plugin subscription is for. If filled, will generate API Key with subscription. \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 6700d56..37441fd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,8 +4,10 @@ get '' => 'server#index', defaults: { format: 'json' } get 'user-subscriptions' => 'user_subscriptions#index', defaults: { format: 'json' } get 'messages' => 'messages#index', defaults: { format: 'json' } + delete 'user-authorizations' => 'user_authorizations#destroy', defaults: { format: 'json' } end Discourse::Application.routes.append do mount ::SubscriptionServer::Engine, at: "/subscription-server" + get '/subscribe' => 'discourse_subscriptions/subscribe#index' end diff --git a/config/settings.yml b/config/settings.yml index 2a21e8a..d7d9bc5 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,6 +1,8 @@ plugins: - subscription_server_enabled: true + subscription_server_enabled: + client: true + default: true subscription_server_supplier_name: '' subscription_server_subscriptions: type: list - default: '' + default: '' \ No newline at end of file diff --git a/coverage/.last_run.json b/coverage/.last_run.json deleted file mode 100644 index dc24454..0000000 --- a/coverage/.last_run.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "result": { - "line": 97.75 - } -} diff --git a/extensions/discourse_subscriptions_coupons_controller_extension.rb b/extensions/discourse_subscriptions_coupons_controller_extension.rb new file mode 100644 index 0000000..094110e --- /dev/null +++ b/extensions/discourse_subscriptions_coupons_controller_extension.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +module DiscourseSubscriptionsCouponsControllerExtension + def create + params.require([:promo, :discount_type, :discount, :active, :applies_to_products]) + begin + coupon_params = { + duration: 'forever', + max_redemptions: params[:max_redemptions] || 1, + applies_to: { + products: params[:applies_to_products] + } + } + + case params[:discount_type] + when 'amount' + coupon_params[:amount_off] = params[:discount].to_i * 100 + coupon_params[:currency] = SiteSetting.discourse_subscriptions_currency + when 'percent' + coupon_params[:percent_off] = params[:discount] + end + + coupon = ::Stripe::Coupon.create(coupon_params) + promo_code = ::Stripe::PromotionCode.create({ coupon: coupon[:id], code: params[:promo] }) if coupon.present? + + render_json_dump promo_code + rescue ::Stripe::InvalidRequestError => e + render_json_error e.message + end + end +end diff --git a/extensions/discourse_subscriptions_products_controller_extension.rb b/extensions/discourse_subscriptions_products_controller_extension.rb new file mode 100644 index 0000000..9fde5c5 --- /dev/null +++ b/extensions/discourse_subscriptions_products_controller_extension.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +module DiscourseSubscriptionsProductsControllerExtension + def show + begin + product = ::Stripe::Product.retrieve(params[:id]) + + if product[:metadata][:hidden].present? + product[:metadata][:hidden] = ActiveRecord::Type::Boolean.new.cast(product[:metadata][:hidden]) + end + + render_json_dump product + + rescue ::Stripe::InvalidRequestError => e + render_json_error e.message + end + end + + private def product_params + params.permit! + + { + name: params[:name], + active: params[:active], + statement_descriptor: params[:statement_descriptor], + metadata: { + description: params.dig(:metadata, :description), + repurchaseable: params.dig(:metadata, :repurchaseable), + hidden: params.dig(:metadata, :hidden) + } + } + end +end diff --git a/extensions/discourse_subscriptions_subscriber_controller_extension.rb b/extensions/discourse_subscriptions_subscriber_controller_extension.rb new file mode 100644 index 0000000..614d787 --- /dev/null +++ b/extensions/discourse_subscriptions_subscriber_controller_extension.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +module DiscourseSubscriptionsSubscribeControllerExtension + private def serialize_product(product) + { + id: product[:id], + name: product[:name], + description: PrettyText.cook(product[:metadata][:description]), + subscribed: current_user_products.include?(product[:id]), + repurchaseable: product[:metadata][:repurchaseable], + hidden: ActiveRecord::Type::Boolean.new.cast(product[:metadata][:hidden]) + } + end + + private def serialize_plans(plans) + plans[:data].reduce([]) do |result, p| + plan = p.to_h + if plan[:nickname] != "hidden" + result << plan.slice(:id, :unit_amount, :currency, :type, :recurring, :nickname) + end + result + end.sort_by { |plan| plan[:amount] } + end +end diff --git a/lib/subscription_server/provider.rb b/lib/subscription_server/provider.rb index 3936c9a..25ca4d4 100644 --- a/lib/subscription_server/provider.rb +++ b/lib/subscription_server/provider.rb @@ -6,7 +6,7 @@ class SubscriptionServer::Provider attr_reader :user - def initialize(user) + def initialize(user = nil) @user = user end diff --git a/lib/subscription_server/providers/stripe.rb b/lib/subscription_server/providers/stripe.rb index 5da8e7a..47309c9 100644 --- a/lib/subscription_server/providers/stripe.rb +++ b/lib/subscription_server/providers/stripe.rb @@ -45,4 +45,8 @@ def subscriptions(product_ids, resource_name) result end end + + def self.discourse_subscriptions_installed? + new.discourse_subscriptions_installed? + end end diff --git a/plugin.rb b/plugin.rb index 7c94329..9420fe2 100644 --- a/plugin.rb +++ b/plugin.rb @@ -9,6 +9,13 @@ enabled_site_setting :subscription_server_enabled +DiscourseEvent.on(:after_plugin_activation) do + sorted_pugins = Discourse.plugins.sort_by do |p| + p&.name == 'discourse-subscription-server' ? 1 : 0 + end + Discourse.instance_variable_set(:@plugins, sorted_pugins) +end + after_initialize do %w[ ../lib/subscription_server/engine.rb @@ -21,6 +28,7 @@ ../lib/subscription_server/extensions/user_api_keys_controller.rb ../config/routes.rb ../app/controllers/subscription_server/user_subscriptions_controller.rb + ../app/controllers/subscription_server/user_authorizations_controller.rb ../app/controllers/subscription_server/messages_controller.rb ../app/controllers/subscription_server/server_controller.rb ../app/serializers/subscription_server/message_serializer.rb @@ -62,6 +70,17 @@ save_custom_fields(true) end + add_to_class(:user, :remove_subscription_domain) do |domain| + self._custom_fields.where( + "name LIKE '#{SubscriptionServer::UserSubscriptions::DOMAINS_KEY_PREFIX}%'" + ).each do |field| + value_arr = field.value.split('|') + value_arr = value_arr.reject { |d| d === domain } + field.value = value_arr.join('|') + field.save! + end + end + add_to_class(:user, :subscription_product_domains) do |resource_name, provider_name, product_id| key = subscription_product_domain_key(resource_name, provider_name, product_id) product_domains = custom_fields[key] @@ -94,4 +113,38 @@ result end end + + add_to_serializer(:user, :subscription_domains) { user.subscription_domains } + + if SubscriptionServer::Stripe.discourse_subscriptions_installed? + %w[ + ../extensions/discourse_subscriptions_coupons_controller_extension.rb + ../extensions/discourse_subscriptions_products_controller_extension.rb + ../extensions/discourse_subscriptions_subscriber_controller_extension.rb + ].each do |path| + load File.expand_path(path, __FILE__) + end + DiscourseSubscriptions::SubscribeController.prepend DiscourseSubscriptionsSubscribeControllerExtension + DiscourseSubscriptions::Admin::CouponsController.prepend DiscourseSubscriptionsCouponsControllerExtension + DiscourseSubscriptions::Admin::ProductsController.prepend DiscourseSubscriptionsProductsControllerExtension + + require 'csv' + module ::DiscourseSubscriptions + def self.class_update_subscriptions_from_csv(path) + rows = CSV.read(path) + return unless rows.present? + + rows.each do |row| + DiscourseSubscriptions::Subscription + .joins(:customer) + .where("discourse_subscriptions_customers.customer_id = :customer_id AND discourse_subscriptions_subscriptions.external_id = :subscription_id", + customer_id: row[0].strip, + subscription_id: row[1].strip + ).update_all( + external_id: row[2].strip + ) + end + end + end + end end diff --git a/spec/components/subscription_server/user_spec.rb b/spec/components/subscription_server/user_spec.rb index 6ad6b53..b95eb25 100644 --- a/spec/components/subscription_server/user_spec.rb +++ b/spec/components/subscription_server/user_spec.rb @@ -46,4 +46,13 @@ expect(user.subscription_domains.first[:domains]).to eq([domain]) expect(user.subscription_domains.first[:domain_limit]).to eq(1) end + + it "#remove_subscription_domain" do + user.add_subscription_product_domain(domain, resource, provider, product_id) + user.add_subscription_product_domain(domain, resource, provider, "prod_12345") + user.remove_subscription_domain(domain) + user.reload + expect(user.custom_fields[user.subscription_product_domain_key(resource, provider, product_id)]).to eq("") + expect(user.custom_fields[user.subscription_product_domain_key(resource, provider, "prod_12345")]).to eq("") + end end diff --git a/spec/requests/subscription_server/user_authorizations_controller_spec.rb b/spec/requests/subscription_server/user_authorizations_controller_spec.rb new file mode 100644 index 0000000..f9c25d4 --- /dev/null +++ b/spec/requests/subscription_server/user_authorizations_controller_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +describe SubscriptionServer::UserAuthorizationsController do + let(:user) { Fabricate(:user) } + let(:provider) { "stripe" } + let(:product_id) { "prod_CBTNpi3fqWWkq0" } + let(:product_slug) { "business" } + let(:resource) { "custom_wizard" } + let(:domain) { "demo.pavilion.tech" } + + it "requires a user" do + delete "/subscription-server/user-authorizations" + expect(response.status).to eq(403) + end + + context "with a user" do + before do + sign_in(user) + end + + describe "#destroy" do + + it "requires a domain" do + delete "/subscription-server/user-authorizations" + expect(response.status).to eq(400) + end + + it "removes the domain from all of the user's products" do + user.add_subscription_product_domain(domain, resource, provider, product_id) + user.add_subscription_product_domain(domain, resource, provider, "prod_12345") + + delete "/subscription-server/user-authorizations", params: { domain: domain } + expect(response.status).to eq(200) + + user.reload + expect(user.custom_fields[user.subscription_product_domain_key(resource, provider, product_id)]).to eq("") + expect(user.custom_fields[user.subscription_product_domain_key(resource, provider, "prod_12345")]).to eq("") + end + end + end +end