A subscription engine for Ruby on Rails.
Pay is a payments engine for Ruby on Rails 4.2 and higher.
Current Payment Providers
2020-08-27)
Want to add a new payment provider? Contributions are welcome and the instructions are here.
Check the CHANGELOG for any required migrations or changes needed if you're upgrading from a previous version of Pay.
Want to see how Pay works? Check out our video getting started guide.
Add these lines to your application's Gemfile:
gem 'pay', '~> 2.0'To use Stripe, also include:
gem 'stripe', '< 6.0', '>= 2.8' gem 'stripe_event', '~> 2.3'
To use Braintree + PayPal, also include:
gem 'braintree', '< 3.0', '>= 2.92.0'
To use Paddle, also include:
gem 'paddle_pay', '~> 0.0.1'
To use Receipts
gem 'receipts', '~> 1.0.0'
And then execute:
bundle
To add the migrations to your application, run the following migration:
bin/rails pay:install:migrations
We also need to run migrations to add Pay to the User, Account, Team, etc models that we want to make payments in our app.
bin/rails g pay User
This will generate a migration to add Pay fields to our User model and automatically includes the
Pay::Billablemodule in our
Usermodel. Repeat this for all the models you want to make payments in your app.
Finally, run the migrations
rake db:migrate
NoMethodError (undefined method 'stripe_customer' for #<0x00007fbc34b9bf20>)
Fully restart your Rails application
bin/spring stop && rails s
The
Pay::Billablemodule should be included in the models you want to make payments and subscriptions.
# app/models/user.rb class User < ActiveRecord::Base include Pay::Billable end
An
Billablemodel is required.
To sync over customer names, your
Billablemodel should respond to the
first_nameand
last_namemethods. Pay will sync these over to your Customer objects in Stripe and Braintree.
Need to make some changes to how Pay is used? You can create an initializer
config/initializers/pay.rb
Pay.setup do |config| config.chargeable_class = 'Pay::Charge' config.chargeable_table = 'pay_charges'For use in the receipt/refund/renewal mailers
config.business_name = "Business Name" config.business_address = "1600 Pennsylvania Avenue NW" config.application_name = "My App" config.support_email = "[email protected]"
config.send_emails = true
config.default_product_name = "default" config.default_plan_name = "default"
config.automount_routes = true config.routes_path = "/pay" # Only when automount_routes is true end
This allows you to create your own Charge class for instance, which could add receipt functionality:
class Charge < Pay::Charge def receipts # do some receipts stuff using the https://github.com/excid3/receipts gem end endPay.setup do |config| config.chargeable_class = 'Charge' end
You'll need to add your private Stripe API key to your Rails secrets
config/secrets.yml, credentials
rails credentials:edit
development: stripe: private_key: xxxx public_key: yyyy signing_secret: zzzz braintree: private_key: xxxx public_key: yyyy merchant_id: aaaa environment: sandbox paddle: vendor_id: xxxx vendor_auth_code: yyyy public_key_base64: MII...==
For Stripe, you can also use the
STRIPE_PUBLIC_KEY,
STRIPE_PRIVATE_KEYand
STRIPE_SIGNING_SECRETenvironment variables. For Braintree, you can also use
BRAINTREE_MERCHANT_ID,
BRAINTREE_PUBLIC_KEY,
BRAINTREE_PRIVATE_KEY, and
BRAINTREE_ENVIRONMENTenvironment variables. For Paddle, you can also use
PADDLE_VENDOR_ID,
PADDLE_VENDOR_AUTH_CODEand
PADDLE_PUBLIC_KEY_BASE64environment variables.
If you want to modify the Stripe SCA template or any other views, you can copy over the view files using:
bin/rails generate pay:views
If you want to modify the email templates, you can copy over the view files using:
bin/rails generate pay:email_views
Emails can be enabled/disabled using the
send_emailsconfiguration option (enabled per default). When enabled, the following emails will be sent:
You can check if the user is on a trial by simply asking:
user = User.find_by(email: '[email protected]')user.on_trial? #=> true or false
The
on_trial?method has two optional arguments with default values.
user = User.find_by(email: '[email protected]')user.on_trial?(name: 'default', plan: 'plan') #=> true or false
For trials that don't require cards upfront:
user = User.create( email: '[email protected]', trial_ends_at: 30.days.from_now )user.on_generic_trial? #=> true
user = User.find_by(email: '[email protected]')user.processor = 'stripe' user.card_token = 'payment_method_id' user.charge(1500) # $15.00 USD
user = User.find_by(email: '[email protected]')
user.processor = 'braintree' user.card_token = 'nonce' user.charge(1500) # $15.00 USD
The
chargemethod takes the amount in cents as the primary argument.
You may pass optional arguments that will be directly passed on to either Stripe or Braintree. You can use these options to charge different currencies, etc.
On failure, a
Pay::Errorwill be raised with details about the payment failure.
It is only possible to create immediate one-time charges on top of an existing subscription.
user = User.find_by(email: '[email protected]')user.processor = 'paddle' user.charge(1500, {charge_name: "Test"}) # $15.00 USD
An existing subscription and a charge name are required.
user = User.find_by(email: '[email protected]')user.processor = 'stripe' user.card_token = 'payment_method_id' user.subscribe
A
card_tokenmust be provided as an attribute.
The subscribe method has three optional arguments with default values.
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options) ... end
For example, you can pass the
quantityoption to subscribe to a plan with for per-seat pricing.
user.subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, quantity: 3)
Name is an internally used name for the subscription.
Plan is the plan ID or price ID from the payment processor. For example:
plan_xxxxxor
price_xxxxx
By default, the trial specified on the subscription will be used.
trial_period_days: 30can be set to override and a trial to the subscription. This works the same for Braintree and Stripe.
It is currently not possible to create a subscription through the API. Instead the subscription in Pay is created by the Paddle Subscription Webhook. In order to be able to assign the subcription to the correct owner, the Paddle passthrough parameter has to be used for checkout.
To ensure that the owner cannot be tampered with, Pay uses a Signed Global ID with a purpose. The purpose string consists of "paddle_" and the subscription plan id (or product id respectively).
Javascript Checkout:
javascript Paddle.Checkout.open({ product: 12345, passthrough: "" });
Paddle Button Checkout:
htmlPassthrough
Pay providers a helper method for generating the passthrough JSON object to associate the purchase with the correct Rails model.
Pay::Paddle.passthrough(owner: current_user, foo: :bar) #=> { owner_sgid: "xxxxxxxx", foo: "bar" }To generate manually without the helper
#=> { owner_sgid: current_user.to_sgid.to_s, foo: "bar" }.to_json
Pay parses the passthrough JSON string and verifies the
owner_sgidhash to match the webhook with the correct billable record. The passthrough parameterowner_sgidis only required for creating a subscription.Retrieving a Subscription from the Database
user = User.find_by(email: '[email protected]')user.subscription
A subscription can be retrieved by name, too.
user = User.find_by(email: '[email protected]')user.subscription(name: 'bananastand+')
Checking a User's Trial/Subscription Status
user = User.find_by(email: '[email protected]') user.on_trial_or_subscribed?
The
on_trial_or_subscribed?method has two optional arguments with default values.def on_trial_or_subscribed?(name: 'default', plan: nil) ... endChecking a User's Subscription Status
user = User.find_by(email: '[email protected]') user.subscribed?
The
subscribed?method has two optional arguments with default values.def subscribed?(name: 'default', plan: nil) ... endName
Name is an internally used name for the subscription.
Plan
Plan is the plan ID from the payment processor.
Retrieving a Payment Processor Account
Stripe and Braintree
user = User.find_by(email: '[email protected]')user.customer #> Stripe or Braintree customer account
Paddle
It is currently not possible to retrieve a payment processor account through the API.
Updating a Customer's Credit Card
Stripe and Braintree
user = User.find_by(email: '[email protected]')user.update_card('payment_method_id')
Paddle
Paddle provides a unique Update URL for each user, which allows them to update the payment method. ```ruby user = User.find_by(email: '[email protected]')
user.subscription.paddleupdateurl ```
Retrieving a Customer's Subscription from the Processor
user = User.find_by(email: '[email protected]')user.processor_subscription(subscription_id) #=> Stripe, Braintree or Paddle Subscription
Subscription API
Checking a Subscription's Trial Status
user = User.find_by(email: '[email protected]')user.subscription.on_trial? #=> true or false
Checking a Subscription's Cancellation Status
user = User.find_by(email: '[email protected]')user.subscription.cancelled? #=> true or false
Checking a Subscription's Grace Period Status
user = User.find_by(email: '[email protected]')user.subscription.on_grace_period? #=> true or false
Checking to See If a Subscription Is Active
user = User.find_by(email: '[email protected]')user.subscription.active? #=> true or false
Checking to See If a Subscription Is Paused
user = User.find_by(email: '[email protected]')user.subscription.paused? #=> true or false
Cancel a Subscription (At End of Billing Cycle)
Stripe, Braintree and Paddle
user = User.find_by(email: '[email protected]')user.subscription.cancel
Paddle
In addition to the API, Paddle provides a subscription Cancel URL that you can redirect customers to cancel their subscription.
user.subscription.paddle_cancel_urlCancel a Subscription Immediately
user = User.find_by(email: '[email protected]')user.subscription.cancel_now!
Pause a Subscription
Paddle
user = User.find_by(email: '[email protected]')user.subscription.pause
Swap a Subscription to another Plan
user = User.find_by(email: '[email protected]')user.subscription.swap("yearly")
Resume a Subscription
Stripe or Braintree Subscription (on Grace Period)
user = User.find_by(email: '[email protected]')user.subscription.resume
Paddle (Paused)
user = User.find_by(email: '[email protected]')user.subscription.resume
Retrieving the Subscription from the Processor
user = User.find_by(email: '[email protected]')user.subscription.processor_subscription
Customizing Pay Models
Want to add methods to
Pay::SubscriptionorPay::Charge? You can define a concern and simply include it in the model when Rails loads the code.Pay uses the
to_preparemethod to allow concerns to be included every time Rails reloads the models in development as well.# app/models/concerns/subscription_extensions.rb module SubscriptionExtensions extend ActiveSupport::Concernincluded do # associations and other class level things go here end
instance methods and code go here
end
# config/initializers/subscription_extensions.rb # Re-include the SubscriptionExtensions every time Rails reloads Rails.application.config.to_prepare do Pay.subscription_model.include SubscriptionExtensions endRoutes & Webhooks
Routes are automatically mounted to
/payby default.We provide a route for confirming SCA payments at
/pay/payments/:payment_intent_idWebhooks are automatically mounted at
/pay/webhooks/{provider}Customizing webhook mount path
If you have a catch all route (for 404s etc) and need to control where/when the webhook endpoints mount, you will need to disable automatic mounting and mount the engine above your catch all route.
# config/initializers/pay.rb config.automount_routes = falseconfig/routes.rb
mount Pay::Engine, at: '/secret-webhook-path'
If you just want to modify where the engine mounts it's routes then you can change the path.
# config/initializers/pay.rbconfig.routes_path = '/secret-webhook-path'
Payment Providers
We support Stripe, Braintree and Paddle and make our best attempt to standardize the three. They function differently so keep that in mind if you plan on doing more complex payments. It would be best to stick with a single payment provider in that case so you don't run into discrepancies.
Braintree
development: braintree: private_key: xxxx public_key: yyyy merchant_id: zzzz environment: sandboxPaddle
paddle: vendor_id: xxxx vendor_auth_code: yyyy public_key_base64: MII...==Paddle receipts can be retrieved by a charge receipt URL. ```ruby user = User.find_by(email: '[email protected]')
charge = user.charges.first charge.paddlereceipturl ```
Stripe
You'll need to add your private Stripe API key to your Rails secrets
config/secrets.yml, credentialsrails credentials:editdevelopment: stripe: private_key: xxxx public_key: yyyy signing_secret: zzzzYou can also use the
STRIPE_PRIVATE_KEYandSTRIPE_SIGNING_SECRETenvironment variables.To see how to use Stripe Elements JS & Devise, click here.
You need the following event types to trigger the webhook:
customer.subscription.updated customer.subscription.deleted customer.subscription.created payment_method.updated invoice.payment_action_required customer.updated customer.deleted charge.succeeded charge.refundedStrong Customer Authentication (SCA)
Our Stripe integration requires the use of Payment Method objects to correctly support Strong Customer Authentication with Stripe. If you've previously been using card tokens, you'll need to upgrade your Javascript integration.
Subscriptions that require SCA are marked as
incompleteby default. Once payment is authenticated, Stripe will send a webhook updating the status of the subscription. You'll need to use the Stripe CLI to forward webhooks to your application to make sure your subscriptions work correctly for SCA payments.stripe listen --forward-to localhost:3000/pay/webhooks/stripeYou should use
stripe.confirmCardSetupon the client to collect card information anytime you want to save the card and charge them later (adding a card, then charging them on the next page for example). Usestripe.confirmCardPaymentif you'd like to charge the customer immediately (think checking out of a shopping cart).The Javascript also needs to have a PaymentIntent or SetupIntent created server-side and the ID passed into the Javascript to do this. That way it knows how to safely handle the card tokenization if it meets the SCA requirements.
Payment Confirmations
Sometimes you'll have a payment that requires extra authentication. In this case, Pay provides a webhook and action for handling these payments. It will automatically email the customer and provide a link with the PaymentIntent ID in the url where the customer will be asked to fill out their name and card number to confirm the payment. Once done, they'll be redirected back to your application.
If you'd like to change the views of the payment confirmation page, you can install the views using the generator and modify the template.
Background jobs
If a user's email is updated and they have a
processor_idset, Pay will enqueue a background job (EmailSyncJob) to sync the email with the payment processor.It's important you set a queue_adapter for this to happen. If you don't, the code will be executed immediately upon user update. More information here
Contributors
👋 Thanks for your interest in contributing. Feel free to fork this repo.
If you have an issue you'd like to submit, please do so using the issue tracker in GitHub. In order for us to help you in the best way possible, please be as detailed as you can.
If you'd like to open a PR please make sure the following things pass:
bin/rails db:test:prepare bin/rails test
The gem is available as open source under the terms of the MIT License.