Question

I'm adding to my application the support for subscriptions.

The way I would like it to work is that user start with a free account, and can switch to premium one. this will be automatically renewed, but will be discontinued at the end of the month if the user stops paying or he cancels the subscription.

I'm getting a bit stuck with how to make it work, I'm not too sure how to handle it.

I have users configured with privileges using cancan.

from models/user.rb:

 ROLES = [:admin, :premium, :free]

  def roles=(roles)
    self.roles_mask= (roles & ROLES).map { |r| 2**ROLES.index(r) }.sum
  end

  def roles
    ROLES.reject { |r| ((roles_mask || 0) & 2**ROLES.index(r)).zero? }
  end

  def has_role?(role)
    roles.include? role
  end

  def has_one_of_roles?(roles_array)
    not (roles & roles_array).empty?
  end

  def upgrade_plan (role)
    return false unless ROLES.include? role
    self.roles = [ role ]
    self.save
  end

and I have a subscription controller controllers/subscription_controller.rb:

  def create

    @subscription = Subscription.new(params[:subscription])
    @subscription.user_id = current_user.id
    @subscription.expiration_date = 1.month.from_now

    respond_to do |format|
      if @subscription.save
        if current_user.upgrade_plan :premium
          format.html { redirect_to trades_path, notice: 'Subscription was successfully created. Compliments you are now subscribed to the premium plan' }
          format.json { render json: trades_path, status: :created, location: @subscription }
        else
          format.html { redirect_to home_pricing_path, notice: 'Error while upgrading your account, please contact us' }
          format.json { render json: home_pricing_path, status: :created, location: @subscription }
        end
      else
        format.html { render action: "new" }
        format.json { render json: @subscription.errors, status: :unprocessable_entity }
      end
    end
  end

Although it seems to me a bit redundant to have subscriptions in the database, but still have the check only on the user role.

Plus in this way I should check the validity of the subscription at every request, so that I could update the user role when it's expired, which seems a too heavy.

How would you deal with this sort of situation?

thanks,

Was it helpful?

Solution 2

I recently implemented a paywall too and I have the following advice.

  • First, your User class can quickly get clogged up when you combine too much into it. Consider separating account mgmt details into a separate model say an Account model. This way functionality about upgrades, downgrades, renewals, and expiration can be better encapsulated away from your core User model. Once you start tying in CanCan and Devise (if you're using that), User can quickly get overly multiplexed -- keep the central purpose of User as singular as possible. I wish I had done this sooner: I had to refactor into Account eventually and it's much cleaner (more testable, simpler, etc.) now. It's a simple has-a/belongs-to relationship.
  • With a separate Account (or Subscription) class your controller can perhaps be easier to manage? TDD tests might help reveal that as you work through the use cases.
  • Keep your expiration date simple. It is reasonably cheap to check the date on many requests as long as your only rely on local data. If you use a external paywall (say Stripe or Recurly) you'll want to minimize checking and it depends on how close to the original time of day you care about. I didn't see the need for a daily task, although that could certainly work, but then you'd have to verify it ran correctly every day.
  • I just used a separate account_type field (say, basic vs premium) to help support logic around expiration dates which is the same as what you're doing with ROLES. Switching back and forth then is reasonably simple and supports expiration date logic. Given what you're doing though be prepared to refactor as your platform's concept of Roles becomes more sophisticated or multi-dimensional (another reason to separate Account now). I used CanCan just to tie to the role without any regard to expiration. Keep the roles simple and let expiration logic handle switching roles as necessary. Again, separating expiration and account data away from user can help this. We largely separated admin away from paid accounts since in a way, they can be seen as different "dimensions" but that's just a design choice based on the dynamics of our specification.

Hope this helps.

UPDATE: 3/18/14 I realize your subject refers to Devise. Is there a more specific question you have. My implementation also involved Devise, so perhaps I can give more targeted advise.

OTHER TIPS

You don't need to check subscription on every request, because expiration are time based, you need to create rake task for expiring subscriptions and add it to cron on server to be called once a day.

You may read about custom rake tasks here.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top