├── test
├── dummy
│ ├── log
│ │ └── .keep
│ ├── lib
│ │ └── assets
│ │ │ └── .keep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── apple-touch-icon.png
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── app
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ ├── javascripts
│ │ │ │ ├── channels
│ │ │ │ │ └── .keep
│ │ │ │ ├── cable.js
│ │ │ │ └── application.js
│ │ │ ├── config
│ │ │ │ └── manifest.js
│ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ ├── models
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ ├── user.rb
│ │ │ ├── application_record.rb
│ │ │ └── team.rb
│ │ ├── controllers
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ ├── main_controller.rb
│ │ │ ├── payment_methods_controller.rb
│ │ │ ├── application_controller.rb
│ │ │ ├── paddle
│ │ │ │ ├── payment_methods_controller.rb
│ │ │ │ ├── charges_controller.rb
│ │ │ │ └── subscriptions_controller.rb
│ │ │ ├── braintree
│ │ │ │ ├── payment_methods_controller.rb
│ │ │ │ ├── charges_controller.rb
│ │ │ │ └── subscriptions_controller.rb
│ │ │ └── stripe
│ │ │ │ ├── payment_methods_controller.rb
│ │ │ │ ├── checkouts_controller.rb
│ │ │ │ ├── charges
│ │ │ │ └── imports_controller.rb
│ │ │ │ ├── charges_controller.rb
│ │ │ │ └── subscriptions_controller.rb
│ │ ├── views
│ │ │ ├── layouts
│ │ │ │ ├── mailer.text.erb
│ │ │ │ └── mailer.html.erb
│ │ │ ├── paddle
│ │ │ │ ├── charges
│ │ │ │ │ ├── show.html.erb
│ │ │ │ │ ├── index.html.erb
│ │ │ │ │ └── new.html.erb
│ │ │ │ ├── payment_methods
│ │ │ │ │ └── edit.html.erb
│ │ │ │ └── subscriptions
│ │ │ │ │ ├── index.html.erb
│ │ │ │ │ ├── show.html.erb
│ │ │ │ │ └── new.html.erb
│ │ │ ├── stripe
│ │ │ │ ├── charges
│ │ │ │ │ ├── show.html.erb
│ │ │ │ │ ├── imports
│ │ │ │ │ │ └── new.html.erb
│ │ │ │ │ ├── index.html.erb
│ │ │ │ │ └── new.html.erb
│ │ │ │ ├── subscriptions
│ │ │ │ │ ├── index.html.erb
│ │ │ │ │ ├── show.html.erb
│ │ │ │ │ └── new.html.erb
│ │ │ │ ├── checkouts
│ │ │ │ │ └── show.html.erb
│ │ │ │ └── payment_methods
│ │ │ │ │ └── edit.html.erb
│ │ │ ├── braintree
│ │ │ │ ├── charges
│ │ │ │ │ ├── show.html.erb
│ │ │ │ │ ├── index.html.erb
│ │ │ │ │ └── new.html.erb
│ │ │ │ ├── subscriptions
│ │ │ │ │ ├── index.html.erb
│ │ │ │ │ ├── show.html.erb
│ │ │ │ │ └── new.html.erb
│ │ │ │ └── payment_methods
│ │ │ │ │ └── edit.html.erb
│ │ │ ├── main
│ │ │ │ └── show.html.erb
│ │ │ └── payment_methods
│ │ │ │ └── show.html.erb
│ │ ├── helpers
│ │ │ ├── application_helper.rb
│ │ │ └── current_helper.rb
│ │ ├── jobs
│ │ │ └── application_job.rb
│ │ ├── channels
│ │ │ └── application_cable
│ │ │ │ ├── channel.rb
│ │ │ │ └── connection.rb
│ │ ├── mailers
│ │ │ └── application_mailer.rb
│ │ └── javascript
│ │ │ ├── processors
│ │ │ └── paddle.js
│ │ │ ├── controllers
│ │ │ ├── index.js
│ │ │ └── braintree_controller.js
│ │ │ └── packs
│ │ │ └── application.js
│ ├── .browserslistrc
│ ├── config
│ │ ├── database.yml
│ │ ├── webpack
│ │ │ ├── environment.js
│ │ │ ├── test.js
│ │ │ ├── production.js
│ │ │ └── development.js
│ │ ├── spring.rb
│ │ ├── environment.rb
│ │ ├── cable.yml
│ │ ├── initializers
│ │ │ ├── session_store.rb
│ │ │ ├── mime_types.rb
│ │ │ ├── application_controller_renderer.rb
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── cookies_serializer.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── pay.rb
│ │ │ ├── assets.rb
│ │ │ ├── wrap_parameters.rb
│ │ │ └── inflections.rb
│ │ ├── boot.rb
│ │ ├── application.rb
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── secrets.yml
│ │ ├── routes.rb
│ │ ├── environments
│ │ │ ├── test.rb
│ │ │ └── development.rb
│ │ ├── puma.rb
│ │ └── webpacker.yml
│ ├── bin
│ │ ├── rake
│ │ ├── rails
│ │ ├── webpack
│ │ ├── webpack-dev-server
│ │ └── setup
│ ├── config.ru
│ ├── Rakefile
│ ├── package.json
│ ├── postcss.config.js
│ ├── db
│ │ └── migrate
│ │ │ ├── 20170205000000_create_users.rb
│ │ │ ├── 20200603150703_add_pay_billable_to_users.rb
│ │ │ └── 20200603152357_add_pay_billable_to_teams.rb
│ └── babel.config.js
├── support
│ └── fixtures
│ │ ├── stripe
│ │ ├── charge_refunded_event.json
│ │ ├── charge_succeeded_event.json
│ │ ├── customer_deleted_event.json
│ │ ├── customer_updated_event.json
│ │ └── payment_method.updated.json
│ │ └── paddle
│ │ ├── verification
│ │ └── paddle_public_key.pem
│ │ ├── subscription_cancelled.json
│ │ ├── subscription_created.json
│ │ ├── subscription_payment_refunded.json
│ │ ├── subscription_updated.json
│ │ └── subscription_payment_succeeded.json
├── pay
│ ├── paddle
│ │ ├── webhooks
│ │ │ ├── signature_verifier_test.rb
│ │ │ ├── subscription_payment_refunded_test.rb
│ │ │ └── subscription_created_test.rb
│ │ ├── error_test.rb
│ │ ├── charge_test.rb
│ │ └── billable_test.rb
│ ├── stripe_test.rb
│ ├── stripe
│ │ ├── error_test.rb
│ │ ├── billable_test.rb
│ │ ├── webhooks
│ │ │ ├── payment_action_required_test.rb
│ │ │ ├── subscription_renewing_test.rb
│ │ │ ├── charge_refunded_test.rb
│ │ │ ├── customer_deleted_test.rb
│ │ │ ├── customer_updated_test.rb
│ │ │ ├── payment_method_updated_test.rb
│ │ │ └── charge_succeeded_test.rb
│ │ └── checkout_test.rb
│ ├── braintree
│ │ ├── webhooks
│ │ │ ├── subscription_trial_ended.rb
│ │ │ ├── subscription_canceled_test.rb
│ │ │ └── subscription_charged_successfully.rb
│ │ ├── error_test.rb
│ │ └── charge_test.rb
│ ├── fake_processor
│ │ ├── charge_test.rb
│ │ ├── subscription_test.rb
│ │ └── billable_test.rb
│ ├── billable
│ │ └── sync_email_test.rb
│ ├── chargeable_test.rb
│ └── webhooks
│ │ └── delegator_test.rb
├── routes
│ └── webhooks_test.rb
├── controllers
│ └── pay
│ │ └── webhooks
│ │ ├── paddle_controller_test.rb
│ │ └── braintree_controller_test.rb
├── jobs
│ └── pay
│ │ └── email_sync_job_test.rb
├── pay_test.rb
├── vcr_cassettes
│ └── test_user_with_braintree_as_processor.yml
└── mailers
│ └── pay
│ └── user_mailer_test.rb
├── app
├── assets
│ ├── images
│ │ └── pay
│ │ │ └── .keep
│ ├── config
│ │ └── pay_manifest.js
│ ├── javascripts
│ │ └── pay
│ │ │ └── application.js
│ └── stylesheets
│ │ └── pay
│ │ └── application.css
├── helpers
│ └── pay
│ │ └── application_helper.rb
├── jobs
│ └── pay
│ │ ├── application_job.rb
│ │ └── email_sync_job.rb
├── controllers
│ └── pay
│ │ ├── application_controller.rb
│ │ ├── payments_controller.rb
│ │ └── webhooks
│ │ ├── braintree_controller.rb
│ │ ├── paddle_controller.rb
│ │ └── stripe_controller.rb
├── mailers
│ └── pay
│ │ ├── application_mailer.rb
│ │ └── user_mailer.rb
├── models
│ └── pay
│ │ ├── application_record.rb
│ │ └── charge.rb
└── views
│ ├── pay
│ ├── user_mailer
│ │ ├── subscription_renewing.html.erb
│ │ ├── payment_action_required.html.erb
│ │ ├── receipt.html.erb
│ │ └── refund.html.erb
│ └── stripe
│ │ └── _checkout_button.html.erb
│ └── layouts
│ └── pay
│ └── application.html.erb
├── docs
├── braintree.md
├── logo.png
├── paddle.md
├── webhooks.md
├── fake_processor.md
├── adding_a_payment_processor.md
└── stripe.md
├── lib
├── pay
│ ├── version.rb
│ ├── fake_processor
│ │ ├── error.rb
│ │ ├── charge.rb
│ │ ├── subscription.rb
│ │ └── billable.rb
│ ├── paddle
│ │ ├── error.rb
│ │ ├── webhooks
│ │ │ ├── subscription_payment_refunded.rb
│ │ │ ├── subscription_cancelled.rb
│ │ │ ├── subscription_updated.rb
│ │ │ └── subscription_payment_succeeded.rb
│ │ └── charge.rb
│ ├── stripe
│ │ ├── error.rb
│ │ ├── webhooks
│ │ │ ├── customer_updated.rb
│ │ │ ├── payment_method_updated.rb
│ │ │ ├── charge_refunded.rb
│ │ │ ├── subscription_deleted.rb
│ │ │ ├── charge_succeeded.rb
│ │ │ ├── customer_deleted.rb
│ │ │ ├── subscription_renewing.rb
│ │ │ ├── payment_intent_succeeded.rb
│ │ │ ├── payment_action_required.rb
│ │ │ ├── subscription_updated.rb
│ │ │ └── subscription_created.rb
│ │ └── charge.rb
│ ├── braintree
│ │ ├── authorization_error.rb
│ │ ├── webhooks
│ │ │ ├── subscription_canceled.rb
│ │ │ ├── subscription_trial_ended.rb
│ │ │ ├── subscription_expired.rb
│ │ │ ├── subscription_went_past_due.rb
│ │ │ ├── subscription_went_active.rb
│ │ │ ├── subscription_charged_successfully.rb
│ │ │ └── subscription_charged_unsuccessfully.rb
│ │ ├── error.rb
│ │ └── charge.rb
│ ├── fake_processor.rb
│ ├── webhooks.rb
│ ├── errors.rb
│ ├── engine.rb
│ ├── env.rb
│ ├── receipts.rb
│ ├── payment.rb
│ ├── billable
│ │ └── sync_email.rb
│ └── webhooks
│ │ └── delegator.rb
└── generators
│ ├── active_record
│ ├── templates
│ │ └── migration.rb
│ └── pay_generator.rb
│ └── pay
│ ├── views_generator.rb
│ ├── email_views_generator.rb
│ ├── pay_generator.rb
│ └── orm_helpers.rb
├── .standard.yml
├── Appraisals
├── .gitignore
├── config
├── routes.rb
└── locales
│ └── en.yml
├── .rubocop.yml
├── gemfiles
├── rails_6.gemfile
└── rails_6_1.gemfile
├── db
└── migrate
│ ├── 20190816015720_add_status_to_pay_subscriptions.rb
│ ├── 20170727235816_create_pay_charges.rb
│ ├── 20170205020145_create_pay_subscriptions.rb
│ └── 20200603134434_add_data_to_pay_models.rb
├── bin
└── rails
├── .github
└── FUNDING.yml
├── Rakefile
├── pay.gemspec
├── Gemfile
└── MIT-LICENSE
/test/dummy/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/pay/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/.browserslistrc:
--------------------------------------------------------------------------------
1 | defaults
2 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/braintree.md:
--------------------------------------------------------------------------------
1 | # Using Pay with Braintree
2 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/javascripts/channels/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/lib/pay/version.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | VERSION = "2.6.7"
3 | end
4 |
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/pay/HEAD/docs/logo.png
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | test:
2 | adapter: sqlite3
3 | database: db/test.sqlite3
4 |
--------------------------------------------------------------------------------
/.standard.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - 'test/dummy/**/*'
3 | - 'lib/generators/active_record/templates/*'
4 |
--------------------------------------------------------------------------------
/app/helpers/pay/application_helper.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module ApplicationHelper
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ApplicationRecord
2 | include Pay::Billable
3 | end
4 |
--------------------------------------------------------------------------------
/app/jobs/pay/application_job.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | class ApplicationJob < ActiveJob::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/assets/config/pay_manifest.js:
--------------------------------------------------------------------------------
1 | //= link_directory ../javascripts/pay .js
2 | //= link_directory ../stylesheets/pay .css
3 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/main_controller.rb:
--------------------------------------------------------------------------------
1 | class MainController < ApplicationController
2 | def show
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/test/dummy/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/lib/pay/fake_processor/error.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module FakeProcessor
3 | class Error < Pay::Error
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/dummy/config/webpack/environment.js:
--------------------------------------------------------------------------------
1 | const { environment } = require('@rails/webpacker')
2 |
3 | module.exports = environment
4 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "rails-6" do
2 | gem "rails", "~> 6.0.0"
3 | end
4 |
5 | appraise "rails-6-1" do
6 | gem "rails", "~> 6.1.0"
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/payment_methods_controller.rb:
--------------------------------------------------------------------------------
1 | class PaymentMethodsController < ApplicationController
2 | def show
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/pay/paddle/error.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Paddle
3 | class Error < Pay::Error
4 | delegate :message, to: :cause
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/pay/stripe/error.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Stripe
3 | class Error < Pay::Error
4 | delegate :message, to: :cause
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: "from@example.com"
3 | layout "mailer"
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/config/spring.rb:
--------------------------------------------------------------------------------
1 | %w[
2 | .ruby-version
3 | .rbenv-vars
4 | tmp/restart.txt
5 | tmp/caching-dev.txt
6 | ].each { |path| Spring.watch(path) }
7 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/test/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../config/application', __dir__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/app/controllers/pay/application_controller.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | class ApplicationController < ActionController::Base
3 | protect_from_forgery with: :exception
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/app/mailers/pay/application_mailer.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | class ApplicationMailer < ActionMailer::Base
3 | default from: Pay.support_email
4 | layout "mailer"
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 |
2 | //= link_tree ../images
3 | //= link_directory ../javascripts .js
4 | //= link_directory ../stylesheets .css
5 | //= link pay_manifest.js
6 |
--------------------------------------------------------------------------------
/test/dummy/app/javascript/processors/paddle.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("turbolinks:load", () => {
2 | Paddle.Environment.set('sandbox');
3 | Paddle.Setup({ vendor: 924 });
4 | })
5 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | include CurrentHelper
3 | protect_from_forgery with: :exception
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: async
6 |
7 | production:
8 | adapter: redis
9 | url: redis://localhost:6379/1
10 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: "_dummy_session"
4 |
--------------------------------------------------------------------------------
/test/dummy/config/webpack/test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/test/dummy/app/models/team.rb:
--------------------------------------------------------------------------------
1 | class Team < ApplicationRecord
2 | include Pay::Billable
3 |
4 | belongs_to :owner, class_name: "User"
5 |
6 | def email
7 | owner.email
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/config/webpack/production.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/test/dummy/config/webpack/development.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/test/support/fixtures/stripe/charge_refunded_event.json:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "id": "ch_chargeid",
4 | "amount_refunded": 500,
5 | "created": 1546324243,
6 | "customer": "cus_customerid"
7 | }
8 | }
--------------------------------------------------------------------------------
/test/dummy/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ApplicationController.renderer.defaults.merge!(
4 | # http_host: 'example.org',
5 | # https: false
6 | # )
7 |
--------------------------------------------------------------------------------
/lib/pay/braintree/authorization_error.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Braintree
3 | class AuthorizationError < Braintree::Error
4 | def message
5 | I18n.t("errors.braintree.authorization")
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/app/views/paddle/charges/show.html.erb:
--------------------------------------------------------------------------------
1 |
11 | <%= link_to "Pay::Charge #{charge.id}", paddle_charge_path(charge) %>
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20170205000000_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :users do |t|
4 | t.string :email
5 | t.string :first_name
6 | t.string :last_name
7 | end
8 |
9 | create_table :teams do |t|
10 | t.string :email
11 | t.string :name
12 | t.references :owner, polymorphic: true
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/stripe/payment_methods_controller.rb:
--------------------------------------------------------------------------------
1 | module Stripe
2 | class PaymentMethodsController < ApplicationController
3 | def edit
4 | @setup_intent = ::Stripe::SetupIntent.create
5 | end
6 |
7 | def update
8 | current_user.processor = params[:processor]
9 | current_user.update_card(params[:card_token])
10 | redirect_to payment_method_path
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/dummy/app/views/paddle/payment_methods/edit.html.erb:
--------------------------------------------------------------------------------
1 | This is just a friendly reminder that your <%= Pay.business_name %> subscription will renew automatically on <%= l params[:date].to_date, format: :long %>.
3 |
4 | You may manage your subscription via your account. If you have any questions, please hit reply and let us know.
5 |
6 |
11 | <%= link_to "Pay::Charge #{charge.id}", braintree_charge_path(charge) %>
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/test/dummy/app/views/stripe/charges/imports/new.html.erb:
--------------------------------------------------------------------------------
1 |
5 | <%= form.label :id, "Charge or PaymentIntent ID" %>
6 | <%= form.text_field :id, class: "form-control", placeholder: "ch_xxxxxx or pi_xxxxxx" %>
7 |
8 |
9 | <%= form.submit "Import", class: "btn btn-primary" %>
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/docs/paddle.md:
--------------------------------------------------------------------------------
1 | # Using Pay with Paddle
2 |
3 | ## Paddle Sandbox
4 |
5 | The [Paddle Sandbox](https://developer.paddle.com/getting-started/sandbox) can be used for testing your Paddle integration.
6 |
7 | ```html
8 |
9 |
13 | ```
14 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/javascripts/cable.js:
--------------------------------------------------------------------------------
1 | // Action Cable provides the framework to deal with WebSockets in Rails.
2 | // You can generate new channels where WebSocket features live using the rails generate channel command.
3 | //
4 | //= require action_cable
5 | //= require_self
6 | //= require_tree ./channels
7 |
8 | (function() {
9 | this.App || (this.App = {});
10 |
11 | App.cable = ActionCable.createConsumer();
12 |
13 | }).call(this);
14 |
--------------------------------------------------------------------------------
/app/views/pay/user_mailer/payment_action_required.html.erb:
--------------------------------------------------------------------------------
1 | Your <%= Pay.business_name %> subscription requires confirmation to process your payment to continue access.
3 |
4 | <%= link_to "Click here to confirm your payment", pay.payment_url(params[:payment_intent_id]) %>. If you have any questions, please hit reply and let us know.
5 |
6 |
10 | <%= link_to "Pay::Subscription #{subscription.id}", paddle_subscription_path(subscription) %>
11 |
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/test/dummy/app/views/stripe/subscriptions/index.html.erb:
--------------------------------------------------------------------------------
1 |
10 | <%= link_to "Pay::Subscription #{subscription.id}", stripe_subscription_path(subscription) %>
11 |
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/test/pay/stripe_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Stripe::Test < ActiveSupport::TestCase
4 | test "finds API keys from env" do
5 | ENV["STRIPE_PUBLIC_KEY"] = "public"
6 | ENV["STRIPE_PRIVATE_KEY"] = "private"
7 | ENV["STRIPE_SIGNING_SECRET"] = "secret"
8 |
9 | assert_equal "public", Pay::Stripe.public_key
10 | assert_equal "private", Pay::Stripe.private_key
11 | assert_equal "secret", Pay::Stripe.signing_secret
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | Exclude:
3 | - db/migrate/*
4 | - lib/pay/version.rb
5 | - test/dummy/**/*
6 | - test/models/subscription_test.rb
7 | - test/pay/billable_test.rb
8 | - test/test_helper.rb
9 |
10 | Documentation:
11 | Enabled: false
12 |
13 | ClassAndModuleChildren:
14 | Enabled: false
15 |
16 | ClassVars:
17 | Enabled: false
18 |
19 | SpecialGlobalVars:
20 | Enabled: false
21 |
22 | AmbiguousBlockAssociation:
23 | Enabled: false
24 |
--------------------------------------------------------------------------------
/test/dummy/app/views/braintree/subscriptions/index.html.erb:
--------------------------------------------------------------------------------
1 |
10 | <%= link_to "Pay::Subscription #{subscription.id}", braintree_subscription_path(subscription) %>
11 |
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/lib/generators/pay/pay_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails/generators/named_base"
4 |
5 | module Pay
6 | module Generators
7 | class PayGenerator < Rails::Generators::NamedBase
8 | include Rails::Generators::ResourceHelpers
9 |
10 | namespace "pay"
11 | source_root File.expand_path("../templates", __FILE__)
12 |
13 | desc "Generates a migration to add Billable fields to a model."
14 |
15 | hook_for :orm
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/pay/fake_processor/charge.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module FakeProcessor
3 | class Charge
4 | attr_reader :pay_charge
5 |
6 | delegate :processor_id, :owner, to: :pay_charge
7 |
8 | def initialize(pay_charge)
9 | @pay_charge = pay_charge
10 | end
11 |
12 | def charge
13 | pay_charge
14 | end
15 |
16 | def refund!(amount_to_refund)
17 | pay_charge.update(amount_refunded: amount_to_refund)
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/dummy/bin/webpack:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "bundler/setup"
11 |
12 | require "webpacker"
13 | require "webpacker/webpack_runner"
14 |
15 | APP_ROOT = File.expand_path("..", __dir__)
16 | Dir.chdir(APP_ROOT) do
17 | Webpacker::WebpackRunner.run(ARGV)
18 | end
19 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20200603150703_add_pay_billable_to_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddPayBillableToUsers < ActiveRecord::Migration[6.0]
4 | def change
5 | change_table :users do |t|
6 | t.string :processor
7 | t.string :processor_id
8 | t.datetime :trial_ends_at
9 | t.string :card_type
10 | t.string :card_last4
11 | t.string :card_exp_month
12 | t.string :card_exp_year
13 | t.text :extra_billing_info
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20200603152357_add_pay_billable_to_teams.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddPayBillableToTeams < ActiveRecord::Migration[6.0]
4 | def change
5 | change_table :teams do |t|
6 | t.string :processor
7 | t.string :processor_id
8 | t.datetime :trial_ends_at
9 | t.string :card_type
10 | t.string :card_last4
11 | t.string :card_exp_month
12 | t.string :card_exp_year
13 | t.text :extra_billing_info
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/dummy/app/views/stripe/charges/index.html.erb:
--------------------------------------------------------------------------------
1 |
12 | <%= link_to "Pay::Charge #{charge.id}", stripe_charge_path(charge) %>
13 |
14 | <% end %>
15 |
--------------------------------------------------------------------------------
/test/dummy/bin/webpack-dev-server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "bundler/setup"
11 |
12 | require "webpacker"
13 | require "webpacker/dev_server_runner"
14 |
15 | APP_ROOT = File.expand_path("..", __dir__)
16 | Dir.chdir(APP_ROOT) do
17 | Webpacker::DevServerRunner.run(ARGV)
18 | end
19 |
--------------------------------------------------------------------------------
/lib/pay/stripe/webhooks/customer_updated.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Stripe
3 | module Webhooks
4 | class CustomerUpdated
5 | def call(event)
6 | object = event.data.object
7 | billable = Pay.find_billable(processor: :stripe, processor_id: object.id)
8 |
9 | # Couldn't find user, we can skip
10 | return unless billable.present?
11 |
12 | Pay::Stripe::Billable.new(billable).sync_card_from_stripe
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/pay/stripe/error_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Stripe::ErrorTest < ActiveSupport::TestCase
4 | setup do
5 | @user = User.create!(email: "gob@bluth.com", processor: :stripe)
6 | end
7 |
8 | test "re-raised stripe exceptions keep the same message" do
9 | exception = assert_raises(Pay::Stripe::Error) { @user.charge(0) }
10 | assert_equal "This value must be greater than or equal to 1.", exception.message
11 | assert_equal ::Stripe::InvalidRequestError, exception.cause.class
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/pay/stripe/webhooks/payment_method_updated.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Stripe
3 | module Webhooks
4 | class PaymentMethodUpdated
5 | def call(event)
6 | object = event.data.object
7 | billable = Pay.find_billable(processor: :stripe, processor_id: object.customer)
8 |
9 | # Couldn't find user, we can skip
10 | return unless billable.present?
11 |
12 | Pay::Stripe::Billable.new(billable).sync_card_from_stripe
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/dummy/app/views/stripe/subscriptions/show.html.erb:
--------------------------------------------------------------------------------
1 |
4 | <%= render partial: "pay/stripe/checkout_button", locals: { session: @payment, title: "Payment" } %>
5 |
6 |
7 |
8 | <%= render partial: "pay/stripe/checkout_button", locals: { session: @subscription, title: "Subscription" } %>
9 |
10 |
11 |
12 | <%= render partial: "pay/stripe/checkout_button", locals: { session: @setup, title: "Setup" } %>
13 |
14 |
15 |
4 |
Processor
5 | <%= current_user.processor&.titleize || "None"%>
6 |
7 |
8 |
Payment Method Details
9 | <% if current_user.card_last4? && current_user.paypal? %>
10 |
<%= current_user.card_type.titleize %> (<%= current_user.card_last4 %>)
11 | <% elsif current_user.card_last4? %>
12 |
<%= current_user.card_type.titleize %> ending in <%= current_user.card_last4 %>
13 |
Expires <%= current_user.card_exp_month %> / <%= current_user.card_exp_year %>
14 | <% else %>
15 | No card on file.
16 | <% end %>
17 |
18 |
19 |
20 |
Update Payment Method
21 | <%= link_to "Stripe", edit_stripe_payment_method_path, class: "d-block" %>
22 | <%= link_to "Braintree", edit_braintree_payment_method_path, class: "d-block" %>
23 | <%= link_to "Paddle", edit_paddle_payment_method_path, class: "d-block" %>
24 |
25 |
--------------------------------------------------------------------------------
/test/dummy/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 1ca8a0578a7f730e9b976b2d4caaaf4a739bb5e391de413127bf748c9ffe8abc9784eaae87eb7fbf88543bc3ee67c74576e1763f75b6a6c6bc2071f834782652
15 |
16 | test:
17 | secret_key_base: ae102b18ad3fcf12beea3626f5dc743633e494919238f2153fcd5720e7625b62006350d5e11cafb0f3383417d9823eb2c8e2133cdddbd7f4b032e0412105d299
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | # Declare your gem's dependencies in pay.gemspec.
5 | # Bundler will treat runtime dependencies like base dependencies, and
6 | # development dependencies will be added by default to the :development group.
7 | gemspec
8 |
9 | # Declare any dependencies that are still in development here instead of in
10 | # your gemspec. These might include edge Rails or gems from your path or
11 | # Git. Remember to move these dependencies to your gemspec before releasing
12 | # your gem to rubygems.org.
13 |
14 | gem "byebug"
15 | gem "appraisal", github: "excid3/appraisal", branch: "fix-bundle-env"
16 |
17 | gem "braintree", ">= 2.92.0", "< 4.0"
18 | gem "stripe", ">= 2.8"
19 | gem "paddle_pay", "~> 0.1.0"
20 |
21 | # Test against different databases
22 | gem "sqlite3", "~> 1.4"
23 | gem "mysql2"
24 | gem "pg"
25 |
26 | # Used for the dummy Rails app integration
27 | gem "puma"
28 | gem "standard"
29 | gem "turbolinks"
30 | gem "web-console", group: :development
31 | gem "webpacker"
32 |
--------------------------------------------------------------------------------
/app/controllers/pay/webhooks/paddle_controller.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Webhooks
3 | class PaddleController < Pay::ApplicationController
4 | if Rails.application.config.action_controller.default_protect_from_forgery
5 | skip_before_action :verify_authenticity_token
6 | end
7 |
8 | def create
9 | delegate_event(verified_event)
10 | head :ok
11 | rescue Pay::Paddle::Error
12 | head :bad_request
13 | end
14 |
15 | private
16 |
17 | def delegate_event(event)
18 | Pay::Webhooks.instrument type: "paddle.#{type}", event: event
19 | end
20 |
21 | def type
22 | params[:alert_name]
23 | end
24 |
25 | def verified_event
26 | event = check_params.as_json
27 | verifier = Pay::Paddle::Webhooks::SignatureVerifier.new(event)
28 | return event if verifier.verify
29 | raise Pay::Paddle::Error, "Unable to verify Paddle webhook event"
30 | end
31 |
32 | def check_params
33 | params.except(:action, :controller).permit!
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/pay/braintree/webhooks/subscription_charged_successfully.rb:
--------------------------------------------------------------------------------
1 | # A subscription successfully moves to the next billing cycle. This will also occur when either a new transaction is created mid-cycle due to proration on an upgrade or a billing cycle is skipped due to the presence of a negative balance that covers the cost of the subscription.
2 |
3 | module Pay
4 | module Braintree
5 | module Webhooks
6 | class SubscriptionChargedSuccessfully
7 | def call(event)
8 | subscription = event.subscription
9 | return if subscription.nil?
10 |
11 | pay_subscription = Pay.subscription_model.find_by(processor: :braintree, processor_id: subscription.id)
12 | return unless pay_subscription.present?
13 |
14 | billable = pay_subscription.owner
15 | charge = Pay::Braintree::Billable.new(billable).save_transaction(subscription.transactions.first)
16 |
17 | if Pay.send_emails
18 | Pay::UserMailer.with(billable: billable, charge: charge).receipt.deliver_later
19 | end
20 | end
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/dummy/app/views/braintree/payment_methods/edit.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= form_with url: braintree_payment_method_path,
4 | method: :patch,
5 | data: {
6 | controller: "braintree",
7 | target: "braintree.form",
8 | braintree_env: Pay.braintree_gateway.config.environment,
9 | braintree_client_token: Pay.braintree_gateway.client_token.generate
10 | } do |form| %>
11 |
12 | <%= tag.div nil, data: { target: "braintree.dropin" } %>
13 |
14 |
15 | <%= form.button "Save Payment Method", class: "btn btn-primary", data: { action: "click->braintree#submit", } %>
16 |
17 | <% end %>
18 |
19 |
20 |
21 |
22 |
Test cards
23 |
24 | 4111111111111111
25 | Visa
26 |
27 |
28 | <%= link_to "All Test Cards", "https://developers.braintreepayments.com/reference/general/testing/ruby", target: :_blank, class: "btn btn-outline-dark btn-sm mt-3" %>
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/test/pay/braintree/charge_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "minitest/mock"
3 |
4 | class Pay::Braintree::Charge::Test < ActiveSupport::TestCase
5 | setup do
6 | @billable = User.new email: "test@example.com"
7 | @billable.processor = "braintree"
8 | end
9 |
10 | test "can partially refund a transaction" do
11 | @billable.card_token = "fake-valid-visa-nonce"
12 |
13 | charge = @billable.charge(29_00)
14 | assert charge.present?
15 |
16 | charge.refund!(10_00)
17 | assert_equal 10_00, charge.amount_refunded
18 | end
19 |
20 | test "can fully refund a transaction" do
21 | @billable.card_token = "fake-valid-visa-nonce"
22 |
23 | charge = @billable.charge(37_00)
24 | assert charge.present?
25 |
26 | charge.refund!
27 | assert_equal 37_00, charge.amount_refunded
28 | end
29 |
30 | test "you can ask the charge for the type" do
31 | assert Pay::Charge.new(processor: "stripe").stripe?
32 | assert Pay::Charge.new(processor: "braintree").braintree?
33 | assert Pay::Charge.new(processor: "braintree", card_type: "PayPal").paypal?
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Jason Charnes
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb:
--------------------------------------------------------------------------------
1 | # A subscription successfully moves to the next billing cycle. This will also occur when either a new transaction is created mid-cycle due to proration on an upgrade or a billing cycle is skipped due to the presence of a negative balance that covers the cost of the subscription.
2 |
3 | module Pay
4 | module Braintree
5 | module Webhooks
6 | class SubscriptionChargedUnsuccessfully
7 | def call(event)
8 | subscription = event.subscription
9 | return if subscription.nil?
10 |
11 | pay_subscription = Pay.subscription_model.find_by(processor: :braintree, processor_id: subscription.id)
12 | return unless pay_subscription.present?
13 |
14 | # billable = pay_subscription.owner
15 | # charge = Pay::Braintree::Billable.new(billable).save_transaction(subscription.transactions.first)
16 |
17 | # if Pay.send_emails
18 | # Pay::UserMailer.with(billable: billable, charge: charge).receipt.deliver_later
19 | # end
20 | end
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/stripe/charges_controller.rb:
--------------------------------------------------------------------------------
1 | module Stripe
2 | class ChargesController < ApplicationController
3 | before_action :set_charge, only: [:show, :refund]
4 |
5 | def index
6 | @charges = Pay::Charge.where(processor: :stripe).order(created_at: :desc)
7 | end
8 |
9 | def show
10 | end
11 |
12 | def new
13 | end
14 |
15 | def create
16 | current_user.processor = params[:processor]
17 | current_user.card_token = params[:card_token]
18 | charge = current_user.charge(params[:amount])
19 | redirect_to stripe_charge_path(charge)
20 | rescue Pay::ActionRequired => e
21 | redirect_to pay.payment_path(e.payment.id)
22 | rescue Pay::Error => e
23 | flash[:alert] = e.message
24 | render :new
25 | end
26 |
27 | def refund
28 | @charge.refund!
29 | rescue Pay::Error => e
30 | flash[:alert] = e.message
31 | ensure
32 | redirect_to stripe_charge_path(@charge)
33 | end
34 |
35 | private
36 |
37 | def set_charge
38 | @charge = Pay::Charge.find(params[:id])
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/docs/webhooks.md:
--------------------------------------------------------------------------------
1 | # Webhooks with Pay
2 |
3 | Pay comes with a bunch of different webhook handlers built-in. Each payment processor has different requirements for handling webhooks and we've implemented all the basic ones for you.
4 |
5 | ## Event Naming
6 |
7 | Since we support multiple payment providers, each event type needs to be prefixed with the payment provider:
8 |
9 | ```ruby
10 | "stripe.charge.succeeded"
11 | "braintree.subscription_charged_successfully"
12 | "paddle.subscription_created"
13 | ```
14 |
15 | ## Adding a custom Webhook Listener
16 |
17 | To add your own listener, you can simply subscribe to the event type.
18 |
19 | ```ruby
20 | Pay::Webhooks.delegator.subscribe "stripe.charge.succeeded", MyChargeSucceededProcessor.new
21 |
22 | class MyChargeSucceededProcessor
23 | def call(event)
24 | # do your processing here
25 | end
26 | end
27 | ```
28 |
29 | ## Unsubscribing from a webhook listener
30 |
31 | Need to unsubscribe or disable one of the default webhook processors? Simply unsubscribe from the event name:
32 |
33 | ```ruby
34 | Pay::Webhooks.delegator.unsubscribe "stripe.charge.succeeded"
35 |
36 | ```
37 |
38 |
--------------------------------------------------------------------------------
/test/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path('..', __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to setup or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?('config/database.yml')
22 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! 'bin/rails db:prepare'
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! 'bin/rails log:clear tmp:clear'
30 |
31 | puts "\n== Restarting application server =="
32 | system! 'bin/rails restart'
33 | end
34 |
--------------------------------------------------------------------------------
/test/pay/stripe/webhooks/payment_action_required_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Stripe::Webhooks::PaymentActionRequiredTest < ActiveSupport::TestCase
4 | setup do
5 | @event = OpenStruct.new
6 | @event.data = JSON.parse(File.read("test/support/fixtures/stripe/invoice.payment_action_required.json"), object_class: OpenStruct)
7 |
8 | # Create user and subscription
9 | @user = User.create!(email: "gob@bluth.com", processor: :stripe, processor_id: @event.data.object.customer)
10 | @subscription = @user.subscriptions.create!(
11 | processor: :stripe,
12 | processor_id: @event.data.object.subscription,
13 | name: "default",
14 | processor_plan: "some-plan",
15 | status: "requires_action"
16 | )
17 | end
18 |
19 | test "it sends an email" do
20 | assert_enqueued_jobs 1 do
21 | Pay::Stripe::Webhooks::PaymentActionRequired.new.call(@event)
22 | end
23 | end
24 |
25 | test "ignores if subscription doesn't exist" do
26 | @subscription.destroy!
27 | assert_no_enqueued_jobs do
28 | Pay::Stripe::Webhooks::PaymentActionRequired.new.call(@event)
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/pay/paddle/webhooks/subscription_payment_refunded_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Paddle::Webhooks::SubscriptionPaymentRefundedTest < ActiveSupport::TestCase
4 | setup do
5 | @data = JSON.parse(File.read("test/support/fixtures/paddle/subscription_payment_refunded.json"))
6 | end
7 |
8 | test "a charge is updated with refunded amount" do
9 | @user = User.create!(email: "gob@bluth.com", processor: :paddle, processor_id: @data["user_id"])
10 | charge = @user.charges.create!(processor: :paddle, processor_id: @data["subscription_payment_id"], amount: 16, card_type: "card")
11 |
12 | Pay::Paddle::Webhooks::SubscriptionPaymentRefunded.new.call(@data)
13 |
14 | assert_equal Integer(@data["gross_refund"].to_f * 100), charge.reload.amount_refunded
15 | end
16 |
17 | test "a charge isn't updated with the refunded amount if a corresponding charge can't be found (obviously)" do
18 | @user = User.create!(email: "gob@bluth.com", processor: :paddle, processor_id: "does-not-exist")
19 | charge = @user.charges.create!(processor: :paddle, processor_id: "doesntexist", amount: 500, card_type: "card")
20 |
21 | assert_nil charge.reload.amount_refunded
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/pay/chargeable_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Charge::Test < ActiveSupport::TestCase
4 | setup do
5 | @charge = Pay.charge_model.new
6 | end
7 |
8 | test "belongs to a polymorphic owner" do
9 | @charge.owner = User.new
10 | assert_equal User, @charge.owner.class
11 | @charge.owner = Team.new
12 | assert_equal Team, @charge.owner.class
13 | end
14 |
15 | test "#charged_to" do
16 | @charge.card_type = "VISA"
17 | @charge.card_last4 = 1234
18 | assert_equal "VISA (**** **** **** 1234)", @charge.charged_to
19 | end
20 |
21 | test "finds polymorphic charge" do
22 | user_chargeable = User.create! email: "test@example.com", id: 1001
23 | team_chargeable = Team.create! id: 1001, owner: user_chargeable
24 |
25 | charge = Pay.charge_model.create!(
26 | owner: team_chargeable, amount: 1, processor: "stripe", processor_id: "1", card_type: "VISA"
27 | )
28 |
29 | assert_equal [], user_chargeable.charges
30 | assert_equal [charge], team_chargeable.charges
31 | end
32 |
33 | test "stores data about the charge" do
34 | data = {"foo" => "bar"}
35 | @charge.update(data: data)
36 | assert_equal data, @charge.data
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/pay/receipts.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Receipts
3 | def filename
4 | "receipt-#{created_at.strftime("%Y-%m-%d")}.pdf"
5 | end
6 |
7 | def product
8 | Pay.application_name
9 | end
10 |
11 | # Must return a file object
12 | def receipt
13 | receipt_pdf.render
14 | end
15 |
16 | def receipt_pdf
17 | ::Receipts::Receipt.new(
18 | id: id,
19 | product: product,
20 | company: {
21 | name: Pay.business_name,
22 | address: Pay.business_address,
23 | email: Pay.support_email
24 | },
25 | line_items: line_items
26 | )
27 | end
28 |
29 | def line_items
30 | line_items = [
31 | [I18n.t("receipt.date"), created_at.to_s],
32 | [I18n.t("receipt.account_billed"), "#{owner.name} (#{owner.email})"],
33 | [I18n.t("receipt.product"), product],
34 | [I18n.t("receipt.amount"), ActionController::Base.helpers.number_to_currency(amount / 100.0)],
35 | [I18n.t("receipt.charged_to"), charged_to]
36 | ]
37 | line_items << [I18n.t("receipt.additional_info"), owner.extra_billing_info] if owner.extra_billing_info?
38 | line_items
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/pay/payment.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | class Payment
3 | attr_reader :intent
4 |
5 | delegate :id, :amount, :client_secret, :status, :confirm, to: :intent
6 |
7 | def self.from_id(id)
8 | intent = id.start_with?("seti_") ? ::Stripe::SetupIntent.retrieve(id) : ::Stripe::PaymentIntent.retrieve(id)
9 | new(intent)
10 | end
11 |
12 | def initialize(intent)
13 | @intent = intent
14 | end
15 |
16 | def requires_payment_method?
17 | status == "requires_payment_method"
18 | end
19 |
20 | def requires_action?
21 | status == "requires_action"
22 | end
23 |
24 | def canceled?
25 | status == "canceled"
26 | end
27 |
28 | def cancelled?
29 | canceled?
30 | end
31 |
32 | def succeeded?
33 | status == "succeeded"
34 | end
35 |
36 | def payment_intent?
37 | intent.is_a?(::Stripe::PaymentIntent)
38 | end
39 |
40 | def setup_intent?
41 | intent.is_a?(::Stripe::SetupIntent)
42 | end
43 |
44 | def validate
45 | if requires_payment_method?
46 | raise Pay::InvalidPaymentMethod.new(self)
47 | elsif requires_action?
48 | raise Pay::ActionRequired.new(self)
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/test/dummy/app/views/braintree/charges/new.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= form_with url: braintree_charges_path,
4 | method: :post,
5 | data: {
6 | controller: "braintree",
7 | target: "braintree.form",
8 | braintree_env: Pay.braintree_gateway.config.environment,
9 | braintree_client_token: Pay.braintree_gateway.client_token.generate
10 | } do |form| %>
11 |
12 |
13 | <%= form.label :amount, "Amount in cents" %>
14 | <%= form.text_field :amount, value: 1500, class: "form-control" %>
15 |
16 |
17 | <%= tag.div nil, data: { target: "braintree.dropin" } %>
18 |
19 |
20 | <%= form.button "Checkout", class: "btn btn-primary", data: { action: "click->braintree#submit", } %>
21 |
22 | <% end %>
23 |
24 |
25 |
26 |
27 |
Test cards
28 |
29 | 4111111111111111
30 | Visa
31 |
32 |
33 | <%= link_to "All Test Cards", "https://developers.braintreepayments.com/reference/general/testing/ruby", target: :_blank, class: "btn btn-outline-dark btn-sm mt-3" %>
34 |
35 |
36 |
--------------------------------------------------------------------------------
/lib/pay/paddle/charge.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Paddle
3 | class Charge
4 | attr_reader :pay_charge
5 |
6 | delegate :processor_id, :owner, to: :pay_charge
7 |
8 | def initialize(pay_charge)
9 | @pay_charge = pay_charge
10 | end
11 |
12 | def charge
13 | return unless owner.subscription
14 | payments = PaddlePay::Subscription::Payment.list({subscription_id: owner.subscription.processor_id})
15 | charges = payments.select { |p| p[:id].to_s == processor_id }
16 | charges.try(:first)
17 | rescue ::PaddlePay::PaddlePayError => e
18 | raise Pay::Paddle::Error, e
19 | end
20 |
21 | def refund!(amount_to_refund)
22 | return unless owner.subscription
23 | payments = PaddlePay::Subscription::Payment.list({subscription_id: owner.subscription.processor_id, is_paid: 1})
24 | if payments.count > 0
25 | PaddlePay::Subscription::Payment.refund(payments.last[:id], {amount: amount_to_refund})
26 | pay_charge.update(amount_refunded: amount_to_refund)
27 | else
28 | raise Error, "Payment not found"
29 | end
30 | rescue ::PaddlePay::PaddlePayError => e
31 | raise Pay::Paddle::Error, e
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/test/dummy/app/views/braintree/subscriptions/new.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= form_with url: braintree_subscriptions_path,
4 | method: :post,
5 | data: {
6 | controller: "braintree",
7 | target: "braintree.form",
8 | braintree_env: Pay.braintree_gateway.config.environment,
9 | braintree_client_token: Pay.braintree_gateway.client_token.generate
10 | } do |form| %>
11 |
12 |
13 | <%= form.label :plan_id, "Plan ID" %>
14 | <%= form.text_field :plan_id, value: "default", class: "form-control" %>
15 |
16 |
17 | <%= tag.div nil, data: { target: "braintree.dropin" } %>
18 |
19 |
20 | <%= form.button "Checkout", class: "btn btn-primary", data: { action: "click->braintree#submit", } %>
21 |
22 | <% end %>
23 |
24 |
25 |
26 |
27 |
Test cards
28 |
29 | 4111111111111111
30 | Visa
31 |
32 |
33 | <%= link_to "All Test Cards", "https://developers.braintreepayments.com/reference/general/testing/ruby", target: :_blank, class: "btn btn-outline-dark btn-sm mt-3" %>
34 |
35 |
36 |
--------------------------------------------------------------------------------
/test/pay/stripe/webhooks/subscription_renewing_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Stripe::Webhooks::SubscriptionRenewingTest < ActiveSupport::TestCase
4 | setup do
5 | @event = OpenStruct.new
6 | @event.data = JSON.parse(File.read("test/support/fixtures/stripe/subscription_renewing_event.json"), object_class: OpenStruct)
7 |
8 | @user = User.create!(email: "gob@bluth.com", processor: :stripe, processor_id: @event.data.object.customer)
9 | end
10 |
11 | test "an email is sent to the user when subscription is renewing" do
12 | create_subscription(processor_id: @event.data.object.subscription)
13 | # Time.zone.at(@event.data.object.next_payment_attempt)
14 |
15 | Pay::Stripe::Webhooks::SubscriptionRenewing.new.call(@event)
16 | assert_enqueued_emails 1
17 | end
18 |
19 | test "an email is not sent when subscription can't be found" do
20 | create_subscription(processor_id: "does-not-exist")
21 |
22 | assert_no_enqueued_emails do
23 | Pay::Stripe::Webhooks::SubscriptionRenewing.new.call(@event)
24 | end
25 | end
26 |
27 | def create_subscription(processor_id:)
28 | @user.subscriptions.create!(processor: :stripe, processor_id: processor_id, name: "default", processor_plan: "some-plan", status: "active")
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literals: true
2 |
3 | Rails.application.routes.draw do
4 | resource :payment_method
5 |
6 | namespace :braintree do
7 | resource :payment_method, namespace: :braintree
8 | resources :subscriptions do
9 | member do
10 | patch :cancel
11 | patch :resume
12 | end
13 | end
14 | resources :charges do
15 | member do
16 | patch :refund
17 | end
18 | end
19 | end
20 |
21 | namespace :paddle do
22 | resource :payment_method, namespace: :paddle
23 | resources :subscriptions do
24 | member do
25 | patch :cancel
26 | patch :resume
27 | end
28 | end
29 | resources :charges do
30 | member do
31 | patch :refund
32 | end
33 | end
34 | end
35 |
36 | namespace :stripe do
37 | resource :payment_method, namespace: :stripe
38 | resources :subscriptions do
39 | member do
40 | patch :cancel
41 | patch :resume
42 | end
43 | end
44 | resources :charges do
45 | member do
46 | patch :refund
47 | end
48 | end
49 | namespace :charges do
50 | resource :import
51 | end
52 | resource :checkout, namespace: :stripe
53 | end
54 |
55 | root to: "main#show"
56 | end
57 |
--------------------------------------------------------------------------------
/test/pay/stripe/webhooks/charge_refunded_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Stripe::Webhooks::ChargeRefundedTest < ActiveSupport::TestCase
4 | setup do
5 | @event = OpenStruct.new
6 | @event.data = JSON.parse(File.read("test/support/fixtures/stripe/charge_refunded_event.json"), object_class: OpenStruct)
7 | end
8 |
9 | test "a charge is updated with refunded amount" do
10 | @user = User.create!(email: "gob@bluth.com", processor: :stripe, processor_id: @event.data.object.customer)
11 | charge = @user.charges.create!(processor: :stripe, processor_id: @event.data.object.id, amount: 500, card_type: "Visa", card_last4: "4444", card_exp_month: 1, card_exp_year: 2019)
12 |
13 | Pay::Stripe::Webhooks::ChargeRefunded.new.call(@event)
14 |
15 | assert_equal 500, charge.reload.amount_refunded
16 | end
17 |
18 | test "a charge isn't updated with the refunded amount if a corresponding charge can't be found (obviously)" do
19 | @user = User.create!(email: "gob@bluth.com", processor: :stripe, processor_id: "does-not-exist")
20 | charge = @user.charges.create!(processor: :stripe, processor_id: "doesntexist", amount: 500, card_type: "Visa", card_last4: "4444", card_exp_month: 1, card_exp_year: 2019)
21 |
22 | assert_nil charge.reload.amount_refunded
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/pay/billable/sync_email.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Billable
3 | module SyncEmail
4 | # Sync email address changes from the model to the processor.
5 | # This way they're kept in sync and email notifications are
6 | # always sent to the correct email address after an update.
7 | #
8 | # Processor classes simply need to implement a method named:
9 | #
10 | # update_PROCESSOR_email!
11 | #
12 | # This method should take the email address on the billable
13 | # object and update the associated API record.
14 |
15 | extend ActiveSupport::Concern
16 |
17 | included do
18 | after_update :enqeue_sync_email_job, if: :should_sync_email_with_processor?
19 | end
20 |
21 | def should_sync_email_with_processor?
22 | try(:saved_change_to_email?)
23 | end
24 |
25 | def sync_email_with_processor
26 | payment_processor.update_email!
27 | end
28 |
29 | private
30 |
31 | def enqeue_sync_email_job
32 | # Only update if the processor id is the same
33 | # This prevents duplicate API hits if this is their first time
34 | if processor_id? && !saved_change_to_processor_id? && saved_change_to_email?
35 | EmailSyncJob.perform_later(id, self.class.name)
36 | end
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/test/support/fixtures/stripe/payment_method.updated.json:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "id": "pm_1INhmtKXBGcbgpbZd73T5GF5",
4 | "object": "payment_method",
5 | "billing_details": {
6 | "address": {
7 | "city": null,
8 | "country": null,
9 | "line1": null,
10 | "line2": null,
11 | "postal_code": "12345",
12 | "state": null
13 | },
14 | "email": "jenny@example.com",
15 | "name": null,
16 | "phone": "+15555555555"
17 | },
18 | "card": {
19 | "brand": "visa",
20 | "checks": {
21 | "address_line1_check": null,
22 | "address_postal_code_check": null,
23 | "cvc_check": "pass"
24 | },
25 | "country": "US",
26 | "exp_month": 8,
27 | "exp_year": 2022,
28 | "fingerprint": "w4XDzQOFakih5EZM",
29 | "funding": "credit",
30 | "generated_from": null,
31 | "last4": "4242",
32 | "networks": {
33 | "available": [
34 | "visa"
35 | ],
36 | "preferred": null
37 | },
38 | "three_d_secure_usage": {
39 | "supported": true
40 | },
41 | "wallet": null
42 | },
43 | "created": 123456789,
44 | "customer": "cus_1234567890",
45 | "livemode": false,
46 | "metadata": {
47 | "order_id": "123456789"
48 | },
49 | "type": "card"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/paddle/subscriptions_controller.rb:
--------------------------------------------------------------------------------
1 | class Paddle::SubscriptionsController < ApplicationController
2 | before_action :set_subscription, only: [:show, :edit, :update, :destroy, :cancel, :resume]
3 |
4 | def index
5 | @subscriptions = Pay::Subscription.where(processor: :paddle).order(created_at: :desc)
6 | end
7 |
8 | def show
9 | end
10 |
11 | def new
12 | end
13 |
14 | def create
15 | current_user.processor = params[:processor]
16 | current_user.card_token = params[:card_token]
17 | subscription = current_user.subscribe(plan: params[:plan_id])
18 | redirect_to paddle_subscription_path(subscription)
19 | rescue Pay::Error => e
20 | flash[:alert] = e.message
21 | redirect_to new_paddle_subscription_path
22 | end
23 |
24 | def edit
25 | end
26 |
27 | def update
28 | end
29 |
30 | def destroy
31 | @subscription.cancel_now!
32 | redirect_to paddle_subscription_path(@subscription)
33 | end
34 |
35 | def cancel
36 | @subscription.cancel
37 | redirect_to paddle_subscription_path(@subscription)
38 | end
39 |
40 | def resume
41 | @subscription.resume
42 | redirect_to paddle_subscription_path(@subscription)
43 | end
44 |
45 | private
46 |
47 | def set_subscription
48 | @subscription = Pay::Subscription.where(processor: :paddle).find(params[:id])
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/braintree/subscriptions_controller.rb:
--------------------------------------------------------------------------------
1 | class Braintree::SubscriptionsController < ApplicationController
2 | before_action :set_subscription, only: [:show, :edit, :update, :destroy, :cancel, :resume]
3 |
4 | def index
5 | @subscriptions = Pay::Subscription.where(processor: :braintree).order(created_at: :desc)
6 | end
7 |
8 | def show
9 | end
10 |
11 | def new
12 | end
13 |
14 | def create
15 | current_user.processor = params[:processor]
16 | current_user.card_token = params[:card_token]
17 | subscription = current_user.subscribe(plan: params[:plan_id])
18 | redirect_to braintree_subscription_path(subscription)
19 | rescue Pay::Error => e
20 | flash[:alert] = e.message
21 | redirect_to new_braintree_subscription_path
22 | end
23 |
24 | def edit
25 | end
26 |
27 | def update
28 | end
29 |
30 | def destroy
31 | @subscription.cancel_now!
32 | redirect_to braintree_subscription_path(@subscription)
33 | end
34 |
35 | def cancel
36 | @subscription.cancel
37 | redirect_to braintree_subscription_path(@subscription)
38 | end
39 |
40 | def resume
41 | @subscription.resume
42 | redirect_to braintree_subscription_path(@subscription)
43 | end
44 |
45 | private
46 |
47 | def set_subscription
48 | @subscription = Pay::Subscription.where(processor: :braintree).find(params[:id])
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/test/pay/stripe/webhooks/customer_deleted_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Stripe::Webhooks::CustomerDeletedTest < ActiveSupport::TestCase
4 | setup do
5 | @event = OpenStruct.new
6 | @event.data = JSON.parse(File.read("test/support/fixtures/stripe/customer_deleted_event.json"), object_class: OpenStruct)
7 | end
8 |
9 | test "a customers subscription information is nulled out upon deletion" do
10 | user = User.create!(
11 | email: "gob@bluth.com",
12 | processor: :stripe,
13 | processor_id: @event.data.object.id,
14 | card_type: "Visa",
15 | card_exp_month: 1,
16 | card_exp_year: 2019,
17 | card_last4: "4444",
18 | trial_ends_at: 3.days.from_now
19 | )
20 | subscription = user.subscriptions.create!(
21 | processor: :stripe,
22 | processor_id: "sub_someid",
23 | name: "default",
24 | processor_plan: "some-plan",
25 | trial_ends_at: 3.days.from_now,
26 | status: "active"
27 | )
28 |
29 | Pay::Stripe::Webhooks::CustomerDeleted.new.call(@event)
30 |
31 | assert_nil user.reload.processor_id
32 | assert_nil user.reload.card_type
33 | assert_nil user.reload.card_exp_month
34 | assert_nil user.reload.card_exp_year
35 | assert_nil user.reload.card_last4
36 | assert_nil user.reload.trial_ends_at
37 |
38 | assert_nil subscription.reload.trial_ends_at
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/pay/stripe/webhooks/subscription_updated.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Stripe
3 | module Webhooks
4 | class SubscriptionUpdated
5 | def call(event)
6 | object = event.data.object
7 | subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: object.id)
8 |
9 | return if subscription.nil?
10 |
11 | # Delete any subscription attempts that have expired
12 | if object.status == "incomplete_expired"
13 | subscription.destroy
14 | return
15 | end
16 |
17 | subscription.status = object.status
18 | subscription.quantity = object.quantity
19 | subscription.processor_plan = object.plan.id
20 | subscription.trial_ends_at = Time.at(object.trial_end) if object.trial_end.present?
21 |
22 | # If user was on trial, their subscription ends at the end of the trial
23 | subscription.ends_at = if object.cancel_at_period_end && subscription.on_trial?
24 | subscription.trial_ends_at
25 |
26 | # User wasn't on trial, so subscription ends at period end
27 | elsif object.cancel_at_period_end
28 | Time.at(object.current_period_end)
29 |
30 | # Subscription isn't marked to cancel at period end
31 | end
32 |
33 | subscription.save!
34 | end
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/pay/fake_processor/subscription_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::FakeProcessor::Subscription::Test < ActiveSupport::TestCase
4 | setup do
5 | @billable = User.create!(email: "gob@bluth.com", processor: :fake_processor, processor_id: "17368056", pay_fake_processor_allowed: true)
6 | @subscription = @billable.subscribe
7 | end
8 |
9 | test "fake processor subscription" do
10 | assert_equal @subscription, @subscription.processor_subscription
11 | end
12 |
13 | test "fake processor cancel" do
14 | freeze_time do
15 | @subscription.cancel
16 | assert_equal Time.current.end_of_month.to_date, @subscription.ends_at.to_date
17 | end
18 | end
19 |
20 | test "fake processor cancel_now!" do
21 | @subscription.cancel_now!
22 | assert_not @subscription.active?
23 | end
24 |
25 | test "fake processor on_grace_period?" do
26 | freeze_time do
27 | @subscription.cancel
28 | assert @subscription.on_grace_period?
29 | end
30 | end
31 |
32 | test "fake processor resume" do
33 | freeze_time do
34 | @subscription.cancel
35 | assert_not_nil @subscription.ends_at
36 | @subscription.resume
37 | assert_nil @subscription.ends_at
38 | end
39 | end
40 |
41 | test "fake processor swap" do
42 | @subscription.swap("another_plan")
43 | assert_equal "another_plan", @subscription.processor_plan
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/pay/fake_processor/subscription.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module FakeProcessor
3 | class Subscription
4 | attr_reader :pay_subscription
5 |
6 | delegate :canceled?,
7 | :ends_at,
8 | :on_trial?,
9 | :owner,
10 | :processor_subscription,
11 | :processor_id,
12 | :prorate,
13 | :processor_plan,
14 | :quantity?,
15 | :quantity,
16 | to: :pay_subscription
17 |
18 | def initialize(pay_subscription)
19 | @pay_subscription = pay_subscription
20 | end
21 |
22 | def cancel
23 | pay_subscription.update(ends_at: Time.current.end_of_month)
24 | end
25 |
26 | def cancel_now!
27 | pay_subscription.update(ends_at: Time.current, status: :canceled)
28 | end
29 |
30 | def on_grace_period?
31 | canceled? && Time.zone.now < ends_at
32 | end
33 |
34 | def paused?
35 | false
36 | end
37 |
38 | def pause
39 | raise NotImplementedError, "FakeProcessor does not support pausing subscriptions"
40 | end
41 |
42 | def resume
43 | unless on_grace_period?
44 | raise StandardError, "You can only resume subscriptions within their grace period."
45 | end
46 |
47 | pay_subscription.update(ends_at: nil, status: :active)
48 | end
49 |
50 | def swap(plan)
51 | pay_subscription.update(processor_plan: plan)
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/test/pay/stripe/checkout_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Stripe::CheckoutTest < ActiveSupport::TestCase
4 | setup do
5 | @user = User.create!(email: "gob@bluth.com", processor: :stripe)
6 |
7 | # Create Stripe customer
8 | @user.customer
9 | end
10 |
11 | test "checkout setup session" do
12 | session = @user.payment_processor.checkout(mode: "setup")
13 | assert_equal "setup", session.mode
14 | end
15 |
16 | test "checkout payment session" do
17 | session = @user.payment_processor.checkout(mode: "payment", line_items: "price_1ILVZaKXBGcbgpbZQ26kgXWG")
18 | assert_equal "payment", session.mode
19 | end
20 |
21 | test "checkout subscription session" do
22 | session = @user.payment_processor.checkout(mode: "subscription", line_items: "default")
23 | assert_equal "subscription", session.mode
24 | end
25 |
26 | test "billing portal session" do
27 | session = @user.payment_processor.billing_portal
28 | assert_not_nil session.url
29 | end
30 |
31 | test "raises an error with empty default_url_options" do
32 | # This should raise:
33 | # ArgumentError: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true
34 |
35 | Rails.application.config.action_mailer.stub :default_url_options, nil do
36 | assert_raises ArgumentError do
37 | @user.payment_processor.checkout(mode: "setup")
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/dummy/app/views/stripe/payment_methods/edit.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= form_with url: stripe_payment_method_path, method: :patch, local: true, id: "payment-form", data: { setup_intent: @setup_intent.client_secret } do |form| %>
4 |
5 |
6 | Name on Card
7 |
8 |
9 |
10 |
11 |
12 |
23 |
24 | <%= form.submit "Save Payment Method", class: "btn btn-primary" %>
25 | <% end %>
26 |
27 |
28 |
29 |
Test Cards
30 |
31 | 4242 4242 4242 4242
32 | No Authentication
33 |
34 |
35 |
36 | 4000 0027 6000 3184
37 | Requires Authentication
38 |
39 |
40 | <%= link_to "All Test Cards", "https://stripe.com/docs/testing", target: :_blank, class: "btn btn-outline-dark btn-sm mt-3" %>
41 |
42 |
43 |
--------------------------------------------------------------------------------
/test/pay/fake_processor/billable_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::FakeProcessor::Billable::Test < ActiveSupport::TestCase
4 | setup do
5 | @billable = User.create!(email: "gob@bluth.com", processor: :fake_processor, processor_id: "17368056", pay_fake_processor_allowed: true)
6 | @billable.reload
7 | end
8 |
9 | test "doesn't allow fake processor by default" do
10 | assert_not User.new(email: "gob@bluth.com", processor: :fake_processor, processor_id: "17368056").valid?
11 | end
12 |
13 | test "allows fake processor if enabled" do
14 | assert User.new(email: "gob@bluth.com", processor: :fake_processor, processor_id: "17368056", pay_fake_processor_allowed: true).valid?
15 | end
16 |
17 | test "doesn't validate fake processor if processor didn't change" do
18 | assert @billable.update(email: "michael@bluth.com")
19 | end
20 |
21 | test "validates fake processor if processor changed" do
22 | @billable.update(processor: :stripe)
23 | assert_not @billable.update(processor: :fake_processor, processor_id: 12345)
24 | end
25 |
26 | test "fake processor customer" do
27 | assert_equal @billable, @billable.payment_processor.customer
28 | end
29 |
30 | test "fake processor charge" do
31 | assert_difference "Pay::Charge.count" do
32 | @billable.charge(10_00)
33 | end
34 | end
35 |
36 | test "fake processor subscribe" do
37 | assert_difference "Pay::Subscription.count" do
38 | @billable.subscribe
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/pay/stripe/webhooks/customer_updated_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Stripe::Webhooks::CustomerUpdatedTest < ActiveSupport::TestCase
4 | setup do
5 | @event = OpenStruct.new
6 | @event.data = JSON.parse(File.read("test/support/fixtures/stripe/customer_updated_event.json"), object_class: OpenStruct)
7 | end
8 |
9 | test "update_card_from stripe is called upon customer update" do
10 | user = User.create!(
11 | email: "gob@bluth.com",
12 | processor: :stripe,
13 | processor_id: @event.data.object.id
14 | )
15 | user.subscriptions.create!(
16 | processor: :stripe,
17 | processor_id: "sub_someid",
18 | name: "default",
19 | processor_plan: "some-plan",
20 | status: "active"
21 | )
22 |
23 | Pay::Stripe::Billable.any_instance.expects(:sync_card_from_stripe)
24 | Pay::Stripe::Webhooks::CustomerUpdated.new.call(@event)
25 | end
26 |
27 | test "update_card_from stripe is not called if user can't be found" do
28 | user = User.create!(
29 | email: "gob@bluth.com",
30 | processor: :stripe,
31 | processor_id: "does-not-exist"
32 | )
33 | user.subscriptions.create!(
34 | processor: :stripe,
35 | processor_id: "sub_someid",
36 | name: "default",
37 | processor_plan: "some-plan",
38 | status: "active"
39 | )
40 |
41 | Pay::Stripe::Billable.any_instance.expects(:sync_card_from_stripe).never
42 | Pay::Stripe::Webhooks::CustomerUpdated.new.call(@event)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/support/fixtures/paddle/subscription_cancelled.json:
--------------------------------------------------------------------------------
1 | {
2 | "alert_id": "1440248908",
3 | "alert_name": "subscription_cancelled",
4 | "cancellation_effective_date": "2020-11-18 11:18:16",
5 | "checkout_id": "7-1bfa34cf3136556-972299a6b8",
6 | "currency": "USD",
7 | "email": "wilkinson.cicero@example.net",
8 | "event_time": "2020-11-16 16:45:09",
9 | "linked_subscriptions": "6, 1, 6",
10 | "marketing_consent": "",
11 | "passthrough": "{\"owner_sgid\": \"BAh7CEkiCGdpZAY6BkVUSSIiZ2lkOi8vZHVtbXkvVXNlci8xP2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIhJwYWRkbGVfMTIzNDU2BjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--0ee181d81896a7e0ef25d322c6081df1ec534cdd\"}",
12 | "quantity": "49",
13 | "status": "deleted",
14 | "subscription_id": "7",
15 | "subscription_plan_id": "9",
16 | "unit_price": "unit_price",
17 | "user_id": "9",
18 | "p_signature": "17ffO2bIL20TjljrhhETh3PfpNyDi8WLvx+cK1DVy1EEDUPPCT3CCAITuUraVKJjWD3hWsOx1/KVNUaqzqMWAA5GWa4cPf/iaAXDUGXCpsNwZQR7XeZXmQnD8xXHH2/BaL7BStGJ4iyGqbZynzJM0gIV+2j6ZXa3uvkReZMVlU9AUZoF1I4su3U15509bsjcPw/2UOhj6GFKH0WNwcsPkhzd9Xzfxcof5NWDlmIpiEnHgnBUGbdqpHp46ey2uavA5hpIFTs8chJgAQGh/suDuuK+GKE0CyIRCnuJP7Jd57qqCUVIWPgucMErKJWeiO3QOyQPRegIF0RlfJhT/i87hg1iJ5loxpc+a+ljNvQcrPpi6fUE6EGiaqOhaSmAD2/+VDy+y5UGwl0E9GU4fPND9lmS9WISbdO9L1Z0tVpb3/LJOXRk/rxkpp3ss4/ZoC91Fak0TD/bKmS48K8KWsb7s3DiMPONGuSJCKtJzAnDHFrr6XN/n+5HZS4xor6AFkH8UtZlBZD03o76i8dCORc/UBhuVfzmjICQbLUAzvu5M8I/4hR21ev9wmaShTNSe8FAjMWV/k2nAmLfDJGNRvoYAqOdGwxij3CviU2ArnIboqbrgu9lA1TNTM55OgAKFOphlHp0tGlfa0WOPtMdB5oYSvpIdnJ1O7+WrnNRcaAl374="
19 | }
20 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/stripe/subscriptions_controller.rb:
--------------------------------------------------------------------------------
1 | module Stripe
2 | class SubscriptionsController < ApplicationController
3 | before_action :set_subscription, only: [:show, :edit, :update, :destroy, :cancel, :resume]
4 |
5 | def index
6 | @subscriptions = Pay::Subscription.where(processor: :stripe).order(created_at: :desc)
7 | end
8 |
9 | def show
10 | end
11 |
12 | def new
13 | end
14 |
15 | def create
16 | current_user.processor = params[:processor]
17 | current_user.card_token = params[:card_token]
18 | subscription = current_user.subscribe(plan: params[:price_id])
19 | redirect_to stripe_subscription_path(subscription)
20 | rescue Pay::ActionRequired => e
21 | redirect_to pay.payment_path(e.payment.id)
22 | rescue Pay::Error => e
23 | flash[:alert] = e.message
24 | render :new
25 | end
26 |
27 | def edit
28 | end
29 |
30 | def update
31 | end
32 |
33 | def destroy
34 | @subscription.cancel_now!
35 | redirect_to stripe_subscription_path(@subscription)
36 | end
37 |
38 | def cancel
39 | @subscription.cancel
40 | redirect_to stripe_subscription_path(@subscription)
41 | end
42 |
43 | def resume
44 | @subscription.resume
45 | redirect_to stripe_subscription_path(@subscription)
46 | end
47 |
48 | private
49 |
50 | def set_subscription
51 | @subscription = Pay::Subscription.where(processor: :stripe).find(params[:id])
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/test/pay_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Test < ActiveSupport::TestCase
4 | test "truth" do
5 | assert_kind_of Module, Pay
6 | end
7 |
8 | test "default chargeable class is Charge" do
9 | assert Pay.chargeable_class, "Pay::Charge"
10 | end
11 |
12 | test "default chargeable table is charges" do
13 | assert Pay.chargeable_table, "charges"
14 | end
15 |
16 | test "default automount_routes is true" do
17 | assert Pay.automount_routes, true
18 | end
19 |
20 | test "default routes_path is /pay" do
21 | assert Pay.routes_path, "/pay"
22 | end
23 |
24 | test "can set business name" do
25 | assert Pay.respond_to?(:business_name)
26 | assert Pay.respond_to?(:business_name=)
27 | end
28 |
29 | test "can set business address" do
30 | assert Pay.respond_to?(:business_address)
31 | assert Pay.respond_to?(:business_address=)
32 | end
33 |
34 | test "can set application name" do
35 | assert Pay.respond_to?(:application_name)
36 | assert Pay.respond_to?(:application_name=)
37 | end
38 |
39 | test "can set support email" do
40 | assert Pay.respond_to?(:support_email)
41 | assert Pay.respond_to?(:support_email=)
42 | end
43 |
44 | test "can set default product name" do
45 | assert Pay.respond_to?(:default_product_name)
46 | assert Pay.respond_to?(:default_product_name=)
47 | end
48 |
49 | test "can set default plan name" do
50 | assert Pay.respond_to?(:default_plan_name)
51 | assert Pay.respond_to?(:default_plan_name=)
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/docs/fake_processor.md:
--------------------------------------------------------------------------------
1 | # Fake Payment Processor
2 |
3 | The fake payment processor is useful for:
4 |
5 | * Testing
6 | * Free subscriptions & charges for users like your team, friends, etc
7 |
8 | ## Usage
9 |
10 | Simply assign `processor: :fake_processor, processor_id: rand(1_000_000), pay_fake_processor_allowed: true` to your user.
11 |
12 | ```ruby
13 | user = User.create!(
14 | email: "gob@bluth.com",
15 | processor: :fake_processor,
16 | processor_id: rand(1_000_000),
17 | pay_fake_processor_allowed: true
18 | )
19 |
20 | user.charge(25_00)
21 | user.subscribe("default")
22 | ```
23 |
24 | ## Security
25 |
26 | You don't want malicious users using the fake processor to give themselves free access to your products.
27 |
28 | Pay provides a virtual attribute and validation to ensure the fake processor is only assigned when explicitly allowed.
29 |
30 | ```ruby
31 | # Inside Pay::Billable
32 | attribute :pay_fake_processor_allowed, :boolean, default: false
33 |
34 | validate :pay_fake_processor_allowed
35 |
36 | def pay_fake_processor_is_allowed
37 | return unless processor == "fake_processor"
38 | errors.add(:processor, "must be a valid payment processor") unless pay_fake_processor_allowed?
39 | end
40 | ```
41 |
42 | `pay_fake_processor_allowed` must be set to `true` before saving. This attribute should *not* included in your permitted_params.
43 |
44 | The validation checks if this attribute is enabled and raises a validation error if not. This prevents malicious uses from submitting `user[processor]=fake_processor` in a form.
45 |
--------------------------------------------------------------------------------
/test/pay/stripe/webhooks/payment_method_updated_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Stripe::Webhooks::PaymentMethodUpdatedtest < ActiveSupport::TestCase
4 | setup do
5 | @event = OpenStruct.new
6 | @event.data = JSON.parse(File.read("test/support/fixtures/stripe/payment_method.updated.json"), object_class: OpenStruct)
7 | end
8 |
9 | test "update_card_from stripe is called upon customer update" do
10 | user = User.create!(
11 | email: "gob@bluth.com",
12 | processor: :stripe,
13 | processor_id: @event.data.object.customer
14 | )
15 | user.subscriptions.create!(
16 | processor: :stripe,
17 | processor_id: "sub_someid",
18 | name: "default",
19 | processor_plan: "some-plan",
20 | status: "active"
21 | )
22 |
23 | Pay::Stripe::Billable.any_instance.expects(:sync_card_from_stripe)
24 | Pay::Stripe::Webhooks::PaymentMethodUpdated.new.call(@event)
25 | end
26 |
27 | test "update_card_from stripe is not called if user can't be found" do
28 | user = User.create!(
29 | email: "gob@bluth.com",
30 | processor: :stripe,
31 | processor_id: "does-not-exist"
32 | )
33 | user.subscriptions.create!(
34 | processor: :stripe,
35 | processor_id: "sub_someid",
36 | name: "default",
37 | processor_plan: "some-plan",
38 | status: "active"
39 | )
40 |
41 | Pay::Stripe::Billable.any_instance.expects(:sync_card_from_stripe).never
42 | Pay::Stripe::Webhooks::PaymentMethodUpdated.new.call(@event)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/pay/webhooks/delegator_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::WebhookDelegatorTest < ActiveSupport::TestCase
4 | class TestEventProcessor
5 | attr_accessor :success
6 |
7 | def call(event)
8 | @success = true
9 | end
10 | end
11 |
12 | setup do
13 | @delegator = Pay::Webhooks::Delegator.new
14 | end
15 |
16 | test "pay has a default delegator" do
17 | assert_not_nil Pay::Webhooks.delegator
18 | end
19 |
20 | test "subscribe includes namespace" do
21 | delegator.subscribe "stripe.test_event", ->(event) {}
22 | assert delegator.backend.notifier.listening?("pay.stripe.test_event")
23 | end
24 |
25 | test "instruments events" do
26 | success = nil
27 |
28 | delegator.subscribe "stripe.test_event" do |event|
29 | success = true
30 | end
31 |
32 | delegator.instrument event: {}, type: "stripe.test_event"
33 | assert success
34 | end
35 |
36 | test "can subscribe with class" do
37 | processor = TestEventProcessor.new
38 | delegator.subscribe "stripe.test_event", processor
39 | delegator.instrument event: {}, type: "stripe.test_event"
40 | assert processor.success
41 | end
42 |
43 | test "can unsubscribe" do
44 | delegator.subscribe "stripe.test_event", ->(event) {}
45 | assert delegator.backend.notifier.listening?("pay.stripe.test_event")
46 | delegator.unsubscribe "stripe.test_event"
47 | assert delegator.backend.notifier.listening?("pay.stripe.test_event")
48 | end
49 |
50 | private
51 |
52 | attr_reader :delegator
53 | end
54 |
--------------------------------------------------------------------------------
/lib/pay/paddle/webhooks/subscription_updated.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Paddle
3 | module Webhooks
4 | class SubscriptionUpdated
5 | def call(event)
6 | subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
7 |
8 | return if subscription.nil?
9 |
10 | case event["status"]
11 | when "deleted"
12 | subscription.status = "canceled"
13 | subscription.ends_at = Time.zone.parse(event["next_bill_date"]) || Time.zone.now if subscription.ends_at.blank?
14 | when "trialing"
15 | subscription.status = "trialing"
16 | subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"])
17 | when "active"
18 | subscription.status = "active"
19 | subscription.paddle_paused_from = Time.zone.parse(event["paused_from"]) if event["paused_from"].present?
20 | else
21 | subscription.status = event["status"]
22 | end
23 |
24 | subscription.quantity = event["new_quantity"]
25 | subscription.processor_plan = event["subscription_plan_id"]
26 | subscription.paddle_update_url = event["update_url"]
27 | subscription.paddle_cancel_url = event["cancel_url"]
28 |
29 | # If user was on trial, their subscription ends at the end of the trial
30 | subscription.ends_at = subscription.trial_ends_at if subscription.on_trial?
31 |
32 | subscription.save!
33 | end
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/dummy/app/views/stripe/charges/new.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= form_with url: stripe_charges_path, local: true, id: "payment-form" do |form| %>
4 |
5 | <%= form.label :amount, "Amount in cents" %>
6 | <%= form.text_field :amount, value: 1500, class: "form-control" %>
7 |
8 |
9 |
10 |
11 | Name on Card
12 |
13 |
14 |
15 |
16 |
17 |
28 |
29 | <%= form.submit "Checkout", class: "btn btn-primary" %>
30 | <% end %>
31 |
32 |
33 |
34 |
Test Cards
35 |
36 | 4242 4242 4242 4242
37 | No Authentication
38 |
39 |
40 |
41 | 4000 0027 6000 3184
42 | Requires Authentication
43 |
44 |
45 | <%= link_to "All Test Cards", "https://stripe.com/docs/testing", target: :_blank, class: "btn btn-outline-dark btn-sm mt-3" %>
46 |
47 |
48 |
--------------------------------------------------------------------------------
/test/dummy/app/views/stripe/subscriptions/new.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= form_with url: stripe_subscriptions_path, local: true, id: "payment-form" do |form| %>
4 |
5 | <%= form.label :price_id, "Price or Plan ID" %>
6 | <%= form.text_field :price_id, value: "default", class: "form-control" %>
7 |
8 |
9 |
10 |
11 | Name on Card
12 |
13 |
14 |
15 |
16 |
17 |
28 |
29 | <%= form.submit "Checkout", class: "btn btn-primary" %>
30 | <% end %>
31 |
32 |
33 |
34 |
Test Cards
35 |
36 | 4242 4242 4242 4242
37 | No Authentication
38 |
39 |
40 |
41 | 4000 0027 6000 3184
42 | Requires Authentication
43 |
44 |
45 | <%= link_to "All Test Cards", "https://stripe.com/docs/testing", target: :_blank, class: "btn btn-outline-dark btn-sm mt-3" %>
46 |
47 |
48 |
--------------------------------------------------------------------------------
/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Paddle
3 | module Webhooks
4 | class SubscriptionPaymentSucceeded
5 | def call(event)
6 | billable = Pay.find_billable(processor: :paddle, processor_id: event["user_id"])
7 | return unless billable.present?
8 | return if billable.charges.where(processor_id: event["subscription_payment_id"]).any?
9 |
10 | charge = create_charge(billable, event)
11 | notify_user(billable, charge)
12 | end
13 |
14 | def create_charge(user, event)
15 | charge = user.charges.find_or_initialize_by(
16 | processor: :paddle,
17 | processor_id: event["subscription_payment_id"]
18 | )
19 |
20 | params = {
21 | amount: Integer(event["sale_gross"].to_f * 100),
22 | card_type: event["payment_method"],
23 | paddle_receipt_url: event["receipt_url"],
24 | created_at: Time.zone.parse(event["event_time"])
25 | }
26 |
27 | payment_information = Pay::Paddle::Billable.new(user).payment_information(event["subscription_id"])
28 |
29 | charge.update(params.merge(payment_information))
30 | user.update(payment_information)
31 |
32 | charge
33 | end
34 |
35 | def notify_user(billable, charge)
36 | if Pay.send_emails && charge.respond_to?(:receipt)
37 | Pay::UserMailer.with(billable: billable, charge: charge).receipt.deliver_later
38 | end
39 | end
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/test/dummy/app/javascript/controllers/braintree_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 | import Rails from "@rails/ujs"
3 |
4 | export default class extends Controller {
5 | static targets = [ "dropin", "form" ]
6 |
7 | connect() {
8 | braintree.dropin.create({
9 | authorization: this.data.get("clientToken"),
10 | container: this.dropinTarget,
11 | //threeDSecure: true,
12 | paypal: {
13 | flow: "vault"
14 | },
15 | // Uncomment this to only display PayPal in the Drop-in UI
16 | //paymentOptionPriority: ['paypal']
17 | },
18 | this.clientCreated.bind(this)
19 | )
20 | }
21 |
22 | clientCreated(error, instance) {
23 | if (error) {
24 | console.error("Error setting up Braintree dropin:", error)
25 | return
26 | }
27 |
28 | this.instance = instance
29 | }
30 |
31 | submit(event) {
32 | event.preventDefault()
33 | this.instance.requestPaymentMethod(this.paymentMethod.bind(this))
34 | }
35 |
36 | paymentMethod(error, payload) {
37 | if (error) {
38 | console.error("Error with payment method:", error)
39 | return
40 | }
41 |
42 | this.addHiddenField("processor", "braintree")
43 | this.addHiddenField("card_token", payload.nonce)
44 |
45 | Rails.fire(this.formTarget, "submit")
46 | }
47 |
48 | addHiddenField(name, value) {
49 | let hiddenInput = document.createElement("input")
50 | hiddenInput.setAttribute("type", "hidden")
51 | hiddenInput.setAttribute("name", name)
52 | hiddenInput.setAttribute("value", value)
53 | this.formTarget.appendChild(hiddenInput)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/models/pay/charge.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | class Charge < Pay::ApplicationRecord
3 | self.table_name = Pay.chargeable_table
4 |
5 | # Only serialize for non-json columns
6 | serialize :data unless json_column?("data")
7 |
8 | # Associations
9 | belongs_to :owner, polymorphic: true
10 |
11 | # Scopes
12 | scope :sorted, -> { order(created_at: :desc) }
13 | default_scope -> { sorted }
14 |
15 | # Validations
16 | validates :amount, presence: true
17 | validates :processor, presence: true
18 | validates :processor_id, presence: true
19 | validates :card_type, presence: true
20 |
21 | store_accessor :data, :paddle_receipt_url
22 |
23 | # Helpers for payment processors
24 | %w[braintree stripe paddle fake_processor].each do |processor_name|
25 | define_method "#{processor_name}?" do
26 | processor == processor_name
27 | end
28 |
29 | scope processor_name, -> { where(processor: processor_name) }
30 | end
31 |
32 | def payment_processor
33 | @payment_processor ||= payment_processor_for(processor).new(self)
34 | end
35 |
36 | def payment_processor_for(name)
37 | "Pay::#{name.to_s.classify}::Charge".constantize
38 | end
39 |
40 | def processor_charge
41 | payment_processor.charge
42 | end
43 |
44 | def refund!(refund_amount = nil)
45 | refund_amount ||= amount
46 | payment_processor.refund!(refund_amount)
47 | end
48 |
49 | def charged_to
50 | "#{card_type} (**** **** **** #{card_last4})"
51 | end
52 |
53 | def paypal?
54 | braintree? && card_type == "PayPal"
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/app/controllers/pay/webhooks/stripe_controller.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Webhooks
3 | class StripeController < Pay::ApplicationController
4 | if Rails.application.config.action_controller.default_protect_from_forgery
5 | skip_before_action :verify_authenticity_token
6 | end
7 |
8 | def create
9 | delegate_event(verified_event)
10 | head :ok
11 | rescue ::Stripe::SignatureVerificationError => e
12 | log_error(e)
13 | head :bad_request
14 | end
15 |
16 | private
17 |
18 | def delegate_event(event)
19 | Pay::Webhooks.instrument type: "stripe.#{event.type}", event: event
20 | end
21 |
22 | def verified_event
23 | payload = request.body.read
24 | signature = request.headers["Stripe-Signature"]
25 | possible_secrets = secrets(payload, signature)
26 |
27 | possible_secrets.each_with_index do |secret, i|
28 | return ::Stripe::Webhook.construct_event(payload, signature, secret.to_s)
29 | rescue ::Stripe::SignatureVerificationError
30 | raise if i == possible_secrets.length - 1
31 | next
32 | end
33 | end
34 |
35 | def secrets(payload, signature)
36 | secret = Pay::Stripe.signing_secret
37 | return Array.wrap(secret) if secret
38 | raise ::Stripe::SignatureVerificationError.new("Cannot verify signature without a Stripe signing secret", signature, http_body: payload)
39 | end
40 |
41 | def log_error(e)
42 | logger.error e.message
43 | e.backtrace.each { |line| logger.error " #{line}" }
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/pay/webhooks/delegator.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Webhooks
3 | class Delegator
4 | attr_reader :backend
5 |
6 | def initialize
7 | @backend = ActiveSupport::Notifications
8 | end
9 |
10 | # Configure DSL
11 | def configure(&block)
12 | raise ArgumentError, "must provide a block" unless block
13 | block.arity.zero? ? instance_eval(&block) : yield(self)
14 | end
15 |
16 | # Subscribe to specific events
17 | def subscribe(name, callable = nil, &block)
18 | callable ||= block
19 | backend.subscribe to_regexp(name), NotificationAdapter.new(callable)
20 | end
21 |
22 | # Listen to all events
23 | def all(callable = nil, &block)
24 | callable ||= block
25 | subscribe nil, callable
26 | end
27 |
28 | # Unsubscribe
29 | def unsubscribe(name)
30 | backend.unsubscribe name
31 | end
32 |
33 | # Called to process an event
34 | def instrument(event:, type:)
35 | backend.instrument name_with_namespace(type), event
36 | end
37 |
38 | # Strips down to event data only
39 | class NotificationAdapter
40 | def initialize(subscriber)
41 | @subscriber = subscriber
42 | end
43 |
44 | def call(*args)
45 | payload = args.last
46 | @subscriber.call(payload)
47 | end
48 | end
49 |
50 | private
51 |
52 | def to_regexp(name)
53 | %r{^#{Regexp.escape name_with_namespace(name)}}
54 | end
55 |
56 | def name_with_namespace(name, delimiter: ".")
57 | [:pay, name].join(delimiter)
58 | end
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/test/pay/stripe/webhooks/charge_succeeded_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Stripe::Webhooks::ChargeSucceededTest < ActiveSupport::TestCase
4 | setup do
5 | @event = OpenStruct.new
6 | @event.data = JSON.parse(File.read("test/support/fixtures/stripe/charge_succeeded_event.json"), object_class: OpenStruct)
7 | end
8 |
9 | test "a charge is created" do
10 | @user = User.create!(email: "gob@bluth.com", processor: :stripe, processor_id: @event.data.object.customer)
11 |
12 | assert_difference "Pay.charge_model.count" do
13 | Pay::Stripe::Webhooks::ChargeSucceeded.new.call(@event)
14 | end
15 |
16 | charge = Pay.charge_model.last
17 | assert_equal 500, charge.amount
18 | assert_equal "4444", charge.card_last4
19 | assert_equal "Visa", charge.card_type
20 | assert_equal "1", charge.card_exp_month
21 | assert_equal "2019", charge.card_exp_year
22 | end
23 |
24 | test "a charge isn't created if no corresponding user can be found" do
25 | @user = User.create!(email: "gob@bluth.com", processor: :stripe, processor_id: "does-not-exist")
26 |
27 | assert_no_difference "Pay.charge_model.count" do
28 | Pay::Stripe::Webhooks::ChargeSucceeded.new.call(@event)
29 | end
30 | end
31 |
32 | test "a charge isn't created if it already exists" do
33 | @user = User.create!(email: "gob@bluth.com", processor: :stripe, processor_id: @event.data.object.customer)
34 |
35 | @user.charges.create!(amount: 100, processor: :stripe, processor_id: "ch_chargeid", card_type: "Visa", card_exp_month: 1, card_exp_year: 2019, card_last4: "4444")
36 |
37 | assert_no_difference "Pay.charge_model.count" do
38 | Pay::Stripe::Webhooks::ChargeSucceeded.new.call(@event)
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/lib/pay/fake_processor/billable.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module FakeProcessor
3 | class Billable
4 | attr_reader :billable
5 |
6 | delegate :processor_id,
7 | :processor_id?,
8 | :email,
9 | :customer_name,
10 | :card_token,
11 | to: :billable
12 |
13 | def initialize(billable)
14 | @billable = billable
15 | end
16 |
17 | def customer
18 | billable
19 | end
20 |
21 | def charge(amount, options = {})
22 | billable.charges.create(
23 | processor: :fake_processor,
24 | processor_id: rand(100_000_000),
25 | amount: amount,
26 | card_type: :fake,
27 | card_last4: 1234,
28 | card_exp_month: Date.today.month,
29 | card_exp_year: Date.today.year
30 | )
31 | end
32 |
33 | def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
34 | subscription = OpenStruct.new id: rand(1_000_000)
35 | billable.create_pay_subscription(subscription, :fake_processor, name, plan, status: :active, quantity: options.fetch(:quantity, 1))
36 | end
37 |
38 | def update_card(payment_method_id)
39 | billable.update(
40 | card_type: :fake,
41 | card_last4: 1234,
42 | card_exp_month: Date.today.month,
43 | card_exp_year: Date.today.year
44 | )
45 | end
46 |
47 | def update_email!
48 | # pass
49 | end
50 |
51 | def processor_subscription(subscription_id, options = {})
52 | billable.subscriptions.find_by(processor: :fake_processor, processor_id: subscription_id)
53 | end
54 |
55 | def trial_end_date(subscription)
56 | Date.today
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/test/support/fixtures/paddle/subscription_created.json:
--------------------------------------------------------------------------------
1 | {
2 | "alert_id": "585171988",
3 | "alert_name": "subscription_created",
4 | "cancel_url": "https://checkout.paddle.com/subscription/cancel?user=5&subscription=1&hash=a1f8c289b995bba3df34d38a0671d714d439d017",
5 | "checkout_id": "4-779e7566d0d342e-42367a752f",
6 | "currency": "GBP",
7 | "email": "kjakubowski@example.net",
8 | "event_time": "2020-11-16 16:40:31",
9 | "linked_subscriptions": "5, 3, 6",
10 | "marketing_consent": "1",
11 | "next_bill_date": "2020-12-08",
12 | "passthrough": "{\"owner_sgid\": \"BAh7CEkiCGdpZAY6BkVUSSIiZ2lkOi8vZHVtbXkvVXNlci8xP2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIhJwYWRkbGVfMTIzNDU2BjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--0ee181d81896a7e0ef25d322c6081df1ec534cdd\"}",
13 | "quantity": "97",
14 | "source": "Trial",
15 | "status": "trialing",
16 | "subscription_id": "1",
17 | "subscription_plan_id": "4",
18 | "unit_price": "unit_price",
19 | "update_url": "https://checkout.paddle.com/subscription/update?user=6&subscription=1&hash=346cadb27c6146bd13d51228d1c4f34e3c6d954c",
20 | "user_id": "4",
21 | "p_signature": "SgjMh3AjXvcktlN016cOpfwsyFjzXb2e+BKwAxCg5zsgiC7posU0KqMEztS4h/nyTmBvnnhjzoQR4BxREktrsYx9QZ0goTHoWMpFhkYiA5ErfKw43pE0X7br7WZlEngs0plMZmXguVDLJoIaasj0w7FI9rlDRjXzdlpO/AAj2leT7byBXPpwxe1iv2boyRaGoYN81qIHAdRZOgjXAGA28CBBKaE8mODmNvizyteZDLOwjEeumzX+FWTY5TpjIxRAVjpwEHj48YtyJNbT7GGsLnW7c2wLkkRja7roENR4B28dr2I0V4gxWphPEKcYviiDG1jTum2RrKu76CbpiWxrkdZ3OzrdXjqGh6tQuorkMRdXIFVnyhL+sNP6LHxSbLSG6TYt6PxvWE4GUx9FoIftiQI0iFcUfkizc1DnZYm5lnzcxSUkNHrMNHItL6TJV9IXALnaTXvqXlly6LMN8wZKrzzWP1Ca2luu9w8IOVXWJ4NGU36pRGrfRL9MxykVx6hAL020oo9IWKDJc+udSP8lQDd0QyDCHWe0sX6Da2NHGXhlb1n7svNBQ1W7m5rr76Xk9c4mkddYtfJzEkg15n/EwCm2uqAAAq75PSSvKwmCTtT1pwNStBTklrUOBMBokfp2t2q4fcQbQQ3OpT325bstTj+ES8A6wpZCtHMoH/+nD/I="
22 | }
23 |
--------------------------------------------------------------------------------
/docs/adding_a_payment_processor.md:
--------------------------------------------------------------------------------
1 | # Adding a Payment Processor to Pay
2 |
3 | Each payment processor has different needs for webhooks. Rather than relying on external gems,
4 | Pay handles webhooks internally.
5 |
6 | ## Webhook Controller
7 |
8 | Each payment processer can define it's own controller for processing any required webhooks.
9 |
10 | For example, `stripe` has [app/controllers/pay/webhooks/stripe_controller.rb](../app/controllers/pay/webhooks/stripe_controller.rb)
11 |
12 | See also [config/routes.rb](../config/routes.rb) for defining routes.
13 |
14 | The webhook controller is responsible for verifying the webhook payload for authenticity and then sending to the Pay Webhook Delegator
15 |
16 | ### Pay Webhook Delegator
17 |
18 | The Webhook Delegator is responsible for taking an event type and sending it for processing.
19 |
20 | It uses [ActiveSupport::Notifications](https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) to subscribe and instrument events.
21 |
22 | ```ruby
23 | Pay::Webhooks.configure do |events|
24 | events.subscribe "stripe.charge.succeeded", Pay::Stripe::Webhooks::ChargeSucceeded.new
25 | end
26 |
27 | module Pay
28 | module Stripe
29 | module Webhooks
30 | class ChargeSucceeded
31 | def call(event)
32 | # processing goes here
33 | end
34 | end
35 | end
36 | end
37 | end
38 | ```
39 |
40 | For example, when a `stripe.charge.succeeded` event gets processed, the webhook delegator sends the event to any classes that are subscribed to the event type.
41 |
42 | Internally, these events are automatically prefaced with the `pay` namespace so they don't conflict with other events. `stripe.charge.succeeded` is internally routed as `pay.stripe.charge.succeeded`. Payment processors should _not_ preface with `pay.` as it is automatically added.
43 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | successful:
3 | header: Payment Successful
4 | description: This payment was already successfully confirmed.
5 | cancelled:
6 | header: Payment Cancelled
7 | description: This payment was cancelled.
8 | requires_action:
9 | header: Confirm your %{amount} payment
10 | description: Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.
11 | full_name: Full name
12 | card: Card
13 | button: Pay %{amount}
14 | name_missing: Please provide your name.
15 | success: The payment was successful.
16 | all_rights_reserved: All rights reserved.
17 | back: Go back
18 | receipt:
19 | date: Date
20 | account_billed: Account Billed
21 | product: Product
22 | amount: Amount
23 | charged_to: Charged to
24 | additional_info: Additional Info
25 | errors:
26 | action_required: "This payment attempt failed because additional action is required before it can be completed."
27 | invalid_payment: "This payment attempt failed because of an invalid payment method."
28 | email_required: "Email is required to create a customer"
29 | no_processor: "No payment processor selected. Make sure to set the %{class_name}'s `processor` attribute to either 'stripe' or 'braintree'."
30 | braintree:
31 | authorization: "Either the data you submitted is malformed and does not match the API or the API key you used may not be authorized to perform this action."
32 |
33 | pay:
34 | user_mailer:
35 | receipt:
36 | subject: "Payment receipt"
37 | refund:
38 | subject: "Payment refunded"
39 | subscription_renewing:
40 | subject: "Your upcoming subscription renewal"
41 | payment_action_required:
42 | subject: "Confirm your payment"
43 |
--------------------------------------------------------------------------------
/test/pay/paddle/webhooks/subscription_created_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Paddle::Webhooks::SubscriptionCreatedTest < ActiveSupport::TestCase
4 | setup do
5 | @data = JSON.parse(File.read("test/support/fixtures/paddle/subscription_created.json"))
6 | @user = User.create!(email: "gob@bluth.com")
7 | end
8 |
9 | test "paddle passthrough" do
10 | passthrough = Pay::Paddle.passthrough(owner: @user, foo: :bar)
11 | parsed = JSON.parse(passthrough)
12 | assert_equal "bar", parsed["foo"]
13 | assert_equal @user, GlobalID::Locator.locate_signed(parsed["owner_sgid"])
14 | end
15 |
16 | test "a subscription is created" do
17 | assert_difference "Pay.subscription_model.count" do
18 | @data["passthrough"] = Pay::Paddle.passthrough(owner: @user)
19 | Pay::Paddle::Webhooks::SubscriptionCreated.new.call(@data)
20 | end
21 |
22 | assert_equal "paddle", @user.reload.processor
23 | assert_equal @data["user_id"], @user.reload.processor_id
24 |
25 | subscription = Pay.subscription_model.last
26 | assert_equal @data["quantity"].to_i, subscription.quantity
27 | assert_equal @data["subscription_plan_id"], subscription.processor_plan
28 | assert_equal @data["update_url"], subscription.paddle_update_url
29 | assert_equal @data["cancel_url"], subscription.paddle_cancel_url
30 | assert_equal Time.zone.parse(@data["next_bill_date"]), subscription.trial_ends_at
31 | assert_nil subscription.ends_at
32 | end
33 |
34 | test "a subscription isn't created if no corresponding owner can be found" do
35 | @user = User.create!(email: "gob@bluth.com")
36 | @data["passthrough"] = "does-not-exist"
37 |
38 | assert_no_difference "Pay.subscription_model.count" do
39 | Pay::Paddle::Webhooks::SubscriptionCreated.new.call(@data)
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/test/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/lib/generators/active_record/pay_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails/generators/active_record"
4 | require "generators/pay/orm_helpers"
5 |
6 | module ActiveRecord
7 | module Generators
8 | class PayGenerator < ActiveRecord::Generators::Base
9 | include Pay::Generators::OrmHelpers
10 | source_root File.expand_path("../templates", __FILE__)
11 |
12 | def copy_pay_billable_migration
13 | if (behavior == :invoke && model_exists?) || (behavior == :revoke && migration_exists?(table_name))
14 | migration_template "migration.rb", "#{migration_path}/add_pay_billable_to_#{table_name}.rb", migration_version: migration_version
15 | end
16 | # TODO: Throw error here that model should already exist if it doesn't
17 | end
18 |
19 | def inject_pay_billable_content
20 | content = model_contents
21 |
22 | class_path = if namespaced?
23 | class_name.to_s.split("::")
24 | else
25 | [class_name]
26 | end
27 |
28 | indent_depth = class_path.size - 1
29 | content = content.split("\n").map { |line| " " * indent_depth + line }.join("\n") << "\n"
30 |
31 | inject_into_class(model_path, class_path.last, content) if model_exists?
32 | end
33 |
34 | def migration_data
35 | <
= 5
49 | end
50 |
51 | def migration_version
52 | if rails5_and_up?
53 | "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
54 | end
55 | end
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = false
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | config.public_file_server.enabled = true
17 | config.public_file_server.headers = {
18 | "Cache-Control" => "public, max-age=3600",
19 | }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 | config.action_mailer.perform_caching = false
31 |
32 | # Tell Action Mailer not to deliver emails to the real world.
33 | # The :test delivery method accumulates sent emails in the
34 | # ActionMailer::Base.deliveries array.
35 | config.action_mailer.delivery_method = :test
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/test/support/fixtures/paddle/subscription_payment_refunded.json:
--------------------------------------------------------------------------------
1 | {
2 | "alert_id": "39297549",
3 | "alert_name": "subscription_payment_refunded",
4 | "amount": "671.87",
5 | "balance_currency": "EUR",
6 | "balance_earnings_decrease": "0",
7 | "balance_fee_refund": "0.35",
8 | "balance_gross_refund": "0.68",
9 | "balance_tax_refund": "0.52",
10 | "checkout_id": "9-3eafeae89135527-aca34a910b",
11 | "currency": "USD",
12 | "earnings_decrease": "0.83",
13 | "email": "glindgren@example.org",
14 | "event_time": "2020-11-16 16:52:45",
15 | "fee_refund": "0.76",
16 | "gross_refund": "0.17",
17 | "initial_payment": "true",
18 | "instalments": "2",
19 | "marketing_consent": "",
20 | "order_id": "4",
21 | "passthrough": "{\"owner_sgid\": \"BAh7CEkiCGdpZAY6BkVUSSIiZ2lkOi8vZHVtbXkvVXNlci8xP2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIhJwYWRkbGVfMTIzNDU2BjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--0ee181d81896a7e0ef25d322c6081df1ec534cdd\"}",
22 | "quantity": "28",
23 | "refund_reason": "refund_reason",
24 | "refund_type": "partial",
25 | "status": "active",
26 | "subscription_id": "7",
27 | "subscription_payment_id": "1",
28 | "subscription_plan_id": "6",
29 | "tax_refund": "0.04",
30 | "unit_price": "unit_price",
31 | "user_id": "7",
32 | "p_signature": "rGXTK+5AM9/MXo0/YQUPKTxT1UfPOdlmTeBfRu5KFFdmXgTAKs6lkiFujnGUdXNr9n58gQ87BhITSnzAYxrHQZPStzQ810S8r/ES0xKeObueDLt/0Pf8NrV5w9C6m26ZMVo/2NqkH2qx23DJRfJ02c7Bm8LIEuT3bLvJsgIbxHyOh50GooAyg9vn/k6XAelbbt8uOoSEte5kU12THautegClljemjl7iASk1QzuAGc7kYFvoj92zZp+hed/5xhkEveKNR7PAh8zhq6u+yyXaySg4M/41S6dFLrfyAf+tYiClewGN1B3o0ZsXK6fqNPQ4ObCO8QAOm9a69oDAQHkm14SuhNuVYZiMzxwaahGfT0sUs0zT3cFsSjZSChNt+LxN30F51FKYvwz+NF1edQWH6pFKC0RXRasKUhbY5/4lUNpxWt/fF9PCQTqNDnXIOl7NlTW4v+SZ5WkjMKkzDk9nM97jQHeowCdCwLxgaraITyjpY8bUf5KEAWZK890BwQTM/gUrv/IhBTgtZdU9AF4C9Ql/p0I4KW7kHQIpoll1ijEScS6PELNftxEmsB54UK2G8N5YudGaZAzIPDDPaG/HEu4JTVAy5dlxi8/LkkRQ02UAzwIOQ28e8YdUSwwUSTFE976coT9AK+OyYZimdsfDCZgKq2Hqc2IuZRTVBMc1wsg="
33 | }
34 |
--------------------------------------------------------------------------------
/test/dummy/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | var validEnv = ['development', 'test', 'production']
3 | var currentEnv = api.env()
4 | var isDevelopmentEnv = api.env('development')
5 | var isProductionEnv = api.env('production')
6 | var isTestEnv = api.env('test')
7 |
8 | if (!validEnv.includes(currentEnv)) {
9 | throw new Error(
10 | 'Please specify a valid `NODE_ENV` or ' +
11 | '`BABEL_ENV` environment variables. Valid values are "development", ' +
12 | '"test", and "production". Instead, received: ' +
13 | JSON.stringify(currentEnv) +
14 | '.'
15 | )
16 | }
17 |
18 | return {
19 | presets: [
20 | isTestEnv && [
21 | '@babel/preset-env',
22 | {
23 | targets: {
24 | node: 'current'
25 | }
26 | }
27 | ],
28 | (isProductionEnv || isDevelopmentEnv) && [
29 | '@babel/preset-env',
30 | {
31 | forceAllTransforms: true,
32 | useBuiltIns: 'entry',
33 | corejs: 3,
34 | modules: false,
35 | exclude: ['transform-typeof-symbol']
36 | }
37 | ]
38 | ].filter(Boolean),
39 | plugins: [
40 | 'babel-plugin-macros',
41 | '@babel/plugin-syntax-dynamic-import',
42 | isTestEnv && 'babel-plugin-dynamic-import-node',
43 | '@babel/plugin-transform-destructuring',
44 | [
45 | '@babel/plugin-proposal-class-properties',
46 | {
47 | loose: true
48 | }
49 | ],
50 | [
51 | '@babel/plugin-proposal-object-rest-spread',
52 | {
53 | useBuiltIns: true
54 | }
55 | ],
56 | [
57 | '@babel/plugin-transform-runtime',
58 | {
59 | helpers: false
60 | }
61 | ],
62 | [
63 | '@babel/plugin-transform-regenerator',
64 | {
65 | async: false
66 | }
67 | ]
68 | ].filter(Boolean)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/pay/stripe/webhooks/subscription_created.rb:
--------------------------------------------------------------------------------
1 | module Pay
2 | module Stripe
3 | module Webhooks
4 | class SubscriptionCreated
5 | def call(event)
6 | object = event.data.object
7 |
8 | # We may already have the subscription in the database, so we can update that record
9 | subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: object.id)
10 |
11 | # Create the subscription in the database if we don't have it already
12 | if subscription.nil?
13 | # The customer should already be in the database
14 | owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
15 |
16 | if owner.nil?
17 | Rails.logger.error("[Pay] Unable to find Pay::Billable with processor: :stripe and processor_id: '#{object.customer}'. Searched these models: #{Pay.billable_models.join(", ")}")
18 | return
19 | end
20 |
21 | subscription = Pay.subscription_model.new(name: Pay.default_product_name, owner: owner, processor: :stripe, processor_id: object.id)
22 | end
23 |
24 | subscription.quantity = object.quantity
25 | subscription.status = object.status
26 | subscription.processor_plan = object.plan.id
27 | subscription.trial_ends_at = Time.at(object.trial_end) if object.trial_end.present?
28 |
29 | # If user was on trial, their subscription ends at the end of the trial
30 | subscription.ends_at = if object.cancel_at_period_end && subscription.on_trial?
31 | subscription.trial_ends_at
32 |
33 | # User wasn't on trial, so subscription ends at period end
34 | elsif object.cancel_at_period_end
35 | Time.at(object.current_period_end)
36 |
37 | # Subscription isn't marked to cancel at period end
38 | end
39 |
40 | subscription.save!
41 | end
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/docs/stripe.md:
--------------------------------------------------------------------------------
1 | # Using Pay with Stripe
2 |
3 | Stripe has multiple integrations:
4 |
5 | * Stripe Checkout - Hosted pages for payments
6 |
7 | ## Stripe Checkout
8 |
9 | [Stripe Checkout](https://stripe.com/docs/payments/checkout) allows you to simply redirect to Stripe for handling payments. The main benefit is that it's super fast to setup payments in your application, they're SCA compatible, and they will get improved automatically by Stripe.
10 |
11 | 
12 |
13 | ### How to use Stripe Checkout with Pay
14 |
15 | 1. Create a checkout session
16 |
17 | Choose the checkout button mode you need and pass any required arguments. Read the [Stripe Checkout Session API docs](https://stripe.com/docs/api/checkout/sessions/create) to see what options are available.
18 |
19 | ```ruby
20 | # Make sure the user's payment processor is Stripe
21 | current_user.processor = :stripe
22 |
23 | # One-time payments
24 | @checkout_session = current_user.payment_processor.checkout(mode: "payment", line_items: "price_1ILVZaKXBGcbgpbZQ26kgXWG")
25 |
26 | # Subscriptions
27 | @checkout_session = current_user.payment_processor.checkout(mode: "subscription", line_items: "default")
28 |
29 | # Setup a new card for future use
30 | @subscription = current_user.payment_processor.checkout(mode: "setup")
31 | ```
32 |
33 | 2. Render the button
34 |
35 | ```erb
36 | <%= render partial: "pay/stripe/checkout_button", locals: { session: @checkout_session, title: "Checkout" } %>
37 | ```
38 |
39 | 3. Link to the Customer Billing Portal
40 |
41 | Customers will want to update their payment method, subscription, etc. This can be done with the Customer Billing Portal. It works the same as the other Stripe Checkout pages.
42 |
43 | First, create a session in your controller:
44 |
45 | ```ruby
46 | @portal_session = current_user.payment_processor.billing_portal
47 | ```
48 |
49 | Then link to it in your view
50 |
51 | ```erb
52 | <%= link_to "Billing Portal", @portal_session.url %>
53 | ```
54 |
55 | That's it!
56 |
--------------------------------------------------------------------------------
/test/pay/paddle/charge_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Paddle::Charge::Test < ActiveSupport::TestCase
4 | setup do
5 | @billable = User.create!(email: "gob@bluth.com", processor: :paddle, processor_id: "17368056")
6 | @billable.subscriptions.create!(
7 | processor: :paddle,
8 | processor_id: "3576390",
9 | name: "default",
10 | processor_plan: "some-plan",
11 | status: "active"
12 | )
13 | end
14 |
15 | test "paddle can get paddle charge" do
16 | charge = @billable.charges.create!(
17 | processor: :paddle,
18 | processor_id: "11018517",
19 | amount: 119,
20 | card_type: "card",
21 | paddle_receipt_url: "https://my.paddle.com/receipt/15124577-11018517/57042319-chre8cc6b3d11d5-1696e10c7c",
22 | created_at: Time.zone.now
23 | )
24 | paddle_charge = charge.processor_charge
25 | assert_equal charge.processor_id, paddle_charge[:id].to_s
26 | end
27 |
28 | test "paddle can fully refund a transaction" do
29 | charge = @billable.charges.create!(
30 | processor: :paddle,
31 | processor_id: "11018517",
32 | amount: 119,
33 | card_type: "card",
34 | paddle_receipt_url: "https://my.paddle.com/receipt/15124577-11018517/57042319-chre8cc6b3d11d5-1696e10c7c",
35 | created_at: Time.zone.now
36 | )
37 |
38 | charge.refund!
39 | assert_equal 119, charge.amount_refunded
40 | end
41 |
42 | test "paddle cannot refund a transaction without payment" do
43 | charge = @billable.charges.create!(
44 | processor: :paddle,
45 | processor_id: "does-not-exist",
46 | amount: 119,
47 | card_type: "card",
48 | paddle_receipt_url: "https://my.paddle.com/receipt/15124577-11018517/57042319-chre8cc6b3d11d5-1696e10c7c",
49 | created_at: Time.zone.now
50 | )
51 |
52 | assert_raises(Pay::Error) { charge.refund! }
53 | end
54 |
55 | test "you can ask the charge for the paddle type" do
56 | assert Pay::Charge.new(processor: "paddle").paddle?
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/test/vcr_cassettes/test_user_with_braintree_as_processor.yml:
--------------------------------------------------------------------------------
1 | ---
2 | http_interactions:
3 | - request:
4 | method: put
5 | uri: https://api.sandbox.braintreegateway.com/merchants/zyfwpztymjqdcc5g/customers/
6 | body:
7 | encoding: UTF-8
8 | string: |
9 |
10 |
11 | johnny@appleseed.com
12 |
13 |
14 |
15 | headers:
16 | Accept-Encoding:
17 | - gzip
18 | Accept:
19 | - application/xml
20 | User-Agent:
21 | - Braintree Ruby Gem 3.2.0
22 | X-Apiversion:
23 | - '6'
24 | Content-Type:
25 | - application/xml
26 | Authorization:
27 | - Basic NXI1OXJyeGhuODlucGM5bjowMGYwZGY3OTMwM2UxMjcwODgxZTVmZWRhNzc4ODkyNw==
28 | response:
29 | status:
30 | code: 404
31 | message: ''
32 | headers:
33 | Date:
34 | - Tue, 16 Feb 2021 03:37:45 GMT
35 | Content-Type:
36 | - application/xml; charset=utf-8
37 | Content-Length:
38 | - '0'
39 | X-Frame-Options:
40 | - SAMEORIGIN
41 | X-Xss-Protection:
42 | - 1; mode=block
43 | X-Content-Type-Options:
44 | - nosniff
45 | X-Download-Options:
46 | - noopen
47 | X-Permitted-Cross-Domain-Policies:
48 | - none
49 | Referrer-Policy:
50 | - strict-origin-when-cross-origin
51 | Cache-Control:
52 | - no-cache
53 | X-Runtime:
54 | - '0.004227'
55 | Vary:
56 | - Origin
57 | X-Request-Id:
58 | - 7e559417-8feb-4d1d-824c-84c88c07510b
59 | Content-Security-Policy:
60 | - frame-ancestors 'self'
61 | X-Broxyid:
62 | - 7e559417-8feb-4d1d-824c-84c88c07510b
63 | Strict-Transport-Security:
64 | - max-age=31536000; includeSubDomains
65 | body:
66 | encoding: UTF-8
67 | string: ''
68 | recorded_at: Tue, 16 Feb 2021 03:37:45 GMT
69 | recorded_with: VCR 6.0.0
70 |
--------------------------------------------------------------------------------
/test/support/fixtures/paddle/subscription_updated.json:
--------------------------------------------------------------------------------
1 | {
2 | "alert_id": "185964151",
3 | "alert_name": "subscription_updated",
4 | "cancel_url": "https://checkout.paddle.com/subscription/cancel?user=4&subscription=7&hash=1a835937b1ba3f65eab36edf2fd1e4e158379589",
5 | "checkout_id": "6-3e32d84e61233cf-6d256382f4",
6 | "currency": "USD",
7 | "email": "little.kasandra@example.com",
8 | "event_time": "2020-11-16 17:02:25",
9 | "linked_subscriptions": "8, 6, 2",
10 | "marketing_consent": "",
11 | "new_price": "new_price",
12 | "new_quantity": "2",
13 | "new_unit_price": "new_unit_price",
14 | "next_bill_date": "2020-12-08",
15 | "old_next_bill_date": "old_next_bill_date",
16 | "old_price": "old_price",
17 | "old_quantity": "old_quantity",
18 | "old_status": "old_status",
19 | "old_subscription_plan_id": "old_subscription_plan_id",
20 | "old_unit_price": "old_unit_price",
21 | "passthrough": "{\"owner_sgid\": \"BAh7CEkiCGdpZAY6BkVUSSIiZ2lkOi8vZHVtbXkvVXNlci8xP2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIhJwYWRkbGVfMTIzNDU2BjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--0ee181d81896a7e0ef25d322c6081df1ec534cdd\"}",
22 | "status": "active",
23 | "subscription_id": "2",
24 | "subscription_plan_id": "8",
25 | "update_url": "https://checkout.paddle.com/subscription/update?user=8&subscription=8&hash=388a46e1fd927fe0662e40c9e37b7b8b2983c74b",
26 | "user_id": "4",
27 | "p_signature": "BLslG1ZJTzXtqBPc89AK9RIhT5rscIcFydJmuZh2dw0xqC+XxlAsyEU2QV2OKKJpcO6qPKPh4MZu7y8L8ukb0kAE0d3y/wfXsCY/Y3HMq0gk2pygPzhEKdOoU9qkPngAVxnFxO+B88XZdmEYy9T24vdW0KW4N5oXkkevyKddLISBienljTXo+ncg3PHxIxnfWYeIVufM1yd0fmTfErW31fIhX7piMPg71R6cgMK4nGlK7BvvpkrCpsrVZuCSt0AmvnsB8YmNuAvSeHXz1GNbExNNEy30UGWzGzO48r1r17h4C2RXhRqxkMycLRHETjVxAvaJ7HMglP1izg+EIPr3vv7dORsq316mi9uETemhctO3Ck89bwMjC7lSdihTIdO9KaySeYZ8sYE9mWkoCMcoPEWi2DCuyiQfdcf0iw/BRIQQnH0wJSmo/viaz2ETm2487ZPNjn16BxnB9usA+hCYgobbWy/TKw1A2k0IMK1sPDohdjI5wZ86KD84cpggqkP8uSWt9GySQrQ21p9/xLltwby8iPrApSoTbqU1JdvUqnaqnodfRImuk72fNhz13otNUHgxc3fW/C5fUq5sHFocRDwgn91wE+RfCT20+7R9s0NY32BOZbb+pWqDBGHg/kSO4p2pnliwvhalodQWUquRgXelXokI3uSl0FJdn17kJOs="
28 | }
29 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | if Rails.root.join("tmp/caching-dev.txt").exist?
17 | config.action_controller.perform_caching = true
18 |
19 | config.cache_store = :memory_store
20 | config.public_file_server.headers = {
21 | "Cache-Control" => "public, max-age=172800",
22 | }
23 | else
24 | config.action_controller.perform_caching = false
25 |
26 | config.cache_store = :null_store
27 | end
28 |
29 | # Don't care if the mailer can't send.
30 | config.action_mailer.raise_delivery_errors = false
31 |
32 | config.action_mailer.perform_caching = false
33 |
34 | # Print deprecation notices to the Rails logger.
35 | config.active_support.deprecation = :log
36 |
37 | # Raise an error on page load if there are pending migrations.
38 | config.active_record.migration_error = :page_load
39 |
40 | # Debug mode disables concatenation and preprocessing of assets.
41 | # This option may cause significant delays in view rendering with a large
42 | # number of complex assets.
43 | config.assets.debug = true
44 |
45 | # Suppress logger output for asset requests.
46 | config.assets.quiet = true
47 |
48 | # Raises error for missing translations
49 | # config.action_view.raise_on_missing_translations = true
50 |
51 | # Use an evented file watcher to asynchronously detect changes in source code,
52 | # routes, locales, etc. This feature depends on the listen gem.
53 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
54 | end
55 |
--------------------------------------------------------------------------------
/test/mailers/pay/user_mailer_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class UserMailerTest < ActionMailer::TestCase
4 | setup do
5 | @user = User.new(email: "john@example.org", extra_billing_info: "extra billing info")
6 | @charge = @user.charges.new(amount: 100, created_at: Time.zone.now)
7 | end
8 |
9 | test "receipt" do
10 | email = Pay::UserMailer.with(billable: @user, charge: @charge).receipt
11 |
12 | assert_equal [@user.email], email.to
13 | assert_equal I18n.t("pay.user_mailer.receipt.subject"), email.subject
14 | end
15 |
16 | test "attaches refunds to receipt" do
17 | filename = "receipt.pdf"
18 |
19 | receipt = mock("receipt")
20 | receipt.stubs(:render).returns("render content")
21 | receipt.stubs(:length).returns(1024)
22 |
23 | @charge.stubs(:filename).returns(filename)
24 | @charge.stubs(:receipt).returns(receipt)
25 |
26 | email = Pay::UserMailer.with(billable: @user, charge: @charge).receipt
27 |
28 | assert_equal filename, email.attachments.first.filename
29 | end
30 |
31 | test "refund" do
32 | email = Pay::UserMailer.with(billable: @user, charge: @charge).refund
33 |
34 | assert_equal [@user.email], email.to
35 | assert_equal I18n.t("pay.user_mailer.refund.subject"), email.subject
36 | end
37 |
38 | test "subscription_renewing" do
39 | time = Time.current
40 | email = Pay::UserMailer.with(billable: @user, subscription: Pay::Subscription.new, date: time).subscription_renewing
41 |
42 | assert_equal [@user.email], email.to
43 | assert_equal I18n.t("pay.user_mailer.subscription_renewing.subject"), email.subject
44 | assert_includes email.body.decoded, I18n.l(time.to_date, format: :long)
45 | end
46 |
47 | test "payment_action_required" do
48 | email = Pay::UserMailer.with(billable: @user, payment_intent_id: "x", subscription: Pay::Subscription.new).payment_action_required
49 |
50 | assert_equal [@user.email], email.to
51 | assert_equal I18n.t("pay.user_mailer.payment_action_required.subject"), email.subject
52 | assert_includes email.body.decoded, Pay::Engine.instance.routes.url_helpers.payment_path("x")
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/test/support/fixtures/paddle/subscription_payment_succeeded.json:
--------------------------------------------------------------------------------
1 | {
2 | "alert_id": "1870734833",
3 | "alert_name": "subscription_payment_succeeded",
4 | "balance_currency": "EUR",
5 | "balance_earnings": "953.67",
6 | "balance_fee": "171.97",
7 | "balance_gross": "628.6",
8 | "balance_tax": "329.68",
9 | "checkout_id": "7-26a7d8e37082d4c-9f1aa08d55",
10 | "country": "CA",
11 | "coupon": "Coupon 5",
12 | "currency": "USD",
13 | "customer_name": "customer_name",
14 | "earnings": "341.05",
15 | "email": "lina85@example.com",
16 | "event_time": "2020-11-16 16:49:17",
17 | "fee": "0.95",
18 | "initial_payment": "true",
19 | "instalments": "1",
20 | "marketing_consent": "",
21 | "next_bill_date": "2020-11-30",
22 | "next_payment_amount": "next_payment_amount",
23 | "order_id": "2",
24 | "passthrough": "{\"owner_sgid\": \"BAh7CEkiCGdpZAY6BkVUSSIiZ2lkOi8vZHVtbXkvVXNlci8xP2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIhJwYWRkbGVfMTIzNDU2BjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--0ee181d81896a7e0ef25d322c6081df1ec534cdd\"}",
25 | "payment_method": "paypal",
26 | "payment_tax": "0.68",
27 | "plan_name": "Example String",
28 | "quantity": "71",
29 | "receipt_url": "https://my.paddle.com/receipt/3/018e8d58bd4f176-12e624ab4a",
30 | "sale_gross": "215.16",
31 | "status": "past_due",
32 | "subscription_id": "8",
33 | "subscription_payment_id": "9",
34 | "subscription_plan_id": "6",
35 | "unit_price": "unit_price",
36 | "user_id": "9",
37 | "p_signature": "YEeKhxHwbdh/KT+6IKf7vqswmbeLQrTr8zcKqcHZmXKaUf1FFiblCkn4UIMyFO84wa3jHAjbXdd+AxDuq8z9wdgVwfaWAZN9594pyOeGhHcPKf4dGDtbuTvmNqBHU9chVZLpIqFtIRoDe0CUQdMp4ET8KIubvy+L5Xvw5HR1/gSaHeVRe6Ob4b86Wgik7Sg2kAyC/g7Z3G3u0pAnlkZsjE4RhMhlWiwXHJLRTJN7eGXAaBU8NlLQJA0WhOQmVlVxT5N9hpxA3aLDcejTbXT9B1KPQiO0ceK4qSsHfwJJZ9WvGrvTk5k0tX3ppSBmRuWQUGU1CeXKTBJQG1l1zr5S1q+eT1jqMGpz6Ich2id8NBbB/LFQd1+rff+tQ8NugdIszbAnTjlYaQn8rTdAoRArLLtJ39LahQo/dbxNWTOgF+9lAoeq+tsAxLZj/U2TmiPvzuyLjnfqrElBcYyRsg0NQNU8RVEIPjzHJypeQ4/1PAtjASLWk5NeM/kj/jbELF/UwBS/l+4fHOETvVHULiZXvDUyDl3ZGHeESBlLCWEElaiyJ/bf9ca896TJ/zN/iI0ju1QI1GqR7hApRpwTe+Og6uC1SscjeAJBIGodkEtOfbKRCL4BMF9vp3iqqThornOavMg1TBZb/vuW9Ild2gcu+/0no5d2tF2ztK3H6hYelR8="
38 | }
39 |
--------------------------------------------------------------------------------
/test/pay/paddle/billable_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Pay::Paddle::Billable::Test < ActiveSupport::TestCase
4 | setup do
5 | @billable = User.create!(email: "gob@bluth.com", processor: :paddle, processor_id: "17368056")
6 | @billable.subscriptions.create!(
7 | processor: :paddle,
8 | processor_id: "3576390",
9 | name: "default",
10 | processor_plan: "some-plan",
11 | status: "active"
12 | )
13 | end
14 |
15 | test "paddle can create a charge" do
16 | charge = @billable.charge(1000, {charge_name: "Test"})
17 | assert_equal Pay::Charge, charge.class
18 | assert_equal 1000, charge.amount
19 | end
20 |
21 | test "paddle cannot create a charge without charge_name" do
22 | assert_raises(Pay::Error) { @billable.charge(1000) }
23 | end
24 |
25 | test "retriving a paddle subscription" do
26 | subscription = ::PaddlePay::Subscription::User.list({subscription_id: "3576390"}, {}).try(:first)
27 | assert_equal @billable.processor_subscription("3576390").subscription_id, subscription[:subscription_id]
28 | end
29 |
30 | test "paddle can sync payment information" do
31 | subscription_user = {
32 | subscription_id: 7654321,
33 | plan_id: 123456,
34 | user_id: 12345678,
35 | user_email: "test@example.com",
36 | marketing_consent: false,
37 | update_url: "https://example.com",
38 | cancel_url: "https://example.com",
39 | state: "active",
40 | signup_date: "2020-12-08 07:52:22",
41 | last_payment: {amount: 0, currency: "USD", date: "2020-12-08"}, linked_subscriptions: [],
42 | payment_information: {payment_method: "card", card_type: "visa", last_four_digits: "0020", expiry_date: "06/2022"},
43 | next_payment: {amount: 0, currency: "USD", date: "2021-01-08"}
44 | }
45 | PaddlePay::Subscription::User.stubs(:list).returns([subscription_user])
46 |
47 | @billable.payment_processor.sync_payment_information
48 |
49 | assert_equal "visa", @billable.card_type
50 | assert_equal "0020", @billable.card_last4
51 | assert_equal "06", @billable.card_exp_month
52 | assert_equal "2022", @billable.card_exp_year
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/test/dummy/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum, this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory. If you use this option
30 | # you need to make sure to reconnect any threads in the `on_worker_boot`
31 | # block.
32 | #
33 | # preload_app!
34 |
35 | # The code in the `on_worker_boot` will be called if you are using
36 | # clustered mode by specifying a number of `workers`. After each worker
37 | # process is booted this block will be run, if you are using `preload_app!`
38 | # option you will want to use this block to reconnect to any threads
39 | # or connections that may have been created at application boot, Ruby
40 | # cannot share connections between processes.
41 | #
42 | # on_worker_boot do
43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
44 | # end
45 |
46 | # Allow puma to be restarted by `rails restart` command.
47 | plugin :tmp_restart
48 |
--------------------------------------------------------------------------------
/test/dummy/config/webpacker.yml:
--------------------------------------------------------------------------------
1 | # Note: You must restart bin/webpack-dev-server for changes to take effect
2 |
3 | default: &default
4 | source_path: app/javascript
5 | source_entry_path: packs
6 | public_root_path: public
7 | public_output_path: packs
8 | cache_path: tmp/cache/webpacker
9 | webpack_compile_output: true
10 |
11 | # Additional paths webpack should lookup modules
12 | # ['app/assets', 'engine/foo/app/assets']
13 | additional_paths: []
14 |
15 | # Reload manifest.json on all requests so we reload latest compiled packs
16 | cache_manifest: false
17 |
18 | # Extract and emit a css file
19 | extract_css: false
20 |
21 | static_assets_extensions:
22 | - .jpg
23 | - .jpeg
24 | - .png
25 | - .gif
26 | - .tiff
27 | - .ico
28 | - .svg
29 | - .eot
30 | - .otf
31 | - .ttf
32 | - .woff
33 | - .woff2
34 |
35 | extensions:
36 | - .mjs
37 | - .js
38 | - .sass
39 | - .scss
40 | - .css
41 | - .module.sass
42 | - .module.scss
43 | - .module.css
44 | - .png
45 | - .svg
46 | - .gif
47 | - .jpeg
48 | - .jpg
49 |
50 | development:
51 | <<: *default
52 | compile: true
53 |
54 | # Reference: https://webpack.js.org/configuration/dev-server/
55 | dev_server:
56 | https: false
57 | host: localhost
58 | port: 3035
59 | public: localhost:3035
60 | hmr: false
61 | # Inline should be set to true if using HMR
62 | inline: true
63 | overlay: true
64 | compress: true
65 | disable_host_check: true
66 | use_local_ip: false
67 | quiet: false
68 | pretty: false
69 | headers:
70 | 'Access-Control-Allow-Origin': '*'
71 | watch_options:
72 | ignored: '**/node_modules/**'
73 |
74 |
75 | test:
76 | <<: *default
77 | compile: true
78 |
79 | # Compile test packs to a separate directory
80 | public_output_path: packs-test
81 |
82 | production:
83 | <<: *default
84 |
85 | # Production depends on precompilation of packs prior to booting for performance.
86 | compile: false
87 |
88 | # Extract and emit a css file
89 | extract_css: true
90 |
91 | # Cache manifest.json for performance
92 | cache_manifest: true
93 |
--------------------------------------------------------------------------------