I'm able to receive Web hook responses from Stripe, through Payola's StripeEvent method blocks. Each response renders me a 200 OK HTTP header when I click the test webhooks button on the Stripe dashboard. The issue that I am having, is the Payola::StripeWebhook's are being stored in the database, but the subscription and sales table are not updating. As a result, users are staying on trial and I cannot log payments, due to their records being stalemates. I was told that missing these events from server shutdowns/hardware failure, can lead to records not being updated correctly. I also see that there is a method called Payola::Subscription#sync_with! that is supposed to synchronize and update the records. How can I fix my StripeEvents to change the Subscription/Sales model data with Stripe Web Hook Responses?
I created events for each web hook response that I felt was necessary. For instance, I made a webhook event for charge.refunded, invoice.payment_failed, invoice.payment_succeeded, customer.subscription.deleted and trial_will_end. I see the response in my server from Stripe, but I see no updates taking place for the tables.
Server Log
Started POST "/payola/events" for 54.187.205.235 at 2019-01-16 19:54:07 -0500
Processing by StripeEvent::WebhookController#event as XML
Parameters: {"id"=>"evt_1DtP922xuZZdQdXfrVD0Iel0", "object"=>"event", "api_version"=>"2017-06-05", "created"=>1547686448, "data"=>{"object"=>{"id"=>"biz_basic_account", "object"=>"plan", "active"=>true, "aggregate_usage"=>nil, "amount"=>4600, "billing_scheme"=>"per_unit", "created"=>1543343985, "currency"=>"usd", "interval"=>"month", "interval_count"=>1, "livemode"=>false, "metadata"=>{}, "name"=>"Business Basics | $4.00", "nickname"=>"Business Basic", "product"=>"prod_E3I35OUSByBfci", "statement_descriptor"=>nil, "tiers"=>nil, "tiers_mode"=>nil, "transform_usage"=>nil, "trial_period_days"=>3, "usage_type"=>"licensed"}, "previous_attributes"=>{"nickname"=>nil, "trial_period_days"=>nil}}, "livemode"=>false, "pending_webhooks"=>1, "request"=>{"id"=>"req_E9ztRRhsBJ1PyU", "idempotency_key"=>nil}, "type"=>"plan.updated", "webhook"=>{"id"=>"evt_1DtP922xuZZdQdXfrVD0Iel0", "object"=>"event", "api_version"=>"2017-06-05", "created"=>1547686448, "data"=>{"object"=>{"id"=>"biz_basic_account", "object"=>"plan", "active"=>true, "aggregate_usage"=>nil, "amount"=>4600, "billing_scheme"=>"per_unit", "created"=>1543343985, "currency"=>"usd", "interval"=>"month", "interval_count"=>1, "livemode"=>false, "metadata"=>{}, "name"=>"Business Basics | $4.00", "nickname"=>"Business Basic", "product"=>"prod_E3I35OUSByBfci", "statement_descriptor"=>nil, "tiers"=>nil, "tiers_mode"=>nil, "transform_usage"=>nil, "trial_period_days"=>3, "usage_type"=>"licensed"}, "previous_attributes"=>{"nickname"=>nil, "trial_period_days"=>nil}}, "livemode"=>false, "pending_webhooks"=>1, "request"=>{"id"=>"req_E9ztRRhsBJ1PyU", "idempotency_key"=>nil}, "type"=>"plan.updated"}}
Payola::StripeWebhook Exists (1.0ms) SELECT 1 AS one FROM "payola_stripe_webhooks" WHERE "payola_stripe_webhooks"."stripe_id" = $1 LIMIT $2 [["stripe_id", "evt_1DtPegrgr922xuZZdQdXfrrgegeD0rgegl0"], ["LIMIT", 1]]
(0.0ms) BEGIN
CACHE Payola::StripeWebhook Exists (0.0ms) SELECT 1 AS one FROM "payola_stripe_webhooks" WHERE "payola_stripe_webhooks"."stripe_id" = $1 LIMIT $2 [["stripe_id", "evt_1DtPegrgr922xuZZdQdXfrrgegeD0rgegl0"], ["LIMIT", 1]]
Payola::StripeWebhook Create (0.0ms) INSERT INTO "payola_stripe_webhooks" ("stripe_id", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["stripe_id", "evt_1DtPegrgr922xuZZdQdXfrrgegeD0rgegl0"], ["created_at", "2019-01-17 00:54:08.269998"], ["updated_at", "2019-01-17 00:54:08.269998"]]
(0.0ms) COMMIT
Completed 200 OK in 469ms (ActiveRecord: 1.0ms)
payola.rb / StripeEvent management
Payola.configure do |config|
# Example subscription:
#
# config.subscribe 'payola.package.sale.finished' do |sale|
# EmailSender.send_an_email(sale.email)
# end
#
# In addition to any event that Stripe sends, you can subscribe
# to the following special payola events:
#
# - payola.<sellable class>.sale.finished
# - payola.<sellable class>.sale.refunded
# - payola.<sellable class>.sale.failed
#
# These events consume a Payola::Sale, not a Stripe::Event
#
# Example charge verifier:
#
# config.charge_verifier = lambda do |sale|
# raise "Nope!" if sale.email.includes?('yahoo.com')
# end
config.secret_key = ENV['STRIPE_SECRET_KEY']
config.publishable_key = ENV['STRIPE_PUBLISHABLE_KEY']
config.charge_verifier = lambda do |event|
user = User.find_by(email: event.email)
if event.is_a?(Payola::Subscription) && user.subscriptions.active.any?
raise 'Error: This user already has a subscription'
end
event.owner = user
event.save!
end
# Keep this subscription unless you want to disable refund handling
config.subscribe 'charge.refunded' do |event|
subscription = Payola::Subscription.find_by(stripe_id: event.data.object.id)
subscription.refund! unless sale.refunded?
end
Payola.subscribe 'invoice.payment_failed' do |event|
subscription = Payola::Subscription.find_by(stripe_id: event.data.object.id)
user = User.find_by(email: subscription.email)
ChargeFailedMailer.charge_failure_user(user)
user.update_attribute(:account_delinquent, true)
end
Payola.subscribe 'invoice.payment_succeeded' do |event|
subscription = Payola::Subscription.find_by(stripe_id: event.data.object.id)
user = User.find_by(email: subscription.email)
sale = Sale.find_by(stripe_id: event.data.object.id)
ChargeSucceededMailer.charge_succeeded_user(user)
user.update_attribute(:account_delinquent, false)
subscription.update_attribute(:status, event.data.object.status)
subscription.save
end
Payola.subscribe 'customer.subscription.deleted' do |event|
subscription = Payola::Subscription.find_by(stripe_id: event.data.object.id)
user = User.find_by(email: subscription.email)
sale = Sale.find_by(stripe_id: event.data.object.id)
CanceledSubscriptionMailer.subscription_canceled_for_user(user).deliver
user.destroy
end
Payola.subscribe 'customer.subscription.trial_will_end' do |event|
subscription = Payola::Subscription.find_by(stripe_id: event.data.object.id)
user = User.find_by(email: subscription.email)
sale = Sale.find_by(stripe_id: event.data.object.id)
TrialEndingMailer.trial_ending_for_user(user)
end
Payola.subscribe 'customer.subscription.updated' do |event|
puts 'Subscription updated!'
end
end
routes
Rails.application.routes.draw do
match '/subscription_expired', to: 'service_disruption#index', :via => :get
#mount Payola::Engine => '/payola', as: :payola
mount ActionCable.server => '/cable'
mount CountryStateSelect::Rails::Engine => '/'
get 'my_review/index'
require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'
post '/rate' => 'rater#create', :as => 'rate'
get 'update_account/index'
get 'u_help', to: 'user_help#index', as: :user_help
get 'username_validator/:username', to: 'usernames#username_validator'
devise_for :users, path: '', path_names: {sign_in: 'login', sign_out: 'logout', sign_up: 'signup'}, controllers: {registrations: 'users/registrations'}
namespace :api do
scope :v1 do
end
end
devise_scope :user do
put 'user_change_plan', to: 'users/registrations#user_change_plan'
put 'user_change_credit_card', to: 'users/registrations#user_change_credit_card'
authenticated do
root to: 'user_dashboard#index', as: 'authenticated_user_root'
end
unauthenticated do
root to: 'home#index', as: 'unauthenticated_user_root'
end
end
controller :home do
get :index, to: 'home#index', as: 'home', path: 'home'
get :pricing, to: 'home#pricing', as: 'pricing', path: 'pricing'
get :about, to: 'home#about', as: 'about', path: 'about'
get :contact, to: 'home#contact', as: 'contact', path: 'contact'
get :login_portal, to: 'home#login_portal', as: 'login_portal', path: 'login_portal'
get :signup_portal, to: 'home#signup_portal', as: 'signup_portal', path: 'signup_portal'
end
scope module: 'payola' do
mount StripeEvent::Engine => 'payola/events', as: :payola
post 'payola/buy/:product_class/:permalink' => 'transactions#create', as: :buy
get 'payola/confirm/:guid' => 'transactions#show', as: :confirm
get 'payola/status/:guid' => 'transactions#status', as: :status
post 'payola/subscribe/:plan_class/:plan_id' => 'subscriptions#create', as: :subscribe
get 'payola/confirm_subscription/:guid' => 'subscriptions#show', as: :confirm_subscription
get 'payola/subscription_status/:guid' => 'subscriptions#status', as: :subscription_status
delete 'payola/cancel_subscription/:guid' => 'subscriptions#destroy', as: :cancel_subscription
post 'payola/change_plan/:guid' => 'subscriptions#change_plan', as: :change_subscription_plan
post 'payola/change_quantity/:guid' => 'subscriptions#change_quantity', as: :change_subscription_quantity
post 'payola/update_card/:guid' => 'subscriptions#update_card', as: :update_card
post 'payola/update_customer/:id' => 'customers#update', as: :update_customer
post 'payola/create_card/:customer_id' => 'cards#create', as: :create_card
delete 'payola/destroy_card/:id/:customer_id' => 'cards#destroy', as: :destroy_card
end
root 'home#index'
end
application.js
//= require jquery3
......
//= require payola/subscription_form_onestep
//= require payola/subscription_form_twostep
//= require payola/checkout_button
//= require payola/form
//= require payola/subscription_checkout_button
//= require payola/subscription_form_register
stripe.rb
Rails.configuration.stripe = {
:publishable_key => ENV['STRIPE_PUBLISHABLE_KEY'],
:secret_key => ENV['STRIPE_SECRET_KEY']
}
Stripe.api_key = Rails.application.secrets.stripe_api_key
StripeEvent.signing_secret = ENV['STRIPE_SIGNING_SECRET']
UPDATE after changing the stripe event route to what the gem shows and placing it at the bottom. I receive a webhook response like the one below.
Completed 200 OK in 260332ms (Views: 259165.4ms | ActiveRecord: 81.9ms)
Started POST "/events" for 54.187.174.169 at 2019-01-18 21:11:30 -0500
Processing by StripeEvent::WebhookController#event as XML
Parameters: {"created"=>1326853478, "livemode"=>false, "id"=>"invoice.payment_00000000000000", "type"=>"invoice.payment_succeeded", "object"=>"event", "request"=>nil, "pending_webhooks"=>1, "api_version"=>"2017-06-05", "data"=>{"object"=>{"id"=>"in_00000000000000", "object"=>"invoice", "amount_due"=>900, "amount_paid"=>900, "amount_remaining"=>0, "application_fee"=>nil, "attempt_count"=>1, "attempted"=>true, "auto_advance"=>false, "billing"=>"charge_automatically", "billing_reason"=>"subscription", "charge"=>"_00000000000000", "closed"=>true, "currency"=>"usd", "custom_fields"=>nil, "customer"=>"cus_00000000000000", "date"=>1455648779, "default_source"=>nil, "description"=>nil, "discount"=>nil, "due_date"=>nil, "ending_balance"=>0, "finalized_at"=>1455648779, "footer"=>nil, "forgiven"=>false, "hosted_invoice_url"=>"https://pay.stripe.com/invoice/invst_1cWFwm2egegegegege", "invoice_pdf"=>"https://pay.stripe.com/invoice/invst_1cWFwm20xegegegegege/pdf", "lines"=>{"data"=>[{"id"=>"sub_00000000000000", "object"=>"line_item", "amount"=>900, "currency"=>"usd", "description"=>nil, "discountable"=>true, "livemode"=>false, "metadata"=>{}, "period"=>{"end"=>1502909579, "start"=>1500231179}, "plan"=>{"id"=>"gbsubscriptionlevel1_00000000000000", "object"=>"plan", "active"=>true, "aggregate_usage"=>nil, "amount"=>900, "billing_scheme"=>"per_unit", "created"=>1455057017, "currency"=>"usd", "interval"=>"month", "interval_count"=>1, "livemode"=>false, "metadata"=>{}, "name"=>"Going Big Subscription Basic", "nickname"=>nil, "product"=>"prod_00000000000000", "statement_descriptor"=>"Going Big SUBSCRIPTION", "tiers"=>nil, "tiers_mode"=>nil, "transform_usage"=>nil, "trial_period_days"=>nil, "usage_type"=>"licensed"}, "proration"=>false, "quantity"=>1, "subscription"=>nil, "subscription_item"=>"si_00000000000000", "type"=>"subscription"}], "has_more"=>false, "object"=>"list", "url"=>"/v1/invoices/in_17fDwV2xuZZdQdXgegegegeNB8uL/lines"}, "livemode"=>false, "metadata"=>{}, "next_payment_attempt"=>nil, "number"=>nil, "paid"=>true, "period_end"=>1455648779, "period_start"=>1455648779, "receipt_number"=>nil, "starting_balance"=>0, "statement_descriptor"=>nil, "status"=>"paid", "subscription"=>"sub_00000000000000", "subtotal"=>900, "tax"=>nil, "tax_percent"=>nil, "total"=>900, "webhooks_delivered_at"=>1455648780}}, "webhook"=>{"created"=>1326853478, "livemode"=>false, "id"=>"invoice.payment_00000000000000", "type"=>"invoice.payment_succeeded", "object"=>"event", "request"=>nil, "pending_webhooks"=>1, "api_version"=>"2017-06-05", "data"=>{"object"=>{"id"=>"in_00000000000000", "object"=>"invoice", "amount_due"=>900, "amount_paid"=>900, "amount_remaining"=>0, "application_fee"=>nil, "attempt_count"=>1, "attempted"=>true, "auto_advance"=>false, "billing"=>"charge_automatically", "billing_reason"=>"subscription", "charge"=>"_00000000000000", "closed"=>true, "currency"=>"usd", "custom_fields"=>nil, "customer"=>"cus_00000000000000", "date"=>1455648779, "default_source"=>nil, "description"=>nil, "discount"=>nil, "due_date"=>nil, "ending_balance"=>0, "finalized_at"=>1455648779, "footer"=>nil, "forgiven"=>false, "hosted_invoice_url"=>"https://pay.stripe.com/invoice/invst_1cWFwm2rggegegegeg", "invoice_pdf"=>"https://pay.stripe.com/invoice/invst_1cWrgegegegegege/pdf", "lines"=>{"data"=>[{"id"=>"sub_00000000000000", "object"=>"line_item", "amount"=>900, "currency"=>"usd", "description"=>nil, "discountable"=>true, "livemode"=>false, "metadata"=>{}, "period"=>{"end"=>1502909579, "start"=>1500231179}, "plan"=>{"id"=>"gbsubscriptionlevel1_00000000000000", "object"=>"plan", "active"=>true, "aggregate_usage"=>nil, "amount"=>900, "billing_scheme"=>"per_unit", "created"=>1455057017, "currency"=>"usd", "interval"=>"month", "interval_count"=>1, "livemode"=>false, "metadata"=>{}, "name"=>"Going Big Subscription Basic", "nickname"=>nil, "product"=>"prod_00000000000000", "statement_descriptor"=>"Going Big SUBSCRIPTION", "tiers"=>nil, "tiers_mode"=>nil, "transform_usage"=>nil, "trial_period_days"=>nil, "usage_type"=>"licensed"}, "proration"=>false, "quantity"=>1, "subscription"=>nil, "subscription_item"=>"si_00000000000000", "type"=>"subscription"}], "has_more"=>false, "object"=>"list", "url"=>"/v1/invoices/in_17fDwV2xuZZdQdegergegeg8uL/lines"}, "livemode"=>false, "metadata"=>{}, "next_payment_attempt"=>nil, "number"=>nil, "paid"=>true, "period_end"=>1455648779, "period_start"=>1455648779, "receipt_number"=>nil, "starting_balance"=>0, "statement_descriptor"=>nil, "status"=>"paid", "subscription"=>"sub_00000000000000", "subtotal"=>900, "tax"=>nil, "tax_percent"=>nil, "total"=>900, "webhooks_delivered_at"=>1455648780}}}}
Payola::StripeWebhook Exists (109.9ms) SELECT 1 AS one FROM "payola_stripe_webhooks" WHERE "payola_stripe_webhooks"."stripe_id" = $1 LIMIT $2 [["stripe_id", "invoice.payment_00000000000000"], ["LIMIT", 1]]
Completed 200 OK in 379ms (ActiveRecord: 114.9ms)
UPDATE 2
I believe that its working now. I received the event below for one of the subscription plans.
Event Details
ID
evt_1Dv7l02xuZZdQdXfaSXmAOSz
Date
2019/01/21 18:44:26
Type
customer.subscription.updated
Source
Automatic
Its not clear exactly what the issue is. So here's a few options to consider trying in case you've not tried them yet:
Check Payola is
mount
ed inconfig/routes.rb
. It should look like this:mount Payola::Engine => '/payments', as: :payments
Check background jobs are being run and watch the job queues to see if any payola-related jobs are being queued.
Ensure
payola.rb
is inconfig/initializers/
Fully restart the app and Spring anytime you change
config/initializers/payola.rb
, by runningbin/spring stop; bin/rails server
Check any
StripeEvent
config throughout the app for anything unexpected. Likewise for anyPayola
config.Update the
Payola.subscribe
calls toconfig.subscribe
in thepayola.rb
file given above.In your development environment only, debug the payola gem by adding your own code into the payola-payments gem's source at any points of interest. Find its location by running
bundle show payola-payments
and then open the directory printed in your code editor. You can make changes to any of the files in the gem to help debug. Each time you make a change to this gem, you will need to do a full restart to pick up the latest changes:bin/spring stop; bin/rails server
. Once you've finished your debugging, return the gem to its original state without your changes by runningbundle exec gem pristine payola-payments
.