simplepay-rails4 0.4.0

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/CHANGELOG.md +34 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +104 -0
  6. data/MIT-LICENSE +22 -0
  7. data/README.md +106 -0
  8. data/Rakefile +13 -0
  9. data/app/helpers/rails_helper.rb +38 -0
  10. data/lib/simplepay/authentication.rb +41 -0
  11. data/lib/simplepay/constants.rb +64 -0
  12. data/lib/simplepay/errors.rb +16 -0
  13. data/lib/simplepay/helpers/form_helper.rb +31 -0
  14. data/lib/simplepay/rails.rb +2 -0
  15. data/lib/simplepay/service.rb +133 -0
  16. data/lib/simplepay/services/donation.rb +90 -0
  17. data/lib/simplepay/services/marketplace.rb +88 -0
  18. data/lib/simplepay/services/marketplace_policy.rb +53 -0
  19. data/lib/simplepay/services/standard.rb +58 -0
  20. data/lib/simplepay/services/subscription.rb +95 -0
  21. data/lib/simplepay/support/amount.rb +55 -0
  22. data/lib/simplepay/support/billing_frequency.rb +14 -0
  23. data/lib/simplepay/support/boolean.rb +28 -0
  24. data/lib/simplepay/support/currency.rb +21 -0
  25. data/lib/simplepay/support/epoch.rb +39 -0
  26. data/lib/simplepay/support/field.rb +147 -0
  27. data/lib/simplepay/support/interval.rb +145 -0
  28. data/lib/simplepay/support/simple_amount.rb +37 -0
  29. data/lib/simplepay/support/subscription_period.rb +25 -0
  30. data/lib/simplepay/support.rb +16 -0
  31. data/lib/simplepay/validator.rb +59 -0
  32. data/lib/simplepay/version.rb +3 -0
  33. data/lib/simplepay.rb +29 -0
  34. data/simplepay.gemspec +27 -0
  35. data/test/simplepay/services/test_donation.rb +81 -0
  36. data/test/simplepay/services/test_marketplace.rb +81 -0
  37. data/test/simplepay/services/test_marketplace_policy.rb +48 -0
  38. data/test/simplepay/services/test_standard.rb +71 -0
  39. data/test/simplepay/services/test_subscription.rb +105 -0
  40. data/test/simplepay/support/test_amount.rb +46 -0
  41. data/test/simplepay/support/test_billing_frequency.rb +43 -0
  42. data/test/simplepay/support/test_boolean.rb +17 -0
  43. data/test/simplepay/support/test_epoch.rb +34 -0
  44. data/test/simplepay/support/test_field.rb +99 -0
  45. data/test/simplepay/support/test_interval.rb +92 -0
  46. data/test/simplepay/support/test_simple_amount.rb +28 -0
  47. data/test/simplepay/support/test_subscription_period.rb +49 -0
  48. data/test/simplepay/test_authentication.rb +25 -0
  49. data/test/simplepay/test_service.rb +118 -0
  50. data/test/simplepay/test_validator.rb +32 -0
  51. data/test/test_helper.rb +75 -0
  52. data/test/test_simplepay.rb +11 -0
  53. metadata +145 -0
@@ -0,0 +1,147 @@
1
+ require 'active_support'
2
+
3
+ module Simplepay
4
+ module Support
5
+
6
+ ##
7
+ # Represents a data field which will ultimately populate a Simple Pay
8
+ # form. These fields are often unique to their service.
9
+ #
10
+ class Field
11
+ include Comparable
12
+
13
+ ALLOWED_OPTIONS = [:as, :class, :required, :value]
14
+
15
+ # The parent service of the field
16
+ attr_reader :service
17
+
18
+ # The name of the field used in your local program (may be different than the service name)
19
+ attr_reader :name
20
+
21
+ ##
22
+ # name:: Name which is used when referring to this field in the code.
23
+ # options:: An optional hash of field options.
24
+ #
25
+ # === Options
26
+ #
27
+ # as:: Overrides the +name+ when used by the service to the exact string or symbol (camelized) given.
28
+ # class:: Delegate the value mechanics to a separate class.
29
+ # required:: Tells whether or not this field is required for its service.
30
+ # value:: Set the value of the field at initialization.
31
+ #
32
+ # === Example
33
+ #
34
+ # Field.new(:example, :required => true, :as => 'ExAmplE')
35
+ # Field.new(:delegated, :class => Simplepay::Support::Boolean, :value => true)
36
+ #
37
+ def initialize(service, name, options)
38
+ @service, @name, @options = service, name, normalize_options!(options)
39
+ @delegate = options[:class]
40
+ @value = nil
41
+ self.value = options[:value] if options[:value]
42
+ end
43
+
44
+ ##
45
+ # Returns the name of the field used by the service. This may be
46
+ # different than the Ruby name given to the field.
47
+ #
48
+ # Symbol names (or :as options) are camelized. If this is not desired,
49
+ # use a String, instead.
50
+ #
51
+ def service_name
52
+ source = @options[:as] || @name
53
+ case source
54
+ when Symbol
55
+ source.to_s.camelize(:lower)
56
+ else
57
+ source
58
+ end
59
+ end
60
+ alias :to_s :service_name
61
+
62
+ ##
63
+ # Returns +true+ if the field is required for the service.
64
+ #
65
+ def required?
66
+ @options[:required] ? true : false
67
+ end
68
+
69
+ ##
70
+ # Converts a Field into an HTML HIDDEN INPUT element as a string.
71
+ #
72
+ def to_input
73
+ raise(RequiredFieldMissing, "Missing Required Field value for #{name}") if required? && value.blank?
74
+ value.blank? ? '' : html_input_tag
75
+ end
76
+
77
+ def delegated? #:nodoc:
78
+ @delegate
79
+ end
80
+
81
+ def value=(v) #:nodoc:
82
+ @value = delegated? ? @delegate.new(v) : v
83
+ end
84
+
85
+ def value #:nodoc:
86
+ if delegated?
87
+ @value.to_s
88
+ else
89
+ case @value
90
+ when Proc
91
+ @value.call(self)
92
+ else
93
+ @value
94
+ end
95
+ end
96
+ end
97
+
98
+ ##
99
+ # Sorting is based solely by +service_name+.
100
+ #
101
+ def <=>(o)
102
+ self.service_name <=> o.service_name
103
+ end
104
+
105
+ def ==(o) #:nodoc:
106
+ self.service == o.service &&
107
+ self.name == o.name
108
+ end
109
+
110
+ def inspect #:nodoc:
111
+ %|#<#{self.class.name} Name:"#{@name}"#{" ServiceName:\"#{service_name}\"" if service_name != @name}#{" REQUIRED" if required?}>|
112
+ end
113
+
114
+ def clone_for(o) #:nodoc:
115
+ self.class.new(o, deep_clone(@name), deep_clone(@options))
116
+ end
117
+
118
+ def delegate #:nodoc:
119
+ @delegate
120
+ end
121
+
122
+
123
+ private
124
+
125
+
126
+ def html_input_tag #:nodoc:
127
+ Simplepay::Helpers::FormHelper.tag(:input, {
128
+ :name => service_name,
129
+ :value => value,
130
+ :type => 'hidden'
131
+ })
132
+ end
133
+
134
+ def normalize_options!(options) #:nodoc:
135
+ options.symbolize_keys!
136
+ raise(InvalidOptions, "Unrecognized options passed: #{(options.keys - ALLOWED_OPTIONS).join(', ')}") unless (options.keys - ALLOWED_OPTIONS).empty?
137
+ options
138
+ end
139
+
140
+ def deep_clone(o)
141
+ Marshal::load(Marshal.dump(o))
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,145 @@
1
+ require 'simplepay/constants'
2
+
3
+ module Simplepay
4
+ module Support
5
+
6
+ class Interval
7
+
8
+ # If set, limits the quantity to a value within this range
9
+ ALLOWED_QUANTITY_RANGE = nil
10
+
11
+ # If set, limits the interval set to a value within this array
12
+ ALLOWED_INTERVALS = nil
13
+
14
+ # Sets the default quantity value for new instances
15
+ DEFAULT_QUANTITY = nil
16
+
17
+ # Sets the default interval value for new instances
18
+ DEFAULT_INTERVAL = nil
19
+
20
+ # The numeric number of intervals
21
+ attr_reader :quantity
22
+
23
+ # The interval, or "period", of time
24
+ attr_reader :interval
25
+
26
+
27
+ class << self
28
+
29
+ def allowed_intervals
30
+ const_get(:ALLOWED_INTERVALS)
31
+ end
32
+
33
+ def allowed_quantity_range
34
+ const_get(:ALLOWED_QUANTITY_RANGE)
35
+ end
36
+
37
+ def default_quantity
38
+ const_get(:DEFAULT_QUANTITY)
39
+ end
40
+
41
+ def default_interval
42
+ const_get(:DEFAULT_INTERVAL)
43
+ end
44
+
45
+ end
46
+
47
+
48
+ ##
49
+ # Creates an instance of the Interval. This can be called in one of
50
+ # several ways:
51
+ #
52
+ # no arguments:: Creates a new interval instance with default values.
53
+ # one argument, string:: Creates a new interval by parsing the given string to set both the quantity and interval. Must be formatted as: "3 day" (quantity, space, interval)
54
+ # one argument, hash:: Creates a new interval and populates it with the given :quantity and :interval. Uses defaults if not given.
55
+ # two arguments:: Creates a new interval with the first argument as the quantity, second argument as the interval.
56
+ #
57
+ # === Examples
58
+ #
59
+ # All of these are equivalent:
60
+ #
61
+ # Interval.new("3 day")
62
+ # Interval.new({:quantity => 3, :interval => 'day'})
63
+ # Interval.new(3, 'day')
64
+ #
65
+ def initialize(*args)
66
+ parse_arguments(*args)
67
+ end
68
+
69
+ ##
70
+ # Set the interval.
71
+ #
72
+ def interval=(i)
73
+ raise(ArgumentError, "Interval '#{i}' should be one of: #{allowed_intervals.join(', ')}") if i && allowed_intervals && !allowed_intervals.include?(i)
74
+ @interval = i
75
+ end
76
+
77
+ ##
78
+ # Set the quantity.
79
+ #
80
+ def quantity=(q)
81
+ raise(ArgumentError, "Quantity '#{q}' should be in #{allowed_quantity_range}") if q && allowed_quantity_range && !allowed_quantity_range.include?(q)
82
+ @quantity = q
83
+ end
84
+
85
+ ##
86
+ # Converts the interval into an Amazon-ready string.
87
+ #
88
+ # Interval.new(3, 'day').to_s # => "3 day"
89
+ #
90
+ def to_s
91
+ "#{quantity} #{interval}" if interval
92
+ end
93
+
94
+ def allowed_intervals #:nodoc:
95
+ self.class.allowed_intervals
96
+ end
97
+
98
+ def allowed_quantity_range #:nodoc:
99
+ self.class.allowed_quantity_range
100
+ end
101
+
102
+ def default_quantity #:nodoc:
103
+ self.class.default_quantity
104
+ end
105
+
106
+ def default_interval #:nodoc:
107
+ self.class.default_interval
108
+ end
109
+
110
+
111
+ private
112
+
113
+
114
+ def parse_arguments(*args)
115
+ case args.size
116
+ when 0
117
+ self.quantity = self.default_quantity
118
+ self.interval = self.default_interval
119
+ when 1
120
+ case args.first
121
+ when String
122
+ parse_values_from(args.first)
123
+ when Hash
124
+ self.quantity = args.first[:quantity] || self.default_quantity
125
+ self.interval = args.first[:interval] || self.default_interval
126
+ end
127
+ else
128
+ self.quantity = args.first
129
+ self.interval = args[1]
130
+ end
131
+ end
132
+
133
+ def parse_values_from(s)
134
+ if s =~ /\A([\d]+)\s+([\w]+)\Z/
135
+ self.quantity = $1.to_i
136
+ self.interval = $2
137
+ else
138
+ raise ArgumentError, "Unrecognzied initialization string given: #{s.inspect}"
139
+ end
140
+ end
141
+
142
+ end
143
+
144
+ end
145
+ end
@@ -0,0 +1,37 @@
1
+ require 'bigdecimal'
2
+
3
+ module Simplepay
4
+ module Support
5
+
6
+ ##
7
+ # In new Amazon API requests the amount does not include the currency, SimpleAmount is used for this, for now.
8
+ #
9
+ # At the present time, Amazon only uses USD.
10
+ #
11
+ class SimpleAmount
12
+
13
+ attr_reader :amount
14
+
15
+ def initialize(amount)
16
+ self.amount = amount
17
+ end
18
+
19
+
20
+ ##
21
+ # Sets the amount of the currency value, such as "1" for 1 USD. This
22
+ # amount cannot be negative.
23
+ #
24
+ def amount=(amount)
25
+ raise(ArgumentError, "Amount cannot be nil") unless amount
26
+ raise(ArgumentError, "Amount cannot be negative") if amount < 0
27
+ @amount = BigDecimal.new(amount.to_s)
28
+ end
29
+
30
+ def to_s
31
+ "#{'%0.2f' % amount}"
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ require 'simplepay/support/interval'
2
+
3
+ module Simplepay
4
+ module Support
5
+
6
+ class SubscriptionPeriod < Interval
7
+ ALLOWED_INTERVALS = Simplepay::Intervals + ['forever']
8
+
9
+ # Limited to 3 digits.
10
+ ALLOWED_QUANTITY_RANGE = (1...1000)
11
+
12
+ ##
13
+ # See Simplepay::Support::Interval.to_s for more information.
14
+ #
15
+ # If the subscription lasts "forever" then Amazon does not need an
16
+ # interval string.
17
+ #
18
+ def to_s
19
+ super unless interval == 'forever'
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ require 'simplepay/support/amount'
2
+ require 'simplepay/support/billing_frequency'
3
+ require 'simplepay/support/boolean'
4
+ require 'simplepay/support/currency'
5
+ require 'simplepay/support/epoch'
6
+ require 'simplepay/support/field'
7
+ require 'simplepay/support/interval'
8
+ require 'simplepay/support/simple_amount'
9
+ require 'simplepay/support/subscription_period'
10
+
11
+ module Simplepay
12
+
13
+ module Support
14
+ end
15
+
16
+ end
@@ -0,0 +1,59 @@
1
+ require 'cgi'
2
+ require 'nokogiri'
3
+ require 'open-uri'
4
+
5
+ module Simplepay
6
+ module Validator
7
+ extend ActiveSupport::Concern
8
+
9
+ protected
10
+
11
+ ##
12
+ # Authenticates the incoming request by validating the +signature+
13
+ # provided.
14
+ #
15
+ # (from within your controller)
16
+ # def receive_ipn
17
+ # if valid_simplepay_request?(params)
18
+ # ...
19
+ # end
20
+ # end
21
+ #
22
+ def valid_simplepay_request?(params, endpoint = request.url[/\A[^?]+/])
23
+ host = Simplepay.use_sandbox ? "https://fps.sandbox.amazonaws.com" :
24
+ "https://fps.amazonaws.com"
25
+ query = build_simplepay_query_string( params.except( :controller,
26
+ :action,
27
+ :id ) )
28
+ request = { "Action" => "VerifySignature",
29
+ "Version" => "2008-09-17",
30
+ "UrlEndPoint" => endpoint,
31
+ "HttpParameters" => query }
32
+ url = "#{host}/?#{build_simplepay_query_string(request)}"
33
+
34
+ uri = URI.parse(url)
35
+ http = Net::HTTP.new(uri.host, uri.port)
36
+ http.use_ssl = true
37
+ http.ca_file = File.join(File.dirname(__FILE__), "ca-bundle.crt")
38
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
39
+ http.verify_depth = 5
40
+
41
+ response = http.start { |session|
42
+ get = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
43
+ session.request(get)
44
+ }
45
+
46
+ xml = Nokogiri.XML(response.body)
47
+ xml && xml.xpath( "//xmlns:VerificationStatus/text()",
48
+ xml.namespaces ).to_s == "Success"
49
+ rescue
50
+ false
51
+ end
52
+
53
+ def build_simplepay_query_string(params)
54
+ params.map { |k, v|
55
+ "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
56
+ }.join("&")
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module Simplepay
2
+ VERSION = '0.4.0'
3
+ end
data/lib/simplepay.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'rails'
2
+ require 'active_support/dependencies'
3
+
4
+ module Simplepay
5
+ autoload :Constants, 'simplepay/constants'
6
+ autoload :Support, 'simplepay/support'
7
+ autoload :Authentication, 'simplepay/authentication'
8
+ autoload :Service, 'simplepay/service'
9
+ autoload :Validator, 'simplepay/validator'
10
+
11
+ mattr_accessor :aws_access_key_id
12
+ @@aws_access_key_id = ''
13
+ mattr_accessor :aws_secret_access_key
14
+ @@aws_secret_access_key = ''
15
+ mattr_accessor :use_sandbox
16
+ @@use_sandbox = true
17
+
18
+ def self.use_sandbox?
19
+ @@use_sandbox
20
+ end
21
+
22
+ def self.setup
23
+ yield self
24
+ end
25
+
26
+ ActiveSupport.on_load(:action_controller) do
27
+ include ::Simplepay::Validator
28
+ end
29
+ end
data/simplepay.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "simplepay-rails4"
8
+ s.version = "0.4.0"
9
+
10
+ s.authors = ["Nathaniel E. Bibler", "Derrick Parkhurst", "Charles DuBose"]
11
+ s.email = ['gem@nathanielbibler.com', 'gem@dubo.se']
12
+
13
+ s.summary = "Amazon SimplePay helpers for Rails 4"
14
+ s.description = "Creates buttons to manage paying with amazon payments. Can be used for direct payments or merchant payments"
15
+
16
+ s.homepage = 'https://github.com/Yakrware/simplepay'
17
+ s.license = 'MIT'
18
+
19
+ s.required_ruby_version = '>= 1.9.3'
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- test/*`.split("\n")
23
+
24
+ s.add_dependency('rails', '~>4')
25
+ s.add_runtime_dependency('nokogiri', '~>1.6')
26
+ end
27
+
@@ -0,0 +1,81 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper'
2
+ require 'simplepay/services/donation'
3
+
4
+ class Simplepay::Services::TestDonation < ActiveSupport::TestCase
5
+
6
+ def self.model_class; Simplepay::Services::Donation; end
7
+
8
+ context 'Simplepay::Services::Donation' do
9
+
10
+ should_have_service_field :access_key,
11
+ :as => 'accessKey',
12
+ :required => true
13
+
14
+ should_have_service_field :signature,
15
+ :as => 'signature',
16
+ :required => true
17
+
18
+ should_have_service_field :description,
19
+ :as => 'description',
20
+ :required => true
21
+
22
+ should_have_service_field :amount,
23
+ :as => 'amount',
24
+ :required => true,
25
+ :class => Simplepay::Support::Amount
26
+
27
+ should_have_service_field :recipient_email,
28
+ :as => 'recipientEmail',
29
+ :required => false
30
+
31
+ should_have_service_field :fixed_marketplace_fee,
32
+ :as => 'fixedMarketplaceFee',
33
+ :required => false,
34
+ :class => Simplepay::Support::Amount
35
+
36
+ should_have_service_field :variable_marketplace_fee,
37
+ :as => 'variableMarketplaceFee',
38
+ :required => false
39
+
40
+ should_have_service_field :cobranding_style,
41
+ :as => 'cobrandingStyle',
42
+ :required => true
43
+
44
+ should_have_service_field :reference_id,
45
+ :as => 'referenceId',
46
+ :required => false
47
+
48
+ should_have_service_field :immediate_return,
49
+ :as => 'immediateReturn',
50
+ :required => false,
51
+ :class => Simplepay::Support::Boolean
52
+
53
+ should_have_service_field :collect_shipping_address,
54
+ :as => 'collectShippingAddress',
55
+ :required => false,
56
+ :class => Simplepay::Support::Boolean
57
+
58
+ should_have_service_field :process_immediately,
59
+ :as => 'processImmediate',
60
+ :required => false,
61
+ :class => Simplepay::Support::Boolean
62
+
63
+ should_have_service_field :return_url,
64
+ :as => 'returnUrl',
65
+ :required => false
66
+
67
+ should_have_service_field :abandon_url,
68
+ :as => 'abandonUrl',
69
+ :required => false
70
+
71
+ should_have_service_field :ipn_url,
72
+ :as => 'ipnUrl',
73
+ :required => false
74
+
75
+ should_have_service_field :donation_widget,
76
+ :as => 'isDonationWidget',
77
+ :required => true
78
+
79
+ end
80
+
81
+ end
@@ -0,0 +1,81 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper'
2
+ require 'simplepay/services/marketplace'
3
+
4
+ class Simplepay::Services::TestMarketplace < ActiveSupport::TestCase
5
+
6
+ def self.model_class; Simplepay::Services::Marketplace; end
7
+
8
+ context 'Simplepay::Services::Marketplace' do
9
+
10
+ should_have_service_field :access_key,
11
+ :as => 'accessKey',
12
+ :required => true
13
+
14
+ should_have_service_field :signature,
15
+ :as => 'signature',
16
+ :required => true
17
+
18
+ should_have_service_field :description,
19
+ :as => 'description',
20
+ :required => true
21
+
22
+ should_have_service_field :amount,
23
+ :as => 'amount',
24
+ :required => true,
25
+ :class => Simplepay::Support::Amount
26
+
27
+ should_have_service_field :recipient_email,
28
+ :as => 'recipientEmail',
29
+ :required => true
30
+
31
+ should_have_service_field :fixed_marketplace_fee,
32
+ :as => 'fixedMarketplaceFee',
33
+ :required => true,
34
+ :class => Simplepay::Support::Amount
35
+
36
+ should_have_service_field :variable_marketplace_fee,
37
+ :as => 'variableMarketplaceFee',
38
+ :required => true
39
+
40
+ should_have_service_field :cobranding_style,
41
+ :as => 'cobrandingStyle',
42
+ :required => true
43
+
44
+ should_have_service_field :reference_id,
45
+ :as => 'referenceId',
46
+ :required => false
47
+
48
+ should_have_service_field :immediate_return,
49
+ :as => 'immediateReturn',
50
+ :required => false,
51
+ :class => Simplepay::Support::Boolean
52
+
53
+ should_have_service_field :collect_shipping_address,
54
+ :as => 'collectShippingAddress',
55
+ :required => false,
56
+ :class => Simplepay::Support::Boolean
57
+
58
+ should_have_service_field :process_immediately,
59
+ :as => 'processImmediate',
60
+ :required => false,
61
+ :class => Simplepay::Support::Boolean
62
+
63
+ should_have_service_field :return_url,
64
+ :as => 'returnUrl',
65
+ :required => false
66
+
67
+ should_have_service_field :abandon_url,
68
+ :as => 'abandonUrl',
69
+ :required => false
70
+
71
+ should_have_service_field :ipn_url,
72
+ :as => 'ipnUrl',
73
+ :required => false
74
+
75
+ should_have_service_field :donation_widget,
76
+ :as => 'isDonationWidget',
77
+ :required => false
78
+
79
+ end
80
+
81
+ end