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.
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
.