├── spec
├── dummy
│ ├── log
│ │ └── .keep
│ ├── app
│ │ ├── mailers
│ │ │ └── .keep
│ │ ├── models
│ │ │ ├── .keep
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ ├── user.rb
│ │ │ ├── owner.rb
│ │ │ ├── subscription_plan.rb
│ │ │ ├── product.rb
│ │ │ └── subscription_plan_without_interval_count.rb
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ ├── stylesheets
│ │ │ │ ├── buy.css
│ │ │ │ └── application.css
│ │ │ └── javascripts
│ │ │ │ ├── buy.js
│ │ │ │ └── application.js
│ │ ├── controllers
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ ├── home_controller.rb
│ │ │ ├── buy_controller.rb
│ │ │ ├── application_controller.rb
│ │ │ └── subscribe_controller.rb
│ │ ├── helpers
│ │ │ ├── buy_helper.rb
│ │ │ └── application_helper.rb
│ │ └── views
│ │ │ ├── subscribe
│ │ │ ├── show.html.erb
│ │ │ └── index.html.erb
│ │ │ ├── home
│ │ │ └── index.html.erb
│ │ │ ├── layouts
│ │ │ └── application.html.erb
│ │ │ └── buy
│ │ │ └── index.html.erb
│ ├── lib
│ │ └── assets
│ │ │ └── .keep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── bin
│ │ ├── rake
│ │ ├── bundle
│ │ └── rails
│ ├── spec
│ │ ├── controllers
│ │ │ └── buy_controller_spec.rb
│ │ └── factories
│ │ │ └── products.rb
│ ├── config
│ │ ├── initializers
│ │ │ ├── cookies_serializer.rb
│ │ │ ├── session_store.rb
│ │ │ ├── mime_types.rb
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── payola.rb
│ │ │ ├── assets.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── wrap_parameters.rb
│ │ │ └── inflections.rb
│ │ ├── environment.rb
│ │ ├── boot.rb
│ │ ├── routes.rb
│ │ ├── database.yml
│ │ ├── locales
│ │ │ ├── en.yml
│ │ │ └── de.yml
│ │ ├── application.rb
│ │ ├── secrets.yml
│ │ └── environments
│ │ │ ├── development.rb
│ │ │ ├── test.rb
│ │ │ └── production.rb
│ ├── config.ru
│ ├── db
│ │ ├── migrate
│ │ │ ├── 20141029140518_create_owners.rb
│ │ │ ├── 20141204170622_create_users.rb
│ │ │ ├── 20141001230848_create_products.rb
│ │ │ ├── 20141120170744_create_subscription_plan_without_interval_counts.rb
│ │ │ └── 20141105010234_create_subscription_plans.rb
│ │ └── schema.rb
│ ├── Rakefile
│ └── README.rdoc
├── factories
│ ├── product.rb
│ ├── sales.rb
│ ├── payola_coupons.rb
│ ├── payola_affiliates.rb
│ ├── subscription_plan.rb
│ └── payola_subscriptions.rb
├── services
│ └── payola
│ │ ├── process_sale_spec.rb
│ │ ├── sync_subscription_spec.rb
│ │ ├── process_subscription_spec.rb
│ │ ├── send_mail_spec.rb
│ │ ├── update_card_spec.rb
│ │ ├── invoice_failed_spec.rb
│ │ ├── change_subscription_quantity_spec.rb
│ │ ├── cancel_subscription_spec.rb
│ │ ├── change_subscription_plan_spec.rb
│ │ ├── create_plan_spec.rb
│ │ ├── create_subscription_spec.rb
│ │ ├── invoice_paid_spec.rb
│ │ ├── create_sale_spec.rb
│ │ ├── charge_card_spec.rb
│ │ └── start_subscription_spec.rb
├── models
│ └── payola
│ │ ├── stripe_webhook_spec.rb
│ │ ├── coupon_spec.rb
│ │ ├── subscription_spec.rb
│ │ └── sale_spec.rb
├── helpers
│ └── payola
│ │ └── price_helper_spec.rb
├── concerns
│ ├── sellable_spec.rb
│ └── plan_spec.rb
├── mailers
│ └── payola
│ │ ├── admin_mailer_spec.rb
│ │ └── receipt_mailer_spec.rb
├── controllers
│ └── payola
│ │ ├── transactions_controller_spec.rb
│ │ └── subscriptions_controller_spec.rb
├── spec_helper.rb
├── worker_spec.rb
└── payola_spec.rb
├── app
├── assets
│ ├── images
│ │ └── payola
│ │ │ └── .keep
│ ├── javascripts
│ │ ├── payola.js
│ │ └── payola
│ │ │ ├── application.js
│ │ │ ├── form.js
│ │ │ ├── checkout_button.js
│ │ │ ├── subscription_form_twostep.js
│ │ │ └── subscription_form_onestep.js
│ └── stylesheets
│ │ └── payola
│ │ └── application.css
├── helpers
│ └── payola
│ │ ├── application_helper.rb
│ │ └── price_helper.rb
├── models
│ ├── payola
│ │ ├── coupon.rb
│ │ ├── affiliate.rb
│ │ ├── stripe_webhook.rb
│ │ ├── sale.rb
│ │ └── subscription.rb
│ └── concerns
│ │ └── payola
│ │ ├── guid_behavior.rb
│ │ ├── sellable.rb
│ │ └── plan.rb
├── controllers
│ ├── payola
│ │ ├── application_controller.rb
│ │ ├── transactions_controller.rb
│ │ └── subscriptions_controller.rb
│ └── concerns
│ │ └── payola
│ │ ├── affiliate_behavior.rb
│ │ ├── status_behavior.rb
│ │ └── async_behavior.rb
├── services
│ └── payola
│ │ ├── process_sale.rb
│ │ ├── process_subscription.rb
│ │ ├── send_mail.rb
│ │ ├── sync_subscription.rb
│ │ ├── update_subscription.rb
│ │ ├── invoice_paid.rb
│ │ ├── invoice_failed.rb
│ │ ├── cancel_subscription.rb
│ │ ├── create_plan.rb
│ │ ├── change_subscription_quantity.rb
│ │ ├── create_sale.rb
│ │ ├── create_subscription.rb
│ │ ├── change_subscription_plan.rb
│ │ ├── update_card.rb
│ │ ├── invoice_behavior.rb
│ │ ├── charge_card.rb
│ │ └── start_subscription.rb
├── views
│ ├── payola
│ │ ├── transactions
│ │ │ ├── new.html.erb
│ │ │ ├── show.html.erb
│ │ │ ├── _stripe_header.html.erb
│ │ │ ├── iframe.html.erb
│ │ │ ├── wait.html.erb
│ │ │ ├── _checkout.html.erb
│ │ │ └── _form.html.erb
│ │ ├── admin_mailer
│ │ │ ├── refund.html.erb
│ │ │ ├── dispute.html.erb
│ │ │ ├── receipt.html.erb
│ │ │ └── failure.html.erb
│ │ ├── receipt_mailer
│ │ │ ├── receipt.text.erb
│ │ │ ├── refund.html.erb
│ │ │ ├── receipt.html.erb
│ │ │ └── receipt_pdf.html.erb
│ │ └── subscriptions
│ │ │ ├── _change_plan.html.erb
│ │ │ └── _cancel.html.erb
│ └── layouts
│ │ └── payola
│ │ └── application.html.erb
└── mailers
│ └── payola
│ ├── admin_mailer.rb
│ └── receipt_mailer.rb
├── lib
├── payola-payments.rb
├── payola
│ ├── version.rb
│ ├── worker
│ │ ├── base.rb
│ │ ├── sidekiq.rb
│ │ ├── sucker_punch.rb
│ │ └── active_job.rb
│ ├── worker.rb
│ └── engine.rb
├── tasks
│ └── payola_tasks.rake
├── generators
│ └── payola
│ │ ├── install_generator.rb
│ │ └── templates
│ │ └── initializer.rb
└── payola.rb
├── config
├── database.yml.ci
└── routes.rb
├── circle.yml
├── db
└── migrate
│ ├── 20141017233304_add_currency_to_payola_sale.rb
│ ├── 20141026101628_add_custom_fields_to_payola_sale.rb
│ ├── 20141213205847_add_active_to_payola_coupon.rb
│ ├── 20141114032013_add_coupon_code_to_payola_subscriptions.rb
│ ├── 20141122020755_add_setup_fee_to_payola_subscriptions.rb
│ ├── 20141109203101_add_stripe_status_to_payola_subscription.rb
│ ├── 20141112024805_add_affiliate_id_to_payola_subscriptions.rb
│ ├── 20141114154223_add_signed_custom_fields_to_payola_subscription.rb
│ ├── 20141107025420_add_guid_to_payola_subscriptions.rb
│ ├── 20141002203725_add_stripe_customer_id_to_sale.rb
│ ├── 20141001203541_create_payola_stripe_webhooks.rb
│ ├── 20141026144800_rename_custom_fields_to_signed_custom_fields_on_payola_sale.rb
│ ├── 20141106034610_add_currency_to_payola_subscriptions.rb
│ ├── 20141002013618_create_payola_coupons.rb
│ ├── 20141114163841_add_addresses_to_payola_subscription.rb
│ ├── 20141029135848_add_owner_to_payola_sale.rb
│ ├── 20141002013701_create_payola_affiliates.rb
│ ├── 20141105043439_create_payola_subscriptions.rb
│ └── 20141001170138_create_payola_sales.rb
├── .gitignore
├── LICENSE
├── bin
└── rails
├── Gemfile
├── Rakefile
├── payola.gemspec
├── CHANGELOG.md
└── README.md
/spec/dummy/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/payola/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/payola-payments.rb:
--------------------------------------------------------------------------------
1 | require_relative './payola'
2 |
--------------------------------------------------------------------------------
/app/assets/javascripts/payola.js:
--------------------------------------------------------------------------------
1 | //= require_tree './payola'
2 |
--------------------------------------------------------------------------------
/spec/dummy/app/helpers/buy_helper.rb:
--------------------------------------------------------------------------------
1 | module BuyHelper
2 | end
3 |
--------------------------------------------------------------------------------
/lib/payola/version.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | VERSION = "1.2.4"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/owner.rb:
--------------------------------------------------------------------------------
1 | class Owner < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/payola/application_helper.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | module ApplicationHelper
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/tasks/payola_tasks.rake:
--------------------------------------------------------------------------------
1 | # desc "Explaining what the task does"
2 | # task :payola do
3 | # # Task goes here
4 | # end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/subscription_plan.rb:
--------------------------------------------------------------------------------
1 | class SubscriptionPlan < ActiveRecord::Base
2 | include Payola::Plan
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/spec/dummy/spec/controllers/buy_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe BuyController do
4 |
5 | end
6 |
--------------------------------------------------------------------------------
/config/database.yml.ci:
--------------------------------------------------------------------------------
1 | test:
2 | adapter: postgresql
3 | database: app_test
4 | pool: 5
5 | username:
6 | password:
--------------------------------------------------------------------------------
/app/models/payola/coupon.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class Coupon < ActiveRecord::Base
3 | validates_uniqueness_of :code
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/payola/affiliate.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class Affiliate < ActiveRecord::Base
3 | validates_uniqueness_of :code
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/payola/stripe_webhook.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class StripeWebhook < ActiveRecord::Base
3 | validates_uniqueness_of :stripe_id
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | database:
2 | override:
3 | - cp config/database.yml.ci config/database.yml
4 | - bundle exec rake db:create db:migrate db:seed --trace
5 |
--------------------------------------------------------------------------------
/spec/dummy/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ActiveRecord::Base
2 | include Payola::Sellable
3 |
4 | def redirect_path(sale)
5 | '/'
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/subscription_plan_without_interval_count.rb:
--------------------------------------------------------------------------------
1 | class SubscriptionPlanWithoutIntervalCount < ActiveRecord::Base
2 | include Payola::Plan
3 | end
4 |
--------------------------------------------------------------------------------
/app/controllers/payola/application_controller.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class ApplicationController < ::ApplicationController
3 | helper PriceHelper
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/services/payola/process_sale.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class ProcessSale
3 | def self.call(guid)
4 | Sale.find_by(guid: guid).process!
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/home_controller.rb:
--------------------------------------------------------------------------------
1 | class HomeController < ApplicationController
2 |
3 | protect_from_forgery :except => [:index]
4 | def index
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../../config/application', __FILE__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/spec/factories/product.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :product do
3 | name "Foo"
4 | sequence(:permalink) { |n| "foo-#{n}" }
5 | price 100
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/stylesheets/buy.css:
--------------------------------------------------------------------------------
1 | /*
2 | Place all the styles related to the matching controller here.
3 | They will automatically be included in application.css.
4 | */
5 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.action_dispatch.cookies_serializer = :json
--------------------------------------------------------------------------------
/spec/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Rails.application
5 |
--------------------------------------------------------------------------------
/app/views/payola/transactions/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'form', stripe_key: Payola.publishable_key, permalink: @product.permalink, sale: @sale, price: formatted_price(@price), coupon: @coupon %>
2 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/buy.js:
--------------------------------------------------------------------------------
1 | // Place all the behaviors and hooks related to the matching controller here.
2 | // All this logic will automatically be available in application.js.
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/buy_controller.rb:
--------------------------------------------------------------------------------
1 | class BuyController < ApplicationController
2 | helper Payola::PriceHelper
3 |
4 | def index
5 | @product = Product.first
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/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 |
--------------------------------------------------------------------------------
/app/services/payola/process_subscription.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class ProcessSubscription
3 | def self.call(guid)
4 | Subscription.find_by(guid: guid).process!
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/payola/transactions/show.html.erb:
--------------------------------------------------------------------------------
1 |
Thanks!
2 |
3 | Thanks for buying "<%= @product.name %>". You'll receive an email shortly with your receipt and download link.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/app/services/payola/send_mail.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class SendMail
3 | def self.call(mailer, method, *args)
4 | mailer.safe_constantize.send(method, *args).deliver
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20141017233304_add_currency_to_payola_sale.rb:
--------------------------------------------------------------------------------
1 | class AddCurrencyToPayolaSale < ActiveRecord::Migration
2 | def change
3 | add_column :payola_sales, :currency, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/helpers/payola/price_helper.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | module PriceHelper
3 | def formatted_price(amount, opts = {})
4 | number_to_currency((amount || 0) / 100.0, opts)
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20141029140518_create_owners.rb:
--------------------------------------------------------------------------------
1 | class CreateOwners < ActiveRecord::Migration
2 | def change
3 | create_table :owners do |t|
4 |
5 | t.timestamps
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20141204170622_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration
2 | def change
3 | create_table :users do |t|
4 |
5 | t.timestamps
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20141026101628_add_custom_fields_to_payola_sale.rb:
--------------------------------------------------------------------------------
1 | class AddCustomFieldsToPayolaSale < ActiveRecord::Migration
2 | def change
3 | add_column :payola_sales, :custom_fields, :text
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/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 |
--------------------------------------------------------------------------------
/spec/factories/sales.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :sale, class: Payola::Sale do
3 | email 'test@example.com'
4 | product
5 | stripe_token 'tok_test'
6 | currency 'usd'
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20141213205847_add_active_to_payola_coupon.rb:
--------------------------------------------------------------------------------
1 | class AddActiveToPayolaCoupon < ActiveRecord::Migration
2 | def change
3 | add_column :payola_coupons, :active, :boolean, default: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/payola/worker/base.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | module Worker
3 | class BaseWorker
4 | def perform(klass, *args)
5 | klass.safe_constantize.call(*args)
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/subscribe/show.html.erb:
--------------------------------------------------------------------------------
1 | Subscription #<%= @subscription.guid %>
2 | <% if @subscription.active? %>
3 | <%= render 'payola/subscriptions/cancel', :subscription => @subscription %>
4 | <% end %>
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle/
2 | log/*.log
3 | pkg/
4 | spec/dummy/db/*.sqlite3
5 | spec/dummy/db/*.sqlite3-journal
6 | spec/dummy/log/*.log
7 | spec/dummy/tmp/
8 | spec/dummy/.sass-cache
9 | Gemfile.lock
10 | coverage
11 | .rspec
--------------------------------------------------------------------------------
/db/migrate/20141114032013_add_coupon_code_to_payola_subscriptions.rb:
--------------------------------------------------------------------------------
1 | class AddCouponCodeToPayolaSubscriptions < ActiveRecord::Migration
2 | def change
3 | add_column :payola_subscriptions, :coupon, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20141122020755_add_setup_fee_to_payola_subscriptions.rb:
--------------------------------------------------------------------------------
1 | class AddSetupFeeToPayolaSubscriptions < ActiveRecord::Migration
2 | def change
3 | add_column :payola_subscriptions, :setup_fee, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20141109203101_add_stripe_status_to_payola_subscription.rb:
--------------------------------------------------------------------------------
1 | class AddStripeStatusToPayolaSubscription < ActiveRecord::Migration
2 | def change
3 | add_column :payola_subscriptions, :stripe_status, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20141112024805_add_affiliate_id_to_payola_subscriptions.rb:
--------------------------------------------------------------------------------
1 | class AddAffiliateIdToPayolaSubscriptions < ActiveRecord::Migration
2 | def change
3 | add_column :payola_subscriptions, :affiliate_id, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/payola/transactions/_stripe_header.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/db/migrate/20141114154223_add_signed_custom_fields_to_payola_subscription.rb:
--------------------------------------------------------------------------------
1 | class AddSignedCustomFieldsToPayolaSubscription < ActiveRecord::Migration
2 | def change
3 | add_column :payola_subscriptions, :signed_custom_fields, :text
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/factories/payola_coupons.rb:
--------------------------------------------------------------------------------
1 | # Read about factories at https://github.com/thoughtbot/factory_girl
2 |
3 | FactoryGirl.define do
4 | factory :payola_coupon, :class => 'Payola::Coupon' do
5 | code "MyString"
6 | percent_off 1
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/views/payola/transactions/iframe.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'form', :username => @product.user.username, stripe_key: @product.user.stripe_publishable_key, :permalink => @product.permalink, :sale => @sale, :price => formatted_price(@price), :coupon => @coupon %>
2 |
3 |
--------------------------------------------------------------------------------
/db/migrate/20141107025420_add_guid_to_payola_subscriptions.rb:
--------------------------------------------------------------------------------
1 | class AddGuidToPayolaSubscriptions < ActiveRecord::Migration
2 | def change
3 | add_column :payola_subscriptions, :guid, :string
4 | add_index :payola_subscriptions, :guid
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/home/index.html.erb:
--------------------------------------------------------------------------------
1 | Hi there!
2 | This is a fake home page. Maybe you'd like to:
3 |
4 | <%= link_to "Buy Something", "/buy" %>
5 | <%= link_to "Subscribe to Something", "/subscribe" %>
6 |
7 |
8 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/spec/dummy/spec/factories/products.rb:
--------------------------------------------------------------------------------
1 | # Read about factories at https://github.com/thoughtbot/factory_girl
2 |
3 | FactoryGirl.define do
4 | factory :product do
5 | name "MyString"
6 | permalink "MyString"
7 | price 1
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20141002203725_add_stripe_customer_id_to_sale.rb:
--------------------------------------------------------------------------------
1 | class AddStripeCustomerIdToSale < ActiveRecord::Migration
2 | def change
3 | add_column :payola_sales, :stripe_customer_id, :string
4 | add_index :payola_sales, :stripe_customer_id
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20141001203541_create_payola_stripe_webhooks.rb:
--------------------------------------------------------------------------------
1 | class CreatePayolaStripeWebhooks < ActiveRecord::Migration
2 | def change
3 | create_table :payola_stripe_webhooks do |t|
4 | t.string :stripe_id
5 |
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20141026144800_rename_custom_fields_to_signed_custom_fields_on_payola_sale.rb:
--------------------------------------------------------------------------------
1 | class RenameCustomFieldsToSignedCustomFieldsOnPayolaSale < ActiveRecord::Migration
2 | def change
3 | rename_column :payola_sales, :custom_fields, :signed_custom_fields
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
3 |
4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
6 |
--------------------------------------------------------------------------------
/db/migrate/20141106034610_add_currency_to_payola_subscriptions.rb:
--------------------------------------------------------------------------------
1 | class AddCurrencyToPayolaSubscriptions < ActiveRecord::Migration
2 | def change
3 | add_column :payola_subscriptions, :currency, :string
4 | add_column :payola_subscriptions, :amount, :integer
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/db/migrate/20141002013618_create_payola_coupons.rb:
--------------------------------------------------------------------------------
1 | class CreatePayolaCoupons < ActiveRecord::Migration
2 | def change
3 | create_table :payola_coupons do |t|
4 | t.string :code
5 | t.integer :percent_off
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20141114163841_add_addresses_to_payola_subscription.rb:
--------------------------------------------------------------------------------
1 | class AddAddressesToPayolaSubscription < ActiveRecord::Migration
2 | def change
3 | add_column :payola_subscriptions, :customer_address, :text
4 | add_column :payola_subscriptions, :business_address, :text
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/payola.rb:
--------------------------------------------------------------------------------
1 | require 'sucker_punch'
2 |
3 | Payola.configure do |payola|
4 |
5 | payola.secret_key = 'sk_test_TYtgGt8qBaUpEJh0ZIY1jUuO'
6 | payola.publishable_key = 'pk_test_KeUPeR6mUmS67g2YdJ9nhqBF'
7 |
8 | payola.background_worker = :sucker_punch
9 | end
10 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20141001230848_create_products.rb:
--------------------------------------------------------------------------------
1 | class CreateProducts < ActiveRecord::Migration
2 | def change
3 | create_table :products do |t|
4 | t.string :name
5 | t.string :permalink
6 | t.integer :price
7 |
8 | t.timestamps
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/factories/payola_affiliates.rb:
--------------------------------------------------------------------------------
1 | # Read about factories at https://github.com/thoughtbot/factory_girl
2 |
3 | FactoryGirl.define do
4 | factory :payola_affiliate, :class => 'Payola::Affiliate' do
5 | code "MyString"
6 | email "foo@example.com"
7 | percent 100
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/services/payola/sync_subscription.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class SyncSubscription
3 | def self.call(event)
4 | stripe_sub = event.data.object
5 |
6 | sub = Payola::Subscription.find_by!(stripe_id: stripe_sub.id)
7 |
8 | sub.sync_with!(stripe_sub)
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20141029135848_add_owner_to_payola_sale.rb:
--------------------------------------------------------------------------------
1 | class AddOwnerToPayolaSale < ActiveRecord::Migration
2 | def change
3 | add_column :payola_sales, :owner_id, :integer
4 | add_column :payola_sales, :owner_type, :string
5 |
6 | add_index :payola_sales, [:owner_id, :owner_type]
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/services/payola/update_subscription.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class UpdateSubscription
3 | def self.call(event)
4 | stripe_sub = event.data.object
5 |
6 | sub = Payola::Subscription.find_by(stripe_id: stripe_sub.id)
7 |
8 | sub.sync_with!(stripe_sub)
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20141002013701_create_payola_affiliates.rb:
--------------------------------------------------------------------------------
1 | class CreatePayolaAffiliates < ActiveRecord::Migration
2 | def change
3 | create_table :payola_affiliates do |t|
4 | t.string :code
5 | t.string :email
6 | t.integer :percent
7 |
8 | t.timestamps
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/payola/admin_mailer/refund.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Refund!
5 | To: <%= @sale.email %>
6 | Guid: <%= @sale.guid %>
7 | Product: <%= @product.name %>
8 | Amount: <%= formatted_price(@sale.amount) %>
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/views/payola/admin_mailer/dispute.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dispute! Boo!
5 | To: <%= @sale.email %>
6 | Guid: <%= @sale.guid %>
7 | Product: <%= @product.name %>
8 | Amount: <%= formatted_price(@sale.amount) %>
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/views/payola/admin_mailer/receipt.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Sale! Woohoo!
5 | To: <%= @sale.email %>
6 | Guid: <%= @sale.guid %>
7 | Product: <%= @product.name %>
8 | Amount: <%= formatted_price(@sale.amount) %>
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/views/payola/receipt_mailer/receipt.text.erb:
--------------------------------------------------------------------------------
1 | Thanks for buying <%= @product.name %>.
2 |
3 | Your transaction ID is: <%= @sale.guid %>
4 |
5 | Click here to pick up your product: <%= payola.confirm_url(guid: @sale.guid) %>
6 |
7 | <% if Payola.pdf_receipt %>
8 | Attached you'll find a PDF receipt for your records.
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/app/services/payola/invoice_paid.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class InvoicePaid
3 | include Payola::InvoiceBehavior
4 |
5 | def self.call(event)
6 | sale, charge = create_sale_from_event(event)
7 |
8 | return unless sale
9 |
10 | sale.save!
11 | sale.finish!
12 |
13 | sale
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/views/payola/receipt_mailer/refund.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | This email confirms that your purchase has been refunded. <%= formatted_price(@sale.amount) %> will be returned to your <%= @sale.card_type %> ending in <%= @sale.card_last4 %> within 5-10 business days.
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/spec/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :buy
3 | get 'subscribe' => 'subscribe#index'
4 | post 'subscribe' => 'subscribe#create'
5 | get 'subscription/:guid' => 'subscribe#show'
6 | mount Payola::Engine => "/subdir/payola", as: :payola
7 | root 'home#index'
8 | post '' => 'home#index'
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/layouts/payola/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Payola
5 | <%= stylesheet_link_tag "payola/application", media: "all" %>
6 | <%= javascript_include_tag "payola/application" %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/views/payola/receipt_mailer/receipt.html.erb:
--------------------------------------------------------------------------------
1 | Thank you for buying <%= @product.name %>
2 | Your transaction ID is: <%= @sale.guid %>
3 | <%= link_to "Click here to pick up your product", payola.confirm_url(guid: @sale.guid) %>
4 | <% if Payola.pdf_receipt %>
5 | Attached you'll find a PDF receipt for your records.
6 | <% end %>
7 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 |
6 | def payola_can_modify_subscription?(subscription)
7 | true
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/payola/admin_mailer/failure.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Failure!
5 | To: <%= @sale.email %>
6 | Guid: <%= @sale.guid %>
7 | Product: <%= @product.name %>
8 | Amount: <%= formatted_price(@sale.amount) %>
9 | Error: <%= @sale.error %>
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/services/payola/invoice_failed.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class InvoiceFailed
3 | include Payola::InvoiceBehavior
4 |
5 | def self.call(event)
6 | sale, charge = create_sale_from_event(event)
7 |
8 | return unless sale
9 |
10 | sale.error = charge.failure_message
11 | sale.save!
12 | sale.fail!
13 |
14 | sale
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Okapi LLC
2 |
3 | Payola is an Open Source project licensed under the terms of
4 | the LGPLv3 license. Please see
5 | for license text.
6 |
7 | Payola Pro has a commercial-friendly license allowing private forks
8 | and modifications of Payola. See https://www.payola.io/pro for
9 | more details. You can find the commercial license terms in COMM-LICENSE.
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20141120170744_create_subscription_plan_without_interval_counts.rb:
--------------------------------------------------------------------------------
1 | class CreateSubscriptionPlanWithoutIntervalCounts < ActiveRecord::Migration
2 | def change
3 | create_table :subscription_plan_without_interval_counts do |t|
4 | t.string :name
5 | t.string :stripe_id
6 | t.integer :amount
7 | t.string :interval
8 |
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Precompile additional assets.
7 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
8 | # Rails.application.config.assets.precompile += %w( search.js )
9 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20141105010234_create_subscription_plans.rb:
--------------------------------------------------------------------------------
1 | class CreateSubscriptionPlans < ActiveRecord::Migration
2 | def change
3 | create_table :subscription_plans do |t|
4 | t.integer :amount
5 | t.string :interval
6 | t.integer :interval_count
7 | t.string :name
8 | t.string :stripe_id
9 | t.integer :trial_period_days
10 |
11 | t.timestamps
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/services/payola/process_sale_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe ProcessSale do
5 | describe "#call" do
6 | it "should call process!" do
7 | sale = create(:sale)
8 | expect(Payola::Sale).to receive(:find_by).with(guid: sale.guid).and_return(sale)
9 | expect(sale).to receive(:process!)
10 |
11 | Payola::ProcessSale.call(sale.guid)
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/payola/worker/sidekiq.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require 'sidekiq'
3 | rescue LoadError
4 | end
5 |
6 | module Payola
7 | module Worker
8 | class Sidekiq < BaseWorker
9 | include ::Sidekiq::Worker if defined? ::Sidekiq::Worker
10 |
11 | def self.can_run?
12 | defined?(::Sidekiq::Worker)
13 | end
14 |
15 | def self.call(klass, *args)
16 | perform_async(klass.to_s, *args)
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/spec/services/payola/sync_subscription_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe SyncSubscription do
5 | it "should call sync_with!" do
6 | expect_any_instance_of(Payola::Subscription).to receive(:sync_with!)
7 | sub = create(:subscription)
8 |
9 | event = StripeMock.mock_webhook_event('customer.subscription.updated', id: sub.stripe_id)
10 |
11 | Payola::SyncSubscription.call(event)
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/payola/worker/sucker_punch.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require 'sucker_punch'
3 | rescue LoadError
4 | end
5 |
6 | module Payola
7 | module Worker
8 | class SuckerPunch < BaseWorker
9 | include ::SuckerPunch::Job if defined? ::SuckerPunch::Job
10 |
11 | def self.can_run?
12 | defined?(::SuckerPunch::Job)
13 | end
14 |
15 | def self.call(klass, *args)
16 | new.async.perform(klass.to_s, *args)
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/services/payola/cancel_subscription.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class CancelSubscription
3 | def self.call(subscription)
4 | secret_key = Payola.secret_key_for_sale(subscription)
5 | Stripe.api_key = secret_key
6 | customer = Stripe::Customer.retrieve(subscription.stripe_customer_id, secret_key)
7 | customer.subscriptions.retrieve(subscription.stripe_id,secret_key).delete({},secret_key)
8 | subscription.cancel!
9 | end
10 |
11 | end
12 | end
13 |
14 |
--------------------------------------------------------------------------------
/spec/services/payola/process_subscription_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe ProcessSubscription do
5 | describe "#call" do
6 | it "should call process!" do
7 | sale = create(:sale)
8 | expect(Payola::Subscription).to receive(:find_by).with(guid: sale.guid).and_return(sale)
9 | expect(sale).to receive(:process!)
10 |
11 | Payola::ProcessSubscription.call(sale.guid)
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/payola/worker/active_job.rb:
--------------------------------------------------------------------------------
1 | module ::ActiveJob
2 | class Base; end
3 | end
4 |
5 | module Payola
6 | module Worker
7 | class ActiveJob < ::ActiveJob::Base
8 | def self.can_run?
9 | defined?(::ActiveJob::Core)
10 | end
11 |
12 | def self.call(klass, *args)
13 | perform_later(klass.to_s, *args)
14 | end
15 |
16 | def perform(klass, *args)
17 | klass.safe_constantize.call(*args)
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/models/payola/stripe_webhook_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe StripeWebhook do
5 | it "should validate" do
6 | s = StripeWebhook.new(stripe_id: 'test_id')
7 | expect(s.valid?).to be true
8 | end
9 |
10 | it "should validate stripe_id" do
11 | s = StripeWebhook.create(stripe_id: 'test_id')
12 | s2 = StripeWebhook.new(stripe_id: 'test_id')
13 |
14 | expect(s2.valid?).to be false
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/controllers/concerns/payola/affiliate_behavior.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | module AffiliateBehavior
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 | before_filter :find_affiliate
7 | end
8 |
9 | def find_affiliate
10 | affiliate_code = cookies[:aff] || params[:aff]
11 | @affiliate = Affiliate.where('lower(code) = lower(?)', affiliate_code).first
12 | if @affiliate
13 | cookies[:aff] = affiliate_code
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/models/concerns/payola/guid_behavior.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 |
3 | module Payola
4 | module GuidBehavior
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | before_save :populate_guid
9 | validates_uniqueness_of :guid
10 | end
11 |
12 | def populate_guid
13 | if new_record?
14 | while !valid? || self.guid.nil?
15 | self.guid = Payola.guid_generator.call
16 | end
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/views/payola/subscriptions/_change_plan.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | button_class = local_assigns.fetch :button_class, 'stripe-button-el'
3 | button_text = local_assigns.fetch :button_text, "Update Plan"
4 | %>
5 |
6 | <%= form_for subscription, method: :post, url: payola.change_subscription_plan_path(subscription) do |f| %>
7 | <%= hidden_field_tag 'plan_class', new_plan.plan_class %>
8 | <%= hidden_field_tag 'plan_id', new_plan.id %>
9 | <%= button_tag button_text, class: button_class %>
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/spec/factories/subscription_plan.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :subscription_plan do
3 | sequence(:name) { |n| "Foo #{n}" }
4 | sequence(:stripe_id) { |n| "foo-#{n}" }
5 | amount 100
6 | interval "month"
7 | interval_count 1
8 | end
9 |
10 | factory :subscription_plan_without_interval_count do
11 | sequence(:name) { |n| "Foo Without Interval #{n}" }
12 | sequence(:stripe_id) { |n| "foo-without-interval-#{n}" }
13 | amount 100
14 | interval "month"
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
3 |
4 | ENGINE_ROOT = File.expand_path('../..', __FILE__)
5 | ENGINE_PATH = File.expand_path('../../lib/payola/engine', __FILE__)
6 |
7 | # Set up gems listed in the Gemfile.
8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
10 |
11 | require 'rails/all'
12 | require 'rails/engine/commands'
13 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/subscribe_controller.rb:
--------------------------------------------------------------------------------
1 | class SubscribeController < ApplicationController
2 | helper Payola::PriceHelper
3 | include Payola::StatusBehavior
4 |
5 | def index
6 | @plan = SubscriptionPlan.first
7 | end
8 |
9 | def show
10 | @subscription = Payola::Subscription.find_by!(guid: params[:guid])
11 | end
12 |
13 | def create
14 | params[:plan] = SubscriptionPlan.first
15 | subscription = Payola::CreateSubscription.call(params)
16 | render_payola_status(subscription)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/views/payola/subscriptions/_cancel.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | button_class = local_assigns.fetch :button_class, 'stripe-button-el'
3 | button_text = local_assigns.fetch :button_text, "Cancel Subscription"
4 | confirm_text = local_assigns.fetch :confirm_text, "Are you sure?"
5 | disabled = subscription.active?
6 | %>
7 |
8 | <%= form_tag payola.cancel_subscription_path(subscription.guid), :method => :delete do %>
9 | <%= submit_tag button_text, html: { disabled: disabled, class: button_class }, data: { confirm: confirm_text } %>
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/app/controllers/concerns/payola/status_behavior.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | module StatusBehavior
3 | extend ActiveSupport::Concern
4 |
5 | def render_payola_status(object)
6 | render nothing: true, status: 404 and return unless object
7 |
8 | errors = ([object.error.presence] + object.errors.full_messages).compact.to_sentence
9 |
10 | render json: {
11 | guid: object.guid,
12 | status: object.state,
13 | error: errors.presence
14 | }, status: errors.blank? ? 200 : 400
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
6 |
7 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
8 | <%= csrf_meta_tags %>
9 | <%= render 'payola/transactions/stripe_header' %>
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/spec/dummy/README.rdoc:
--------------------------------------------------------------------------------
1 | == README
2 |
3 | This README would normally document whatever steps are necessary to get the
4 | application up and running.
5 |
6 | Things you may want to cover:
7 |
8 | * Ruby version
9 |
10 | * System dependencies
11 |
12 | * Configuration
13 |
14 | * Database creation
15 |
16 | * Database initialization
17 |
18 | * How to run the test suite
19 |
20 | * Services (job queues, cache servers, search engines, etc.)
21 |
22 | * Deployment instructions
23 |
24 | * ...
25 |
26 |
27 | Please feel free to use a different markup language if you do not plan to run
28 | rake doc:app .
29 |
--------------------------------------------------------------------------------
/lib/generators/payola/install_generator.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class InstallGenerator < Rails::Generators::Base
3 | source_root File.expand_path('../templates', __FILE__)
4 |
5 | def install_initializer
6 | initializer 'payola.rb', File.read(File.expand_path('../templates/initializer.rb', __FILE__))
7 | end
8 |
9 | def install_js
10 | inject_into_file 'app/assets/javascripts/application.js', after: "//= require jquery\n" do <<-'JS'
11 | //= require payola
12 | JS
13 | end
14 | end
15 |
16 | def install_route
17 | route "mount Payola::Engine => '/payola', as: :payola"
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/app/assets/javascripts/payola/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require_tree .
14 |
--------------------------------------------------------------------------------
/app/models/concerns/payola/sellable.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 |
3 | module Payola
4 | module Sellable
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | validates_presence_of :name
9 | validates_presence_of :permalink
10 | validates_presence_of :price
11 | validates_uniqueness_of :permalink
12 |
13 | Payola.register_sellable(self)
14 | end
15 |
16 | def product_class
17 | self.class.product_class
18 | end
19 |
20 | module ClassMethods
21 | def sellable?
22 | true
23 | end
24 |
25 | def product_class
26 | self.to_s.underscore
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Declare your gem's dependencies in payola.gemspec.
4 | # Bundler will treat runtime dependencies like base dependencies, and
5 | # development dependencies will be added by default to the :development group.
6 | gemspec
7 |
8 | # Declare any dependencies that are still in development here instead of in
9 | # your gemspec. These might include edge Rails or gems from your path or
10 | # Git. Remember to move these dependencies to your gemspec before releasing
11 | # your gem to rubygems.org.
12 |
13 | # To use debugger
14 | # gem 'debugger'
15 | gem 'simplecov', :require => false, :group => :test
16 | gem "codeclimate-test-reporter", group: :test, require: nil
17 |
18 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require_tree .
14 | //= require payola
15 |
--------------------------------------------------------------------------------
/app/services/payola/create_plan.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class CreatePlan
3 | def self.call(plan)
4 | secret_key = Payola.secret_key_for_sale(plan)
5 |
6 | Stripe::Plan.create({
7 | id: plan.stripe_id,
8 | amount: plan.amount,
9 | interval: plan.interval,
10 | name: plan.name,
11 | interval_count: plan.respond_to?(:interval_count) ? plan.interval_count : nil,
12 | currency: plan.respond_to?(:currency) ? plan.currency : Payola.default_currency,
13 | trial_period_days: plan.respond_to?(:trial_period_days) ? plan.trial_period_days : nil
14 | }, secret_key)
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/services/payola/send_mail_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 |
5 | class TestMailer < ActionMailer::Base
6 | def test_mail(to, text)
7 | mail(
8 | to: to,
9 | from: 'from@example.com',
10 | body: text
11 | )
12 | end
13 | end
14 |
15 | describe SendMail do
16 | describe "#call" do
17 | it "should send a mail" do
18 | mail = double
19 | expect(TestMailer).to receive(:test_mail).with('to@example.com', 'Some Text').and_return(mail)
20 | expect(mail).to receive(:deliver)
21 | Payola::SendMail.call('Payola::TestMailer', 'test_mail', 'to@example.com', 'Some Text')
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/spec/helpers/payola/price_helper_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require 'spec_helper'
3 |
4 | describe Payola::PriceHelper do
5 |
6 | describe "#formatted_price" do
7 | it { expect(helper.formatted_price(nil)).to eq "$0.00" }
8 | it { expect(helper.formatted_price(0)).to eq "$0.00" }
9 | it { expect(helper.formatted_price(2)).to eq "$0.02" }
10 | it { expect(helper.formatted_price(20)).to eq "$0.20" }
11 | it { expect(helper.formatted_price(2000)).to eq "$20.00" }
12 | it { expect(helper.formatted_price(200.0)).to eq "$2.00" }
13 |
14 | it { expect(helper.formatted_price(200, unit: '£', separator: ',')).to eq "£2,00" }
15 | it { expect(helper.formatted_price(200, locale: :de)).to eq "2,00 €" }
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/payola/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/spec/models/payola/coupon_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe Coupon do
5 | describe "validations" do
6 | it "should validate uniqueness of coupon code" do
7 | c1 = Coupon.create(code: 'abc')
8 | expect(c1.valid?).to be_truthy
9 |
10 | c2 = Coupon.new(code: 'abc')
11 | expect(c2.valid?).to be_falsey
12 | end
13 | end
14 |
15 | describe "active" do
16 | it "should allow active flag" do
17 | c1 = Coupon.create(code: 'abc', active: false)
18 | expect(c1.active?).to be_falsey
19 | end
20 |
21 | it "should be true by default" do
22 | c1 = Coupon.create(code: 'abc')
23 | expect(c1.active?).to be_truthy
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/mailers/payola/admin_mailer.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class AdminMailer < ActionMailer::Base
3 | helper Payola::PriceHelper
4 |
5 | def receipt(sale_guid)
6 | send_admin_mail(sale_guid)
7 | end
8 |
9 | def refund(sale_guid)
10 | send_admin_mail(sale_guid)
11 | end
12 |
13 | def dispute(sale_guid)
14 | send_admin_mail(sale_guid)
15 | end
16 |
17 | def failure(sale_guid)
18 | send_admin_mail(sale_guid)
19 | end
20 |
21 | def send_admin_mail(sale_guid)
22 | ActiveRecord::Base.connection_pool.with_connection do
23 | @sale = Payola::Sale.find_by(guid: sale_guid)
24 | @product = @sale.product
25 | mail(from: Payola.support_email, to: Payola.support_email)
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/services/payola/change_subscription_quantity.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class ChangeSubscriptionQuantity
3 | def self.call(subscription, quantity)
4 | secret_key = Payola.secret_key_for_sale(subscription)
5 | old_quantity = subscription.quantity
6 |
7 | begin
8 | customer = Stripe::Customer.retrieve(subscription.stripe_customer_id, secret_key)
9 | sub = customer.subscriptions.retrieve(subscription.stripe_id)
10 | sub.quantity = quantity
11 | sub.save
12 |
13 | subscription.quantity = quantity
14 | subscription.save!
15 |
16 | subscription.instrument_plan_changed(old_quantity)
17 |
18 | rescue RuntimeError, Stripe::StripeError => e
19 | subscription.errors[:base] << e.message
20 | end
21 |
22 | subscription
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/services/payola/create_sale.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class CreateSale
3 | def self.call(params)
4 | product = params[:product]
5 | affiliate = params[:affiliate]
6 | coupon = params[:coupon]
7 |
8 | Payola::Sale.new do |s|
9 | s.product = product
10 | s.email = params[:stripeEmail]
11 | s.stripe_token = params[:stripeToken]
12 | s.affiliate_id = affiliate.try(:id)
13 | s.currency = product.respond_to?(:currency) ? product.currency : Payola.default_currency
14 | s.signed_custom_fields = params[:signed_custom_fields]
15 |
16 | if coupon
17 | s.coupon = coupon
18 | s.amount = product.price * (1 - s.coupon.percent_off / 100.0)
19 | else
20 | s.amount = product.price
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/factories/payola_subscriptions.rb:
--------------------------------------------------------------------------------
1 | # Read about factories at https://github.com/thoughtbot/factory_girl
2 |
3 | FactoryGirl.define do
4 | factory :subscription, :class => 'Payola::Subscription' do
5 | plan_type "SubscriptionPlan"
6 | plan_id 1
7 | start "2014-11-04 22:34:39"
8 | status "MyString"
9 | owner_type "Owner"
10 | owner_id 1
11 | stripe_customer_id "MyString"
12 | cancel_at_period_end false
13 | current_period_start "2014-11-04 22:34:39"
14 | current_period_end "2014-11-04 22:34:39"
15 | ended_at "2014-11-04 22:34:39"
16 | trial_start "2014-11-04 22:34:39"
17 | trial_end "2014-11-04 22:34:39"
18 | canceled_at "2014-11-04 22:34:39"
19 | email "jeremy@octolabs.com"
20 | stripe_token "yyz123"
21 | currency 'usd'
22 | quantity 1
23 | stripe_id 'sub_123456'
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/views/payola/transactions/wait.html.erb:
--------------------------------------------------------------------------------
1 | Please wait...
2 |
3 |
4 |
5 |
6 |
7 |
26 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/buy/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'payola/transactions/checkout', sellable: @product, button_class: 'not-a-class' %>
2 |
3 |
4 |
5 | <%= form_for @product, url: '/', method: :post, html: { class: 'payola-payment-form', 'data-payola-base-path' => '/subdir/payola', 'data-payola-product' => @product.product_class, 'data-payola-permalink' => @product.permalink } do |f| %>
6 |
7 | Email:
8 |
9 | Card Number
10 |
11 | Exp Month
12 |
13 | Exp Year
14 |
15 | CVC
16 |
17 |
18 | <% end %>
19 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | begin
2 | require 'bundler/setup'
3 | rescue LoadError
4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5 | end
6 |
7 | require 'rdoc/task'
8 |
9 | RDoc::Task.new(:rdoc) do |rdoc|
10 | rdoc.rdoc_dir = 'rdoc'
11 | rdoc.title = 'Payola'
12 | rdoc.options << '--line-numbers'
13 | rdoc.rdoc_files.include('README.rdoc')
14 | rdoc.rdoc_files.include('lib/**/*.rb')
15 | end
16 |
17 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18 | load 'rails/tasks/engine.rake'
19 |
20 | Bundler::GemHelper.install_tasks
21 |
22 | Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each {|f| load f }
23 |
24 | require 'rspec/core'
25 | require 'rspec/core/rake_task'
26 |
27 | desc "Run all specs in spec directory (excluding plugin specs)"
28 | RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
29 |
30 | task :default => :spec
31 |
--------------------------------------------------------------------------------
/app/services/payola/create_subscription.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class CreateSubscription
3 | def self.call(params, owner=nil)
4 | plan = params[:plan]
5 | affiliate = params[:affiliate]
6 |
7 | sub = Payola::Subscription.new do |s|
8 | s.plan = plan
9 | s.email = params[:stripeEmail]
10 | s.stripe_token = params[:stripeToken]
11 | s.affiliate_id = affiliate.try(:id)
12 | s.currency = plan.respond_to?(:currency) ? plan.currency : Payola.default_currency
13 | s.coupon = params[:coupon]
14 | s.signed_custom_fields = params[:signed_custom_fields]
15 | s.setup_fee = params[:setup_fee]
16 | s.quantity = params[:quantity]
17 |
18 | s.owner = owner
19 | s.amount = plan.amount
20 | end
21 |
22 | if sub.save
23 | Payola.queue!(Payola::ProcessSubscription, sub.guid)
24 | end
25 |
26 | sub
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/services/payola/change_subscription_plan.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class ChangeSubscriptionPlan
3 | def self.call(subscription, plan)
4 | secret_key = Payola.secret_key_for_sale(subscription)
5 | old_plan = subscription.plan
6 |
7 | begin
8 | customer = Stripe::Customer.retrieve(subscription.stripe_customer_id, secret_key)
9 | sub = customer.subscriptions.retrieve(subscription.stripe_id)
10 |
11 | prorate = plan.respond_to?(:should_prorate?) ? plan.should_prorate?(subscription) : true
12 |
13 | sub.plan = plan.stripe_id
14 | sub.prorate = prorate
15 | sub.save
16 |
17 | subscription.plan = plan
18 | subscription.save!
19 |
20 | subscription.instrument_plan_changed(old_plan)
21 |
22 | rescue RuntimeError, Stripe::StripeError => e
23 | subscription.errors[:base] << e.message
24 | end
25 |
26 | subscription
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/services/payola/update_card.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class UpdateCard
3 | def self.call(subscription, token)
4 | secret_key = Payola.secret_key_for_sale(subscription)
5 | begin
6 | customer = Stripe::Customer.retrieve(subscription.stripe_customer_id, secret_key)
7 |
8 | customer.card = token
9 | customer.save
10 |
11 | customer = Stripe::Customer.retrieve(subscription.stripe_customer_id, secret_key)
12 | card = customer.cards.retrieve(customer.default_card, secret_key)
13 |
14 | subscription.update_attributes(
15 | card_type: card.brand,
16 | card_last4: card.last4,
17 | card_expiration: Date.parse("#{card.exp_year}/#{card.exp_month}/1")
18 | )
19 | subscription.save!
20 | rescue RuntimeError, Stripe::StripeError => e
21 | subscription.errors[:base] << e.message
22 | end
23 |
24 | subscription
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/concerns/sellable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe Sellable do
5 | describe "validations" do
6 | it "should validate" do
7 | product = build(:product)
8 | expect(product.valid?).to be true
9 | end
10 | it "should validate name" do
11 | product = build(:product, name: nil)
12 | expect(product.valid?).to be false
13 | end
14 | it "should validate permalink" do
15 | product = build(:product, permalink: nil)
16 | expect(product.valid?).to be false
17 | end
18 | end
19 |
20 | describe "#product_class" do
21 | it "should return the underscore'd version of the class" do
22 | expect(build(:product).product_class).to eq 'product'
23 | end
24 | end
25 |
26 | describe "#sellable?" do
27 | it "should return true" do
28 | expect(Product.sellable?).to be true
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/services/payola/update_card_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe UpdateCard do
5 | let(:stripe_helper) { StripeMock.create_test_helper }
6 |
7 | describe "#call" do
8 | before do
9 | @plan = create(:subscription_plan)
10 |
11 | token = StripeMock.generate_card_token({})
12 | @subscription = create(:subscription, plan: @plan, stripe_token: token)
13 | StartSubscription.call(@subscription)
14 | token2 = StripeMock.generate_card_token({last4: '2233', exp_year: '2021', exp_month: '11', brand: 'JCB'})
15 | Payola::UpdateCard.call(@subscription, token2)
16 | end
17 |
18 | it "should change the card" do
19 | @subscription.reload
20 | expect(@subscription.card_last4).to eq '2233'
21 | expect(@subscription.card_expiration).to eq Date.new(2021,11,1)
22 | expect(@subscription.card_type).to eq 'JCB'
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/db/migrate/20141105043439_create_payola_subscriptions.rb:
--------------------------------------------------------------------------------
1 | class CreatePayolaSubscriptions < ActiveRecord::Migration
2 | def change
3 | create_table :payola_subscriptions do |t|
4 | t.string :plan_type
5 | t.integer :plan_id
6 | t.timestamp :start
7 | t.string :status
8 | t.string :owner_type
9 | t.integer :owner_id
10 | t.string :stripe_customer_id
11 | t.boolean :cancel_at_period_end
12 | t.timestamp :current_period_start
13 | t.timestamp :current_period_end
14 | t.timestamp :ended_at
15 | t.timestamp :trial_start
16 | t.timestamp :trial_end
17 | t.timestamp :canceled_at
18 | t.integer :quantity
19 | t.string :stripe_id
20 | t.string :stripe_token
21 | t.string :card_last4
22 | t.date :card_expiration
23 | t.string :card_type
24 | t.text :error
25 | t.string :state
26 | t.string :email
27 |
28 | t.timestamps
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/generators/payola/templates/initializer.rb:
--------------------------------------------------------------------------------
1 | Payola.configure do |config|
2 | # Example subscription:
3 | #
4 | # config.subscribe 'payola.package.sale.finished' do |sale|
5 | # EmailSender.send_an_email(sale.email)
6 | # end
7 | #
8 | # In addition to any event that Stripe sends, you can subscribe
9 | # to the following special payola events:
10 | #
11 | # - payola..sale.finished
12 | # - payola..sale.refunded
13 | # - payola..sale.failed
14 | #
15 | # These events consume a Payola::Sale, not a Stripe::Event
16 | #
17 | # Example charge verifier:
18 | #
19 | # config.charge_verifier = lambda do |sale|
20 | # raise "Nope!" if sale.email.includes?('yahoo.com')
21 | # end
22 |
23 | # Keep this subscription unless you want to disable refund handling
24 | config.subscribe 'charge.refunded' do |event|
25 | sale = Payola::Sale.find_by(stripe_id: event.data.object.id)
26 | sale.refund!
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/mailers/payola/admin_mailer_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe AdminMailer do
5 | let(:sale) { create(:sale) }
6 |
7 | describe '#receipt' do
8 | it "should send a receipt notification" do
9 | mail = AdminMailer.receipt(sale.guid)
10 | expect(mail.subject).to eq 'Receipt'
11 | end
12 | end
13 |
14 | describe '#refund' do
15 | it "should send a refund notification" do
16 | mail = AdminMailer.refund(sale.guid)
17 | expect(mail.subject).to eq 'Refund'
18 | end
19 | end
20 |
21 | describe '#dispute' do
22 | it "should send a dispute notification" do
23 | mail = AdminMailer.dispute(sale.guid)
24 | expect(mail.subject).to eq 'Dispute'
25 | end
26 | end
27 |
28 | describe '#failure' do
29 | it "should send a failure notification" do
30 | mail = AdminMailer.failure(sale.guid)
31 | expect(mail.subject).to eq 'Failure'
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | Bundler.require(*Rails.groups)
6 | require "payola"
7 |
8 | module Dummy
9 | class Application < Rails::Application
10 | # Settings in config/environments/* take precedence over those specified here.
11 | # Application configuration should go into files in config/initializers
12 | # -- all .rb files in that directory are automatically loaded.
13 |
14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
16 | # config.time_zone = 'Central Time (US & Canada)'
17 |
18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
20 | # config.i18n.default_locale = :de
21 | config.i18n.available_locales = [:en, :de]
22 | end
23 | end
24 |
25 |
--------------------------------------------------------------------------------
/spec/services/payola/invoice_failed_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe InvoiceFailed do
5 | let(:stripe_helper) { StripeMock.create_test_helper }
6 | it "should create a failed sale" do
7 | plan = create(:subscription_plan)
8 |
9 | customer = Stripe::Customer.create(
10 | email: 'foo',
11 | card: stripe_helper.generate_card_token,
12 | plan: plan.stripe_id
13 | )
14 |
15 | sub = create(:subscription, plan: plan, stripe_customer_id: customer.id, stripe_id: customer.subscriptions.first.id)
16 |
17 | charge = Stripe::Charge.create(failure_message: 'Failed! OMG!')
18 | event = StripeMock.mock_webhook_event('invoice.payment_failed', subscription: sub.stripe_id, charge: charge.id)
19 |
20 | count = Payola::Sale.count
21 | sale = Payola::InvoiceFailed.call(event)
22 |
23 | expect(Payola::Sale.count).to eq count + 1
24 |
25 | expect(sale.errored?).to be true
26 | expect(sale.error).to eq 'Failed! OMG!'
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/services/payola/change_subscription_quantity_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe ChangeSubscriptionQuantity do
5 | let(:stripe_helper) { StripeMock.create_test_helper }
6 |
7 | describe "#call" do
8 | before do
9 | @plan = create(:subscription_plan)
10 |
11 | token = StripeMock.generate_card_token({})
12 | @subscription = create(:subscription, quantity: 1, stripe_token: token)
13 | StartSubscription.call(@subscription)
14 | Payola::ChangeSubscriptionQuantity.call(@subscription, 2)
15 | end
16 |
17 | it "should change the quantity on the stripe subscription" do
18 | customer = Stripe::Customer.retrieve(@subscription.stripe_customer_id)
19 | sub = customer.subscriptions.retrieve(@subscription.stripe_id)
20 |
21 | expect(sub.quantity).to eq 2
22 | end
23 |
24 | it "should change the quantity on the payola subscription" do
25 | expect(@subscription.reload.quantity).to eq 2
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/services/payola/cancel_subscription_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe CancelSubscription do
5 | let(:stripe_helper) { StripeMock.create_test_helper }
6 | let(:token){ StripeMock.generate_card_token({}) }
7 | describe "#call" do
8 | before :each do
9 | plan = create(:subscription_plan)
10 | @subscription = create(:subscription, plan: plan, stripe_token: token)
11 | @subscription.process!
12 | end
13 | it "should cancel a subscription" do
14 | CancelSubscription.call(@subscription)
15 | expect(@subscription.reload.state).to eq 'canceled'
16 | end
17 | it "should not change the state if an error occurs" do
18 | custom_error = StandardError.new("Customer not found")
19 | StripeMock.prepare_error(custom_error, :get_customer)
20 | expect { CancelSubscription.call(@subscription) }.to raise_error
21 |
22 | expect(@subscription.reload.state).to eq 'active'
23 | end
24 | end
25 | end
26 | end
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/models/concerns/payola/plan.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 |
3 | module Payola
4 | module Plan
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | validates_presence_of :amount
9 | validates_presence_of :interval
10 | validates_presence_of :stripe_id
11 | validates_presence_of :name
12 |
13 | validates_uniqueness_of :stripe_id
14 |
15 | before_create :create_stripe_plan
16 |
17 | has_many :subscriptions, :class_name => "Payola::Subscription"
18 |
19 | Payola.register_subscribable(self)
20 | end
21 |
22 | def create_stripe_plan
23 | Payola::CreatePlan.call(self)
24 | end
25 |
26 | def plan_class
27 | self.class.plan_class
28 | end
29 |
30 | def product_class
31 | plan_class
32 | end
33 |
34 | def price
35 | amount
36 | end
37 |
38 | module ClassMethods
39 | def subscribable?
40 | true
41 | end
42 |
43 | def plan_class
44 | self.to_s.underscore
45 | end
46 | end
47 |
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/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 `rake 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: f1f2572d010168e61cc6b901cadc66cc1ba2df5fb11f1aa16e60c96cff6dbbe2248ceacf81ea96e9015ca0ab2d219e616c3471260c97e302af75b12fcaf6ccd3
15 |
16 | test:
17 | secret_key_base: 5f23907a09acc8c65c6a4f05da1fdbc394320545717323b910d308e685b814f2789c9df0227492bbca8420221939ad04fedb6ec1a8c15af8e6e475bdf5f19ea5
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 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/subscribe/index.html.erb:
--------------------------------------------------------------------------------
1 | <%#= render 'payola/transactions/checkout', sellable: @product, button_class: 'not-a-class' %>
2 |
3 |
4 |
5 | <%= form_for @plan, url: '/', method: :post, html: { class: 'payola-subscription-form', 'data-payola-base-path' => '/subdir/payola', 'data-payola-plan-type' => @plan.plan_class, 'data-payola-plan-id' => @plan.id } do |f| %>
6 |
7 | Email:
8 |
9 | Card Number
10 |
11 | Exp Month
12 |
13 | Exp Year
14 |
15 | CVC
16 |
17 | Coupon Code
18 |
19 | Quantity
20 |
21 |
22 | <% end %>
23 |
24 |
--------------------------------------------------------------------------------
/spec/services/payola/change_subscription_plan_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe ChangeSubscriptionPlan do
5 | let(:stripe_helper) { StripeMock.create_test_helper }
6 |
7 | describe "#call" do
8 | before do
9 | @plan1 = create(:subscription_plan)
10 | @plan2 = create(:subscription_plan)
11 |
12 | token = StripeMock.generate_card_token({})
13 | @subscription = create(:subscription, plan: @plan1, stripe_token: token)
14 | StartSubscription.call(@subscription)
15 | Payola::ChangeSubscriptionPlan.call(@subscription, @plan2)
16 | end
17 |
18 | it "should change the plan on the stripe subscription" do
19 | customer = Stripe::Customer.retrieve(@subscription.stripe_customer_id)
20 | sub = customer.subscriptions.retrieve(@subscription.stripe_id)
21 |
22 | expect(sub.plan.id).to eq @plan2.stripe_id
23 | end
24 |
25 | it "should change the plan on the payola subscription" do
26 | expect(@subscription.reload.plan).to eq @plan2
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/payola/worker.rb:
--------------------------------------------------------------------------------
1 | require 'payola/worker/base'
2 | require 'payola/worker/active_job'
3 | require 'payola/worker/sidekiq'
4 | require 'payola/worker/sucker_punch'
5 |
6 | module Payola
7 | module Worker
8 | class << self
9 | attr_accessor :registry
10 |
11 | def find(symbol)
12 | if registry.has_key? symbol
13 | return registry[symbol]
14 | else
15 | raise "No such worker type: #{symbol}"
16 | end
17 | end
18 |
19 | def autofind
20 | # prefer ActiveJob over the other workers
21 | if Payola::Worker::ActiveJob.can_run?
22 | return Payola::Worker::ActiveJob
23 | end
24 |
25 | registry.values.each do |worker|
26 | if worker.can_run?
27 | return worker
28 | end
29 | end
30 |
31 | raise "No eligible background worker systems found."
32 | end
33 | end
34 |
35 | self.registry = {
36 | sidekiq: Payola::Worker::Sidekiq,
37 | sucker_punch: Payola::Worker::SuckerPunch,
38 | active_job: Payola::Worker::ActiveJob
39 | }
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Payola::Engine.routes.draw do
2 | match '/buy/:product_class/:permalink' => 'transactions#create', via: :post, as: :buy
3 | match '/confirm/:guid' => 'transactions#show', via: :get, as: :confirm
4 | match '/status/:guid' => 'transactions#status', via: :get, as: :status
5 |
6 | match '/subscribe/:plan_class/:plan_id' => 'subscriptions#create', via: :post, as: :subscribe
7 | match '/confirm_subscription/:guid' => 'subscriptions#show', via: :get, as: :confirm_subscription
8 | match '/subscription_status/:guid' => 'subscriptions#status', via: :get, as: :subscription_status
9 | match '/cancel_subscription/:guid' => 'subscriptions#destroy', via: :delete, as: :cancel_subscription
10 | match '/change_plan/:guid' => 'subscriptions#change_plan', via: :post, as: :change_subscription_plan
11 | match '/change_quantity/:guid' => 'subscriptions#change_quantity', via: :post, as: :change_subscription_quantity
12 | match '/update_card/:guid' => 'subscriptions#update_card', via: :post, as: :update_card
13 |
14 | mount StripeEvent::Engine => '/events'
15 | end
16 |
--------------------------------------------------------------------------------
/spec/services/payola/create_plan_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe CreatePlan do
5 | before do
6 | @subscription_plan = create(:subscription_plan)
7 | end
8 |
9 | describe "#call" do
10 | it "should create a plan at Stripe" do
11 | plan = Stripe::Plan.retrieve(@subscription_plan.stripe_id)
12 |
13 | expect(plan.name).to eq @subscription_plan.name
14 | expect(plan.amount).to eq @subscription_plan.amount
15 | expect(plan.id).to eq @subscription_plan.stripe_id
16 | expect(plan.interval).to eq @subscription_plan.interval
17 | expect(plan.interval_count).to eq @subscription_plan.interval_count
18 | expect(plan.currency).to eq 'usd'
19 | expect(plan.trial_period_days).to eq @subscription_plan.trial_period_days
20 | end
21 |
22 | it "should default interval_count" do
23 | our_plan = create(:subscription_plan_without_interval_count)
24 |
25 | expect(our_plan.respond_to?(:interval_count)).to eq false
26 |
27 | plan = Stripe::Plan.retrieve(our_plan.stripe_id)
28 | expect(plan.interval_count).to be_nil
29 | end
30 | end
31 |
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/db/migrate/20141001170138_create_payola_sales.rb:
--------------------------------------------------------------------------------
1 | class CreatePayolaSales < ActiveRecord::Migration
2 | def change
3 | create_table :payola_sales do |t|
4 | t.string "email"
5 | t.string "guid"
6 | t.integer "product_id"
7 | t.string "product_type"
8 | t.datetime "created_at"
9 | t.datetime "updated_at"
10 | t.string "state"
11 | t.string "stripe_id"
12 | t.string "stripe_token"
13 | t.string "card_last4"
14 | t.date "card_expiration"
15 | t.string "card_type"
16 | t.text "error"
17 | t.integer "amount"
18 | t.integer "fee_amount"
19 | t.integer "coupon_id"
20 | t.boolean "opt_in"
21 | t.integer "download_count"
22 | t.integer "affiliate_id"
23 | t.text "customer_address"
24 | t.text "business_address"
25 | t.timestamps
26 | end
27 |
28 | add_index "payola_sales", ["coupon_id"], name: "index_payola_sales_on_coupon_id", using: :btree
29 | add_index "payola_sales", ["product_id", "product_type"], name: "index_payola_sales_on_product", using: :btree
30 | add_index "payola_sales", ["email"], name: "index_payola_sales_on_email", using: :btree
31 | add_index "payola_sales", ["guid"], name: "index_payola_sales_on_guid", using: :btree
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/services/payola/create_subscription_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe CreateSubscription do
5 | before do
6 | @plan = create(:subscription_plan)
7 | Payola.background_worker = Payola::FakeWorker
8 | end
9 |
10 | describe "#call" do
11 | it "should create a subscription and queue the job" do
12 | expect(Payola).to receive(:queue!)
13 |
14 | sale = CreateSubscription.call(
15 | stripeEmail: 'pete@bugsplat.info',
16 | stripeToken: 'test_tok',
17 | plan: @plan
18 | )
19 |
20 | expect(sale.email).to eq 'pete@bugsplat.info'
21 | expect(sale.stripe_token).to eq 'test_tok'
22 | expect(sale.plan_id).to eq @plan.id
23 | expect(sale.plan).to eq @plan
24 | expect(sale.plan_type).to eq 'SubscriptionPlan'
25 | expect(sale.currency).to eq 'usd'
26 | end
27 |
28 | it "should include the affiliate if given" do
29 | affiliate = create(:payola_affiliate)
30 | sale = CreateSubscription.call(
31 | email: 'pete@bugsplat.info',
32 | stripeToken: 'test_tok',
33 | plan: @plan,
34 | affiliate: affiliate
35 | )
36 |
37 | expect(sale.affiliate).to eq affiliate
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/payola/engine.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class Engine < ::Rails::Engine
3 | isolate_namespace Payola
4 | engine_name 'payola'
5 |
6 | config.generators do |g|
7 | g.test_framework :rspec, fixture: false
8 | g.fixture_replacement :factory_girl, dir: 'spec/factories'
9 | g.assets false
10 | g.helper false
11 | end
12 |
13 | initializer :append_migrations do |app|
14 | unless app.root.to_s.match root.to_s
15 | config.paths["db/migrate"].expanded.each do |expanded_path|
16 | app.config.paths["db/migrate"] << expanded_path
17 | end
18 | end
19 | end
20 |
21 | initializer :inject_helpers do |app|
22 | ActiveSupport.on_load :action_controller do
23 | ::ActionController::Base.send(:helper, Payola::PriceHelper)
24 | end
25 |
26 | ActiveSupport.on_load :action_mailer do
27 | ::ActionMailer::Base.send(:helper, Payola::PriceHelper)
28 | end
29 | end
30 |
31 | initializer :configure_subscription_listeners do |app|
32 | Payola.configure do |config|
33 | config.subscribe 'invoice.payment_succeeded', Payola::InvoicePaid
34 | config.subscribe 'invoice.payment_failed', Payola::InvoiceFailed
35 | config.subscribe 'customer.subscription.updated', Payola::SyncSubscription
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/app/controllers/concerns/payola/async_behavior.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | module AsyncBehavior
3 | extend ActiveSupport::Concern
4 |
5 | def show_object(object_class)
6 | object = object_class.find_by!(guid: params[:guid])
7 | redirector = object.redirector
8 |
9 | new_path = redirector.respond_to?(:redirect_path) ? redirector.redirect_path(object) : '/'
10 | redirect_to new_path
11 | end
12 |
13 | def object_status(object_class)
14 | object = object_class.find_by(guid: params[:guid])
15 | render_payola_status(object)
16 | end
17 |
18 | def create_object(object_class, object_creator_class, object_processor_class, product_key, product)
19 | create_params = if object_class == Subscription
20 | params.permit!.merge(
21 | product_key => product,
22 | coupon: @coupon,
23 | quantity: @quantity,
24 | affiliate: @affiliate
25 | )
26 | else
27 | params.permit!.merge(
28 | product_key => product,
29 | coupon: @coupon,
30 | affiliate: @affiliate
31 | )
32 | end
33 |
34 | object = object_creator_class.call(create_params)
35 |
36 | if object.save && object_processor_class.present?
37 | Payola.queue!(object_processor_class, object.guid)
38 | end
39 |
40 | render_payola_status(object)
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/app/controllers/payola/transactions_controller.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class TransactionsController < ApplicationController
3 | include Payola::AffiliateBehavior
4 | include Payola::StatusBehavior
5 | include Payola::AsyncBehavior
6 |
7 | before_filter :find_product_and_coupon, only: [:create]
8 |
9 | def show
10 | show_object(Sale)
11 | end
12 |
13 | def status
14 | object_status(Sale)
15 | end
16 |
17 | def create
18 | create_object(Sale, CreateSale, ProcessSale, :product, @product)
19 | end
20 |
21 | private
22 | def find_product_and_coupon
23 | find_product
24 | find_coupon
25 | end
26 |
27 | def find_product
28 | @product_class = Payola.sellables[params[:product_class]]
29 |
30 | raise ActionController::RoutingError.new('Not Found') unless @product_class && @product_class.sellable?
31 |
32 | @product = @product_class.find_by!(permalink: params[:permalink])
33 | end
34 |
35 | def find_coupon
36 | coupon_code = cookies[:cc] || params[:cc] || params[:coupon_code]
37 | @coupon = Coupon.where('lower(code) = lower(?)', coupon_code).first
38 | if @coupon && @coupon.active?
39 | cookies[:cc] = coupon_code
40 | @price = @product.price * (1 - @coupon.percent_off / 100.0)
41 | else
42 | @price = @product.price
43 | end
44 | end
45 |
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/payola.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 |
3 | # Maintain your gem's version:
4 | require "payola/version"
5 |
6 | # Describe your gem and declare its dependencies:
7 | Gem::Specification.new do |s|
8 | s.name = "payola-payments"
9 | s.version = Payola::VERSION
10 | s.authors = ["Pete Keen"]
11 | s.email = ["pete@payola.io"]
12 | s.homepage = "https://www.payola.io"
13 | s.summary = "Drop-in Rails engine for accepting payments with Stripe"
14 | s.description = "One-off and subscription payments for your Rails application"
15 | s.license = "LGPL-3.0"
16 |
17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
18 | s.test_files = Dir["spec/**/*"]
19 |
20 | s.add_dependency "rails", ">= 4.1"
21 | s.add_dependency "jquery-rails"
22 | s.add_dependency "stripe", ">= 1.16.0"
23 | s.add_dependency "aasm", ">= 4.0.7"
24 | s.add_dependency "stripe_event", ">= 1.3.0"
25 |
26 | s.add_development_dependency "sqlite3"
27 | s.add_development_dependency "rspec-rails"
28 | s.add_development_dependency 'factory_girl_rails'
29 | s.add_development_dependency "stripe-ruby-mock", "~> 2.0"
30 | s.add_development_dependency "sucker_punch", "~> 1.2.1"
31 | s.add_development_dependency "docverter"
32 |
33 | s.post_install_message = <<-HERE
34 | Please ensure that your Payola route has 'as: :payola' included. Prior to v1.0.7 this was not added automatically.
35 | HERE
36 | end
37 |
--------------------------------------------------------------------------------
/spec/concerns/plan_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe Plan do
5 |
6 | it "should validate" do
7 | subscription_plan = build(:subscription_plan)
8 | expect(subscription_plan.valid?).to be true
9 | end
10 |
11 | it "should validate amount" do
12 | subscription_plan = build(:subscription_plan, amount: nil)
13 | expect(subscription_plan.valid?).to be false
14 | end
15 |
16 | it "should validate interval" do
17 | subscription_plan = build(:subscription_plan, interval: nil)
18 | expect(subscription_plan.valid?).to be false
19 | end
20 |
21 | it "should validate stripe_id" do
22 | subscription_plan = build(:subscription_plan, stripe_id: nil)
23 | expect(subscription_plan.valid?).to be false
24 | end
25 |
26 | it "should validate name" do
27 | subscription_plan = build(:subscription_plan, name: nil)
28 | expect(subscription_plan.valid?).to be false
29 | end
30 |
31 | it "should create the plan at stripe before the model is created" do
32 | subscription_plan = build(:subscription_plan)
33 | Payola::CreatePlan.should_receive(:call)
34 | subscription_plan.save!
35 | end
36 |
37 | it "should not try to create the plan at stripe before the model is updated" do
38 | subscription_plan = build(:subscription_plan)
39 | subscription_plan.save!
40 | subscription_plan.name = "new name"
41 |
42 | Payola::CreatePlan.should_not_receive(:call)
43 | subscription_plan.save!
44 | end
45 |
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/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 and disable caching.
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send.
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger.
20 | config.active_support.deprecation = :log
21 |
22 | # Raise an error on page load if there are pending migrations.
23 | config.active_record.migration_error = :page_load
24 |
25 | # Debug mode disables concatenation and preprocessing of assets.
26 | # This option may cause significant delays in view rendering with a large
27 | # number of complex assets.
28 | config.assets.debug = true
29 |
30 | # Adds additional error checking when serving assets at runtime.
31 | # Checks for improperly declared sprockets dependencies.
32 | # Raises helpful error messages.
33 | config.assets.raise_runtime_errors = true
34 |
35 | # Raises error for missing translations
36 | # config.action_view.raise_on_missing_translations = true
37 | end
38 |
--------------------------------------------------------------------------------
/spec/services/payola/invoice_paid_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe InvoicePaid do
5 | let(:stripe_helper) { StripeMock.create_test_helper }
6 | it "should do nothing if the invoice has no charge" do
7 | # create a Payola::Subscription
8 | plan = create(:subscription_plan)
9 |
10 | customer = Stripe::Customer.create(
11 | email: 'foo',
12 | card: stripe_helper.generate_card_token,
13 | plan: plan.stripe_id
14 | )
15 |
16 | sub = create(:subscription, plan: plan, stripe_customer_id: customer.id, stripe_id: customer.subscriptions.first.id)
17 |
18 | event = StripeMock.mock_webhook_event('invoice.payment_succeeded', subscription: sub.stripe_id, charge: nil)
19 |
20 | count = Payola::Sale.count
21 |
22 | Payola::InvoicePaid.call(event)
23 |
24 | expect(Payola::Sale.count).to eq count
25 | end
26 |
27 | it "should create a sale" do
28 | plan = create(:subscription_plan)
29 | customer = Stripe::Customer.create(
30 | email: 'foo',
31 | card: stripe_helper.generate_card_token,
32 | plan: plan.stripe_id
33 | )
34 |
35 | sub = create(:subscription, plan: plan, stripe_customer_id: customer.id, stripe_id: customer.subscriptions.first.id)
36 |
37 | charge = Stripe::Charge.create
38 | event = StripeMock.mock_webhook_event('invoice.payment_succeeded', subscription: sub.stripe_id, charge: charge.id)
39 |
40 | count = Payola::Sale.count
41 |
42 | sale = Payola::InvoicePaid.call(event)
43 |
44 | expect(Payola::Sale.count).to eq count + 1
45 |
46 | expect(sale.finished?).to be true
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/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 |
--------------------------------------------------------------------------------
/spec/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 |
--------------------------------------------------------------------------------
/app/mailers/payola/receipt_mailer.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class ReceiptMailer < ActionMailer::Base
3 | add_template_helper ::ApplicationHelper
4 | helper Payola::PriceHelper
5 |
6 | def receipt(sale_guid)
7 | ActiveRecord::Base.connection_pool.with_connection do
8 | @sale = Payola::Sale.find_by(guid: sale_guid)
9 | @product = @sale.product
10 |
11 | if Payola.pdf_receipt
12 | require 'docverter'
13 |
14 | pdf = Docverter::Conversion.run do |c|
15 | c.from = 'html'
16 | c.to = 'pdf'
17 | c.content = render_to_string('payola/receipt_mailer/receipt_pdf.html')
18 | end
19 | attachments["receipt-#{@sale.guid}.pdf"] = pdf
20 | end
21 |
22 | mail_params = {
23 | to: @sale.email,
24 | from: Payola.support_email,
25 | subject: @product.respond_to?(:receipt_subject) ? @product.receipt_subject(@sale) : 'Purchase Receipt',
26 | }
27 |
28 | if @product.respond_to?(:receipt_from_address)
29 | mail_params[:from] = @product.receipt_from_address(@sale)
30 | end
31 |
32 | mail(mail_params)
33 | end
34 | end
35 |
36 | def refund(sale_guid)
37 | ActiveRecord::Base.connection_pool.with_connection do
38 | @sale = Payola::Sale.find_by(guid: sale_guid)
39 | @product = @sale.product
40 |
41 | mail_params = {
42 | to: @sale.email,
43 | from: Payola.support_email,
44 | subject: @product.respond_to?(:refund_subject) ? @product.refund_subject(@sale) : 'Refund Confirmation',
45 | }
46 |
47 | if @product.respond_to?(:refund_from_address)
48 | mail_params[:from] = @product.refund_from_address(@sale)
49 | end
50 |
51 | mail(mail_params)
52 | end
53 | end
54 |
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/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 |
--------------------------------------------------------------------------------
/app/services/payola/invoice_behavior.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 |
3 | module Payola
4 | module InvoiceBehavior
5 | extend ActiveSupport::Concern
6 |
7 | module ClassMethods
8 | def create_sale_from_event(event)
9 | invoice = event.data.object
10 |
11 | return unless invoice.charge
12 |
13 | subscription = Payola::Subscription.find_by!(stripe_id: invoice.subscription)
14 | secret_key = Payola.secret_key_for_sale(subscription)
15 |
16 | stripe_sub = Stripe::Customer.retrieve(subscription.stripe_customer_id, secret_key).subscriptions.retrieve(invoice.subscription, secret_key)
17 | subscription.sync_with!(stripe_sub)
18 |
19 | sale = create_sale(subscription, invoice)
20 |
21 | charge = Stripe::Charge.retrieve(invoice.charge, secret_key)
22 |
23 | update_sale_with_charge(sale, charge)
24 |
25 | return sale, charge
26 | end
27 |
28 | def create_sale(subscription, invoice)
29 | Payola::Sale.new do |s|
30 | s.email = subscription.email
31 | s.state = 'processing'
32 | s.owner = subscription
33 | s.product = subscription.plan
34 | s.stripe_token = 'invoice'
35 | s.amount = invoice.total
36 | s.currency = invoice.currency
37 | end
38 | end
39 |
40 | def update_sale_with_charge(sale, charge)
41 | sale.stripe_id = charge.id
42 | sale.card_type = charge.card.respond_to?(:brand) ? charge.card.brand : charge.card.type
43 | sale.card_last4 = charge.card.last4
44 |
45 | if charge.respond_to?(:fee)
46 | sale.fee_amount = charge.fee
47 | else
48 | balance = Stripe::BalanceTransaction.retrieve(charge.balance_transaction, secret_key)
49 | sale.fee_amount = balance.fee
50 | end
51 | end
52 | end
53 |
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/spec/services/payola/create_sale_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe CreateSale do
5 | before do
6 | @product = create(:product)
7 | end
8 |
9 | describe "#call" do
10 | it "should create a sale" do
11 | sale = CreateSale.call(
12 | stripeEmail: 'pete@bugsplat.info',
13 | stripeToken: 'test_tok',
14 | product: @product
15 | )
16 |
17 | expect(sale.email).to eq 'pete@bugsplat.info'
18 | expect(sale.stripe_token).to eq 'test_tok'
19 | expect(sale.product_id).to eq @product.id
20 | expect(sale.product).to eq @product
21 | expect(sale.product_type).to eq 'Product'
22 | expect(sale.currency).to eq 'usd'
23 | end
24 |
25 | it "should include the affiliate if given" do
26 | affiliate = create(:payola_affiliate)
27 | sale = CreateSale.call(
28 | email: 'pete@bugsplat.info',
29 | stripeToken: 'test_tok',
30 | product: @product,
31 | affiliate: affiliate
32 | )
33 |
34 | expect(sale.affiliate).to eq affiliate
35 | end
36 |
37 | describe "with coupon" do
38 | it "should include the coupon" do
39 | coupon = create(:payola_coupon)
40 |
41 | sale = CreateSale.call(
42 | email: 'pete@bugsplat.info',
43 | stripeToken: 'test_tok',
44 | product: @product,
45 | coupon: coupon
46 | )
47 |
48 | expect(sale.coupon).to eq coupon
49 | end
50 | it "should set the price correctly" do
51 | coupon = create(:payola_coupon)
52 |
53 | sale = CreateSale.call(
54 | email: 'pete@bugsplat.info',
55 | stripeToken: 'test_tok',
56 | product: @product,
57 | coupon: coupon
58 | )
59 |
60 | expect(sale.amount).to eq 99
61 | end
62 | end
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/spec/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 = true
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 static asset server for tests with Cache-Control for performance.
16 | if Rails::VERSION::MAJOR >= 4 && Rails::VERSION::MINOR >= 2
17 | config.serve_static_files = true
18 | else
19 | config.serve_static_asses = true
20 | end
21 | config.static_cache_control = 'public, max-age=3600'
22 |
23 | # Show full error reports and disable caching.
24 | config.consider_all_requests_local = true
25 | config.action_controller.perform_caching = false
26 |
27 | # Raise exceptions instead of rendering exception templates.
28 | config.action_dispatch.show_exceptions = false
29 |
30 | # Disable request forgery protection in test environment.
31 | config.action_controller.allow_forgery_protection = false
32 |
33 | # Tell Action Mailer not to deliver emails to the real world.
34 | # The :test delivery method accumulates sent emails in the
35 | # ActionMailer::Base.deliveries array.
36 | config.action_mailer.delivery_method = :test
37 |
38 | # Print deprecation notices to the stderr.
39 | config.active_support.deprecation = :stderr
40 |
41 | # Raises error for missing translations
42 | # config.action_view.raise_on_missing_translations = true
43 |
44 | config.action_mailer.default_url_options = {host: 'www.example.com'}
45 | end
46 |
--------------------------------------------------------------------------------
/spec/services/payola/charge_card_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe ChargeCard do
5 | let(:stripe_helper) { StripeMock.create_test_helper }
6 | describe "#call" do
7 | it "should create a customer" do
8 | sale = create(:sale, state: 'processing', stripe_token: stripe_helper.generate_card_token)
9 | ChargeCard.call(sale)
10 | expect(sale.reload.stripe_customer_id).to_not be_nil
11 | end
12 |
13 | it "should not create a customer if one already exists" do
14 | customer = Stripe::Customer.create
15 | sale = create(:sale, state: 'processing', stripe_customer_id: customer.id)
16 | expect(Stripe::Customer).to receive(:retrieve).and_return(customer)
17 | ChargeCard.call(sale)
18 | expect(sale.reload.stripe_customer_id).to eq customer.id
19 | expect(sale.state).to eq 'finished'
20 | end
21 |
22 | it "should create a charge" do
23 | sale = create(:sale, state: 'processing', stripe_token: stripe_helper.generate_card_token)
24 | ChargeCard.call(sale)
25 | expect(sale.reload.stripe_id).to_not be_nil
26 | expect(sale.reload.card_last4).to_not be_nil
27 | expect(sale.reload.card_expiration).to_not be_nil
28 | expect(sale.reload.card_type).to_not be_nil
29 | end
30 |
31 | it "should get the fee from the balance transaction" do
32 | sale = create(:sale, state: 'processing', stripe_token: stripe_helper.generate_card_token)
33 | ChargeCard.call(sale)
34 | expect(sale.reload.fee_amount).to_not be_nil
35 | end
36 |
37 | describe "on error" do
38 | it "should update the error attribute" do
39 |
40 | StripeMock.prepare_card_error(:card_declined)
41 | sale = create(:sale, state: 'processing', stripe_token: stripe_helper.generate_card_token)
42 | ChargeCard.call(sale)
43 | expect(sale.reload.error).to_not be_nil
44 | expect(sale.errored?).to be true
45 | end
46 | end
47 | end
48 | end
49 | end
50 |
51 |
--------------------------------------------------------------------------------
/app/services/payola/charge_card.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class ChargeCard
3 | def self.call(sale)
4 | sale.save!
5 | secret_key = Payola.secret_key_for_sale(sale)
6 |
7 | begin
8 | sale.verify_charge!
9 |
10 | customer = create_customer(sale, secret_key)
11 | charge = create_charge(sale, customer, secret_key)
12 |
13 | update_sale(sale, customer, charge, secret_key)
14 |
15 | sale.finish!
16 | rescue Stripe::StripeError, RuntimeError => e
17 | sale.update_attributes(error: e.message)
18 | sale.fail!
19 | end
20 |
21 | sale
22 | end
23 |
24 | def self.create_customer(sale, secret_key)
25 | if sale.stripe_customer_id.present?
26 | Stripe::Customer.retrieve(sale.stripe_customer_id, secret_key)
27 | else
28 | Stripe::Customer.create({
29 | card: sale.stripe_token,
30 | email: sale.email
31 | }, secret_key)
32 | end
33 | end
34 |
35 | def self.create_charge(sale, customer, secret_key)
36 | charge_attributes = {
37 | amount: sale.amount,
38 | currency: sale.currency,
39 | customer: customer.id,
40 | description: sale.guid,
41 | }.merge(Payola.additional_charge_attributes.call(sale, customer))
42 |
43 | Stripe::Charge.create(charge_attributes, secret_key)
44 | end
45 |
46 | def self.update_sale(sale, customer, charge, secret_key)
47 | if charge.respond_to?(:fee)
48 | fee = charge.fee
49 | else
50 | balance = Stripe::BalanceTransaction.retrieve(charge.balance_transaction, secret_key)
51 | fee = balance.fee
52 | end
53 |
54 | sale.update_attributes(
55 | stripe_id: charge.id,
56 | stripe_customer_id: customer.id,
57 | card_last4: charge.card.last4,
58 | card_expiration: Date.new(charge.card.exp_year, charge.card.exp_month, 1),
59 | card_type: charge.card.respond_to?(:brand) ? charge.card.brand : charge.card.type,
60 | fee_amount: fee
61 | )
62 | end
63 |
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/app/views/payola/receipt_mailer/receipt_pdf.html.erb:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | PDF Receipt
6 |
32 |
33 |
34 |
35 |
36 |
37 | Transaction # <%= @sale.guid %>
38 | Created on <%= @sale.created_at.strftime('%Y-%m-%d') %>
39 |
40 |
41 |
42 |
43 |
44 | Customer Details
45 | Email: <%= @sale.email %>
46 | Payment Method:
47 | <% if @sale.stripe_id %>
48 | <%= @sale.card_type %> ending in <%= @sale.card_last4 %>
49 | <% else %>
50 | <%= @sale.stripe_token %>
51 | <% end %>
52 | <% if @sale.customer_address %>
53 |
54 | <%= @sale.customer_address.html_safe %>
55 | <% end %>
56 |
57 |
58 |
59 |
60 | Description
61 | Price
62 | Amount
63 |
64 |
65 | <%= truncate(@product.name, length: 50) %>
66 | <%= formatted_price(@product.price) %>
67 | <%= formatted_price(@sale.amount) %>
68 |
69 |
70 |
71 | Total
72 | <%= formatted_price(@sale.amount) %>
73 |
74 |
75 |
76 |
77 | Thank you for your order! If you have any questions please email <%= Payola.support_email %>.
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/spec/mailers/payola/receipt_mailer_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'docverter'
3 |
4 | module Payola
5 | describe ReceiptMailer do
6 | let(:sale) { create(:sale) }
7 |
8 | before do
9 | Payola.pdf_receipt = false
10 | end
11 |
12 | describe '#receipt' do
13 | it 'should send a receipt' do
14 | mail = Payola::ReceiptMailer.receipt(sale.guid)
15 | expect(mail.subject).to eq 'Purchase Receipt'
16 | end
17 |
18 | it 'should send a receipt with a pdf' do
19 | Payola.pdf_receipt = true
20 | expect(Docverter::Conversion).to receive(:run).and_return('pdf')
21 | mail = Payola::ReceiptMailer.receipt(sale.guid)
22 | expect(mail.attachments["receipt-#{sale.guid}.pdf"]).to_not be nil
23 | end
24 |
25 | it "should allow product to override subject" do
26 | expect_any_instance_of(Product).to receive(:receipt_subject).and_return("Override Subject")
27 | mail = Payola::ReceiptMailer.receipt(sale.guid)
28 | expect(mail.subject).to eq 'Override Subject'
29 | end
30 |
31 | it "should allow product to override from address" do
32 | expect_any_instance_of(Product).to receive(:receipt_from_address).and_return("Override ")
33 | mail = Payola::ReceiptMailer.receipt(sale.guid)
34 | expect(mail.from.first).to eq 'override@example.com'
35 | end
36 | end
37 |
38 | describe '#refund' do
39 | it "should send refund email" do
40 | mail = Payola::ReceiptMailer.refund(sale.guid)
41 | expect(mail.subject).to eq 'Refund Confirmation'
42 | end
43 |
44 | it "should allow product to override subject" do
45 | expect_any_instance_of(Product).to receive(:refund_subject).and_return("Override Subject")
46 | mail = Payola::ReceiptMailer.refund(sale.guid)
47 | expect(mail.subject).to eq 'Override Subject'
48 | end
49 |
50 | it "should allow product to override from address" do
51 | expect_any_instance_of(Product).to receive(:refund_from_address).and_return("Override ")
52 | mail = Payola::ReceiptMailer.refund(sale.guid)
53 | expect(mail.from.first).to eq 'override@example.com'
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Payola Changelog
2 |
3 | * v1.2.4 - 2015-01-06
4 | - Fix regressions in v1.2.3
5 |
6 | * v1.2.3 - 2015-01-03
7 | - Add support for Rails 4.2
8 | - Re-use customers and create invoice items for setup fees
9 | - Add an active flag on `Payola::Coupon`
10 | - Fix load-order problems
11 | - Add support for subscription quantities
12 | - Properly handle form errors
13 |
14 | * v1.2.2 - 2014-11-29
15 | - Optionally invert subscription controller flow
16 | - Fix the CSRF token behavior
17 |
18 | * v1.2.1 - 2014-11-20
19 | - Make guid generator overrideable
20 | - Bumped minimum version of AASM to 4.0
21 | - Fixed a bug with the auto emails not working for webhook events
22 | - Code cleanup
23 | - Test coverage improvements
24 |
25 | * v1.2.0 - 2014-11-17
26 | - Subscriptions
27 |
28 | * v1.1.4 - 2014-11-07
29 | - Pass the created customer to `additional_charge_attributes`
30 | - Add Payola Pro license
31 |
32 | * v1.1.3 - 2014-11-07
33 | - Add options for requesting billing and shipping addresses via Checkout
34 | - Add a callable to add additional attributes to a Stripe::Charge
35 | - Only talk about PDFs if PDFs are enabled
36 |
37 | * v1.1.2 - 2014-11-06
38 | - Default the `From` address on receipt emails to `Payola.support_email`
39 |
40 | * v1.1.1 - 2014-11-03
41 | - ActiveJob can't serialize a class or a symbol so we have to `to_s` them
42 |
43 | * v1.1.0 - 2014-11-03
44 | - Add customizable mailers
45 | - Pass currency through properly and add a `default_currency` config option
46 | - Use data attributes to set up the checkout button instead of a JS snippet
47 | - Add a polymorphic `owner` association on `Payola::Sale`.
48 | - Allow the price to be overridden on the Checkout form
49 |
50 | * v1.0.8 - 2014-10-27
51 | - Add basic support for custom forms
52 | - Allow passing signed data from checkout button into the charge verifier
53 | - Correctly pass the price into the Checkout button, which allows the `{{amount}}` macro to work properly
54 | - I18n the formatted_price helper
55 |
56 | * v1.0.7 - 2014-10-21
57 | - Add support for ActiveJob workers
58 | - Document how to set Stripe keys
59 | - Add a callback to fetch the publishable key
60 | - Unpin the Rails version to allow anything >= 4.1
61 | - Allow Payola to be mounted anywhere, as long as it has 'as: :payola' in the mount spec
62 |
63 | * v1.0.6 - 2014-10-19
64 | - First public release
65 | <
66 |
--------------------------------------------------------------------------------
/spec/models/payola/subscription_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe Subscription do
5 |
6 | describe "validations" do
7 | it "should validate" do
8 | subscription = build(:subscription)
9 | expect(subscription.valid?).to be true
10 | end
11 |
12 | it "should validate plan" do
13 | subscription = build(:subscription, plan: nil)
14 | expect(subscription.valid?).to be false
15 | end
16 |
17 | it "should validate lack of email" do
18 | sale = build(:sale, email: nil)
19 | expect(sale.valid?).to be false
20 | end
21 |
22 | it "should validate stripe_token" do
23 | sale = build(:sale, stripe_token: nil)
24 | expect(sale.valid?).to be false
25 | end
26 |
27 | end
28 |
29 | describe "#sync_with!" do
30 | it "should sync timestamps" do
31 | plan = create(:subscription_plan)
32 | subscription = build(:subscription, plan: plan)
33 | stripe_sub = Stripe::Customer.create.subscriptions.create(plan: plan.stripe_id, card: StripeMock.generate_card_token(last4: '1234', exp_year: Time.now.year + 1))
34 | old_start = subscription.current_period_start
35 | old_end = subscription.current_period_end
36 | trial_start = subscription.trial_start
37 | trial_end = subscription.trial_end
38 |
39 | now = Time.now.to_i
40 | expect(stripe_sub).to receive(:canceled_at).and_return(now).at_least(1)
41 |
42 | subscription.sync_with!(stripe_sub)
43 |
44 | subscription.reload
45 |
46 | expect(subscription.current_period_start).to eq Time.at(stripe_sub.current_period_start)
47 | expect(subscription.current_period_start).to_not eq old_start
48 | expect(subscription.current_period_end).to eq Time.at(stripe_sub.current_period_end)
49 | expect(subscription.current_period_end).to_not eq old_end
50 | expect(subscription.canceled_at).to eq Time.at(now)
51 | end
52 |
53 | it "should sync non-timestamp fields" do
54 | plan = create(:subscription_plan)
55 | subscription = build(:subscription, plan: plan)
56 | stripe_sub = Stripe::Customer.create.subscriptions.create(plan: plan.stripe_id, card: StripeMock.generate_card_token(last4: '1234', exp_year: Time.now.year + 1))
57 |
58 | expect(stripe_sub).to receive(:quantity).and_return(10).at_least(1)
59 | expect(stripe_sub).to receive(:cancel_at_period_end).and_return(true).at_least(1)
60 |
61 | subscription.sync_with!(stripe_sub)
62 |
63 | subscription.reload
64 |
65 | expect(subscription.quantity).to eq 10
66 | expect(subscription.stripe_status).to eq 'active'
67 | expect(subscription.cancel_at_period_end).to eq true
68 | end
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Payola
2 |
3 | [](http://badge.fury.io/rb/payola-payments) [](https://circleci.com/gh/peterkeen/payola) [](https://codeclimate.com/github/peterkeen/payola) [](https://codeclimate.com/github/peterkeen/payola) [](https://gemnasium.com/peterkeen/payola)
4 |
5 | Payments with Stripe for your Rails application.
6 |
7 | ## What does this do?
8 |
9 | Payola is a drop-in Rails engine that lets you sell one or more products by just including a module in your models. It includes:
10 |
11 | * An easy to embed, easy to customize, async Stripe Checkout button
12 | * Asynchronous payments, usable with any background processing system
13 | * Full webhook integration
14 | * Easy extension hooks for adding your own functionality
15 | * Customizable emails
16 |
17 | To see Payola in action, check out the site for [Mastering Modern Payments: Using Stripe with Rails](https://www.masteringmodernpayments.com). Read the book to find out the whys behind Payola's design.
18 |
19 | ## Installation
20 |
21 | Add Payola to your Gemfile:
22 |
23 | ```ruby
24 | gem 'payola-payments'
25 | ```
26 |
27 | Run the installer and install the migrations:
28 |
29 | ```bash
30 | $ rails g payola:install
31 | $ rake db:migrate
32 | ```
33 |
34 | ## Additional Setup Resources
35 |
36 |
37 | [One-time payments](https://github.com/peterkeen/payola/wiki/One-time-payments)
38 |
39 | [Configuration options](https://github.com/peterkeen/payola/wiki/Configuration-options)
40 |
41 | [Subscriptions](https://github.com/peterkeen/payola/wiki/Subscriptions)
42 |
43 |
44 | ## Upgrade to Pro
45 |
46 | I also sell **Payola Pro**, a collection of add-ons to Payola that enables things like drop-in Mailchimp and Mixpanel integration, as well as Stripe Connect support. It also comes with priority support and a lawyer-friendly commercial license. You can see all of the details on the [Payola Pro homepage](https://www.payola.io/pro).
47 |
48 | ## TODO
49 |
50 | * Multiple subscriptions per customer
51 | * Affiliate tracking
52 | * Easy metered billing
53 |
54 | ## License
55 |
56 | Please see the LICENSE file for licensing details.
57 |
58 | ## Contributing
59 |
60 | 1. Fork the project
61 | 2. Make your changes, including tests that exercise the code
62 | 3. Make a pull request
63 |
64 | Version announcements happen on the [Payola Payments Google group](https://groups.google.com/forum/#!forum/payola-payments) and [@payolapayments](https://twitter.com/payolapayments).
65 |
66 | ## Author
67 |
68 | Pete Keen, [@zrail](https://twitter.com/zrail), [https://www.petekeen.net](https://www.petekeen.net)
69 |
70 |
--------------------------------------------------------------------------------
/app/controllers/payola/subscriptions_controller.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class SubscriptionsController < ApplicationController
3 | include Payola::AffiliateBehavior
4 | include Payola::StatusBehavior
5 | include Payola::AsyncBehavior
6 |
7 | before_filter :find_plan_coupon_and_quantity, only: [:create, :change_plan]
8 | before_filter :check_modify_permissions, only: [:destroy, :change_plan, :change_quantity, :update_card]
9 |
10 | def show
11 | show_object(Subscription)
12 | end
13 |
14 | def status
15 | object_status(Subscription)
16 | end
17 |
18 | def create
19 | create_object(Subscription, CreateSubscription, nil, :plan, @plan)
20 | end
21 |
22 | def destroy
23 | subscription = Subscription.find_by!(guid: params[:guid])
24 | Payola::CancelSubscription.call(subscription)
25 | redirect_to confirm_subscription_path(subscription)
26 | end
27 |
28 | def change_plan
29 | @subscription = Subscription.find_by!(guid: params[:guid])
30 | Payola::ChangeSubscriptionPlan.call(@subscription, @plan)
31 |
32 | confirm_with_message("Subscription plan updated")
33 | end
34 |
35 | def change_quantity
36 | find_quantity
37 | @subscription = Subscription.find_by!(guid: params[:guid])
38 | Payola::ChangeSubscriptionQuantity.call(@subscription, @quantity)
39 |
40 | confirm_with_message("Subscription quantity updated")
41 | end
42 |
43 | def update_card
44 | @subscription = Subscription.find_by!(guid: params[:guid])
45 | Payola::UpdateCard.call(@subscription, params[:stripeToken])
46 |
47 | confirm_with_message("Card updated")
48 | end
49 |
50 | private
51 |
52 | def find_plan_coupon_and_quantity
53 | find_plan
54 | find_coupon
55 | find_quantity
56 | end
57 |
58 | def find_plan
59 | @plan_class = Payola.subscribables[params[:plan_class]]
60 |
61 | raise ActionController::RoutingError.new('Not Found') unless @plan_class && @plan_class.subscribable?
62 |
63 | @plan = @plan_class.find_by!(id: params[:plan_id])
64 | end
65 |
66 | def find_coupon
67 | @coupon = cookies[:cc] || params[:cc] || params[:coupon_code] || params[:coupon]
68 | end
69 |
70 | def find_quantity
71 | @quantity = params[:quantity].blank? ? 1 : params[:quantity].to_i
72 | end
73 |
74 | def check_modify_permissions
75 | subscription = Subscription.find_by!(guid: params[:guid])
76 | if self.respond_to?(:payola_can_modify_subscription?)
77 | redirect_to(
78 | confirm_subscription_path(subscription),
79 | alert: "You cannot modify this subscription."
80 | ) and return unless self.payola_can_modify_subscription?(subscription)
81 | end
82 | end
83 |
84 | def confirm_with_message(message)
85 | if @subscription.valid?
86 | redirect_to confirm_subscription_path(@subscription), notice: message
87 | else
88 | redirect_to confirm_subscription_path(@subscription), alert: @subscription.errors.full_messages.to_sentence
89 | end
90 | end
91 |
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/spec/models/payola/sale_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe Sale do
5 |
6 | before do
7 | Payola.secret_key = 'sk_test_12345'
8 | end
9 |
10 | describe "validations" do
11 | it "should validate" do
12 | sale = build(:sale)
13 | expect(sale.valid?).to be true
14 | end
15 |
16 | it "should validate lack of email" do
17 | sale = build(:sale, email: nil)
18 | expect(sale.valid?).to be false
19 | end
20 | it "should validate product" do
21 | sale = build(:sale, product: nil)
22 | expect(sale.valid?).to be false
23 | end
24 | it "should validate stripe_token" do
25 | sale = build(:sale, stripe_token: nil)
26 | expect(sale.valid?).to be false
27 | end
28 | end
29 |
30 | describe "#guid" do
31 | it "should generate a unique guid" do
32 | sale = create(:sale)
33 | expect(sale.valid?).to be true
34 | expect(sale.guid).to_not be_nil
35 |
36 | sale2 = build(:sale, guid: sale.guid)
37 | expect(sale2.valid?).to be false
38 | end
39 | end
40 |
41 | describe "#process!" do
42 | it "should charge the card" do
43 | Payola::ChargeCard.should_receive(:call)
44 |
45 | sale = create(:sale)
46 | sale.process!
47 | end
48 | end
49 |
50 | describe "#finish" do
51 | it "should instrument finish" do
52 | sale = create(:sale, state: 'processing')
53 | Payola.should_receive(:instrument).with('payola.product.sale.finished', sale)
54 | Payola.should_receive(:instrument).with('payola.sale.finished', sale)
55 |
56 | sale.finish!
57 | end
58 | end
59 |
60 | describe "#fail" do
61 | it "should instrument fail" do
62 | sale = create(:sale, state: 'processing')
63 | Payola.should_receive(:instrument).with('payola.product.sale.failed', sale)
64 | Payola.should_receive(:instrument).with('payola.sale.failed', sale)
65 |
66 | sale.fail!
67 | end
68 | end
69 |
70 | describe "#refund" do
71 | it "should instrument refund" do
72 | sale = create(:sale, state: 'finished')
73 | Payola.should_receive(:instrument).with('payola.product.sale.refunded', sale)
74 | Payola.should_receive(:instrument).with('payola.sale.refunded', sale)
75 | sale.refund!
76 | end
77 | end
78 |
79 | describe "#verifier" do
80 | it "should store and recall verified custom fields" do
81 | sale = create(:sale)
82 | sale.signed_custom_fields = sale.verifier.generate({"field" => "value"})
83 | sale.save!
84 | sale.reload
85 | expect(sale.custom_fields["field"]).to eq "value"
86 | end
87 | end
88 |
89 | describe "#owner" do
90 | it "should store and recall owner" do
91 | sale = create(:sale)
92 | owner = Owner.create
93 |
94 | sale.owner = owner
95 | sale.save!
96 |
97 | expect(sale.owner_id).to eq owner.id
98 | expect(sale.owner_type).to eq 'Owner'
99 | end
100 | end
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/app/views/payola/transactions/_checkout.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | button_class = local_assigns.fetch :button_class, "stripe-button-el"
3 | button_inner_style = local_assigns.fetch :button_inner_style, 'min-height: 30px'
4 | button_text = local_assigns.fetch :button_text, "Pay Now"
5 | description = local_assigns.fetch :description, "#{sellable.name} (#{formatted_price(sellable.price)})"
6 | name = local_assigns.fetch :name, sellable.name
7 | product_image_path = local_assigns.fetch :product_image_path , ''
8 | panel_label = local_assigns.fetch :panel_label, ''
9 | allow_remember_me = local_assigns.fetch :allow_remember_me, true
10 | email = local_assigns.fetch :email, ''
11 | verify_zip_code = local_assigns.fetch :verify_zip_code, false
12 | custom_fields = local_assigns.fetch :custom_fields, nil
13 | price = local_assigns.fetch :price, sellable.price
14 | billing_address = local_assigns.fetch :billing_address, false
15 | shipping_address = local_assigns.fetch :shipping_address, false
16 |
17 | sale = Payola::Sale.new(product: sellable)
18 |
19 | button_id = "payola-button-#{sellable.product_class}-#{sellable.permalink}"
20 |
21 | form_id = "#{button_id}-form"
22 |
23 | currency = sellable.respond_to?(:currency) ? sellable.currency : Payola.default_currency
24 |
25 | error_div_id = local_assigns.fetch :error_div_id, ''
26 | if error_div_id.present?
27 | show_default_error_div = false
28 | else
29 | error_div_id = "#{button_id}-errors"
30 | show_default_error_div = true
31 | end
32 |
33 | raw_data = {
34 | base_path: main_app.payola_path,
35 | form_id: form_id,
36 | button_id: button_id,
37 | error_div_id: error_div_id,
38 | product_class: sellable.product_class,
39 | product_permalink: sellable.permalink,
40 | price: price,
41 | name: name,
42 | description: description,
43 | currency: currency,
44 | product_image_path: product_image_path,
45 | publishable_key: Payola.publishable_key_for_sale(sale),
46 | panel_label: panel_label,
47 | allow_remember_me: allow_remember_me,
48 | email: email,
49 | verify_zip_code: verify_zip_code,
50 | billing_address: billing_address,
51 | shipping_address: shipping_address
52 | }
53 |
54 | raw_data[:signed_custom_fields] = sale.verifier.generate(custom_fields) if custom_fields
55 |
56 | html_hash = {}
57 | raw_data.each do |k,v|
58 | html_hash["data-#{k}"] = v
59 | end
60 | html_hash["id"] = form_id
61 | %>
62 |
63 |
64 |
65 |
66 | <%= form_tag payola.buy_path(product_class: sellable.product_class, permalink: sellable.permalink), html_hash do %>
67 |
68 | <%= button_text %>
69 | Please wait...
70 |
71 | <% if show_default_error_div %>
72 |
73 | <% end %>
74 | <% end %>
75 |
--------------------------------------------------------------------------------
/app/models/payola/sale.rb:
--------------------------------------------------------------------------------
1 | require 'aasm'
2 |
3 | module Payola
4 | class Sale < ActiveRecord::Base
5 | include Payola::GuidBehavior
6 |
7 | has_paper_trail if respond_to? :has_paper_trail
8 |
9 | validates_presence_of :email
10 | validates_presence_of :product_id
11 | validates_presence_of :product_type
12 | validates_presence_of :stripe_token
13 | validates_presence_of :currency
14 |
15 | belongs_to :product, polymorphic: true
16 | belongs_to :owner, polymorphic: true
17 | belongs_to :coupon
18 | belongs_to :affiliate
19 |
20 | include AASM
21 |
22 | aasm column: 'state', skip_validation_on_save: true do
23 | state :pending, initial: true
24 | state :processing
25 | state :finished
26 | state :errored
27 | state :refunded
28 |
29 | event :process, after: :charge_card do
30 | transitions from: :pending, to: :processing
31 | end
32 |
33 | event :finish, after: :instrument_finish do
34 | transitions from: :processing, to: :finished
35 | end
36 |
37 | event :fail, after: :instrument_fail do
38 | transitions from: [:pending, :processing], to: :errored
39 | end
40 |
41 | event :refund, after: :instrument_refund do
42 | transitions from: :finished, to: :refunded
43 | end
44 | end
45 |
46 | def verifier
47 | @verifier ||= ActiveSupport::MessageVerifier.new(Payola.secret_key_for_sale(self), digest: 'SHA256')
48 | end
49 |
50 | def verify_charge
51 | begin
52 | self.verify_charge!
53 | rescue RuntimeError => e
54 | self.error = e.message
55 | self.fail!
56 | end
57 | end
58 |
59 | def verify_charge!
60 | if Payola.charge_verifier.arity > 1
61 | Payola.charge_verifier.call(self, custom_fields)
62 | else
63 | Payola.charge_verifier.call(self)
64 | end
65 | end
66 |
67 | def custom_fields
68 | if self.signed_custom_fields.present?
69 | verifier.verify(self.signed_custom_fields)
70 | else
71 | nil
72 | end
73 | end
74 |
75 | def redirector
76 | product
77 | end
78 |
79 | private
80 |
81 | def charge_card
82 | Payola::ChargeCard.call(self)
83 | end
84 |
85 | def instrument_finish
86 | Payola.instrument(instrument_key('finished'), self)
87 | Payola.instrument(instrument_key('finished', false), self)
88 | end
89 |
90 | def instrument_fail
91 | Payola.instrument(instrument_key('failed'), self)
92 | Payola.instrument(instrument_key('failed', false), self)
93 | end
94 |
95 | def instrument_refund
96 | Payola.instrument(instrument_key('refunded'), self)
97 | Payola.instrument(instrument_key('refunded', false), self)
98 | end
99 |
100 | def product_class
101 | product.product_class
102 | end
103 |
104 | def instrument_key(instrument_type, include_class=true)
105 | if include_class
106 | "payola.#{product_class}.sale.#{instrument_type}"
107 | else
108 | "payola.sale.#{instrument_type}"
109 | end
110 | end
111 |
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/app/services/payola/start_subscription.rb:
--------------------------------------------------------------------------------
1 | module Payola
2 | class StartSubscription
3 | attr_reader :subscription, :secret_key
4 |
5 | def self.call(subscription)
6 | subscription.save!
7 | secret_key = Payola.secret_key_for_sale(subscription)
8 |
9 | new(subscription, secret_key).run
10 | end
11 |
12 | def initialize(subscription, secret_key)
13 | @subscription = subscription
14 | @secret_key = secret_key
15 | end
16 |
17 | def run
18 | begin
19 | subscription.verify_charge!
20 |
21 | customer = find_or_create_customer
22 |
23 | create_params = {
24 | plan: subscription.plan.stripe_id,
25 | quantity: subscription.quantity
26 | }
27 | create_params[:coupon] = subscription.coupon if subscription.coupon.present?
28 | stripe_sub = customer.subscriptions.create(create_params)
29 |
30 | card = customer.cards.data.first
31 | subscription.update_attributes(
32 | stripe_id: stripe_sub.id,
33 | stripe_customer_id: customer.id,
34 | card_last4: card.last4,
35 | card_expiration: Date.new(card.exp_year, card.exp_month, 1),
36 | card_type: card.respond_to?(:brand) ? card.brand : card.type,
37 | current_period_start: Time.at(stripe_sub.current_period_start),
38 | current_period_end: Time.at(stripe_sub.current_period_end),
39 | ended_at: stripe_sub.ended_at ? Time.at(stripe_sub.ended_at) : nil,
40 | trial_start: stripe_sub.trial_start ? Time.at(stripe_sub.trial_start) : nil,
41 | trial_end: stripe_sub.trial_end ? Time.at(stripe_sub.trial_end) : nil,
42 | canceled_at: stripe_sub.canceled_at ? Time.at(stripe_sub.canceled_at) : nil,
43 | quantity: stripe_sub.quantity,
44 | stripe_status: stripe_sub.status,
45 | cancel_at_period_end: stripe_sub.cancel_at_period_end
46 | )
47 | subscription.activate!
48 | rescue Stripe::StripeError, RuntimeError => e
49 | subscription.update_attributes(error: e.message)
50 | subscription.fail!
51 | end
52 |
53 | subscription
54 | end
55 |
56 | def find_or_create_customer
57 | subs = Subscription.where(owner: subscription.owner) if subscription.owner
58 | if subs && subs.length > 1
59 | first_sub = subs.first
60 | customer_id = first_sub.stripe_customer_id
61 | return Stripe::Customer.retrieve(customer_id, secret_key)
62 | else
63 | customer_create_params = {
64 | card: subscription.stripe_token,
65 | email: subscription.email
66 | }
67 |
68 | customer = Stripe::Customer.create(customer_create_params, secret_key)
69 | end
70 |
71 | if subscription.setup_fee.present?
72 | plan = subscription.plan
73 | description = plan.try(:setup_fee_description, subscription) || 'Setup Fee'
74 | Stripe::InvoiceItem.create({
75 | customer: customer.id,
76 | amount: subscription.setup_fee,
77 | currency: subscription.currency,
78 | description: description
79 | }, secret_key)
80 | end
81 |
82 | customer
83 | end
84 | end
85 |
86 | end
87 |
--------------------------------------------------------------------------------
/spec/controllers/payola/transactions_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe TransactionsController do
5 | routes { Payola::Engine.routes }
6 |
7 | before do
8 | @product = create(:product)
9 | Payola.register_sellable(@product.class)
10 | end
11 |
12 | describe '#create' do
13 | it "should pass args to CreateSale and queue the job" do
14 | sale = double
15 | errors = double
16 | errors.should_receive(:full_messages).and_return([])
17 | sale.should_receive(:state).and_return('pending')
18 | sale.should_receive(:error).and_return(nil)
19 | sale.should_receive(:errors).and_return(errors)
20 | sale.should_receive(:save).and_return(true)
21 | sale.should_receive(:guid).at_least(1).times.and_return('blah')
22 |
23 | CreateSale.should_receive(:call).with(
24 | 'product_class' => 'product',
25 | 'permalink' => @product.permalink,
26 | 'controller' => 'payola/transactions',
27 | 'action' => 'create',
28 | 'product' => @product,
29 | 'coupon' => nil,
30 | 'affiliate' => nil
31 | ).and_return(sale)
32 |
33 | Payola.should_receive(:queue!)
34 | post :create, product_class: @product.product_class, permalink: @product.permalink
35 |
36 | expect(response.status).to eq 200
37 | parsed_body = JSON.load(response.body)
38 | expect(parsed_body['guid']).to eq 'blah'
39 | end
40 |
41 | describe "with an error" do
42 | it "should return an error in json" do
43 | sale = double
44 | sale.should_receive(:error).and_return(nil)
45 | sale.should_receive(:save).and_return(false)
46 | sale.should_receive(:state).and_return('failed')
47 | sale.should_receive(:guid).at_least(1).times.and_return('blah')
48 | error = double
49 | error.should_receive(:full_messages).and_return(['done did broke'])
50 | sale.should_receive(:errors).and_return(error)
51 |
52 | CreateSale.should_receive(:call).and_return(sale)
53 | Payola.should_not_receive(:queue!)
54 |
55 | post :create, product_class: @product.product_class, permalink: @product.permalink
56 |
57 | expect(response.status).to eq 400
58 | parsed_body = JSON.load(response.body)
59 | expect(parsed_body['error']).to eq 'done did broke'
60 | end
61 | end
62 | end
63 |
64 | describe '#status' do
65 | it "should return 404 if it can't find the sale" do
66 | get :status, guid: 'doesnotexist'
67 | expect(response.status).to eq 404
68 | end
69 | it "should return json with properties" do
70 | sale = create(:sale)
71 | get :status, guid: sale.guid
72 |
73 | expect(response.status).to eq 200
74 |
75 | parsed_body = JSON.load(response.body)
76 |
77 | expect(parsed_body['guid']).to eq sale.guid
78 | expect(parsed_body['status']).to eq sale.state
79 | expect(parsed_body['error']).to be_nil
80 | end
81 | end
82 |
83 | describe '#show' do
84 | it "should redirect to the product's redirect path" do
85 | sale = create(:sale)
86 | get :show, guid: sale.guid
87 |
88 | expect(response).to redirect_to '/'
89 | end
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/app/assets/javascripts/payola/form.js:
--------------------------------------------------------------------------------
1 | var PayolaPaymentForm = {
2 | initialize: function() {
3 | $(document).on('submit', '.payola-payment-form', function() {
4 | return PayolaPaymentForm.handleSubmit($(this));
5 | });
6 | },
7 |
8 | handleSubmit: function(form) {
9 | form.find(':submit').prop('disabled', true);
10 | $('.payola-spinner').show();
11 | Stripe.card.createToken(form, function(status, response) {
12 | PayolaPaymentForm.stripeResponseHandler(form, status, response);
13 | });
14 | return false;
15 | },
16 |
17 | stripeResponseHandler: function(form, status, response) {
18 | if (response.error) {
19 | PayolaPaymentForm.showError(form, response.error.message);
20 | } else {
21 | var email = form.find("[data-payola='email']").val();
22 |
23 | var base_path = form.data('payola-base-path');
24 | var product = form.data('payola-product');
25 | var permalink = form.data('payola-permalink');
26 |
27 | var data_form = $('');
28 | data_form.append($(' ').val(response.id));
29 | data_form.append($(' ').val(email));
30 | data_form.append(PayolaPaymentForm.authenticityTokenInput());
31 |
32 | $.ajax({
33 | type: "POST",
34 | url: base_path + "/buy/" + product + "/" + permalink,
35 | data: data_form.serialize(),
36 | success: function(data) { PayolaPaymentForm.poll(form, 60, data.guid, base_path); },
37 | error: function(data) { PayolaPaymentForm.showError(form, data.responseJSON.error); }
38 | });
39 | }
40 | },
41 |
42 | poll: function(form, num_retries_left, guid, base_path) {
43 | if (num_retries_left === 0) {
44 | PayolaPaymentForm.showError(form, "This seems to be taking too long. Please contact support and give them transaction ID: " + guid);
45 | }
46 | $.get(base_path + '/status/' + guid, function(data) {
47 | if (data.status === "finished") {
48 | form.append($(' ').val(guid));
49 | form.append(PayolaPaymentForm.authenticityTokenInput());
50 | form.get(0).submit();
51 | } else if (data.status === "errored") {
52 | PayolaPaymentForm.showError(form, data.error);
53 | } else {
54 | setTimeout(function() { PayolaPaymentForm.poll(form, num_retries_left - 1, guid, base_path); }, 500);
55 | }
56 | });
57 | },
58 |
59 | showError: function(form, message) {
60 | $('.payola-spinner').hide();
61 | form.find(':submit').prop('disabled', false);
62 | var error_selector = form.data('payola-error-selector');
63 | if (error_selector) {
64 | $(error_selector).text(message);
65 | } else {
66 | form.find('.payola-payment-error').text(message);
67 | }
68 | },
69 |
70 | authenticityTokenInput: function() {
71 | return $(' ').val($('meta[name="csrf-token"]').attr("content"))
72 | }
73 | };
74 |
75 | $(function() { PayolaPaymentForm.initialize(); } );
76 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 | require "codeclimate-test-reporter"
3 |
4 | if ENV['CIRCLE_ARTIFACTS']
5 | dir = File.join("..", "..", "..", ENV['CIRCLE_ARTIFACTS'], "coverage")
6 | SimpleCov.coverage_dir(dir)
7 | CodeClimate::TestReporter.start
8 | end
9 | SimpleCov.start 'rails' do
10 | add_filter 'app/secrets'
11 | end
12 |
13 |
14 | # This file is copied to spec/ when you run 'rails generate rspec:install'
15 | ENV["RAILS_ENV"] ||= 'test'
16 | require File.expand_path("../dummy/config/environment", __FILE__)
17 | require 'rspec/rails'
18 | require 'rspec/autorun'
19 | require 'factory_girl_rails'
20 | require 'stripe_mock'
21 |
22 | # Requires supporting ruby files with custom matchers and macros, etc, in
23 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
24 | # run as spec files by default. This means that files in spec/support that end
25 | # in _spec.rb will both be required and run as specs, causing the specs to be
26 | # run twice. It is recommended that you do not name files matching this glob to
27 | # end with _spec.rb. You can configure this pattern with with the --pattern
28 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
29 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
30 |
31 | # Checks for pending migrations before tests are run.
32 | # If you are not using ActiveRecord, you can remove this line.
33 | ActiveRecord::Migration.maintain_test_schema!
34 |
35 | RSpec.configure do |config|
36 | # ## Mock Framework
37 | #
38 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
39 | #
40 | # config.mock_with :mocha
41 | # config.mock_with :flexmock
42 | # config.mock_with :rr
43 |
44 | config.mock_with :rspec
45 |
46 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
47 | # examples within a transaction, remove the following line or assign false
48 | # instead of true.
49 | config.use_transactional_fixtures = true
50 |
51 | # If true, the base class of anonymous controllers will be inferred
52 | # automatically. This will be the default behavior in future versions of
53 | # rspec-rails.
54 | config.infer_base_class_for_anonymous_controllers = false
55 |
56 | # Run specs in random order to surface order dependencies. If you find an
57 | # order dependency and want to debug it, you can fix the order by providing
58 | # the seed, which is printed after each run.
59 | # --seed 1234
60 | config.order = "random"
61 |
62 | # RSpec Rails can automatically mix in different behaviours to your tests
63 | # based on their file location, for example enabling you to call `get` and
64 | # `post` in specs under `spec/controllers`.
65 | #
66 | # You can disable this behaviour by removing the line below, and instead
67 | # explictly tag your specs with their type, e.g.:
68 | #
69 | # describe UsersController, :type => :controller do
70 | # # ...
71 | # end
72 | #
73 | # The different available types are documented in the features, such as in
74 | # https://relishapp.com/rspec/rspec-rails/v/3-0/docs
75 | config.infer_spec_type_from_file_location!
76 |
77 | config.include FactoryGirl::Syntax::Methods
78 |
79 | config.before(:each) do
80 | StripeMock.start
81 | end
82 |
83 | config.after(:each) do
84 | StripeMock.stop
85 | end
86 | end
87 |
88 | module Payola
89 | class FakeWorker
90 | def self.can_run?
91 | false
92 | end
93 |
94 | def self.call(*args)
95 | end
96 | end
97 | end
98 |
--------------------------------------------------------------------------------
/spec/services/payola/start_subscription_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe StartSubscription do
5 | let(:stripe_helper) { StripeMock.create_test_helper }
6 | let(:token){ StripeMock.generate_card_token({}) }
7 | let(:user){ User.create }
8 |
9 | describe "#call" do
10 | it "should create a customer" do
11 | plan = create(:subscription_plan)
12 | subscription = create(:subscription, state: 'processing', plan: plan, stripe_token: token)
13 | StartSubscription.call(subscription)
14 | expect(subscription.reload.stripe_customer_id).to_not be_nil
15 | end
16 | it "should capture credit card info" do
17 | plan = create(:subscription_plan)
18 | subscription = create(:subscription, state: 'processing', plan: plan, stripe_token: token)
19 | StartSubscription.call(subscription)
20 | expect(subscription.reload.stripe_id).to_not be_nil
21 | expect(subscription.reload.card_last4).to_not be_nil
22 | expect(subscription.reload.card_expiration).to_not be_nil
23 | expect(subscription.reload.card_type).to_not be_nil
24 | end
25 | describe "on error" do
26 | it "should update the error attribute" do
27 | StripeMock.prepare_card_error(:card_declined, :new_customer)
28 | plan = create(:subscription_plan)
29 | subscription = create(:subscription, state: 'processing', plan: plan, stripe_token: token)
30 | StartSubscription.call(subscription)
31 | expect(subscription.reload.error).to_not be_nil
32 | expect(subscription.errored?).to be true
33 | end
34 | end
35 |
36 | it "should re-use an existing customer" do
37 | plan = create(:subscription_plan)
38 | subscription = create(:subscription, state: 'processing', plan: plan, stripe_token: token, owner: user)
39 | StartSubscription.call(subscription)
40 | CancelSubscription.call(subscription)
41 |
42 | subscription2 = create(:subscription, state: 'processing', plan: plan, owner: user)
43 | StartSubscription.call(subscription2)
44 | expect(subscription2.reload.stripe_customer_id).to_not be_nil
45 | expect(subscription2.reload.stripe_customer_id).to eq subscription.reload.stripe_customer_id
46 | end
47 |
48 | it "should create an invoice item with a setup fee" do
49 | plan = create(:subscription_plan)
50 | subscription = create(:subscription, state: 'processing', plan: plan, stripe_token: token, owner: user, setup_fee: 100)
51 | StartSubscription.call(subscription)
52 |
53 | ii = Stripe::InvoiceItem.all(customer: subscription.stripe_customer_id).first
54 | expect(ii).to_not be_nil
55 | expect(ii.amount).to eq 100
56 | expect(ii.description).to eq "Setup Fee"
57 | end
58 |
59 | it "should allow the plan to override the setup fee description" do
60 | plan = create(:subscription_plan)
61 | subscription = create(:subscription, state: 'processing', plan: plan, stripe_token: token, owner: user, setup_fee: 100)
62 |
63 | expect(plan).to receive(:setup_fee_description).with(subscription).and_return('Random Mystery Fee')
64 | StartSubscription.call(subscription)
65 |
66 | ii = Stripe::InvoiceItem.all(customer: subscription.stripe_customer_id).first
67 | expect(ii).to_not be_nil
68 | expect(ii.amount).to eq 100
69 | expect(ii.description).to eq 'Random Mystery Fee'
70 | end
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
20 | # config.action_dispatch.rack_cache = true
21 |
22 | # Disable Rails's static asset server (Apache or nginx will already do this).
23 | config.serve_static_assets = false
24 |
25 | # Compress JavaScripts and CSS.
26 | config.assets.js_compressor = :uglifier
27 | # config.assets.css_compressor = :sass
28 |
29 | # Do not fallback to assets pipeline if a precompiled asset is missed.
30 | config.assets.compile = false
31 |
32 | # Generate digests for assets URLs.
33 | config.assets.digest = true
34 |
35 | # `config.assets.precompile` has moved to config/initializers/assets.rb
36 |
37 | # Specifies the header that your server uses for sending files.
38 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
40 |
41 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
42 | # config.force_ssl = true
43 |
44 | # Set to :debug to see everything in the log.
45 | config.log_level = :info
46 |
47 | # Prepend all log lines with the following tags.
48 | # config.log_tags = [ :subdomain, :uuid ]
49 |
50 | # Use a different logger for distributed setups.
51 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
52 |
53 | # Use a different cache store in production.
54 | # config.cache_store = :mem_cache_store
55 |
56 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
57 | # config.action_controller.asset_host = "http://assets.example.com"
58 |
59 | # Precompile additional assets.
60 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
61 | # config.assets.precompile += %w( search.js )
62 |
63 | # Ignore bad email addresses and do not raise email delivery errors.
64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
65 | # config.action_mailer.raise_delivery_errors = false
66 |
67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
68 | # the I18n.default_locale when a translation cannot be found).
69 | config.i18n.fallbacks = true
70 |
71 | # Send deprecation notices to registered listeners.
72 | config.active_support.deprecation = :notify
73 |
74 | # Disable automatic flushing of the log to improve performance.
75 | # config.autoflush_log = false
76 |
77 | # Use default logging formatter so that PID and timestamp are not suppressed.
78 | config.log_formatter = ::Logger::Formatter.new
79 |
80 | # Do not dump schema after migrations.
81 | config.active_record.dump_schema_after_migration = false
82 | end
83 |
--------------------------------------------------------------------------------
/app/assets/javascripts/payola/checkout_button.js:
--------------------------------------------------------------------------------
1 | var PayolaCheckout = {
2 | initialize: function() {
3 | $(document).on('click', '.payola-checkout-button', function(e) {
4 | e.preventDefault();
5 | PayolaCheckout.handleCheckoutButtonClick($(this));
6 | });
7 | },
8 |
9 | handleCheckoutButtonClick: function(button) {
10 | var form = button.parent('form');
11 | var options = form.data();
12 |
13 | var handler = StripeCheckout.configure({
14 | key: options.publishable_key,
15 | image: options.product_image_path,
16 | token: function(token) { PayolaCheckout.tokenHandler(token, options); },
17 | name: options.name,
18 | description: options.description,
19 | amount: options.price,
20 | panelLabel: options.panel_label,
21 | allowRememberMe: options.allow_remember_me,
22 | zipCode: options.verify_zip_code,
23 | billingAddress: options.billing_address,
24 | shippingAddress: options.shipping_address,
25 | currency: options.currency,
26 | email: options.email || undefined
27 | });
28 |
29 | handler.open();
30 | },
31 |
32 | tokenHandler: function(token, options) {
33 | var form = $("#" + options.form_id);
34 | console.log(options.form_id);
35 | form.append($(' ').val(token.id));
36 | form.append($(' ').val(token.email));
37 | if (options.signed_custom_fields) {
38 | form.append($(' ').val(options.signed_custom_fields));
39 | }
40 |
41 | $(".payola-checkout-button").prop("disabled", true);
42 | $(".payola-checkout-button-text").hide();
43 | $(".payola-checkout-button-spinner").show();
44 | $.ajax({
45 | type: "POST",
46 | url: options.base_path + "/buy/" + options.product_class + "/" + options.product_permalink,
47 | data: form.serialize(),
48 | success: function(data) { PayolaCheckout.poll(data.guid, 60, options); },
49 | error: function(data) { PayolaCheckout.showError(data.responseJSON.error, options); }
50 | });
51 | },
52 |
53 | showError: function(error, options) {
54 | var error_div = $("#" + options.error_div_id);
55 | error_div.html(error);
56 | error_div.show();
57 | $(".payola-checkout-button").prop("disabled", false);
58 | $(".payola-checkout-button-spinner").hide();
59 | $(".payola-checkout-button-text").show();
60 | },
61 |
62 | poll: function(guid, num_retries_left, options) {
63 | if (num_retries_left === 0) {
64 | PayolaCheckout.showError("This seems to be taking too long. Please contact support and give them transaction ID: " + guid, options);
65 | return;
66 | }
67 |
68 | var handler = function(data) {
69 | if (data.status === "finished") {
70 | window.location = options.base_path + "/confirm/" + guid;
71 | } else if (data.status === "errored") {
72 | PayolaCheckout.showError(data.error, options);
73 | } else {
74 | setTimeout(function() { PayolaCheckout.poll(guid, num_retries_left - 1, options); }, 500);
75 | }
76 | };
77 |
78 | $.ajax({
79 | type: "GET",
80 | url: options.base_path + "/status/" + guid,
81 | success: handler,
82 | error: handler
83 | });
84 | }
85 | };
86 | $(function() { PayolaCheckout.initialize(); });
87 |
--------------------------------------------------------------------------------
/spec/worker_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 |
5 | class TestService
6 | def self.call(thing)
7 | thing.guid
8 | end
9 | end
10 |
11 | describe Worker do
12 | describe "#autofind" do
13 | it "should return ActiveJob if available" do
14 | expect(Payola::Worker::ActiveJob).to receive(:can_run?).and_return(true)
15 | expect(Payola::Worker.autofind).to eq Payola::Worker::ActiveJob
16 | end
17 | it "should return something else if available" do
18 | expect(Payola::Worker::ActiveJob).to receive(:can_run?).and_return(false)
19 | expect(Payola::Worker::Sidekiq).to receive(:can_run?).and_return(false)
20 | expect(Payola::Worker::SuckerPunch).to receive(:can_run?).and_return(true)
21 | expect(Payola::Worker.autofind).to eq Payola::Worker::SuckerPunch
22 | end
23 |
24 | it "should raise if nothing available" do
25 | expect(Payola::Worker::ActiveJob).to receive(:can_run?).and_return(false).at_least(1)
26 | expect(Payola::Worker::Sidekiq).to receive(:can_run?).and_return(false)
27 | expect(Payola::Worker::SuckerPunch).to receive(:can_run?).and_return(false)
28 |
29 | expect { Payola::Worker.autofind }.to raise_error("No eligible background worker systems found.")
30 | end
31 | end
32 | end
33 |
34 | describe Worker::Sidekiq do
35 | before do
36 | module ::Sidekiq
37 | module Worker
38 | def perform_async
39 | end
40 | end
41 | end
42 | end
43 |
44 | describe "#can_run?" do
45 | it "should return true if ::Sidekiq::Worker is defined" do
46 | expect(Payola::Worker::Sidekiq.can_run?).to be_truthy
47 | end
48 | end
49 |
50 | describe "#call" do
51 | it "should call perform_async" do
52 | Payola::Worker::Sidekiq.should_receive(:perform_async)
53 | Payola::Worker::Sidekiq.call(Payola::TestService, double)
54 | end
55 | end
56 | end
57 |
58 | describe Worker::SuckerPunch do
59 | describe "#can_run?" do
60 | it "should return true if SuckerPunch is defined" do
61 | expect(Payola::Worker::SuckerPunch.can_run?).to be_truthy
62 | end
63 | end
64 |
65 | describe "#call" do
66 | it "should call async" do
67 | worker = double
68 | expect(Payola::Worker::SuckerPunch).to receive(:new).and_return(worker)
69 | expect(worker).to receive(:async).and_return(worker)
70 | expect(worker).to receive(:perform)
71 | Payola::Worker::SuckerPunch.call(Payola::TestService, double)
72 | end
73 | end
74 | end
75 |
76 | describe Worker::ActiveJob do
77 | before do
78 | module ::ActiveJob
79 | module Core; end
80 | end
81 | end
82 |
83 | describe "#can_run?" do
84 | it "should return true if ::ActiveJob::Core is defined" do
85 | expect(Payola::Worker::ActiveJob.can_run?).to be_truthy
86 | end
87 | end
88 |
89 | describe "#call" do
90 | it "should call perform_later" do
91 | Payola::Worker::ActiveJob.should_receive(:perform_later)
92 | Payola::Worker::ActiveJob.call(Payola::TestService, double)
93 | end
94 | end
95 | end
96 |
97 | describe Worker::BaseWorker do
98 |
99 | class SomeTestService
100 | def self.call; end
101 | end
102 |
103 | describe "#perform" do
104 | it "should call the given service" do
105 | expect(SomeTestService).to receive(:call)
106 | Payola::Worker::BaseWorker.new.perform('Payola::SomeTestService')
107 | end
108 | end
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/app/assets/javascripts/payola/subscription_form_twostep.js:
--------------------------------------------------------------------------------
1 | var PayolaSubscriptionForm = {
2 | initialize: function() {
3 | $(document).on('submit', '.payola-subscription-form', function() {
4 | return PayolaSubscriptionForm.handleSubmit($(this));
5 | });
6 | },
7 |
8 | handleSubmit: function(form) {
9 | $(':submit').prop('disabled', true);
10 | $('.payola-spinner').show();
11 | Stripe.card.createToken(form, function(status, response) {
12 | PayolaSubscriptionForm.stripeResponseHandler(form, status, response);
13 | });
14 | return false;
15 | },
16 |
17 | stripeResponseHandler: function(form, status, response) {
18 | if (response.error) {
19 | PayolaSubscriptionForm.showError(form, response.error.message);
20 | } else {
21 | var email = form.find("[data-payola='email']").val();
22 | var coupon = form.find("[data-payola='coupon']").val();
23 | var quantity = form.find("[data-payola='quantity']").val();
24 |
25 | var base_path = form.data('payola-base-path');
26 | var plan_type = form.data('payola-plan-type');
27 | var plan_id = form.data('payola-plan-id');
28 |
29 | var data_form = $('');
30 | data_form.append($(' ').val(response.id));
31 | data_form.append($(' ').val(email));
32 | data_form.append($(' ').val(coupon));
33 | data_form.append($(' ').val(quantity));
34 | data_form.append(PayolaSubscriptionForm.authenticityTokenInput());
35 | $.ajax({
36 | type: "POST",
37 | url: base_path + "/subscribe/" + plan_type + "/" + plan_id,
38 | data: data_form.serialize(),
39 | success: function(data) { PayolaSubscriptionForm.poll(form, 60, data.guid, base_path); },
40 | error: function(data) { PayolaSubscriptionForm.showError(form, data.responseJSON.error); }
41 | });
42 | }
43 | },
44 |
45 | poll: function(form, num_retries_left, guid, base_path) {
46 | if (num_retries_left === 0) {
47 | PayolaSubscriptionForm.showError(form, "This seems to be taking too long. Please contact support and give them transaction ID: " + guid);
48 | }
49 | var handler = function(data) {
50 | if (data.status === "active") {
51 | form.append($(' ').val(guid));
52 | form.append(PayolaSubscriptionForm.authenticityTokenInput());
53 | form.get(0).submit();
54 | } else if (data.status === "errored") {
55 | PayolaSubscriptionForm.showError(form, data.error);
56 | } else {
57 | setTimeout(function() { PayolaSubscriptionForm.poll(form, num_retries_left - 1, guid, base_path); }, 500);
58 | }
59 | };
60 |
61 | $.ajax({
62 | type: 'GET',
63 | url: base_path + '/subscription_status/' + guid,
64 | success: handler,
65 | error: handler
66 | });
67 | },
68 |
69 | showError: function(form, message) {
70 | $('.payola-spinner').hide();
71 | $(':submit').prop('disabled', false);
72 | var error_selector = form.data('payola-error-selector');
73 | if (error_selector) {
74 | $(error_selector).text(message);
75 | $(error_selector).show();
76 | } else {
77 | form.find('.payola-payment-error').text(message);
78 | form.find('.payola-payment-error').show();
79 | }
80 | },
81 |
82 | authenticityTokenInput: function() {
83 | return $(' ').val($('meta[name="csrf-token"]').attr("content"));
84 | }
85 | };
86 |
87 | $(function() { PayolaSubscriptionForm.initialize() } );
88 |
--------------------------------------------------------------------------------
/app/assets/javascripts/payola/subscription_form_onestep.js:
--------------------------------------------------------------------------------
1 | var PayolaOnestepSubscriptionForm = {
2 | initialize: function() {
3 | $(document).on('submit', '.payola-onestep-subscription-form', function() {
4 | return PayolaOnestepSubscriptionForm.handleSubmit($(this));
5 | });
6 | },
7 |
8 | handleSubmit: function(form) {
9 | $(':submit').prop('disabled', true);
10 | $('.payola-spinner').show();
11 | Stripe.card.createToken(form, function(status, response) {
12 | PayolaOnestepSubscriptionForm.stripeResponseHandler(form, status, response);
13 | });
14 | return false;
15 | },
16 |
17 | stripeResponseHandler: function(form, status, response) {
18 | if (response.error) {
19 | PayolaOnestepSubscriptionForm.showError(form, response.error.message);
20 | } else {
21 | var email = form.find("[data-payola='email']").val();
22 | var coupon = form.find("[data-payola='coupon']").val();
23 | var quantity = form.find("[data-payola='quantity']").val();
24 |
25 | var base_path = form.data('payola-base-path');
26 | var plan_type = form.data('payola-plan-type');
27 | var plan_id = form.data('payola-plan-id');
28 |
29 | var action = $(form).attr('action');
30 |
31 | form.append($(' ').val(plan_type));
32 | form.append($(' ').val(plan_id));
33 | form.append($(' ').val(response.id));
34 | form.append($(' ').val(email));
35 | form.append($(' ').val(coupon));
36 | form.append($(' ').val(quantity));
37 | form.append(PayolaOnestepSubscriptionForm.authenticityTokenInput());
38 | $.ajax({
39 | type: "POST",
40 | url: action,
41 | data: form.serialize(),
42 | success: function(data) { PayolaOnestepSubscriptionForm.poll(form, 60, data.guid, base_path); },
43 | error: function(data) { PayolaOnestepSubscriptionForm.showError(form, data.responseJSON.error); }
44 | });
45 | }
46 | },
47 |
48 | poll: function(form, num_retries_left, guid, base_path) {
49 | if (num_retries_left === 0) {
50 | PayolaOnestepSubscriptionForm.showError(form, "This seems to be taking too long. Please contact support and give them transaction ID: " + guid);
51 | }
52 | var handler = function(data) {
53 | if (data.status === "active") {
54 | window.location = base_path + '/confirm_subscription/' + guid;
55 | } else if (data.status === "errored") {
56 | PayolaOnestepSubscriptionForm.showError(form, data.error);
57 | } else {
58 | setTimeout(function() { PayolaOnestepSubscriptionForm.poll(form, num_retries_left - 1, guid, base_path); }, 500);
59 | }
60 | };
61 |
62 | $.ajax({
63 | type: 'GET',
64 | url: base_path + '/subscription_status/' + guid,
65 | success: handler,
66 | error: handler
67 | });
68 | },
69 |
70 | showError: function(form, message) {
71 | $('.payola-spinner').hide();
72 | $(':submit').prop('disabled', false);
73 | var error_selector = form.data('payola-error-selector');
74 | if (error_selector) {
75 | $(error_selector).text(message);
76 | $(error_selector).show();
77 | } else {
78 | form.find('.payola-payment-error').text(message);
79 | form.find('.payola-payment-error').show();
80 | }
81 | },
82 |
83 | authenticityTokenInput: function() {
84 | return $(' ').val($('meta[name="csrf-token"]').attr("content"));
85 | }
86 | };
87 |
88 | $(function() { PayolaOnestepSubscriptionForm.initialize() } );
89 |
--------------------------------------------------------------------------------
/spec/payola_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe "#configure" do
5 | it "should pass the class back to the given block" do
6 | Payola.configure do |payola|
7 | expect(payola).to eq Payola
8 | end
9 | end
10 | end
11 |
12 | describe "keys" do
13 | it "should set publishable key from env" do
14 | ENV['STRIPE_PUBLISHABLE_KEY'] = 'some_key'
15 | Payola.reset!
16 | expect(Payola.publishable_key).to eq 'some_key'
17 | end
18 |
19 | it "should set secret key from env" do
20 | ENV['STRIPE_SECRET_KEY'] = 'some_secret'
21 | Payola.reset!
22 | expect(Payola.secret_key).to eq 'some_secret'
23 | end
24 | end
25 |
26 | describe "instrumentation" do
27 | it "should pass subscribe to StripeEvent" do
28 | StripeEvent.should_receive(:subscribe)
29 | Payola.subscribe('foo', 'blah')
30 | end
31 | it "should pass instrument to StripeEvent.backend" do
32 | ActiveSupport::Notifications.should_receive(:instrument)
33 | Payola.instrument('foo', 'blah')
34 | end
35 | it "should pass all to StripeEvent" do
36 | StripeEvent.should_receive(:all)
37 | Payola.all('blah')
38 | end
39 | end
40 |
41 | describe "#queue" do
42 | before do
43 | Payola.reset!
44 |
45 | Payola::Worker.registry ||= {}
46 | Payola::Worker.registry[:fake] = FakeWorker
47 | end
48 |
49 | describe "with symbol" do
50 | it "should find the correct background worker" do
51 | FakeWorker.should_receive(:call)
52 |
53 | Payola.background_worker = :fake
54 | Payola.queue!('blah')
55 | end
56 |
57 | it "should not find a background worker for an unknown symbol" do
58 | Payola.background_worker = :another_fake
59 | expect { Payola.queue!('blah') }.to raise_error(RuntimeError)
60 | end
61 | end
62 |
63 | describe "with callable" do
64 | it "should call the callable" do
65 | foo = nil
66 |
67 | Payola.background_worker = lambda do |sale|
68 | foo = sale
69 | end
70 |
71 | Payola.queue!('blah')
72 |
73 | expect(foo).to eq 'blah'
74 | end
75 | end
76 |
77 | describe "with nothing" do
78 | it "should call autofind" do
79 | FakeWorker.should_receive(:call).and_return(:true)
80 | Payola::Worker.should_receive(:autofind).and_return(FakeWorker)
81 | Payola.queue!('blah')
82 | end
83 | end
84 | end
85 |
86 | describe "#send_mail" do
87 | before do
88 | Payola.reset!
89 |
90 | Payola::Worker.registry ||= {}
91 | Payola::Worker.registry[:fake] = FakeWorker
92 | Payola.background_worker = :fake
93 | end
94 |
95 | it "should queue the SendMail service" do
96 | class FakeMailer < ActionMailer::Base
97 | def receipt(first, second)
98 | end
99 | end
100 |
101 | FakeWorker.should_receive(:call).with(Payola::SendMail, 'Payola::FakeMailer', 'receipt', 1, 2)
102 | Payola.send_mail(FakeMailer, :receipt, 1, 2)
103 | end
104 | end
105 |
106 | describe '#auto_emails' do
107 | before do
108 | Payola.reset!
109 | end
110 |
111 | it "should set up listeners for auto emails" do
112 | Payola.should_receive(:subscribe).with('payola.sale.finished').at_least(2)
113 | Payola.send_email_for :receipt, :admin_receipt
114 | end
115 | end
116 |
117 | describe "#secret_key_retriever" do
118 | it "should get called" do
119 | Payola.secret_key_retriever = lambda { |sale| 'foo' }
120 | expect(Payola.secret_key_for_sale('blah')).to eq 'foo'
121 | end
122 | end
123 |
124 | describe "#publishable_key_retriever" do
125 | it "should get called" do
126 | Payola.publishable_key_retriever = lambda { |sale| 'foo' }
127 | expect(Payola.publishable_key_for_sale('blah')).to eq 'foo'
128 | end
129 | end
130 |
131 | describe '#additional_charge_attributes' do
132 | it "should return a hash" do
133 | sale = double
134 | customer = double
135 | expect(Payola.additional_charge_attributes.call(sale, customer)).to eq({})
136 | end
137 | end
138 | end
139 |
--------------------------------------------------------------------------------
/lib/payola.rb:
--------------------------------------------------------------------------------
1 | require "payola/engine"
2 | require "payola/worker"
3 | require 'stripe_event'
4 | require 'jquery-rails'
5 |
6 | module Payola
7 |
8 | DEFAULT_EMAILS = {
9 | receipt: [ 'payola.sale.finished', 'Payola::ReceiptMailer', :receipt ],
10 | refund: [ 'charge.refunded', 'Payola::ReceiptMailer', :refund ],
11 | admin_receipt: [ 'payola.sale.finished', 'Payola::AdminMailer', :receipt ],
12 | admin_dispute: [ 'dispute.created', 'Payola::AdminMailer', :dispute ],
13 | admin_refund: [ 'payola.sale.refunded', 'Payola::AdminMailer', :refund ],
14 | admin_failure: [ 'payola.sale.failed', 'Payola::AdminMailer', :failure ],
15 | }
16 |
17 | class << self
18 | attr_accessor :publishable_key,
19 | :publishable_key_retriever,
20 | :secret_key,
21 | :secret_key_retriever,
22 | :background_worker,
23 | :event_filter,
24 | :support_email,
25 | :sellables,
26 | :subscribables,
27 | :charge_verifier,
28 | :default_currency,
29 | :additional_charge_attributes,
30 | :guid_generator,
31 | :pdf_receipt
32 |
33 | def configure(&block)
34 | raise ArgumentError, "must provide a block" unless block_given?
35 | block.arity.zero? ? instance_eval(&block) : yield(self)
36 | end
37 |
38 | def secret_key_for_sale(sale)
39 | return secret_key_retriever.call(sale).to_s
40 | end
41 |
42 | def publishable_key_for_sale(sale)
43 | return publishable_key_retriever.call(sale).to_s
44 | end
45 |
46 | def subscribe(name, callable = Proc.new)
47 | StripeEvent.subscribe(name, callable)
48 | end
49 |
50 | def instrument(name, object)
51 | StripeEvent.backend.instrument(StripeEvent.namespace.call(name), object)
52 | end
53 |
54 | def all(callable = Proc.new)
55 | StripeEvent.all(callable)
56 | end
57 |
58 | def queue!(klass, *args)
59 | if background_worker.is_a? Symbol
60 | Payola::Worker.find(background_worker).call(klass, *args)
61 | elsif background_worker.respond_to?(:call)
62 | background_worker.call(klass, *args)
63 | else
64 | Payola::Worker.autofind.call(klass, *args)
65 | end
66 | end
67 |
68 | def send_mail(mailer, method, *args)
69 | Payola.queue!(Payola::SendMail, mailer.to_s, method.to_s, *args)
70 | end
71 |
72 | def reset!
73 | StripeEvent.event_retriever = Retriever
74 |
75 | self.background_worker = nil
76 | self.event_filter = lambda { |event| event }
77 | self.charge_verifier = lambda { |event| true }
78 | self.publishable_key = EnvWrapper.new('STRIPE_PUBLISHABLE_KEY')
79 | self.secret_key = EnvWrapper.new('STRIPE_SECRET_KEY')
80 | self.secret_key_retriever = lambda { |sale| Payola.secret_key }
81 | self.publishable_key_retriever = lambda { |sale| Payola.publishable_key }
82 | self.support_email = 'sales@example.com'
83 | self.default_currency = 'usd'
84 | self.sellables = {}
85 | self.subscribables = {}
86 | self.additional_charge_attributes = lambda { |sale, customer| { } }
87 | self.pdf_receipt = false
88 | self.guid_generator = lambda { SecureRandom.random_number(1_000_000_000).to_s(32) }
89 | end
90 |
91 | def register_sellable(klass)
92 | sellables[klass.product_class] = klass
93 | end
94 |
95 | def register_subscribable(klass)
96 | subscribables[klass.plan_class] = klass
97 | end
98 |
99 | def send_email_for(*emails)
100 | emails.each do |email|
101 | spec = DEFAULT_EMAILS[email].dup
102 | if spec
103 | Payola.subscribe(spec.shift) do |sale|
104 | if sale.is_a?(Stripe::Event)
105 | sale = Payola::Sale.find_by!(stripe_id: sale.data.object.id)
106 | end
107 |
108 | Payola.send_mail(*(spec + [sale.guid]))
109 | end
110 | end
111 | end
112 | end
113 | end
114 |
115 | class Retriever
116 | def self.call(params)
117 | return nil if StripeWebhook.exists?(stripe_id: params[:id])
118 | StripeWebhook.create!(stripe_id: params[:id])
119 | event = Stripe::Event.retrieve(params[:id], Payola.secret_key)
120 | Payola.event_filter.call(event)
121 | end
122 | end
123 |
124 | class EnvWrapper
125 | def initialize(key)
126 | @key = key
127 | end
128 |
129 | def to_s
130 | ENV[@key]
131 | end
132 |
133 | def ==(other)
134 | to_s == other.to_s
135 | end
136 | end
137 |
138 | self.reset!
139 | end
140 |
--------------------------------------------------------------------------------
/app/models/payola/subscription.rb:
--------------------------------------------------------------------------------
1 | require 'aasm'
2 |
3 | module Payola
4 | class Subscription < ActiveRecord::Base
5 | include Payola::GuidBehavior
6 |
7 | has_paper_trail if respond_to? :has_paper_trail
8 |
9 | validates_presence_of :email
10 | validates_presence_of :plan_id
11 | validates_presence_of :plan_type
12 | validates_presence_of :stripe_token
13 | validates_presence_of :currency
14 |
15 | belongs_to :plan, polymorphic: true
16 | belongs_to :owner, polymorphic: true
17 | belongs_to :affiliate
18 |
19 | has_many :sales, class_name: 'Payola::Sale'
20 |
21 | include AASM
22 |
23 | attr_accessor :old_plan, :old_quantity
24 |
25 | aasm column: 'state', skip_validation_on_save: true do
26 | state :pending, initial: true
27 | state :processing
28 | state :active
29 | state :canceled
30 | state :errored
31 |
32 | event :process, after: :start_subscription do
33 | transitions from: :pending, to: :processing
34 | end
35 |
36 | event :activate, after: :instrument_activate do
37 | transitions from: :processing, to: :active
38 | end
39 |
40 | event :cancel, after: :instrument_canceled do
41 | transitions from: :active, to: :canceled
42 | end
43 |
44 | event :fail, after: :instrument_fail do
45 | transitions from: [:pending, :processing], to: :errored
46 | end
47 |
48 | event :refund, after: :instrument_refund do
49 | transitions from: :finished, to: :refunded
50 | end
51 | end
52 |
53 | def name
54 | self.plan.name
55 | end
56 |
57 | def price
58 | self.plan.amount
59 | end
60 |
61 | def redirect_path(sale)
62 | self.plan.redirect_path(self)
63 | end
64 |
65 | def verifier
66 | @verifier ||= ActiveSupport::MessageVerifier.new(Payola.secret_key_for_sale(self), digest: 'SHA256')
67 | end
68 |
69 | def verify_charge
70 | begin
71 | self.verify_charge!
72 | rescue RuntimeError => e
73 | self.error = e.message
74 | self.fail!
75 | end
76 | end
77 |
78 | def verify_charge!
79 | if Payola.charge_verifier.arity > 1
80 | Payola.charge_verifier.call(self, custom_fields)
81 | else
82 | Payola.charge_verifier.call(self)
83 | end
84 | end
85 |
86 | def custom_fields
87 | if self.signed_custom_fields.present?
88 | verifier.verify(self.signed_custom_fields)
89 | else
90 | nil
91 | end
92 | end
93 |
94 | def sync_with!(stripe_sub)
95 | self.current_period_start = Time.at(stripe_sub.current_period_start)
96 | self.current_period_end = Time.at(stripe_sub.current_period_end)
97 | self.ended_at = Time.at(stripe_sub.ended_at) if stripe_sub.ended_at
98 | self.trial_start = Time.at(stripe_sub.trial_start) if stripe_sub.trial_start
99 | self.trial_end = Time.at(stripe_sub.trial_end) if stripe_sub.trial_end
100 | self.canceled_at = Time.at(stripe_sub.canceled_at) if stripe_sub.canceled_at
101 | self.quantity = stripe_sub.quantity
102 | self.stripe_status = stripe_sub.status
103 | self.cancel_at_period_end = stripe_sub.cancel_at_period_end
104 |
105 | self.save!
106 | self
107 | end
108 |
109 | def to_param
110 | guid
111 | end
112 |
113 | def instrument_plan_changed(old_plan)
114 | self.old_plan = old_plan
115 | Payola.instrument(instrument_key('plan_changed'), self)
116 | Payola.instrument(instrument_key('plan_changed', false), self)
117 | end
118 |
119 | def instrument_quantity_changed(old_quantity)
120 | self.old_quantity = old_quantity
121 | Payola.instrument(instrument_key('quantity_changed'), self)
122 | Payola.instrument(instrument_key('quantity_changed', false), self)
123 | end
124 |
125 | def redirector
126 | plan
127 | end
128 |
129 | private
130 |
131 | def start_subscription
132 | Payola::StartSubscription.call(self)
133 | end
134 |
135 | def instrument_activate
136 | Payola.instrument(instrument_key('active'), self)
137 | Payola.instrument(instrument_key('active', false), self)
138 | end
139 |
140 | def instrument_canceled
141 | Payola.instrument(instrument_key('canceled'), self)
142 | Payola.instrument(instrument_key('canceled', false), self)
143 | end
144 |
145 | def instrument_fail
146 | Payola.instrument(instrument_key('failed'), self)
147 | Payola.instrument(instrument_key('failed', false), self)
148 | end
149 |
150 | def instrument_refund
151 | Payola.instrument(instrument_key('refunded'), self)
152 | Payola.instrument(instrument_key('refunded', false), self)
153 | end
154 |
155 | def instrument_key(instrument_type, include_class=true)
156 | if include_class
157 | "payola.#{plan_type}.subscription.#{instrument_type}"
158 | else
159 | "payola.subscription.#{instrument_type}"
160 | end
161 | end
162 |
163 | end
164 | end
165 |
--------------------------------------------------------------------------------
/spec/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 20141213205847) do
15 |
16 | create_table "owners", force: :cascade do |t|
17 | t.datetime "created_at"
18 | t.datetime "updated_at"
19 | end
20 |
21 | create_table "payola_affiliates", force: :cascade do |t|
22 | t.string "code"
23 | t.string "email"
24 | t.integer "percent"
25 | t.datetime "created_at"
26 | t.datetime "updated_at"
27 | end
28 |
29 | create_table "payola_coupons", force: :cascade do |t|
30 | t.string "code"
31 | t.integer "percent_off"
32 | t.datetime "created_at"
33 | t.datetime "updated_at"
34 | t.boolean "active", default: true
35 | end
36 |
37 | create_table "payola_sales", force: :cascade do |t|
38 | t.string "email"
39 | t.string "guid"
40 | t.integer "product_id"
41 | t.string "product_type"
42 | t.datetime "created_at"
43 | t.datetime "updated_at"
44 | t.string "state"
45 | t.string "stripe_id"
46 | t.string "stripe_token"
47 | t.string "card_last4"
48 | t.date "card_expiration"
49 | t.string "card_type"
50 | t.text "error"
51 | t.integer "amount"
52 | t.integer "fee_amount"
53 | t.integer "coupon_id"
54 | t.boolean "opt_in"
55 | t.integer "download_count"
56 | t.integer "affiliate_id"
57 | t.text "customer_address"
58 | t.text "business_address"
59 | t.string "stripe_customer_id"
60 | t.string "currency"
61 | t.text "signed_custom_fields"
62 | t.integer "owner_id"
63 | t.string "owner_type"
64 | end
65 |
66 | add_index "payola_sales", ["coupon_id"], name: "index_payola_sales_on_coupon_id"
67 | add_index "payola_sales", ["email"], name: "index_payola_sales_on_email"
68 | add_index "payola_sales", ["guid"], name: "index_payola_sales_on_guid"
69 | add_index "payola_sales", ["owner_id", "owner_type"], name: "index_payola_sales_on_owner_id_and_owner_type"
70 | add_index "payola_sales", ["product_id", "product_type"], name: "index_payola_sales_on_product"
71 | add_index "payola_sales", ["stripe_customer_id"], name: "index_payola_sales_on_stripe_customer_id"
72 |
73 | create_table "payola_stripe_webhooks", force: :cascade do |t|
74 | t.string "stripe_id"
75 | t.datetime "created_at"
76 | t.datetime "updated_at"
77 | end
78 |
79 | create_table "payola_subscriptions", force: :cascade do |t|
80 | t.string "plan_type"
81 | t.integer "plan_id"
82 | t.datetime "start"
83 | t.string "status"
84 | t.string "owner_type"
85 | t.integer "owner_id"
86 | t.string "stripe_customer_id"
87 | t.boolean "cancel_at_period_end"
88 | t.datetime "current_period_start"
89 | t.datetime "current_period_end"
90 | t.datetime "ended_at"
91 | t.datetime "trial_start"
92 | t.datetime "trial_end"
93 | t.datetime "canceled_at"
94 | t.integer "quantity"
95 | t.string "stripe_id"
96 | t.string "stripe_token"
97 | t.string "card_last4"
98 | t.date "card_expiration"
99 | t.string "card_type"
100 | t.text "error"
101 | t.string "state"
102 | t.string "email"
103 | t.datetime "created_at"
104 | t.datetime "updated_at"
105 | t.string "currency"
106 | t.integer "amount"
107 | t.string "guid"
108 | t.string "stripe_status"
109 | t.integer "affiliate_id"
110 | t.string "coupon"
111 | t.text "signed_custom_fields"
112 | t.text "customer_address"
113 | t.text "business_address"
114 | t.integer "setup_fee"
115 | end
116 |
117 | add_index "payola_subscriptions", ["guid"], name: "index_payola_subscriptions_on_guid"
118 |
119 | create_table "products", force: :cascade do |t|
120 | t.string "name"
121 | t.string "permalink"
122 | t.integer "price"
123 | t.datetime "created_at"
124 | t.datetime "updated_at"
125 | end
126 |
127 | create_table "subscription_plan_without_interval_counts", force: :cascade do |t|
128 | t.string "name"
129 | t.string "stripe_id"
130 | t.integer "amount"
131 | t.string "interval"
132 | t.datetime "created_at"
133 | t.datetime "updated_at"
134 | end
135 |
136 | create_table "subscription_plans", force: :cascade do |t|
137 | t.integer "amount"
138 | t.string "interval"
139 | t.integer "interval_count"
140 | t.string "name"
141 | t.string "stripe_id"
142 | t.integer "trial_period_days"
143 | t.datetime "created_at"
144 | t.datetime "updated_at"
145 | end
146 |
147 | create_table "users", force: :cascade do |t|
148 | t.datetime "created_at"
149 | t.datetime "updated_at"
150 | end
151 |
152 | end
153 |
--------------------------------------------------------------------------------
/spec/dummy/config/locales/de.yml:
--------------------------------------------------------------------------------
1 | # source: https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/de.yml
2 | de:
3 | date:
4 | abbr_day_names:
5 | - So
6 | - Mo
7 | - Di
8 | - Mi
9 | - Do
10 | - Fr
11 | - Sa
12 | abbr_month_names:
13 | -
14 | - Jan
15 | - Feb
16 | - Mär
17 | - Apr
18 | - Mai
19 | - Jun
20 | - Jul
21 | - Aug
22 | - Sep
23 | - Okt
24 | - Nov
25 | - Dez
26 | day_names:
27 | - Sonntag
28 | - Montag
29 | - Dienstag
30 | - Mittwoch
31 | - Donnerstag
32 | - Freitag
33 | - Samstag
34 | formats:
35 | default: ! '%d.%m.%Y'
36 | long: ! '%e. %B %Y'
37 | short: ! '%e. %b'
38 | month_names:
39 | -
40 | - Januar
41 | - Februar
42 | - März
43 | - April
44 | - Mai
45 | - Juni
46 | - Juli
47 | - August
48 | - September
49 | - Oktober
50 | - November
51 | - Dezember
52 | order:
53 | - :day
54 | - :month
55 | - :year
56 | datetime:
57 | distance_in_words:
58 | about_x_hours:
59 | one: etwa eine Stunde
60 | other: etwa %{count} Stunden
61 | about_x_months:
62 | one: etwa ein Monat
63 | other: etwa %{count} Monate
64 | about_x_years:
65 | one: etwa ein Jahr
66 | other: etwa %{count} Jahre
67 | almost_x_years:
68 | one: fast ein Jahr
69 | other: fast %{count} Jahre
70 | half_a_minute: eine halbe Minute
71 | less_than_x_minutes:
72 | one: weniger als eine Minute
73 | other: weniger als %{count} Minuten
74 | less_than_x_seconds:
75 | one: weniger als eine Sekunde
76 | other: weniger als %{count} Sekunden
77 | over_x_years:
78 | one: mehr als ein Jahr
79 | other: mehr als %{count} Jahre
80 | x_days:
81 | one: ein Tag
82 | other: ! '%{count} Tage'
83 | x_minutes:
84 | one: eine Minute
85 | other: ! '%{count} Minuten'
86 | x_months:
87 | one: ein Monat
88 | other: ! '%{count} Monate'
89 | x_seconds:
90 | one: eine Sekunde
91 | other: ! '%{count} Sekunden'
92 | prompts:
93 | day: Tag
94 | hour: Stunden
95 | minute: Minuten
96 | month: Monat
97 | second: Sekunden
98 | year: Jahr
99 | errors:
100 | format: ! '%{attribute} %{message}'
101 | messages:
102 | accepted: muss akzeptiert werden
103 | blank: muss ausgefüllt werden
104 | present: darf nicht ausgefüllt werden
105 | confirmation: stimmt nicht mit %{attribute} überein
106 | empty: muss ausgefüllt werden
107 | equal_to: muss genau %{count} sein
108 | even: muss gerade sein
109 | exclusion: ist nicht verfügbar
110 | greater_than: muss größer als %{count} sein
111 | greater_than_or_equal_to: muss größer oder gleich %{count} sein
112 | inclusion: ist kein gültiger Wert
113 | invalid: ist nicht gültig
114 | less_than: muss kleiner als %{count} sein
115 | less_than_or_equal_to: muss kleiner oder gleich %{count} sein
116 | not_a_number: ist keine Zahl
117 | not_an_integer: muss ganzzahlig sein
118 | odd: muss ungerade sein
119 | record_invalid: ! 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}'
120 | restrict_dependent_destroy:
121 | one: ! 'Datensatz kann nicht gelöscht werden, da ein abhängiger %{record}-Datensatz existiert.'
122 | many: ! 'Datensatz kann nicht gelöscht werden, da abhängige %{record} existieren.'
123 | taken: ist bereits vergeben
124 | too_long: ist zu lang (mehr als %{count} Zeichen)
125 | too_short: ist zu kurz (weniger als %{count} Zeichen)
126 | wrong_length: hat die falsche Länge (muss genau %{count} Zeichen haben)
127 | other_than: darf nicht gleich %{count} sein
128 | template:
129 | body: ! 'Bitte überprüfen Sie die folgenden Felder:'
130 | header:
131 | one: ! 'Konnte %{model} nicht speichern: ein Fehler.'
132 | other: ! 'Konnte %{model} nicht speichern: %{count} Fehler.'
133 | helpers:
134 | select:
135 | prompt: Bitte wählen
136 | submit:
137 | create: ! '%{model} erstellen'
138 | submit: ! '%{model} speichern'
139 | update: ! '%{model} aktualisieren'
140 | number:
141 | currency:
142 | format:
143 | delimiter: .
144 | format: ! '%n %u'
145 | precision: 2
146 | separator: ! ','
147 | significant: false
148 | strip_insignificant_zeros: false
149 | unit: €
150 | format:
151 | delimiter: .
152 | precision: 2
153 | separator: ! ','
154 | significant: false
155 | strip_insignificant_zeros: false
156 | human:
157 | decimal_units:
158 | format: ! '%n %u'
159 | units:
160 | billion:
161 | one: Milliarde
162 | other: Milliarden
163 | million: Millionen
164 | quadrillion:
165 | one: Billiarde
166 | other: Billiarden
167 | thousand: Tausend
168 | trillion: Billionen
169 | unit: ''
170 | format:
171 | delimiter: ''
172 | precision: 1
173 | significant: true
174 | strip_insignificant_zeros: true
175 | storage_units:
176 | format: ! '%n %u'
177 | units:
178 | byte:
179 | one: Byte
180 | other: Bytes
181 | gb: GB
182 | kb: KB
183 | mb: MB
184 | tb: TB
185 | percentage:
186 | format:
187 | delimiter: ''
188 | format: "%n%"
189 | precision:
190 | format:
191 | delimiter: ''
192 | support:
193 | array:
194 | last_word_connector: ! ' und '
195 | two_words_connector: ! ' und '
196 | words_connector: ! ', '
197 | time:
198 | am: vormittags
199 | formats:
200 | default: ! '%A, %d. %B %Y, %H:%M Uhr'
201 | long: ! '%A, %d. %B %Y, %H:%M Uhr'
202 | short: ! '%d. %B, %H:%M Uhr'
203 | pm: nachmittags
204 |
--------------------------------------------------------------------------------
/app/views/payola/transactions/_form.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= form_tag payola.buy_path(product_class: sale.product.product_class, permalink: permalink), :class => '', :id => 'payment-form' do %>
3 | <% if coupon %>
4 |
5 | <% end %>
6 |
7 |
Email Address Secure
8 |
9 |
10 |
11 |
Card Number
12 |
13 |
14 |
27 |
28 |
<%= price %>
29 |
33 |
34 | <% end %>
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | <% sale.errors.full_messages.each do |msg| %>
43 | <%= msg %>
44 | <% end %>
45 |
46 |
47 |
48 |
49 |
142 |
--------------------------------------------------------------------------------
/spec/controllers/payola/subscriptions_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Payola
4 | describe SubscriptionsController do
5 | routes { Payola::Engine.routes }
6 |
7 | before do
8 | @plan = create(:subscription_plan)
9 | Payola.register_subscribable(@plan.class)
10 | end
11 |
12 | describe '#create' do
13 | it "should pass args to CreateSubscription" do
14 | subscription = double
15 | subscription.should_receive(:save).and_return(true)
16 | subscription.should_receive(:guid).at_least(1).times.and_return(1)
17 | subscription.should_receive(:error).and_return(nil)
18 | errors = double
19 | errors.should_receive(:full_messages).and_return([])
20 | subscription.should_receive(:errors).and_return(errors)
21 | subscription.should_receive(:state).and_return('pending')
22 |
23 | CreateSubscription.should_receive(:call).with(
24 | 'plan_class' => 'subscription_plan',
25 | 'plan_id' => @plan.id.to_s,
26 | 'controller' => 'payola/subscriptions',
27 | 'action' => 'create',
28 | 'plan' => @plan,
29 | 'coupon' => nil,
30 | 'quantity' => 1,
31 | 'affiliate' => nil
32 | ).and_return(subscription)
33 |
34 | post :create, plan_class: @plan.plan_class, plan_id: @plan.id
35 |
36 | expect(response.status).to eq 200
37 | parsed_body = JSON.load(response.body)
38 | expect(parsed_body['guid']).to eq 1
39 | end
40 |
41 | describe "with an error" do
42 | it "should return an error in json" do
43 | subscription = double
44 | subscription.should_receive(:save).and_return(false)
45 | error = double
46 | error.should_receive(:full_messages).and_return(['done did broke'])
47 | subscription.should_receive(:errors).and_return(error)
48 | subscription.should_receive(:state).and_return('errored')
49 | subscription.should_receive(:error).and_return('')
50 | subscription.should_receive(:guid).and_return('blah')
51 |
52 |
53 | CreateSubscription.should_receive(:call).and_return(subscription)
54 | Payola.should_not_receive(:queue!)
55 |
56 | post :create, plan_class: @plan.plan_class, plan_id: @plan.id
57 |
58 | expect(response.status).to eq 400
59 | parsed_body = JSON.load(response.body)
60 | expect(parsed_body['error']).to eq 'done did broke'
61 | end
62 | end
63 | end
64 |
65 | describe '#status' do
66 | it "should return 404 if it can't find the subscription" do
67 | get :status, guid: 'doesnotexist'
68 | expect(response.status).to eq 404
69 | end
70 | it "should return json with properties" do
71 | subscription = create(:subscription)
72 | get :status, guid: subscription.guid
73 |
74 | expect(response.status).to eq 200
75 |
76 | parsed_body = JSON.load(response.body)
77 |
78 | expect(parsed_body['guid']).to eq subscription.guid
79 | expect(parsed_body['status']).to eq subscription.state
80 | expect(parsed_body['error']).to be_nil
81 | end
82 | end
83 |
84 | describe '#show' do
85 | it "should redirect to the product's redirect path" do
86 | plan = create(:subscription_plan)
87 | subscription = create(:subscription, :plan => plan)
88 | get :show, guid: subscription.guid
89 |
90 | expect(response).to redirect_to '/'
91 | end
92 | end
93 |
94 | describe '#destroy' do
95 | before :each do
96 | @subscription = create(:subscription, :state => :active)
97 | end
98 | it "call Payola::CancelSubscription and redirect" do
99 | Payola::CancelSubscription.should_receive(:call)
100 | delete :destroy, guid: @subscription.guid
101 | # TODO : Figure out why this needs to be a hardcoded path.
102 | # Why doesn't subscription_path(@subscription) work?
103 | expect(response).to redirect_to "/subdir/payola/confirm_subscription/#{@subscription.guid}"
104 | end
105 |
106 | it "should redirect with an error if it can't cancel the subscription" do
107 | expect(Payola::CancelSubscription).to_not receive(:call)
108 | expect_any_instance_of(::ApplicationController).to receive(:payola_can_modify_subscription?).and_return(false)
109 |
110 | delete :destroy, guid: @subscription.guid
111 | expect(response).to redirect_to "/subdir/payola/confirm_subscription/#{@subscription.guid}"
112 | expect(request.flash[:alert]).to eq 'You cannot modify this subscription.'
113 | end
114 | end
115 |
116 | describe '#change_plan' do
117 | before :each do
118 | @subscription = create(:subscription, state: :active)
119 | @plan = create(:subscription_plan)
120 | end
121 |
122 | it "should call Payola::ChangeSubscriptionPlan and redirect" do
123 | expect(Payola::ChangeSubscriptionPlan).to receive(:call).with(@subscription, @plan)
124 |
125 | post :change_plan, guid: @subscription.guid, plan_class: @plan.plan_class, plan_id: @plan.id
126 |
127 | expect(response).to redirect_to "/subdir/payola/confirm_subscription/#{@subscription.guid}"
128 | expect(request.flash[:notice]).to eq 'Subscription plan updated'
129 | end
130 |
131 | it "should redirect with an error if it can't update the subscription" do
132 | expect(Payola::ChangeSubscriptionPlan).to_not receive(:call)
133 | expect_any_instance_of(::ApplicationController).to receive(:payola_can_modify_subscription?).and_return(false)
134 |
135 | post :change_plan, guid: @subscription.guid, plan_class: @plan.plan_class, plan_id: @plan.id
136 | expect(response).to redirect_to "/subdir/payola/confirm_subscription/#{@subscription.guid}"
137 | expect(request.flash[:alert]).to eq 'You cannot modify this subscription.'
138 | end
139 | end
140 |
141 | describe '#change_quantity' do
142 | before :each do
143 | @subscription = create(:subscription, state: :active)
144 | @plan = create(:subscription_plan)
145 | end
146 |
147 | it "should call Payola::ChangeSubscriptionQuantity and redirect" do
148 | expect(Payola::ChangeSubscriptionQuantity).to receive(:call).with(@subscription, 5)
149 |
150 | post :change_quantity, guid: @subscription.guid, quantity: 5
151 |
152 | expect(response).to redirect_to "/subdir/payola/confirm_subscription/#{@subscription.guid}"
153 | expect(request.flash[:notice]).to eq 'Subscription quantity updated'
154 | end
155 |
156 | it "should redirect with an error if it can't update the subscription" do
157 | expect(Payola::ChangeSubscriptionQuantity).to_not receive(:call)
158 | expect_any_instance_of(::ApplicationController).to receive(:payola_can_modify_subscription?).and_return(false)
159 |
160 | post :change_quantity, guid: @subscription.guid, quantity: 5
161 | expect(response).to redirect_to "/subdir/payola/confirm_subscription/#{@subscription.guid}"
162 | expect(request.flash[:alert]).to eq 'You cannot modify this subscription.'
163 | end
164 | end
165 |
166 | describe "#update_card" do
167 | before :each do
168 | @subscription = create(:subscription, state: :active)
169 | @plan = create(:subscription_plan)
170 | end
171 |
172 | it "should call UpdateCard and redirect" do
173 | expect(Payola::UpdateCard).to receive(:call).with(@subscription, 'tok_1234')
174 |
175 | post :update_card, guid: @subscription.guid, stripeToken: 'tok_1234'
176 |
177 | expect(response).to redirect_to "/subdir/payola/confirm_subscription/#{@subscription.guid}"
178 | expect(request.flash[:notice]).to eq 'Card updated'
179 | end
180 |
181 | it "should redirect with an error" do
182 | expect(Payola::UpdateCard).to receive(:call).never
183 | expect_any_instance_of(::ApplicationController).to receive(:payola_can_modify_subscription?).and_return(false)
184 |
185 | post :update_card, guid: @subscription.guid, stripeToken: 'tok_1234'
186 |
187 | expect(response).to redirect_to "/subdir/payola/confirm_subscription/#{@subscription.guid}"
188 | expect(request.flash[:alert]).to eq 'You cannot modify this subscription.'
189 | end
190 | end
191 |
192 | end
193 | end
194 |
--------------------------------------------------------------------------------