diff --git a/Gemfile.saas b/Gemfile.saas index 39aa49a71..e57fcf206 100644 --- a/Gemfile.saas +++ b/Gemfile.saas @@ -10,6 +10,7 @@ gem "queenbee", bc: "queenbee-plugin" gem "fizzy-saas", path: "saas" gem "console1984", bc: "console1984" gem "audits1984", bc: "audits1984", branch: "flavorjones/coworker-api" +gem "csv" # Native push notifications (iOS/Android) gem "action_push_native" diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 37ff08294..e4ed7406e 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -281,6 +281,7 @@ GEM bigdecimal rexml crass (1.0.6) + csv (3.3.5) date (3.5.1) debug (1.11.1) irb (~> 1.10) @@ -720,6 +721,7 @@ DEPENDENCIES bundler-audit capybara console1984! + csv debug faker fizzy-saas! diff --git a/saas/app/controllers/admin/stats_controller.rb b/saas/app/controllers/admin/stats_controller.rb index 6b9592fca..287b49024 100644 --- a/saas/app/controllers/admin/stats_controller.rb +++ b/saas/app/controllers/admin/stats_controller.rb @@ -6,9 +6,9 @@ class Admin::StatsController < AdminController @accounts_last_7_days = Account.where(created_at: 7.days.ago..).count @accounts_last_24_hours = Account.where(created_at: 24.hours.ago..).count - @paid_accounts_total = paid_subscriptions.distinct.count(:account_id) - @paid_accounts_last_7_days = paid_subscriptions.where(created_at: 7.days.ago..).distinct.count(:account_id) - @paid_accounts_last_24_hours = paid_subscriptions.where(created_at: 24.hours.ago..).distinct.count(:account_id) + @paid_accounts_total = Account::Subscription.paid.distinct.count(:account_id) + @paid_accounts_last_7_days = Account::Subscription.paid.where(created_at: 7.days.ago..).distinct.count(:account_id) + @paid_accounts_last_24_hours = Account::Subscription.paid.where(created_at: 24.hours.ago..).distinct.count(:account_id) @identities_total = Identity.count @identities_last_7_days = Identity.where(created_at: 7.days.ago..).count @@ -21,12 +21,4 @@ class Admin::StatsController < AdminController @recent_accounts = Account.order(created_at: :desc).limit(10) end - - private - def paid_subscriptions - Account::Subscription - .where(status: %w[active trialing past_due]) - .where(plan_key: %w[monthly_v1 monthly_extra_storage_v1]) - .where.not(account_id: Account::BillingWaiver.select(:account_id)) - end end diff --git a/saas/app/models/account/subscription.rb b/saas/app/models/account/subscription.rb index 2ae7e3171..c1ec9e47c 100644 --- a/saas/app/models/account/subscription.rb +++ b/saas/app/models/account/subscription.rb @@ -3,6 +3,12 @@ class Account::Subscription < SaasRecord enum :status, %w[ active past_due unpaid canceled incomplete incomplete_expired trialing paused ].index_by(&:itself) + scope :paid, -> { + where(status: %w[active trialing past_due]) + .where(plan_key: Plan.all.select(&:paid?).map(&:key)) + .where.not(account_id: Account::BillingWaiver.select(:account_id)) + } + validates :plan_key, presence: true, inclusion: { in: Plan::PLANS.keys.map(&:to_s) } delegate :paid?, to: :plan diff --git a/saas/lib/tasks/fizzy/usage_report.rake b/saas/lib/tasks/fizzy/usage_report.rake new file mode 100644 index 000000000..d8b3ba108 --- /dev/null +++ b/saas/lib/tasks/fizzy/usage_report.rake @@ -0,0 +1,37 @@ +require "csv" + +namespace :saas do + desc "Generate a CSV usage report for all active accounts" + task usage_report: :environment do + output_path = Rails.root.join("tmp/usage_report.csv") + + CSV.open(output_path, "w") do |csv| + csv << [ "Queenbee ID", "Account Name", "Sign Up Date", "Paid Date", "Comped", "Card Count", "Storage Used (Bytes)", "Last Active" ] + + Account.active.includes(:storage_total).in_batches do |batch| + batch_ids = batch.pluck(:id) + paid_dates = Account::Subscription.paid.where(account_id: batch_ids) + .group(:account_id).minimum(:created_at) + comped_account_ids = Account::BillingWaiver.where(account_id: batch_ids) + .pluck(:account_id).to_set + last_active_dates = Card.where(account_id: batch_ids) + .group(:account_id).maximum(:last_active_at) + + batch.each do |account| + csv << [ + account.external_account_id, + account.name, + account.created_at.to_date, + paid_dates[account.id]&.to_date, + comped_account_ids.include?(account.id), + account.cards_count, + account.bytes_used, + last_active_dates[account.id]&.to_date + ] + end + end + end + + puts "Report written to #{output_path}" + end +end diff --git a/saas/test/models/account/subscription_test.rb b/saas/test/models/account/subscription_test.rb index eaf7e103d..f9f16f2bb 100644 --- a/saas/test/models/account/subscription_test.rb +++ b/saas/test/models/account/subscription_test.rb @@ -16,6 +16,57 @@ class Account::SubscriptionTest < ActiveSupport::TestCase assert_not Account::Subscription.new(plan_key: "free_v1", status: "active").paid? end + test "paid scope includes active paid subscriptions" do + subscription = Account::Subscription.create!( + account: accounts(:initech), plan_key: "monthly_v1", status: "active", + stripe_customer_id: "cus_paid" + ) + + assert_includes Account::Subscription.paid, subscription + end + + test "paid scope includes trialing and past_due subscriptions" do + trialing = Account::Subscription.create!( + account: accounts(:initech), plan_key: "monthly_v1", status: "trialing", + stripe_customer_id: "cus_trialing" + ) + past_due = Account::Subscription.create!( + account: accounts(:acme), plan_key: "monthly_extra_storage_v1", status: "past_due", + stripe_customer_id: "cus_past_due" + ) + + assert_includes Account::Subscription.paid, trialing + assert_includes Account::Subscription.paid, past_due + end + + test "paid scope excludes free plan subscriptions" do + subscription = Account::Subscription.create!( + account: accounts(:initech), plan_key: "free_v1", status: "active", + stripe_customer_id: "cus_free" + ) + + assert_not_includes Account::Subscription.paid, subscription + end + + test "paid scope excludes canceled subscriptions" do + subscription = Account::Subscription.create!( + account: accounts(:initech), plan_key: "monthly_v1", status: "canceled", + stripe_customer_id: "cus_canceled" + ) + + assert_not_includes Account::Subscription.paid, subscription + end + + test "paid scope excludes comped accounts" do + subscription = Account::Subscription.create!( + account: accounts(:initech), plan_key: "monthly_v1", status: "active", + stripe_customer_id: "cus_comped" + ) + Account::BillingWaiver.create!(account: accounts(:initech)) + + assert_not_includes Account::Subscription.paid, subscription + end + test "pause pauses Stripe subscription with void behavior" do subscription = Account::Subscription.new(stripe_subscription_id: "sub_123")