payola-payments 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|