harleytt-simplepay 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/History.txt +25 -0
  2. data/Manifest.txt +50 -0
  3. data/README.rdoc +127 -0
  4. data/Rakefile +25 -0
  5. data/lib/simplepay.rb +27 -0
  6. data/lib/simplepay/authentication.rb +41 -0
  7. data/lib/simplepay/constants.rb +64 -0
  8. data/lib/simplepay/errors.rb +16 -0
  9. data/lib/simplepay/helpers/form_helper.rb +29 -0
  10. data/lib/simplepay/helpers/notification_helper.rb +54 -0
  11. data/lib/simplepay/helpers/rails_helper.rb +40 -0
  12. data/lib/simplepay/rails.rb +9 -0
  13. data/lib/simplepay/service.rb +133 -0
  14. data/lib/simplepay/services/donation.rb +91 -0
  15. data/lib/simplepay/services/marketplace.rb +89 -0
  16. data/lib/simplepay/services/marketplace_policy.rb +54 -0
  17. data/lib/simplepay/services/standard.rb +58 -0
  18. data/lib/simplepay/services/subscription.rb +96 -0
  19. data/lib/simplepay/support.rb +16 -0
  20. data/lib/simplepay/support/amount.rb +55 -0
  21. data/lib/simplepay/support/billing_frequency.rb +14 -0
  22. data/lib/simplepay/support/boolean.rb +28 -0
  23. data/lib/simplepay/support/currency.rb +21 -0
  24. data/lib/simplepay/support/epoch.rb +39 -0
  25. data/lib/simplepay/support/field.rb +147 -0
  26. data/lib/simplepay/support/interval.rb +143 -0
  27. data/lib/simplepay/support/simple_amount.rb +37 -0
  28. data/lib/simplepay/support/subscription_period.rb +25 -0
  29. data/script/console +10 -0
  30. data/script/destroy +14 -0
  31. data/script/generate +14 -0
  32. data/simplepay.gemspec +41 -0
  33. data/test/simplepay/helpers/test_notifier.rb +32 -0
  34. data/test/simplepay/services/test_donation.rb +85 -0
  35. data/test/simplepay/services/test_marketplace.rb +85 -0
  36. data/test/simplepay/services/test_marketplace_policy.rb +52 -0
  37. data/test/simplepay/services/test_standard.rb +71 -0
  38. data/test/simplepay/services/test_subscription.rb +109 -0
  39. data/test/simplepay/support/test_amount.rb +46 -0
  40. data/test/simplepay/support/test_billing_frequency.rb +43 -0
  41. data/test/simplepay/support/test_boolean.rb +17 -0
  42. data/test/simplepay/support/test_epoch.rb +34 -0
  43. data/test/simplepay/support/test_field.rb +99 -0
  44. data/test/simplepay/support/test_interval.rb +92 -0
  45. data/test/simplepay/support/test_simple_amount.rb +28 -0
  46. data/test/simplepay/support/test_subscription_period.rb +49 -0
  47. data/test/simplepay/test_authentication.rb +25 -0
  48. data/test/simplepay/test_service.rb +118 -0
  49. data/test/test_helper.rb +87 -0
  50. data/test/test_simplepay.rb +11 -0
  51. metadata +184 -0
@@ -0,0 +1,96 @@
1
+ module Simplepay
2
+ module Services
3
+
4
+ ##
5
+ # A Simple Pay Subscription is an automatically recurring payment which is
6
+ # charged every interval (Simplepay::Support::BillingFrequency) until
7
+ # a limiting period (Simplepay::Support::SubscriptionPeriod) is met.
8
+ #
9
+ # With this type of payment, for example, you may charge your customer:
10
+ #
11
+ # $10.00 every 3 days until 9 days.
12
+ # $9.95 every 1 month until forever.
13
+ #
14
+ # === Simple Pay Subscription Fields
15
+ #
16
+ # === Required Fields
17
+ #
18
+ # The following attributes are required when creating a Simple Pay
19
+ # Subscription form (in addition to those listed in +Simplepay::Service+):
20
+ #
21
+ # amount:: The dollar value you'd like to collect.
22
+ # description:: A summary of the reason for the payment, this is displayed to your customer during checkout.
23
+ # recurring_frequency:: Defines how often to charge your customer (ex. "1 month")
24
+ #
25
+ # ==== Optional Fields
26
+ #
27
+ # abandon_url:: The fully-qualified URL to send your custom if they cancel during payment.
28
+ # auto_renew:: Instructs Amazon to automatically renew the subscription after the +subscription_period+ ends.
29
+ # cobranding_style:: Defines the type of cobranding to use during the checkout process.
30
+ # collect_shipping_address:: Tells Amazon whether or not to ask for shipping address and contact information.
31
+ # immediate_return:: Immediately returns the customer to your +return_url+ directly after payment.
32
+ # ipn_url:: Fully-qualified URL to which Amazon will POST instant payment notifications.
33
+ # process_immediately:: Instructs Amazon to immediately process the payment.
34
+ # reference_id:: A custom string your can set to identify this transaction, it will be returned with the IPNs and other returned data.
35
+ # return_url:: Fully-qualified URL for where to send your customer following payment.
36
+ # start_date:: Instructs Amazon with the timestamp to start the recurring subscription charges.
37
+ # subscription_period:: Defines the expiration window of the subscription (i.e. charge +amount+ every +recurring_frequency+ for "36 months")
38
+ #
39
+ # === Example
40
+ #
41
+ # (in your view, using the form helper)
42
+ #
43
+ # <%= simplepay_form_for(:subscription, {
44
+ # :amount => 12.95,
45
+ # :recurring_frequency => "1 year",
46
+ # :description => "My.Url Yearly Dues"
47
+ # }) %>
48
+ #
49
+ class Subscription < Service
50
+
51
+ required_field :access_key
52
+ required_field :signature
53
+ required_field :account_id, :as => :amazon_payments_account_id
54
+ required_field :signature_method, :value => 'HmacSHA256'
55
+ required_field :signature_version, :value => '2'
56
+
57
+
58
+
59
+ required_field :recurring_frequency, :class => Support::BillingFrequency
60
+ required_field :description
61
+ required_field :amount, :class => Support::Amount
62
+ required_field :cobranding_style, :value => 'logo'
63
+
64
+ field :subscription_period, :class => Support::SubscriptionPeriod
65
+ field :reference_id
66
+ field :immediate_return, :class => Support::Boolean
67
+
68
+ field :start_date, :class => Support::Epoch,
69
+ :as => :recurring_start_date
70
+
71
+ field :collect_shipping_address, :class => Support::Boolean
72
+
73
+ field :process_immediately, :class => Support::Boolean,
74
+ :as => :process_immediate
75
+
76
+ field :auto_renew, :class => Support::Boolean,
77
+ :as => :is_auto_renewal
78
+
79
+ field :return_url
80
+ field :ipn_url
81
+ field :abandon_url
82
+
83
+ # Used for trial periods
84
+ field :promotion_amount, :class => Support::Amount
85
+ field :number_of_promotion_transactions, :as => :no_of_promotion_transactions
86
+
87
+ # These fields are not currently utilized by the service
88
+ field :variable_marketplace_fee, :value => ''
89
+ field :donation_widget, :as => :is_donation_widget,
90
+ :value => '0'
91
+ field :fixed_marketplace_fee, :value => ''
92
+
93
+ end
94
+
95
+ end
96
+ 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,55 @@
1
+ require 'bigdecimal'
2
+
3
+ module Simplepay
4
+ module Support
5
+
6
+ ##
7
+ # Amazon often represents dollar values as a combination of a value and a
8
+ # currency. In several types of requests, the combination is required for
9
+ # communication.
10
+ #
11
+ # At the present time, Amazon only uses USD.
12
+ #
13
+ class Amount
14
+
15
+ attr_reader :amount
16
+ attr_reader :currency
17
+
18
+ def initialize(amount, currency = Simplepay::Currency::USD)
19
+ self.amount = amount
20
+ self.currency = currency
21
+ end
22
+
23
+
24
+ ##
25
+ # Sets the amount of the currency value, such as "1" for 1 USD. This
26
+ # amount cannot be negative.
27
+ #
28
+ def amount=(amount)
29
+ raise(ArgumentError, "Amount cannot be nil") unless amount
30
+ raise(ArgumentError, "Amount cannot be negative") if amount < 0
31
+ @amount = BigDecimal.new(amount.to_s)
32
+ end
33
+
34
+ ##
35
+ # Sets the type of currency to use in the transaction. The parameter
36
+ # can either be a known currency code (see Simplepay::Currency) or a
37
+ # custom Simplepay::Currency::Currency instance.
38
+ #
39
+ def currency=(currency)
40
+ raise(ArgumentError, "Invalid currency, expected Simplepay::Support::Currency or currency code string.") unless currency.kind_of?(Simplepay::Support::Currency) || currency.kind_of?(String)
41
+ if currency.kind_of?(String)
42
+ currency = Simplepay::Currencies.detect { |c| c.code == currency } ||
43
+ raise(ArgumentError, "Invalid currency, '#{currency}'. Must be one of: #{Simplepay::Currencies.join(', ')}")
44
+ end
45
+ @currency = currency
46
+ end
47
+
48
+ def to_s
49
+ "#{currency.code} #{currency.format % amount}"
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,14 @@
1
+ require 'simplepay/support/interval'
2
+
3
+ module Simplepay
4
+ module Support
5
+
6
+ class BillingFrequency < Interval
7
+ ALLOWED_INTERVALS = Simplepay::Intervals
8
+
9
+ # Limited to 2 digits.
10
+ ALLOWED_QUANTITY_RANGE = (1...100)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ module Simplepay
2
+ module Support
3
+
4
+ ##
5
+ # Acts as a delegator for Simplepay::Support::Field <tt>:class</tt>.
6
+ #
7
+ # This class acts as a helper for sending boolean values to Amazon. In
8
+ # their forms, booleans are expected to be either "0" or "1", for false or
9
+ # true, respectively.
10
+ #
11
+ class Boolean
12
+
13
+ def initialize(value)
14
+ @value = value
15
+ end
16
+
17
+ ##
18
+ # Returns "1" if the boolean is true, "0" otherwise.
19
+ #
20
+ def to_s
21
+ return '' if @value.nil?
22
+ @value ? Simplepay::Boolean::True : Simplepay::Boolean::False
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ module Simplepay
2
+ module Support
3
+
4
+ ##
5
+ # Contains a name, recognized code, and basic formatting for a particular
6
+ # international monetary currency.
7
+ #
8
+ class Currency
9
+ attr_reader :name, :code, :format
10
+
11
+ def initialize(name, code, format)
12
+ @name, @code, @format = name, code, format
13
+ end
14
+
15
+ def to_s
16
+ "#{code}"
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ module Simplepay
2
+ module Support
3
+
4
+ ##
5
+ # Acts as a Simplepay::Support::Field <tt>:class</tt> delegator.
6
+ #
7
+ # This class provides a means to have Time values returned as an integer
8
+ # since epoch (January 1, 1970).
9
+ #
10
+ class Epoch
11
+
12
+ def initialize(time)
13
+ @value = time ? parse(time) : Time.now
14
+ end
15
+
16
+ ##
17
+ # Returns a String of Integers, representing seconds since epoch.
18
+ #
19
+ def to_s
20
+ @value.to_i.to_s
21
+ end
22
+
23
+
24
+ private
25
+
26
+
27
+ def parse(time)
28
+ case time
29
+ when Time, Date, DateTime
30
+ time
31
+ else
32
+ Time.parse(time)
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
@@ -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,143 @@
1
+ module Simplepay
2
+ module Support
3
+
4
+ class Interval
5
+
6
+ # If set, limits the quantity to a value within this range
7
+ ALLOWED_QUANTITY_RANGE = nil
8
+
9
+ # If set, limits the interval set to a value within this array
10
+ ALLOWED_INTERVALS = nil
11
+
12
+ # Sets the default quantity value for new instances
13
+ DEFAULT_QUANTITY = nil
14
+
15
+ # Sets the default interval value for new instances
16
+ DEFAULT_INTERVAL = nil
17
+
18
+ # The numeric number of intervals
19
+ attr_reader :quantity
20
+
21
+ # The interval, or "period", of time
22
+ attr_reader :interval
23
+
24
+
25
+ class << self
26
+
27
+ def allowed_intervals
28
+ const_get(:ALLOWED_INTERVALS)
29
+ end
30
+
31
+ def allowed_quantity_range
32
+ const_get(:ALLOWED_QUANTITY_RANGE)
33
+ end
34
+
35
+ def default_quantity
36
+ const_get(:DEFAULT_QUANTITY)
37
+ end
38
+
39
+ def default_interval
40
+ const_get(:DEFAULT_INTERVAL)
41
+ end
42
+
43
+ end
44
+
45
+
46
+ ##
47
+ # Creates an instance of the Interval. This can be called in one of
48
+ # several ways:
49
+ #
50
+ # no arguments:: Creates a new interval instance with default values.
51
+ # 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)
52
+ # one argument, hash:: Creates a new interval and populates it with the given :quantity and :interval. Uses defaults if not given.
53
+ # two arguments:: Creates a new interval with the first argument as the quantity, second argument as the interval.
54
+ #
55
+ # === Examples
56
+ #
57
+ # All of these are equivalent:
58
+ #
59
+ # Interval.new("3 day")
60
+ # Interval.new({:quantity => 3, :interval => 'day'})
61
+ # Interval.new(3, 'day')
62
+ #
63
+ def initialize(*args)
64
+ parse_arguments(*args)
65
+ end
66
+
67
+ ##
68
+ # Set the interval.
69
+ #
70
+ def interval=(i)
71
+ raise(ArgumentError, "Interval '#{i}' should be one of: #{allowed_intervals.join(', ')}") if i && allowed_intervals && !allowed_intervals.include?(i)
72
+ @interval = i
73
+ end
74
+
75
+ ##
76
+ # Set the quantity.
77
+ #
78
+ def quantity=(q)
79
+ raise(ArgumentError, "Quantity '#{q}' should be in #{allowed_quantity_range}") if q && allowed_quantity_range && !allowed_quantity_range.include?(q)
80
+ @quantity = q
81
+ end
82
+
83
+ ##
84
+ # Converts the interval into an Amazon-ready string.
85
+ #
86
+ # Interval.new(3, 'day').to_s # => "3 day"
87
+ #
88
+ def to_s
89
+ "#{quantity} #{interval}" if interval
90
+ end
91
+
92
+ def allowed_intervals #:nodoc:
93
+ self.class.allowed_intervals
94
+ end
95
+
96
+ def allowed_quantity_range #:nodoc:
97
+ self.class.allowed_quantity_range
98
+ end
99
+
100
+ def default_quantity #:nodoc:
101
+ self.class.default_quantity
102
+ end
103
+
104
+ def default_interval #:nodoc:
105
+ self.class.default_interval
106
+ end
107
+
108
+
109
+ private
110
+
111
+
112
+ def parse_arguments(*args)
113
+ case args.size
114
+ when 0
115
+ self.quantity = self.default_quantity
116
+ self.interval = self.default_interval
117
+ when 1
118
+ case args.first
119
+ when String
120
+ parse_values_from(args.first)
121
+ when Hash
122
+ self.quantity = args.first[:quantity] || self.default_quantity
123
+ self.interval = args.first[:interval] || self.default_interval
124
+ end
125
+ else
126
+ self.quantity = args.first
127
+ self.interval = args[1]
128
+ end
129
+ end
130
+
131
+ def parse_values_from(s)
132
+ if s =~ /\A([\d]+)\s+([\w]+)\Z/
133
+ self.quantity = $1.to_i
134
+ self.interval = $2
135
+ else
136
+ raise ArgumentError, "Unrecognzied initialization string given: #{s.inspect}"
137
+ end
138
+ end
139
+
140
+ end
141
+
142
+ end
143
+ end