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.
Files changed (58) hide show
  1. checksums.yaml +15 -0
  2. data/Rakefile +34 -0
  3. data/app/assets/javascripts/payola/application.js +13 -0
  4. data/app/assets/stylesheets/payola/application.css +15 -0
  5. data/app/controllers/payola/application_controller.rb +4 -0
  6. data/app/controllers/payola/transactions_controller.rb +75 -0
  7. data/app/helpers/payola/application_helper.rb +4 -0
  8. data/app/models/payola/sale.rb +71 -0
  9. data/app/services/payola/charge_card.rb +44 -0
  10. data/app/services/payola/create_sale.rb +19 -0
  11. data/app/views/layouts/payola/application.html.erb +14 -0
  12. data/config/routes.rb +10 -0
  13. data/db/migrate/20141001170138_create_payola_sales.rb +33 -0
  14. data/lib/payola.rb +43 -0
  15. data/lib/payola/engine.rb +5 -0
  16. data/lib/payola/sellable.rb +19 -0
  17. data/lib/payola/version.rb +3 -0
  18. data/lib/payola/worker.rb +40 -0
  19. data/lib/tasks/payola_tasks.rake +4 -0
  20. data/test/dummy/README.rdoc +28 -0
  21. data/test/dummy/Rakefile +6 -0
  22. data/test/dummy/app/assets/javascripts/application.js +13 -0
  23. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  24. data/test/dummy/app/controllers/application_controller.rb +5 -0
  25. data/test/dummy/app/helpers/application_helper.rb +2 -0
  26. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  27. data/test/dummy/bin/bundle +3 -0
  28. data/test/dummy/bin/rails +4 -0
  29. data/test/dummy/bin/rake +4 -0
  30. data/test/dummy/config.ru +4 -0
  31. data/test/dummy/config/application.rb +23 -0
  32. data/test/dummy/config/boot.rb +5 -0
  33. data/test/dummy/config/database.yml +25 -0
  34. data/test/dummy/config/environment.rb +5 -0
  35. data/test/dummy/config/environments/development.rb +37 -0
  36. data/test/dummy/config/environments/production.rb +82 -0
  37. data/test/dummy/config/environments/test.rb +39 -0
  38. data/test/dummy/config/initializers/assets.rb +8 -0
  39. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  40. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  41. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  42. data/test/dummy/config/initializers/inflections.rb +16 -0
  43. data/test/dummy/config/initializers/mime_types.rb +4 -0
  44. data/test/dummy/config/initializers/session_store.rb +3 -0
  45. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  46. data/test/dummy/config/locales/en.yml +23 -0
  47. data/test/dummy/config/routes.rb +4 -0
  48. data/test/dummy/config/secrets.yml +22 -0
  49. data/test/dummy/public/404.html +67 -0
  50. data/test/dummy/public/422.html +67 -0
  51. data/test/dummy/public/500.html +66 -0
  52. data/test/dummy/public/favicon.ico +0 -0
  53. data/test/fixtures/payola/sales.yml +11 -0
  54. data/test/integration/navigation_test.rb +10 -0
  55. data/test/models/payola/sale_test.rb +9 -0
  56. data/test/payola_test.rb +7 -0
  57. data/test/test_helper.rb +15 -0
  58. 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,4 @@
1
+ module Payola
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -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,4 @@
1
+ module Payola
2
+ module ApplicationHelper
3
+ end
4
+ 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,5 @@
1
+ module Payola
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Payola
4
+ end
5
+ 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