Search code examples
ruby-on-railsruby-on-rails-4stripe-paymentscoupon

Stripe coupon not reflected in charge or saved in database for rails 4 app


We've gotten Stripe working for subscriptions payments in our rails 4 app using the Mastering Modern Payments approach. We're now trying to add coupons. We want to use Stripe to manage the coupons (create the coupons in the Stripe dashboard).

The problem is that when the user pays, the coupon is not reducing the amount charged, and is not getting saved into the app database with the user and subscription information. We are not getting any errors.

What is working: We see the coupon code in the hash when the user pays, and the coupon is set up in Stripe. Overall the user can sign up and pay fine. We do a two step create user in Devise then have them pay. When they pay their subscription flips to active in our app.

We know there is a missing piece here for coupons to work, but we are trying to figure out what that is and how should it be implemented? I read about creating a virtual attribute in rails, so perhaps we could do that for coupons, since there is not currently any coupon information in the Users table.

The output looks like this when user signs up for subscription (actual data replaced for posting):

... Started POST "/subscriptions" for 127.0.0.1 at 2015-11-30 10:33:40 -0800
Processing by SubscriptionsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"XYZ", "plan_id"=>"5", "email_address"=>"test55@gmail.com", "coupon"=>"123", "stripeToken"=>"tok_xyz"}
  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1  ORDER BY "users"."id" ASC LIMIT 1  [["id", 52]]
  Plan Load (0.3ms)  SELECT  "plans".* FROM "plans" WHERE "plans"."id" = $1 LIMIT 1  [["id", 5]]
  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = $1 LIMIT 1  [["email", "test55@gmail.com"]]
   (0.3ms)  BEGIN
  SQL (0.4ms)  UPDATE "users" SET "stripe_customer_id" = $1, "updated_at" = $2 WHERE "users"."id" = $3  [["stripe_customer_id", "cus_7Rq9Kv4nGh6uD2"], ["updated_at", "2015-11-30 18:33:41.930863"], ["id", 52]]
  SQL (48.6ms)  INSERT INTO "subscriptions" ("plan_id", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["plan_id", 5], ["user_id", 52], ["created_at", "2015-11-30 18:33:41.934364"], ["updated_at", "2015-11-30 18:33:41.934364"]]
DEPRECATION WARNING: `serialized_attributes` is deprecated without replacement, and will be removed in Rails 5.0. (called from serialized_attributes at /usr/local/rvm/gems/ruby-2.2.1/gems/activerecord-4.2.1/lib/active_record/attribute_methods/serialization.rb:56)...

Schema for Users and Subscriptions (coupons is a column in subscriptions table):

schema.rb:

...
 create_table "subscriptions", force: :cascade do |t|
    t.date     "purchase_date"
    t.boolean  "active"
    t.datetime "created_at",    null: false
    t.datetime "updated_at",    null: false
    t.integer  "user_id"
    t.integer  "plan_id"
    t.string   "stripe_id"
    t.string   "coupon"
  end

  add_index "subscriptions", ["plan_id"], name: "index_subscriptions_on_plan_id", using: :btree
  add_index "subscriptions", ["user_id"], name: "index_subscriptions_on_user_id", using: :btree

  create_table "users", force: :cascade do |t|
    t.string   "email",                  default: "",    null: false
    t.string   "encrypted_password",     default: ""
    t.string   "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.integer  "sign_in_count",          default: 0,     null: false
    t.datetime "current_sign_in_at"
    t.datetime "last_sign_in_at"
    t.string   "current_sign_in_ip"
    t.string   "last_sign_in_ip"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string   "name"
    t.string   "confirmation_token"
    t.datetime "confirmed_at"
    t.datetime "confirmation_sent_at"
    t.string   "unconfirmed_email"
    t.integer  "role"
    t.string   "invitation_token"
    t.datetime "invitation_created_at"
    t.datetime "invitation_sent_at"
    t.datetime "invitation_accepted_at"
    t.integer  "invitation_limit"
    t.integer  "invited_by_id"
    t.string   "invited_by_type"
    t.integer  "invitations_count",      default: 0
    t.boolean  "terms_accepted",         default: false
    t.string   "phone_number"
    t.string   "plan_id"
    t.string   "employer"
    t.string   "stripe_customer_id"
  end

  add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
  add_index "users", ["invitation_token"], name: "index_users_on_invitation_token", unique: true, using: :btree
  add_index "users", ["invitations_count"], name: "index_users_on_invitations_count", using: :btree
  add_index "users", ["invited_by_id"], name: "index_users_on_invited_by_id", using: :btree
  add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
...

Coupon is in the Create Subscription service object:

create_subscription.rb

class CreateSubscription
  def self.call(plan, email_address, token)
    user, raw_token = CreateUser.call(email_address)

    subscription = Subscription.new(
        plan: plan,
        user: user
    )

    begin
      stripe_sub = nil
      if user.stripe_customer_id.blank?
        customer = Stripe::Customer.create(
            source: token,
            email: user.email,
            plan: plan.stripe_id,
            coupon: subscription.coupon,
        )
        user.stripe_customer_id = customer.id
        user.save!
        stripe_sub = customer.subscriptions.first
      else
        customer = Stripe::Customer.retrieve(user.stripe_customer_id)
        stripe_sub = customer.subscriptions.create(
            plan: plan.stripe_id
        )
      end

      subscription.stripe_id = stripe_sub.id

      subscription.save!
    rescue Stripe::StripeError => e
      subscription.errors[:base] << e.message
    end

    subscription
  end
end

Coupon is on the pay page:

app/views/subscriptions/new.html.haml

= content_for :header do
    %script{src: 'https://js.stripe.com/v2/', type: 'text/javascript'}

    :javascript
        $(function(){
            Stripe.setPublishableKey("#{Rails.configuration.stripe[:publishable_key]}");
        });

    %script{src: '/subscriptions.js', type: 'text/javascript'}

- unless @subscription.errors.blank?
    = @subscription.errors.full_messages.to_sentence
%h2
    Subscribing to #{@plan.name}
= form_for @subscription, html: { id: 'payment-form' } do |f|
    %input{name: 'plan_id', type: 'hidden', value: @plan.id}/
    %span.payment-errors
    .form-row
        %label
            %span Email Address
            %input{name: 'email_address', size: '20', type: 'email', value: @user.email}
    .form-row
        %label
            %span Card Number
            %input{size: '20', type: 'text', data: {stripe: 'number'}}
    .form-row
        %label
            %span CVC
            %input{size: '4', type: 'text', data: {stripe: 'cvc'}}
    .form-row
        %label
            %span Expiration (MM/YYYY)
            %input{size: '2', type: 'text', data: {stripe: 'exp-month'}}
        %span /
        %input{size: '4', type: 'text', data: {stripe: 'exp-year'}}
    .form-row
        %label
            %span Promo code, if any
            %input{name: 'coupon', size: '20', type: 'text', value: @coupon}
    %button{type: 'submit', class: 'btn btn-lg btn-primary'} Pay Now

And our subscription model looks like this:

subscription.rb:

    class Subscription < ActiveRecord::Base
      belongs_to :user
      belongs_to :plan

      has_paper_trail

      def inactive?
        active ? false : true
      end

  def active?
    active
  end
end

How do we get Stripe coupon reflected in the charge and saved in our app database? Thank you.


Solution

  • Your CreateSubscription service doesn't provide a way for you to pass your coupon code.

    You'll need to add a new parameter for that eg.

    class CreateSubscription
      def self.call(plan, email_address, token, coupon = nil)
        # ...
        subscription = Subscription.new(
          plan: plan,
          user: user,
          coupon: coupon
        )
        # ...
      end
    end
    

    Also, there are a couple other things to be aware of with the code you've provided:

    customer = Stripe::Customer.create(
      source: token,
      email: user.email,
      plan: plan.stripe_id,
      coupon: subscription.coupon
    )
    

    Stipe allows you to set coupon on both Customers and Subscriptions. When billing, a discount applied to a subscription overrides a discount applied on a customer-wide basis. See https://stripe.com/docs/api#subscription_object

    So if your customer cancels their subscription, or has other (multiple) subscriptions. They'd potentially be getting their discount applied to those as well, depending on the settings of your coupon.

    So depending on what you want to have happen, you may only want to pass the coupon to the Stripe::Subscription instead of to the Stripe::Customer.