payola-payments 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/Rakefile +34 -0
- data/app/assets/javascripts/payola/application.js +13 -0
- data/app/assets/stylesheets/payola/application.css +15 -0
- data/app/controllers/payola/application_controller.rb +4 -0
- data/app/controllers/payola/transactions_controller.rb +75 -0
- data/app/helpers/payola/application_helper.rb +4 -0
- data/app/models/payola/sale.rb +71 -0
- data/app/services/payola/charge_card.rb +44 -0
- data/app/services/payola/create_sale.rb +19 -0
- data/app/views/layouts/payola/application.html.erb +14 -0
- data/config/routes.rb +10 -0
- data/db/migrate/20141001170138_create_payola_sales.rb +33 -0
- data/lib/payola.rb +43 -0
- data/lib/payola/engine.rb +5 -0
- data/lib/payola/sellable.rb +19 -0
- data/lib/payola/version.rb +3 -0
- data/lib/payola/worker.rb +40 -0
- data/lib/tasks/payola_tasks.rake +4 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +82 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/initializers/assets.rb +8 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/fixtures/payola/sales.yml +11 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/models/payola/sale_test.rb +9 -0
- data/test/payola_test.rb +7 -0
- data/test/test_helper.rb +15 -0
- metadata +194 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YmZmZmQzZDhkODliYWJmODllZWUyNzdhNzVlZGU5NWM3NWQ4MTUzMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ODFjNDZlM2FlMWYzYjZmMjM5ZDRkN2Y0MTI1MDViYTVjYTczYzYyNA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MzlkOTFlN2E3Mzc2ZjNhYmZmZDI1ZjJmZWEyNTdmMmI4MWM2YWJjNjFkMmQx
|
10
|
+
YTJhMjBhYzgyNThiNzE1NDdmNWJhNDJkZGMxYzZlMTU4ZTRjOWUwOWU5ZGI3
|
11
|
+
Mjg3ODliYTU0Y2M0YzM4NjM5NGE5ZDYwMTVjNjkzZjU3M2UyMDQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZWRiOGMzMzNkOWE3MWEzZmY4NjBhZjRkZWZiMjEzODhhNjViNDA2NWZlMTk2
|
14
|
+
NTAyMzkxMGM0MWIxMGM0ZjNjM2U2ODQ4MjBmMmJiN2UzODRmMDU0ZDFlYmY2
|
15
|
+
Mzc1ZjBiZmQ2M2MwMTFkMDJhMzVlYTVkNzk1NzEzZTVmZDYzYWU=
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
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("../test/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
Bundler::GemHelper.install_tasks
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'lib'
|
28
|
+
t.libs << 'test'
|
29
|
+
t.pattern = 'test/**/*_test.rb'
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
task default: :test
|
@@ -0,0 +1,13 @@
|
|
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 .
|
@@ -0,0 +1,15 @@
|
|
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
|
+
*/
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class TransactionsController < ApplicationController
|
2
|
+
before_filter :strip_iframe_protection
|
3
|
+
|
4
|
+
before_filter :find_product_and_coupon_and_affiliate, only: [:iframe, :new, :create]
|
5
|
+
|
6
|
+
def new
|
7
|
+
@sale = Sale.new(product: @product)
|
8
|
+
set_page_title "Buy #{@product.name}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def iframe
|
12
|
+
@sale = Sale.new(product: @product)
|
13
|
+
end
|
14
|
+
|
15
|
+
def show
|
16
|
+
@sale = Sale.find_by!(guid: params[:guid])
|
17
|
+
@product = @sale.product
|
18
|
+
end
|
19
|
+
|
20
|
+
def status
|
21
|
+
@sale = Sale.where(guid: params[:guid]).first
|
22
|
+
render nothing: true, status: 404 and return unless @sale
|
23
|
+
render json: {guid: @sale.guid, status: @sale.state, error: @sale.error}
|
24
|
+
end
|
25
|
+
|
26
|
+
def create
|
27
|
+
@sale = CreateSale.call(sale_params, @product, @coupon, @affiliate)
|
28
|
+
|
29
|
+
if @sale.save
|
30
|
+
Payola.queue(@sale)
|
31
|
+
render json: { guid: @sale.guid }
|
32
|
+
else
|
33
|
+
render json: { error: @sale.errors.full_messages.join(". ") }, status: 400
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def pickup
|
38
|
+
@sale = Sale.find_by!(guid: params[:guid])
|
39
|
+
@product = @sale.product
|
40
|
+
end
|
41
|
+
|
42
|
+
def index
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def strip_iframe_protection
|
48
|
+
response.headers.delete('X-Frame-Options')
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_product_and_coupon_and_affiliate
|
52
|
+
@product_class = params[:product_class].camelize.constantize
|
53
|
+
|
54
|
+
raise ActionController::RoutingError.new('Not Found') unless @product_class.payola_sellable?
|
55
|
+
|
56
|
+
@product = @product_class.find_by!(permalink: params[:permalink])
|
57
|
+
coupon_code = cookies[:cc] || params[:cc] || params[:coupon_code]
|
58
|
+
|
59
|
+
@coupon = Coupon.where('lower(code) = lower(?)', coupon_code).first
|
60
|
+
if @coupon
|
61
|
+
cookies[:cc] = coupon_code
|
62
|
+
@price = @product.price * (1 - @coupon.percent_off / 100.0)
|
63
|
+
else
|
64
|
+
@price = @product.price
|
65
|
+
end
|
66
|
+
|
67
|
+
affiliate_code = cookies[:aff] || params[:aff]
|
68
|
+
@affiliate = Affiliate.where('lower(code) = lower(?)', affiliate_code).first
|
69
|
+
if @affiliate
|
70
|
+
cookies[:aff] = affiliate_code
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Payola
|
2
|
+
class Sale < ActiveRecord::Base
|
3
|
+
has_paper_trail if respond_to? :has_paper_trail
|
4
|
+
|
5
|
+
validates_presence_of :email
|
6
|
+
validates_presence_of :product_id
|
7
|
+
validates_presence_of :stripe_token
|
8
|
+
|
9
|
+
validates_uniqueness_of :guid
|
10
|
+
|
11
|
+
before_save :populate_guid
|
12
|
+
|
13
|
+
belongs_to :product
|
14
|
+
belongs_to :coupon
|
15
|
+
belongs_to :affiliate
|
16
|
+
|
17
|
+
include AASM
|
18
|
+
|
19
|
+
aasm column: 'state', skip_validation_on_save: true do
|
20
|
+
state :pending, initial: true
|
21
|
+
state :processing
|
22
|
+
state :finished
|
23
|
+
state :errored
|
24
|
+
state :refunded
|
25
|
+
|
26
|
+
event :process, after: :charge_card do
|
27
|
+
transitions from: :pending, to: :processing
|
28
|
+
end
|
29
|
+
|
30
|
+
event :finish, after: :instrument_finish do
|
31
|
+
transitions from: :processing, to: :finished
|
32
|
+
end
|
33
|
+
|
34
|
+
event :fail, after: :instrument_fail do
|
35
|
+
transitions from: :processing, to: :errored
|
36
|
+
end
|
37
|
+
|
38
|
+
event :refund, after: :instrument_refund do
|
39
|
+
transitions from: :finished, to: :refunded
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def charge_card
|
47
|
+
Payola::ChargeCard.call(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
def instrument_finish
|
51
|
+
Payola.instrument('payola.sale.finished', self)
|
52
|
+
end
|
53
|
+
|
54
|
+
def instrument_fail
|
55
|
+
Payola.instrument('payola.sale.failed', self)
|
56
|
+
end
|
57
|
+
|
58
|
+
def instrument_refund
|
59
|
+
Payola.instrument('payola.sale.refunded', self)
|
60
|
+
end
|
61
|
+
|
62
|
+
def populate_guid
|
63
|
+
if new_record?
|
64
|
+
while !valid? || self.guid.nil?
|
65
|
+
self.guid = SecureRandom.random_number(1_000_000_000).to_s(32)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,44 @@
|
|
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
|
+
customer = Stripe::Customer.create({
|
9
|
+
card: sale.stripe_token,
|
10
|
+
email: sale.email
|
11
|
+
}, secret_key)
|
12
|
+
|
13
|
+
charge = Stripe::Charge.create({
|
14
|
+
amount: sale.amount,
|
15
|
+
currency: "usd",
|
16
|
+
customer: customer.id,
|
17
|
+
description: sale.guid,
|
18
|
+
}, secret_key)
|
19
|
+
|
20
|
+
if charge.respond_to?(:fee)
|
21
|
+
fee = charge.fee
|
22
|
+
else
|
23
|
+
balance = Stripe::BalanceTransaction.retrieve(charge.balance_transaction, secret_key)
|
24
|
+
fee = balance.fee
|
25
|
+
end
|
26
|
+
|
27
|
+
sale.update_attributes(
|
28
|
+
stripe_id: charge.id,
|
29
|
+
card_last4: charge.card.last4,
|
30
|
+
card_expiration: Date.new(charge.card.exp_year, charge.card.exp_month, 1),
|
31
|
+
card_type: charge.card.respond_to?(:brand) ? charge.card.brand : charge.card.type,
|
32
|
+
fee_amount: fee
|
33
|
+
)
|
34
|
+
sale.finish!
|
35
|
+
rescue Stripe::StripeError => e
|
36
|
+
sale.update_attributes(error: e.message)
|
37
|
+
sale.fail!
|
38
|
+
end
|
39
|
+
|
40
|
+
sale
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Payola
|
2
|
+
class CreateSale
|
3
|
+
def self.call(params, product, coupon, affiliate)
|
4
|
+
Payola::Sale.new do |s|
|
5
|
+
s.product = product
|
6
|
+
s.email = params[:email]
|
7
|
+
s.stripe_token = params[:stripe_token]
|
8
|
+
s.affiliate_id = affiliate.try(:id)
|
9
|
+
|
10
|
+
if coupon
|
11
|
+
s.coupon = coupon
|
12
|
+
s.amount = options[:product].price * (1 - s.coupon.percent_off / 100.0)
|
13
|
+
else
|
14
|
+
s.amount = options[:product].price
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Payola</title>
|
5
|
+
<%= stylesheet_link_tag "payola/application", media: "all" %>
|
6
|
+
<%= javascript_include_tag "payola/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
Payola::Engine.routes.draw do
|
2
|
+
match '/buy/:product_class/:permalink' => 'transactions#new', via: :get, as: :show_buy
|
3
|
+
match '/buy/:product_class/:permalink' => 'transactions#create', via: :post, as: :buy
|
4
|
+
match '/confirm/:guid' => 'transactions#show', via: :get, as: :confirm
|
5
|
+
match '/pickup/:guid' => 'transactions#pickup', via: :get, as: :pickup
|
6
|
+
match '/iframe/:username/:permalink' => 'transactions#iframe', via: :get, as: :buy_iframe
|
7
|
+
match '/status/:guid' => 'transactions#status', via: :get, as: :status
|
8
|
+
|
9
|
+
mount StripeEvent::Engine => '/events'
|
10
|
+
end
|
@@ -0,0 +1,33 @@
|
|
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 "sales", ["coupon_id"], name: "index_sales_on_coupon_id", using: :btree
|
29
|
+
add_index "sales", ["product_id", "product_type"], name: "index_sales_on_product", using: :btree
|
30
|
+
add_index "sales", ["email"], name: "index_sales_on_email", using: :btree
|
31
|
+
add_index "sales", ["guid"], name: "index_sales_on_guid", using: :btree
|
32
|
+
end
|
33
|
+
end
|
data/lib/payola.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require "payola/engine"
|
2
|
+
require "payola/worker"
|
3
|
+
|
4
|
+
module Payola
|
5
|
+
class << self
|
6
|
+
attr_accessor :publishable_key, :secret_key, :secret_key_retriever, :background_worker
|
7
|
+
|
8
|
+
def configure(&block)
|
9
|
+
raise ArgumentError, "must provide a block" unless block_given?
|
10
|
+
block.arity.zero? ? instance_eval(&block) : yield(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def secret_key_for_sale(sale)
|
14
|
+
return secret_key_retriever.call(sale)
|
15
|
+
end
|
16
|
+
|
17
|
+
def subscribe(name, callable = Proc.new)
|
18
|
+
StripeEvent.subscribe(name, callable)
|
19
|
+
end
|
20
|
+
|
21
|
+
def instrument(name, object)
|
22
|
+
StripeEvent.backend.instrument(StripeEvent.namespace.call(name), object)
|
23
|
+
end
|
24
|
+
|
25
|
+
def all(callable = Proc.new)
|
26
|
+
StripeEvent.all(callable)
|
27
|
+
end
|
28
|
+
|
29
|
+
def queue!(sale)
|
30
|
+
if background_worker.is_a? Symbol
|
31
|
+
Payola::Worker.find(:symbol).call(sale)
|
32
|
+
elsif background_worker.respond_to?(:call)
|
33
|
+
background_worker.call(sale)
|
34
|
+
else
|
35
|
+
Payola::Worker.autofind.call(sale)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
self.publishable_key = ENV['STIRPE_PUBLISHABLE_KEY']
|
41
|
+
self.secret_key = ENV['STRIPE_SECRET_KEY']
|
42
|
+
self.secret_key_retriever = lambda { |sale| Payola.secret_key }
|
43
|
+
end
|
@@ -0,0 +1,19 @@
|
|
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_presense_of :permalink
|
10
|
+
validates_uniqueness_of :permalink
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def sellable?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|