Files
fizzy/saas/app/models/account/subscription.rb
T
Mike Dalessio ab5283441d SaaS usage reporting (#2690)
* Add saas:usage_report rake task and extract Subscription.paid scope

Add a rake task to generate a CSV usage report with per-account data:
Queenbee ID, sign up date, paid date, card count, storage used, and
last active date.

Extract the paid subscriptions query from Admin::StatsController into
an Account::Subscription.paid scope so both the controller and the
new rake task can share it.

* Add comped and account name columns to usage report

* Update saas/lib/tasks/fizzy/usage_report.rake

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Preload storage_total to avoid N+1 in usage report

* Batch last_active_at queries to avoid per-account aggregates

* Add tests for Account::Subscription.paid scope

* Update saas/app/models/account/subscription.rb

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Aggregate paid dates in SQL instead of loading all subscriptions

* Fix paid scope to derive plan keys from Plan.all instead of PLANS hash

* Move paid dates and comped lookups into per-batch queries

* Materialize batch IDs to avoid cross-database subquery

SaasRecord models live on a separate database (fizzy_saas) that doesn't
have the accounts table. Using batch.select(:id) generated a subquery
that ran on the saas database, causing a table-not-found error. Using
pluck(:id) materializes the IDs into an array instead.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 10:11:13 -04:00

74 lines
2.1 KiB
Ruby

class Account::Subscription < SaasRecord
belongs_to :account
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
def plan
@plan ||= Plan.find(plan_key)
end
def plan=(plan)
self.plan_key = plan.key
end
def to_be_canceled?
active? && cancel_at.present?
end
def next_amount_due
next_amount_due_in_cents ? next_amount_due_in_cents / 100.0 : plan.price
end
def pause
if stripe_subscription_id.present?
Stripe::Subscription.update(
stripe_subscription_id,
pause_collection: { behavior: "void" }
)
end
end
def resume
if stripe_subscription_id.present?
Stripe::Subscription.update(
stripe_subscription_id,
pause_collection: ""
)
end
end
def cancel
Stripe::Subscription.cancel(stripe_subscription_id) if stripe_subscription_id.present?
rescue Stripe::InvalidRequestError => e
# Subscription already deleted/canceled in Stripe - treat as success
Rails.logger.warn "Stripe subscription #{stripe_subscription_id} not found during cancel: #{e.message}"
end
def sync_customer_email_to_stripe
if stripe_customer_id && (email = owner_email)
Stripe::Customer.update(stripe_customer_id, email: email)
end
rescue Stripe::InvalidRequestError => e
# Customer already deleted in Stripe - treat as success
Rails.logger.warn "Stripe customer #{stripe_customer_id} not found during email sync: #{e.message}"
end
private
# Account owner email for Stripe customer record. Returns nil when:
# - No owner exists (ownership being transferred, account in limbo)
# - Owner has no identity (deactivated user)
def owner_email
account.users.owner.first&.identity&.email_address
end
end