sinatra-paypal 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ffeb452cdbca569c58812c31d78e17710f934e56
4
+ data.tar.gz: 0de4a6bd12fb5739d09449ee50056fa46b5738b7
5
+ SHA512:
6
+ metadata.gz: 1fe847202845a369bf73bdc01599e265ca5d0e486e3db82b1bf6f33c78d4115792aa0762998ac341193027707acd239240d1da0fe5ceeead6e19f1184e6da29d
7
+ data.tar.gz: 7f227512ad14d5b6dfe88d0f611c48a9f771ea11a5fdc5ff2616c8ee85719b67e00c9158a88fa10b18e7daf9d42f73d72b2432af9f7ea918321036425cd41e94
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sinatra-paypal.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Nathan Reed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,97 @@
1
+ # Sinatra::Paypal
2
+
3
+ This gem makes it easy to validate and process Paypal IPN payments.
4
+
5
+ It automatically adds a route at `/payment/validate` to use as the IPN endpoint, and then adds a number of new methods to the sinatra DSL allow payment events to be captured and processed
6
+
7
+ ## Usage
8
+
9
+ Install the gem and then add it to your project with:
10
+
11
+ require 'sinatra-paypal'
12
+
13
+ Then set the email of the paypal account where you want the payments to be sent:
14
+
15
+ configure do
16
+ settings.paypal.email = 'my.company@gmail.com'
17
+ end
18
+
19
+ There are other configuration options you could use, but only setting the destination email is required.
20
+
21
+ Add the following to your app. It will get called only when the payment is complete:
22
+
23
+ # if this gets called, then you can be sure that the payment has passed validation
24
+ # with paypal, but you will still have to do your own validation - users can fiddle
25
+ # with the form fields before it gets sent to paypal - the only thing you can be sure
26
+ # of it the $ amount - if the payment is complete then this is the amount in your
27
+ # account.
28
+ #
29
+ # all the payment 'routes' get called with the payment object ('p')s containing all
30
+ # the data for the transaction
31
+ payment :complete do |p|
32
+ #
33
+ # check that the price for the item matches ours - it's possible for users to
34
+ # change the price to whatever they want, and there is no way for paypal to check
35
+ # this
36
+ #
37
+ # You could also do this in the 'payment :validate' method
38
+ #
39
+ halt 500, 'invalid price' if p.amount != ITEM_PRICE
40
+
41
+ # you can put a json string in the custom_data property of the payment
42
+ # form, and it will be deserialized into the data object - this is very
43
+ # useful for tracking user names etc, but remember that the contents of this
44
+ # field are not secure - a user could set it to whatever they want
45
+ upgrade_account p.data[:user_id]
46
+ end
47
+
48
+ Other routes that you can use for processing:
49
+
50
+ # validate! is called for every transaction - not just complete ones
51
+ #
52
+ # if it fails validation, then 'halt' with the error message - the
53
+ # return value is ignored
54
+ payment :validate! do |p|
55
+ # we need a 'user_id' field in the data, otherwise its not valid
56
+ halt 500, 'user_id required' if p.data[:user_id].nil?
57
+ end
58
+
59
+ # finsh is called last after all other processing is complete. It is called for
60
+ # every transaction type. It is useful for doing logging etc.
61
+ payment :finish do |p|
62
+ log_payment p.id, p.amount
63
+ end
64
+
65
+ # repeated should return true if this transaction has been seen before - we want
66
+ # to make sure we don't process it more than once
67
+ payment :repeated? do |p|
68
+ return true if transaction_list.include? p.id
69
+
70
+ transaction_list.push p.id
71
+ return false
72
+ end
73
+
74
+ ## The Payment Object
75
+
76
+ Each payment route gets passed a payment object containing all the data from paypal for the transaction. It has several useful methods to make it easier to process the payment.
77
+
78
+ p.id # unique sha1 for the payment notification
79
+ p.transaction_id # paypal transaction_id
80
+ p.item_number # the item number set in the payment form
81
+ p.amount # payment amount
82
+ p.payment_fee # paypal fee amount
83
+ p.profit # payment amount minus any fees
84
+ p.data # custom data sent through as JSON
85
+ p.status # payment status - COMPLETE, PENDING, REFUNDED etc
86
+ p.complete? # true if the payment status is COMPLETE
87
+ p.is_accountable? # true if this notification has caused you paypal balance to change
88
+ p.reason_code # if the transaction is a refund the reason code will appear here
89
+ p.fields # object with the raw paypal notification data
90
+
91
+ ## Contributing
92
+
93
+ 1. Fork it ( https://github.com/reednj/sinatra-paypal/fork )
94
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
95
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
96
+ 4. Push to the branch (`git push origin my-new-feature`)
97
+ 5. Create a new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sinatra/paypal"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,89 @@
1
+
2
+ require 'sinatra/base'
3
+ require 'sinatra/paypal/version'
4
+ require 'sinatra/paypal/paypal-helper'
5
+ require 'sinatra/paypal/paypal-request'
6
+
7
+ PAYPAL_BLOCKS = {}
8
+
9
+ module PayPal
10
+
11
+ module Helpers
12
+ def paypal_block(name)
13
+ return Proc.new{} if !PAYPAL_BLOCKS.key? name
14
+ PAYPAL_BLOCKS[name]
15
+ end
16
+
17
+ def paypal_form_url
18
+ PaypalHelper.form_url(settings.paypal.sandbox?)
19
+ end
20
+
21
+ def html_payment_form(offer_data, data = nil)
22
+ data ||= {}
23
+ data[:username] = session[:reddit_user] if !data.nil? && data[:username].nil?
24
+ data[:offer_code] = offer_data.code if !offer_data.nil?
25
+
26
+ raise 'cannot generate a payment form without settings.paypal.email' if settings.paypal.email.nil?
27
+
28
+ erb :_payment, :views => File.join(File.dirname(__FILE__), '/paypal'), :locals => {
29
+ :custom_data => data,
30
+ :offer_data => offer_data
31
+ }
32
+ end
33
+ end
34
+
35
+ def self.registered(app)
36
+ app.helpers PayPal::Helpers
37
+
38
+ app.set :paypal, OpenStruct.new({
39
+ :return_url => '/payment/complete',
40
+ :notify_url => '/payment/validate',
41
+ :sandbox? => app.settings.development?,
42
+ :email => nil
43
+ })
44
+
45
+ app.post '/payment/validate' do
46
+ paypal_helper = PaypalHelper.new(app.settings.paypal.sandbox?)
47
+ paypal_request = PaypalRequest.new(params)
48
+
49
+ # first we check the request against paypal to make sure it is ok
50
+ if settings.production?
51
+ halt 400, 'request could not be validated' if !paypal_helper.ipn_valid? params
52
+ end
53
+
54
+ halt 400, 'no username provided' if paypal_request.username.nil?
55
+
56
+ # check transaction log to make sure this not a replay attack
57
+ if instance_exec(paypal_request, &paypal_block(:repeated?))
58
+ # we want to log this, so we know about it, but we also want to return 200, because
59
+ # if it is paypal sending this, it will send it again and again until it gets a 200
60
+ # back
61
+ log_error 'already processed' if respond_to? :log_error
62
+ halt 200, 'already processed'
63
+ else
64
+ instance_exec(paypal_request, &paypal_block(:save))
65
+ end
66
+
67
+ instance_exec(paypal_request, &paypal_block(:validate!))
68
+
69
+ # check that the payment is complete. we still return 200 if not, but
70
+ # we don't need to do anymore processing (except for marking it as accountable, if it is)
71
+ if paypal_request.complete?
72
+ instance_exec(paypal_request, &paypal_block(:complete))
73
+ end
74
+
75
+ instance_exec(paypal_request, &paypal_block(:finish))
76
+
77
+ return 200
78
+ end
79
+ end
80
+
81
+ def payment(name, &block)
82
+ PAYPAL_BLOCKS[name] = block
83
+ end
84
+ end
85
+
86
+
87
+ module Sinatra
88
+ register PayPal
89
+ end
@@ -0,0 +1,25 @@
1
+
2
+ <form id='paypal-form' action="<%= paypal_form_url %>" method="post" style='display:none'>
3
+ <input type="hidden" name="cmd" value="_xclick">
4
+
5
+ <input type="hidden" name="business" value="<%= settings.paypal.email %>">
6
+ <input type='hidden' name='custom' value='<%= custom_data.to_json %>'>
7
+ <input type="hidden" name="return" value="<%= url(settings.paypal.return_url) %>">
8
+ <input type="hidden" name="notify_url" value="<%= url(settings.paypal.notify_url) %>">
9
+
10
+ <% if offer_data.nil? %>
11
+ <input type="hidden" name="item_number" id='paypal-item-number' value="0">
12
+ <input type="hidden" name="item_name" id='paypal-item-name' value="0">
13
+ <input type="hidden" name="amount" id='paypal-amount' value="1.00">
14
+ <% else %>
15
+ <input type="hidden" name="item_number" id='paypal-item-number' value="<%= offer_data.offer.item_code %>">
16
+ <input type="hidden" name="item_name" id='paypal-item-name' value="<%= offer_data.item.name %>">
17
+ <input type="hidden" name="amount" id='paypal-amount' value="<%= offer_data.offer.amount %>">
18
+ <% end %>
19
+
20
+ <input type="hidden" name="no_shipping" value="1">
21
+ <input type="hidden" name="no_note" value="1">
22
+ <input type="hidden" name="currency_code" value="USD">
23
+ <input type="hidden" name="lc" value="US">
24
+ <input type="hidden" name="bn" value="PP-BuyNowBF">
25
+ </form>
@@ -0,0 +1,22 @@
1
+ require 'rest-client'
2
+
3
+ class PaypalHelper
4
+ def initialize(use_sandbox)
5
+ @use_sandbox = use_sandbox
6
+ end
7
+
8
+ def form_url
9
+ @use_sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr'
10
+ end
11
+
12
+ def self.form_url(use_sandbox)
13
+ new(use_sandbox).form_url
14
+ end
15
+
16
+ def ipn_valid?(params)
17
+ return false if params.nil?
18
+
19
+ params[:cmd] = '_notify-validate'
20
+ return RestClient.post(self.form_url, params) == 'VERIFIED'
21
+ end
22
+ end
@@ -0,0 +1,109 @@
1
+
2
+ class PaypalRequest
3
+ def initialize(params)
4
+ if params.is_a? String
5
+ params = JSON.parse(params, {:symbolize_names => true})
6
+ end
7
+
8
+ @fields = params
9
+ @custom_data = nil
10
+ end
11
+
12
+
13
+ def id
14
+ self.transaction_hash
15
+ end
16
+
17
+ # the same transaction id can come in mulitple times with different statuses
18
+ # so we need to check both of them in order to see if the txn is unquie
19
+ def transaction_hash
20
+ "#{self.transaction_id}-#{@fields[:payment_status]}".sha1
21
+ end
22
+
23
+ def transaction_id
24
+ @fields[:txn_id]
25
+ end
26
+
27
+ def item_valid?(item_list, offer_data = nil)
28
+ if item_list[self.item_number] != nil
29
+ if item_list[self.item_number][:amount] == self.amount
30
+ # this is a regular purchase at the regular price
31
+ return true
32
+ elsif self.uses_offer?(offer_data)
33
+ # the price comes from a special offer, its valid
34
+ return true
35
+ end
36
+ end
37
+
38
+ return false
39
+ end
40
+
41
+ def uses_offer?(offer_data)
42
+ # to check if the offer is valid on this purchase we need to check the item code and the price matches
43
+ return !offer_data.nil? && offer_data[:offer][:item_code] == self.item_number && offer_data[:offer][:amount] == self.amount
44
+ end
45
+
46
+ def item_number
47
+ @fields[:item_number]
48
+ end
49
+
50
+ def amount
51
+ Float(@fields[:mc_gross] || 0)
52
+ end
53
+
54
+ def payment_fee
55
+ Float(@fields[:mc_fee] || 0)
56
+ end
57
+
58
+ # defined as the gross amount, minus transaction fees
59
+ # could also subtract shipping and tax here as well, but we don't have to deal with
60
+ # any of that yet
61
+ def profit
62
+ self.amount - self.payment_fee
63
+ end
64
+
65
+ def username
66
+ self.custom_data[:username]
67
+ end
68
+
69
+ def custom_data
70
+ if @custom_data.nil?
71
+ if @fields[:custom].strip.start_with? '{'
72
+ # we could get a json object through, in which case it needs to be parsed...
73
+ @custom_data = JSON.parse(@fields[:custom], {:symbolize_names => true})
74
+ else
75
+ # ...or would just have a string (which we assume is the target username) in that
76
+ # case we need to normalize it into an object
77
+ @custom_data = {:username => @fields[:custom]}
78
+ end
79
+ end
80
+
81
+ return @custom_data
82
+ end
83
+
84
+ def data
85
+ custom_data
86
+ end
87
+
88
+ def status
89
+ @fields[:payment_status]
90
+ end
91
+
92
+ def complete?
93
+ self.status == 'Completed'
94
+ end
95
+
96
+ # these are payment statues that actually result in the paypal balance changing, so we should set them as
97
+ # accountable in the payment_log
98
+ def is_accountable?
99
+ (self.complete? || self.status == 'Refunded' || self.status == 'Reversed' || self.status == 'Canceled_Reversal')
100
+ end
101
+
102
+ def reason_code
103
+ @fields[:reason_code] || @fields[:pending_reason]
104
+ end
105
+
106
+ def fields
107
+ @fields
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ module Sinatra
2
+ module Paypal
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sinatra/paypal/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sinatra-paypal"
8
+ spec.version = Sinatra::Paypal::VERSION
9
+ spec.authors = ["Nathan Reed"]
10
+ spec.email = ["reednj@gmail.com"]
11
+
12
+ spec.summary = %q{Easy validation and processing of Paypal IPN payments}
13
+ spec.homepage = "http://github.com/reednj/sinatra-paypal"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.9"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "test-unit"
24
+ spec.add_runtime_dependency "sinatra"
25
+ spec.add_runtime_dependency "rest-client"
26
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sinatra-paypal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Reed
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-10-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: test-unit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sinatra
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rest-client
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ - reednj@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".travis.yml"
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - bin/console
97
+ - bin/setup
98
+ - lib/sinatra/paypal.rb
99
+ - lib/sinatra/paypal/_payment.erb
100
+ - lib/sinatra/paypal/paypal-helper.rb
101
+ - lib/sinatra/paypal/paypal-request.rb
102
+ - lib/sinatra/paypal/version.rb
103
+ - sinatra-paypal.gemspec
104
+ homepage: http://github.com/reednj/sinatra-paypal
105
+ licenses:
106
+ - MIT
107
+ metadata: {}
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 2.4.5
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: Easy validation and processing of Paypal IPN payments
128
+ test_files: []