bborn-simplepay 0.2.2

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 (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