Skip to content

Conversation

@swinner2
Copy link

@swinner2 swinner2 commented Oct 22, 2024

This PR adds support for a polymorphic parent of Billing::Usage::Tracker.

@swinner2 swinner2 self-assigned this Oct 22, 2024
model ||= self.class

send(BulletTrain::Billing::Usage.parent_association).billing_usage_trackers.current.each do |tracker|
billing_usage_trackers.current.each do |tracker|
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the send(BulletTrain::Billing::Usage.parent_association) because with these changes, parent_association needs to be dynamic, but being a class var makes it tricky. Removing it makes it work for our case, but it may not be ideal for the main gem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with this gem, so I'm not entirely sure, but I think this may cause trouble for apps that are already using this gem.

To take the example from the docs imagine that someone has done this:

class Blogs::Post < ApplicationRecord
  # ...

  def publish!
    update(published_at: Time.zone.now)
    track_billing_usage(:published)
  end
end

Before this change the track_billing_usage call would be equivalent to doing:

Billing::Usage::Trackers.where(team_id: self.team).current.each do |tracker|
  # ...
end

And after if would be doing:

Billing::Usage::Trackers.where(trackable_type: "Blog", trackable_id: self.id).current.each do |tracker|
  # ...
end

It seems like the usage would start getting tracked in a different place and the counts would be thrown off. Do we need to figure out a way to migrate the existing records into a new place? Or is there some way to rebuild the counts from scratch after making the update?

Comment on lines +10 to +22
workspace_id = if proxy_association.owner.is_a?(Workspace)
proxy_association.owner.id
else
proxy_association.owner.workspace.id
end

# This will grab the most recent tracker for this usage cycle.
# If it doesn't exist, it will be created. This can happen if developers introduce new usage cycles to track by.
order(created_at: :desc).includes(:counts).find_or_create_by(duration: duration, interval: interval)
order(created_at: :desc).includes(:counts).find_or_create_by(
workspace_id: workspace_id,
duration: duration,
interval: interval
)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes are specific to clickfunnels. I think once workspace_id is dropped, we can drop these changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that we want to release clickfunnels specific stuff in the gem. Most apps won't have a workspace_id and this will cause problems. Maybe we could remove this workspace_id stuff from this PR and you all could eject this file and maintain a custom version of it?

@andrewculver
Copy link
Contributor

@swinner2 Can we get more context on the use case here? What situation required this?

@swinner2
Copy link
Author

@swinner2 Can we get more context on the use case here? What situation required this?

Yes, so currently, there is one parent for all limits. We needed the ability to track on a more granular level. For example, take a limit rule like a team can have blogs, but each blog can only have 10 posts. We'd need the ability to track the number of posts on a specific blog instead of the number of posts on the whole Team. Making the parent dynamic solves this.

@andrewculver
Copy link
Contributor

Amazing, makes total sense. Thanks for this!

Copy link
Contributor

@jagthedrummer jagthedrummer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋 Hey @swinner2, long time no see! I hope you're doing well.

For the most part this is looking good. My biggest questions are:

  1. Can we remove the Clickfunnels specific bits so that they don't break other non-Cf apps?
  2. How can we help people make the transition from trackers having the equivalent of belongs_to :team (or whatever association has been configured) to having belongs_to :trackable, polymorphic: true. (Related question: How likely is it that someone would have configured a different parent_association and what all hoops would they have had to jump through to do so? I kinda seems like just changing parent_class wouldn't be sufficient since there's a team_id column on the billing_usage_trackers table...)

I left some specific comments/questions inline.

Comment on lines +10 to +22
workspace_id = if proxy_association.owner.is_a?(Workspace)
proxy_association.owner.id
else
proxy_association.owner.workspace.id
end

# This will grab the most recent tracker for this usage cycle.
# If it doesn't exist, it will be created. This can happen if developers introduce new usage cycles to track by.
order(created_at: :desc).includes(:counts).find_or_create_by(duration: duration, interval: interval)
order(created_at: :desc).includes(:counts).find_or_create_by(
workspace_id: workspace_id,
duration: duration,
interval: interval
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that we want to release clickfunnels specific stuff in the gem. Most apps won't have a workspace_id and this will cause problems. Maybe we could remove this workspace_id stuff from this PR and you all could eject this file and maintain a custom version of it?

class AddTrackableToBillingUsageTracker < ActiveRecord::Migration[7.1]
def change
add_column :billing_usage_trackers, :trackable_id, :bigint
add_column :billing_usage_trackers, :trackable_type, :string, default: "Team"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of defaulting to "Team" should we maybe default to BulletTrain::Billing::Usage.parent_association in case someone has overridden that association?

t.integer "count", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["action", "name", "tracker_id"], name: "index_billing_usage_counts_on_action_and_name_and_tracker_id", unique: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be a migration for this change. Do we need one? Or should we drop this line? (I realize this is the schema for the dummy app, just wondering if downstream apps need this change or not.)

t.bigint "team_id", null: false
t.integer "duration", null: false
t.string "interval", null: false
t.jsonb "usage", default: {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another schema change without a matching migration.

duration { 1 }
interval { "month" }
association :team
association :trackable, factory: :team
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to somehow use BulletTrain::Billing::Usage.parent_association here in case somebody has changed the default?

model ||= self.class

send(BulletTrain::Billing::Usage.parent_association).billing_usage_trackers.current.each do |tracker|
billing_usage_trackers.current.each do |tracker|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with this gem, so I'm not entirely sure, but I think this may cause trouble for apps that are already using this gem.

To take the example from the docs imagine that someone has done this:

class Blogs::Post < ApplicationRecord
  # ...

  def publish!
    update(published_at: Time.zone.now)
    track_billing_usage(:published)
  end
end

Before this change the track_billing_usage call would be equivalent to doing:

Billing::Usage::Trackers.where(team_id: self.team).current.each do |tracker|
  # ...
end

And after if would be doing:

Billing::Usage::Trackers.where(trackable_type: "Blog", trackable_id: self.id).current.each do |tracker|
  # ...
end

It seems like the usage would start getting tracked in a different place and the counts would be thrown off. Do we need to figure out a way to migrate the existing records into a new place? Or is there some way to rebuild the counts from scratch after making the update?

@jagthedrummer
Copy link
Contributor

Another couple of questions regarding this:

take a limit rule like a team can have blogs, but each blog can only have 10 posts

Considering that someone might want a rule more like:

a team can have unlimited blogs, but only 10 posts across all blogs

How would these two things be configured differently in config/models/billing/products.yml?

If someone already had the "10 posts across all blogs" rule in place, would this change kind of forcibly switch them to the "each blog can have 10 posts" rule?

If I'm understanding this PR correctly I think it would fundamentally change the meaning of a limit like this:

basic:
  prices:
    # ...
  limits:
    posts:
      have:
        count: 10
        enforcement: hard
        upgradable: true

I think that before this change that block would mean "10 posts across all blogs" and after this change it would mean "10 posts per blog". Is that accurate? And is it what we want? Do we need a way to configure the parent association at the limit level inside products.yml?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants