ab5283441d
* 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>
74 lines
2.1 KiB
Ruby
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
|