FEATURE: Campaigns (#67)

diff --git a/app/controllers/discourse_subscriptions/admin/products_controller.rb b/app/controllers/discourse_subscriptions/admin/products_controller.rb
index 5962637..4fede95 100644
--- a/app/controllers/discourse_subscriptions/admin/products_controller.rb
+++ b/app/controllers/discourse_subscriptions/admin/products_controller.rb
@@ -13,7 +13,7 @@ module DiscourseSubscriptions
           products = []
 
           if product_ids.present? && is_stripe_configured?
-            products = ::Stripe::Product.list({ ids: product_ids })
+            products = ::Stripe::Product.list({ ids: product_ids, limit: 100 })
             products = products[:data]
           elsif !is_stripe_configured?
             products = nil
diff --git a/app/controllers/discourse_subscriptions/admin_controller.rb b/app/controllers/discourse_subscriptions/admin_controller.rb
index c0cdb2d..62e4508 100644
--- a/app/controllers/discourse_subscriptions/admin_controller.rb
+++ b/app/controllers/discourse_subscriptions/admin_controller.rb
@@ -5,5 +5,19 @@ module DiscourseSubscriptions
     def index
       head 200
     end
+
+    def refresh_campaign
+      Jobs.enqueue(:manually_update_campaign_data)
+      render json: success_json
+    end
+
+    def create_campaign
+      begin
+        DiscourseSubscriptions::Campaign.new.create_campaign
+        render json: success_json
+      rescue => e
+        render_json_error e.message
+      end
+    end
   end
 end
diff --git a/app/controllers/discourse_subscriptions/subscribe_controller.rb b/app/controllers/discourse_subscriptions/subscribe_controller.rb
index eb88e71..34a7467 100644
--- a/app/controllers/discourse_subscriptions/subscribe_controller.rb
+++ b/app/controllers/discourse_subscriptions/subscribe_controller.rb
@@ -5,7 +5,7 @@ module DiscourseSubscriptions
     include DiscourseSubscriptions::Stripe
     include DiscourseSubscriptions::Group
     before_action :set_api_key
-    requires_login except: [:index, :show]
+    requires_login except: [:index, :contributors, :show]
 
     def index
       begin
@@ -31,6 +31,18 @@ module DiscourseSubscriptions
       end
     end
 
+    def contributors
+      return unless SiteSetting.discourse_subscriptions_campaign_show_contributors
+      contributor_ids = Set.new
+
+      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))
+
+      contributors = ::User.where(id: contributor_ids)
+
+      render_serialized(contributors, UserSerializer)
+    end
+
     def show
       params.require(:id)
       begin
diff --git a/app/jobs/regular/manually_update_campaign_data.rb b/app/jobs/regular/manually_update_campaign_data.rb
new file mode 100644
index 0000000..954b90a
--- /dev/null
+++ b/app/jobs/regular/manually_update_campaign_data.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module ::Jobs
+  class ManuallyUpdateCampaignData < ::Jobs::Base
+
+    def execute(args)
+      return unless SiteSetting.discourse_subscriptions_campaign_enabled
+      DiscourseSubscriptions::Campaign.new.refresh_data
+    end
+  end
+end
diff --git a/app/jobs/scheduled/refresh_subscriptions_campaign_data.rb b/app/jobs/scheduled/refresh_subscriptions_campaign_data.rb
new file mode 100644
index 0000000..d691b60
--- /dev/null
+++ b/app/jobs/scheduled/refresh_subscriptions_campaign_data.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module ::Jobs
+  class RefreshSubscriptionsCampaignData < ::Jobs::Scheduled
+    every 30.minutes
+
+    def execute(args)
+      return unless SiteSetting.discourse_subscriptions_campaign_enabled
+      DiscourseSubscriptions::Campaign.new.refresh_data
+    end
+  end
+end
diff --git a/app/services/discourse_subscriptions/campaign.rb b/app/services/discourse_subscriptions/campaign.rb
new file mode 100644
index 0000000..955b15a
--- /dev/null
+++ b/app/services/discourse_subscriptions/campaign.rb
@@ -0,0 +1,170 @@
+# frozen_string_literal: true
+
+module DiscourseSubscriptions
+  class Campaign
+    include DiscourseSubscriptions::Stripe
+    def initialize
+      set_api_key # instantiates Stripe API
+    end
+
+    def refresh_data
+      product_ids = Set.new(Product.all.pluck(:external_id))
+
+      # if a product id is set for the campaign, we only want to return those results.
+      # if it's blank, return them all.
+      campaign_product = SiteSetting.discourse_subscriptions_campaign_product
+      if campaign_product.present?
+        product_ids = product_ids.include?(campaign_product) ? [campaign_product] : []
+      end
+
+      amount = 0
+      subscriptions = get_subscription_data
+      subscriptions = filter_to_subscriptions_products(subscriptions, product_ids)
+
+      # get number of subscribers
+      SiteSetting.discourse_subscriptions_campaign_subscribers = subscriptions&.length.to_i
+
+      # calculate amount raised
+      subscriptions&.each do |sub|
+        sub_amount = calculate_monthly_amount(sub)
+        amount += sub_amount
+      end
+
+      SiteSetting.discourse_subscriptions_campaign_amount_raised = amount
+    end
+
+    def create_campaign
+      begin
+        group = create_campaign_group
+        product = create_campaign_product
+        create_campaign_prices(product, group)
+
+        SiteSetting.discourse_subscriptions_campaign_enabled = true
+        SiteSetting.discourse_subscriptions_campaign_product = product[:id]
+      rescue ::Stripe::InvalidRequestError => e
+        e
+      end
+    end
+
+    protected
+
+    def create_campaign_group
+      campaign_group = SiteSetting.discourse_subscriptions_campaign_group
+      group = ::Group.find_by_id(campaign_group) if campaign_group.present?
+
+      unless group
+        group = ::Group.create(name: "campaign_supporters")
+
+        SiteSetting.discourse_subscriptions_campaign_group = group[:id]
+
+        params = {
+          full_name: I18n.t('js.discourse_subscriptions.campaign.supporters'),
+          title: I18n.t('js.discourse_subscriptions.campaign.supporter'),
+          flair_icon: "donate"
+        }
+
+        group.update(params)
+      end
+
+      group[:name]
+    end
+
+    def create_campaign_product
+      product_params = {
+        name: I18n.t('js.discourse_subscriptions.campaign.title'),
+        active: true,
+        metadata: {
+          description: I18n.t('js.discourse_subscriptions.campaign.body'),
+        }
+      }
+
+      product = ::Stripe::Product.create(product_params)
+
+      Product.create(external_id: product[:id])
+
+      product
+    end
+
+    def create_campaign_prices(product, group)
+      # hard coded defaults to make setting this up as simple as possible
+      monthly_prices = [3, 5, 10, 25]
+      yearly_prices = [50, 100]
+
+      monthly_prices.each do |price|
+        create_price(product[:id], group, price, "month")
+      end
+
+      yearly_prices.each do |price|
+        create_price(product[:id], group, price, "year")
+      end
+    end
+
+    def create_price(product_id, group_name, amount, recurrence)
+      price_object = {
+        nickname: "#{amount}/#{recurrence}",
+        unit_amount: amount * 100,
+        product: product_id,
+        currency: SiteSetting.discourse_subscriptions_currency,
+        active: true,
+        recurring: {
+          interval: recurrence
+        },
+        metadata: {
+          group_name: group_name
+        }
+      }
+
+      plan = ::Stripe::Price.create(price_object)
+    end
+
+    def get_subscription_data
+      subscriptions = []
+      current_set = {
+        has_more: true,
+        last_record: nil
+      }
+
+      until current_set[:has_more] == false
+        current_set = ::Stripe::Subscription.list(
+          expand: ['data.plan.product'],
+          limit: 100,
+          starting_after: current_set[:last_record]
+        )
+

[... diff too long, it was truncated ...]

GitHub sha: f596a0f7

This commit appears in #67 which was approved by eviltrout. It was merged by justindirose.