Payments with Stripe for your Rails application.
Payola is a drop-in Rails engine that lets you sell one or more products by just including a module in your models. It includes:
- An easy to embed, easy to customize, async Stripe Checkout button
- Asynchronous payments, usable with any background processing system
- Full webhook integration
- Easy extension hooks for adding your own functionality
- Customizable emails
To see Payola in action, check out the site for Mastering Modern Payments: Using Stripe with Rails. Read the book to find out the whys behind Payola's design.
Add Payola to your Gemfile:
gem 'payola-payments'Run the installer and install the migrations:
$ rails g payola:install
$ rake db:migrateTo start selling products, just include Payola::Sellable. For example, if you have a Book model:
class Book < ActiveRecord::Base
include Payola::Sellable
endEach sellable model requires three attributes:
price, (attribute) an amount in the format that Stripe expects. For USD this is cents.permalink, (attribute) a human-readable slug that is exposed in the URLname, (attribute) a human-readable name exposed on product pages
There are also two optional methods you can implement on your sellable:
redirect_pathtakes the sale as an argument and returns a path. The buyer's browser will be redirected to that path after a successful sale. This defaults to/.currencyreturns the currency for this product. Payola will default tousd, which can be changed with thedefault_currencyconfig setting.
When people buy your product, Payola records information in Payola::Sale records and will record history if you have the paper_trail gem installed. It is highly recommended to install paper_trail.
To sell a product, use the checkout partial like this:
<%= render 'payola/transactions/checkout', sellable: YourProductClass.first %>This will insert a Stripe Checkout button. The checkout partial has a bunch of options:
sellable: The product to sell. Required.button_text: What to put on the button. Defaults to "Pay Now"button_class: What class to put on the actual button. Defaults to "stripe-button-el".name: What to put at the top of the Checkout popup. Defaults toproduct.name.description: What to show as the description in the popup. Defaults to product name + the formatted price.product_image_path: An image to insert into the Checkout popup. Defaults to blank.panel_label: The label of the button in the Checkout popup.allow_remember_me: Whether to show the Remember me checkbox. Defaults to true.email: Email address to pre-fill. Defaults to blank.custom_fields: Data to pass to thecharge_verifier(see below)
# config/initializers/payola.rb
Payola.configure do |payola|
payola.secret_key = 'sk_live_iwillnevertell'
payola.publishable_key = 'pk_live_iguessicantell'
# payola.default_currency = 'gbp'
payola.subscribe 'payola.book.sale.finished' do |sale|
SaleMailer.receipt(sale.guid).deliver
end
payola.subscribe 'payola.book.sale.failed' do |sale|
SaleMailer.admin_failed(sale.guid).deliver
end
payola.subscribe 'payola.book.sale.refunded' do |sale|
SaleMailer.admin_refunded(sale.guid).deliver
end
endYou can set your Stripe keys in two ways:
- By setting
STRIPE_SECRET_KEYandSTRIPE_PUBLISHABLE_KEYenvironment variables to their corresponding values. - By setting the
secret_keyandpublishable_keysettings in your Payola config initializer as shown above.
Payola wraps the StripeEvent gem for event processing and adds a few special sale-related events. Each one of these events passes the related Sale instance instead of a Stripe::Event. They are sent in-process so you don't have to wait for Stripe to send the corresponding webhooks.
payola.<product_class>.sale.finished, when a sale completes successfullypayola.<product_class>.sale.failed, when a charge failspayola.<product_class>.sale.refunded, when a charge is refunded
(In these examples, <product_class> is the underscore'd version of the product's class name.)
You can also subscribe to generic events that do not have the product_class included in them. Those are:
payola.sale.finished, when a sale completes successfullypayola.sale.failed, when a charge failspayola.sale.refunded, when a charge is refunded
You can set a callback that Payola will call immediately before attempting to make a charge. You can use this to, for example, check to see if the email address has been used already. To stop Payola from making a charge, throw a RuntimeError. The sale will be set to errored state and the message attached to the runtime error will be propogated back to the user.
Payola.configure do |payola|
payola.charge_verifier = lambda do |sale|
raise "Improper sale!" unless sale.amount > 10_000_000
end
endYou can optionally pass some data through the checkout button partial using the custom_fields option. This will be presented as a second argument to charge_verifier. For example:
<%= render 'payola/transactions/checkout', custom_data: {'hi' => 'there'} %>Payola.configure do |payola|
payola.charge_verifier = lambda do |sale, custom_data|
raise "Rude charge did not say hi!" unless custom_data['hi']
end
endWhatever data you pass through the custom_data option will be serialized and then signed with your Stripe secret key. You should stick to simple types like numbers, string, and hashes here, and try to minimize what you pass because it will end up both in the HTML you send to the user as well as the database.
You can subscribe to any webhook events you want as well. Payola will dedupe events as they come in. Make sure to set your webhook address in Stripe's management interface to:
https://www.example.com/payola/events
To subscribe to a webhook event:
Payola.configure do |payola|
payola.subscribe 'charge.succeeded' do |event|
sale = Sale.find_by(stripe_id: event.data.object.id)
SaleMailer.admin_receipt(sale.guid)
end
endPayola uses StripeEvent#event_retriever internally. If you would like to customize or filter the events that come through, use Payola's event_filter:
Payola.configure do |payola|
payola.event_filter = lambda do |event|
return nil unless event.blah?
event
end
endevent_filter takes an event and returns either nil or an event. If you return nil, the event ID will be recorded in the database but no further action will be taken. Returning the event allows processing to continue.
Payola will attempt to auto-detect the job queuing system you are using. It currently supports the following systems:
- ActiveJob (
:active_job) - Sidekiq (
:sidekiq) - SuckerPunch (
:sucker_punch)
Payola will attempt ActiveJob first and then move on to try autodetecting other systems. If you want to force Payola to use a specific supported system, just set background_worker to the appropriate symbol. For example:
Payola.background_worker = :sidekiqYou can also set this to anything with a call method, for complete control over how Payola's jobs get queued. For example, you can run jobs in-process like this:
Payola.background_worker = lambda do |klass, *args|
klass.call(*args)
endPayola includes basic emails that you can optionally send to your customers and yourself. Opt into them like this:
Payola.configure do |config|
config.send_email_for :receipt, :admin_receipt
endPossible emails include:
:receipt:refund:admin_receipt:admin_dispute:admin_refund:admin_failure
:receipt and :refund both send to the email address on the
Payola::Sale instance from the support_email address. All of
the :admin messages are sent from and to the support_email
address.
To customize the content of the emails, copy the appropriate views (receipt, admin) into your app at the same path (app/views/payola/<whatever>) and modify them as you like. You have access to @sale and @product, which is just a shortcut to @sale.product.
You can include a PDF with your receipt by setting the include_pdf_receipt option to true. This will send the receipt_pdf.html template to Docverter for conversion to PDF. See the Docverter README for installation instructions if you would like to run your own instance.
Payola's custom form support is basic but functional. Setting up a custom form has two steps. First, include the stripe_header partial in your layout's <head> tag:
<%= render 'payola/transactions/stripe_header' %>Now, to set up your form, give you need to give it the class payola-payment-form and set a few data attributes:
<%= form_for @whatever,
html: {
class: 'payola-payment-form',
'data-payola-base-path' => main_app.payola_path,
'data-payola-product' => @product.product_class,
'data-payola-permalink' => @product.permalink
} do |f| %>
<span class="payola-payment-error"></span>
Email:<br>
<input type="email" name="stripeEmail"
data-payola="email"></input><br>
Card Number<br>
<input type="text" data-stripe="number"></input><br>
Exp Month<br>
<input type="text" data-stripe="exp_month"></input><br>
Exp Year<br>
<input type="text" data-stripe="exp_year"></input><br>
CVC<br>
<input type="text" data-stripe="cvc"></input><br>
<input type="submit"></input>
<% end %>You need to set these three data attributes:
data-payola-base-path: should always be set tomain_app.payola_pathdata-payola-product: theproduct_classof the sellable you're sellingdata-payola-permalink: the permalink for this specific sellable
In addition, you should mark up the email input in your form with data-payola="email" in order for it to be set up in your Payola::Sale properly.
After that, you should mark up your card fields as laid out in the Stripe docs. Ensure that these fields do not have name attributes because you do not want them to be submitted to your application.
Payola::Sale has a polymorphic belongs_to :owner association which you can use to assign a sale to a particular business object in your application. One way to do this is in the charge_verifier:
Payola.configure do |config|
config.charge_verifier = lambda do |sale, custom_fields|
sale.owner = User.find(custom_fields[:user_id])
sale.save!
end
endIn this example you would have set the user_id custom field in the Checkout partial to the proper ID.
Payola has comprehensive support for Stripe subscriptions.
To create a subscription you first need a SubscriptionPlan model, which should includePayola::Plan.
class SubscriptionPlan < ActiveRecord::Base
include Payola::Plan
endA plan model requires a few attributes:
amount, (attribute) an amount in the format that Stripe expects. For USD this is cents.interval, (attribute) one of'day','week','month', or'year'interval_count, (attribute) the number of intervals between each subscriptionstripe_id, (attribute) a unique identifier used at Stripe to identify this planname, (attribute) a name describing this plan that will appear on customer invoicestrial_period_days, (attribute) optional the number of days for the trial period on this plan
Currently we only support custom subscription forms. Here's an example:
<%= form_for @plan, url: '/', method: :post, html: {
class: 'payola-subscription-form',
'data-payola-base-path' => '/payola',
'data-payola-plan-type' => @plan.plan_class,
'data-payola-plan-id' => @plan.id
} do |f| %>
<span class="payola-payment-error"></span>
Email:<br>
<input type="email" name="stripeEmail" data-payola="email"></input><br>
Card Number<br>
<input type="text" data-stripe="number"></input><br>
Exp Month<br>
<input type="text" data-stripe="exp_month"></input><br>
Exp Year<br>
<input type="text" data-stripe="exp_year"></input><br>
CVC<br>
<input type="text" data-stripe="cvc"></input><br>
<input type="submit"></input>
<% end %>You trigger the subscription behavior by setting the class payola-subscription-form and configure it with data attributes. There are currently three data attributes that all must be present:
payola-base-pathis the path where you've mounted Payola in your routes, which is usually/payola.payola-plan-typeis the value returned byplan_typeon the object that includesPayola::Plan.payola-plan-idis the value returned byidon the object that includesPayola::Plan.
When you submit the form Payola takes over, contacting Stripe to generate a token from the data-stripe inputs. When Payola is done processing, it will submit the form to the original URL which will receive a param named payola_subscription_guid. You can look up the corresponding Payola::Subscription like this:
subscription = Payola::Subscription.find_by(guid: params[:payola_subscription_guid])In this action you should set the subscription's owner attribute to something useful, like current_user for Devise.
You can add a button to cancel a subscription like this:
<%= render 'payola/subscriptions/cancel', subscription: @subscription %>Important Note: by default, Payola does no checking to verify that the current user actually has permission to cancel the given subscription. To add that, implement a method in your ApplicationController named payola_can_modify_subscription, which takes the subscription in question and returns true or false. For Devise this should look something like:
def payola_can_modify_subscription?(subscription)
subscription.owner == current_user
endYou can upgrade and downgrade subscriptions by POSTing to payola.change_subscription_plan_path(subscription) and passing plan_class and plan_id for the new plan as params. Payola provides a partial for you:
<%= render 'payola/subscriptions/change_plan',
subscription: @subscription,
new_plan: @new_plan %>Important Note: by default, Payola does no checking to verify that the current user actually has permission to modify the given subscription. To add that, implement a method in your ApplicationController named payola_can_modify_subscription, which takes the subscription in question and returns true or false. For Devise this should look something like:
- Affiliate tracking
- Coupon codes
Please see the LICENSE file for licensing details.
- Fork the project
- Make your changes, including tests that exercise the code
- Make a pull request
Version announcements happen on the Payola Payments Google group.
Pete Keen, @zrail, https://www.petekeen.net