servicemerchant 0.1.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 (69) hide show
  1. data/MIT-LICENSE.txt +20 -0
  2. data/README.txt +231 -0
  3. data/Rakefile +122 -0
  4. data/demo.rb +69 -0
  5. data/recurring_billing/lib/am_extensions.rb +1 -0
  6. data/recurring_billing/lib/am_extensions/paypal_extension.rb +170 -0
  7. data/recurring_billing/lib/dependencies.rb +14 -0
  8. data/recurring_billing/lib/gateways.rb +5 -0
  9. data/recurring_billing/lib/gateways/authorize_net.rb +103 -0
  10. data/recurring_billing/lib/gateways/paypal.rb +124 -0
  11. data/recurring_billing/lib/recurring_billing.rb +130 -0
  12. data/recurring_billing/lib/recurring_billing.rdoc +87 -0
  13. data/recurring_billing/lib/utils.rb +81 -0
  14. data/recurring_billing/test/fixtures.yml +33 -0
  15. data/recurring_billing/test/remote/authorize_net_test.rb +36 -0
  16. data/recurring_billing/test/remote/paypal_test.rb +46 -0
  17. data/recurring_billing/test/remote/recurring_billing_test.rb +41 -0
  18. data/recurring_billing/test/test_helper.rb +153 -0
  19. data/recurring_billing/test/unit/authorize_net_gateway_class_test.rb +42 -0
  20. data/recurring_billing/test/unit/paypal_gateway_class_test.rb +23 -0
  21. data/recurring_billing/test/unit/recurring_billing_gateway_class_test.rb +35 -0
  22. data/recurring_billing/test/unit/utils_test.rb +17 -0
  23. data/subscription_management/Rakefile +29 -0
  24. data/subscription_management/lib/models/subscription.rb +9 -0
  25. data/subscription_management/lib/models/subscription_profile.rb +4 -0
  26. data/subscription_management/lib/subscription_management.rb +326 -0
  27. data/subscription_management/samples/backpack.yml +101 -0
  28. data/subscription_management/samples/basecamp.yml +71 -0
  29. data/subscription_management/samples/brainkeeper.yml +90 -0
  30. data/subscription_management/samples/campfire.yml +74 -0
  31. data/subscription_management/samples/clickandpledge.yml +24 -0
  32. data/subscription_management/samples/demo.rb +19 -0
  33. data/subscription_management/samples/elm.yml +174 -0
  34. data/subscription_management/samples/freshbooks.yml +78 -0
  35. data/subscription_management/samples/highrise.yml +100 -0
  36. data/subscription_management/samples/presets.yml +10 -0
  37. data/subscription_management/samples/tariff.outline.yml +0 -0
  38. data/subscription_management/samples/taxes.yml +21 -0
  39. data/subscription_management/subscription_management.rb +7 -0
  40. data/subscription_management/tasks/schema.rb +50 -0
  41. data/subscription_management/test/connection.rb +10 -0
  42. data/subscription_management/test/remote/subscription_management_test.rb +112 -0
  43. data/subscription_management/test/test_helper.rb +84 -0
  44. data/subscription_management/test/unit/subscription_management_test.rb +40 -0
  45. data/tracker/README +12 -0
  46. data/tracker/Rakefile +40 -0
  47. data/tracker/db/migrations/empty-directory +0 -0
  48. data/tracker/demo.rb +12 -0
  49. data/tracker/lib/models/recurring_payment_profile.rb +134 -0
  50. data/tracker/lib/models/transaction.rb +19 -0
  51. data/tracker/lib/recurring_billing_extension.rb +103 -0
  52. data/tracker/lib/recurring_billing_extension.rdoc +34 -0
  53. data/tracker/tasks/schema.rb +66 -0
  54. data/tracker/test/connection.rb +10 -0
  55. data/tracker/test/recurring_payment_profile.rb +35 -0
  56. data/tracker/test/remote/authorize_net_test.rb +68 -0
  57. data/tracker/test/remote/paypal_test.rb +115 -0
  58. data/tracker/test/test_helper.rb +87 -0
  59. data/tracker/test/unit/recurring_payment_profile_test.rb +62 -0
  60. data/tracker/tracker.rb +10 -0
  61. data/vendor/money-1.7.1/MIT-LICENSE +20 -0
  62. data/vendor/money-1.7.1/README +75 -0
  63. data/vendor/money-1.7.1/lib/bank/no_exchange_bank.rb +9 -0
  64. data/vendor/money-1.7.1/lib/bank/variable_exchange_bank.rb +30 -0
  65. data/vendor/money-1.7.1/lib/money.rb +29 -0
  66. data/vendor/money-1.7.1/lib/money/core_extensions.rb +26 -0
  67. data/vendor/money-1.7.1/lib/money/money.rb +209 -0
  68. data/vendor/money-1.7.1/lib/support/cattr_accessor.rb +57 -0
  69. metadata +153 -0
@@ -0,0 +1,170 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class PaypalGateway < Gateway#:nodoc:
4
+
5
+ # this file was originally located in activemerchant-1.3.2/lib/active_merchant/billing/gateways
6
+ # MANUAL https://www.paypal.com/en_US/pdf/PP_APIReference.pdf
7
+ # See also:
8
+ # http://jadedpixel.lighthouseapp.com/projects/11599/tickets/17-patch-creating-paypal-recurring-payments-profile-with-activemerchant
9
+
10
+ remove_const("RECURRING_ACTIONS") if defined? RECURRING_ACTIONS
11
+ RECURRING_ACTIONS = Set.new([:add, :modify, :cancel, :inquiry])
12
+
13
+ # :interval - cannot exceed 1 year
14
+ # :interval[:unit] = :week | :semimonth | :month | :year
15
+
16
+ def recurring(money, credit_card, options = {})
17
+ options[:name] = credit_card.name if options[:name].blank? && credit_card
18
+ request = build_recurring_request(options[:profile_id].nil? ? :add : :modify, money, options) do |xml|
19
+ add_credit_card(xml, credit_card, options[:billing_address], options) if credit_card
20
+ end
21
+ commit options[:profile_id].nil? ? 'CreateRecurringPaymentsProfile' : 'UpdateRecurringPaymentsProfile', request
22
+ end
23
+
24
+ def cancel_recurring(profile_id, options)
25
+ request = build_recurring_request(:cancel, nil, options.update( :profile_id => profile_id ))
26
+ commit 'ManageRecurringPaymentsProfileStatus', request
27
+ end
28
+
29
+ def inquiry_recurring(profile_id, options = {})
30
+ request = build_recurring_request(:inquiry, nil, options.update( :profile_id => profile_id ))
31
+ commit 'GetRecurringPaymentsProfileDetails', request
32
+ end
33
+
34
+ private
35
+
36
+ def build_recurring_request(action, money, options)
37
+ unless RECURRING_ACTIONS.include?(action)
38
+ raise StandardError, "Invalid Recurring Profile Action: #{action}"
39
+ end
40
+
41
+ xml = Builder::XmlMarkup.new :indent => 2
42
+
43
+ if action == :add
44
+ xml.tag! 'CreateRecurringPaymentsProfileReq', 'xmlns' => PAYPAL_NAMESPACE do
45
+ xml.tag! 'CreateRecurringPaymentsProfileRequest', 'xmlns:n2' => EBAY_NAMESPACE do
46
+ xml.tag! 'n2:Version', 50.0 # API_VERSION # must be >= 50.0
47
+ xml.tag! 'n2:CreateRecurringPaymentsProfileRequestDetails' do
48
+
49
+ yield xml # put card information : CreditCardDetails
50
+
51
+
52
+ xml.tag! 'n2:RecurringPaymentsProfileDetails' do
53
+ xml.tag! 'n2:BillingStartDate', format_date(options[:starting_at])
54
+ # SubscriberName (optional)
55
+ # SubscriberShippingAddress (optional)
56
+ # ProfileReference (optional) = The merchant’s own unique reference or invoice number.
57
+ end
58
+
59
+ xml.tag! 'n2:ScheduleDetails' do
60
+ xml.tag! 'n2:Description', options[:description] # <= 127 single-byte alphanumeric characters!!!
61
+ # This field must match the corresponding billing agreement description included in the SetExpressCheckout reques
62
+ # ? MaxFailedPayments
63
+ # ? AutoBillOutstandingAmount = NoAutoBill / AddToNextBilling
64
+
65
+ xml.tag! 'n2:PaymentPeriod' do
66
+ # if == :semimonth, then payed at 1 & 15 day of month
67
+ xml.tag! 'n2:BillingFrequency', options[:interval][:length]
68
+ xml.tag! 'n2:BillingPeriod', format_unit(options[:interval][:unit])
69
+ xml.tag! 'n2:Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
70
+ # ShippingAmount (optional)
71
+ # TaxAmount (optional)
72
+ xml.tag! 'n2:TotalBillingCycles', options[:total_payments].to_s unless options[:total_payments].nil?
73
+ end
74
+
75
+ # WARNING: Activation not tested
76
+ unless options[:activation].nil?
77
+ xml.tag! 'n2:ActivationDetails' do
78
+ xml.tag! 'n2:InitialAmount', amount(options[:activation][:amount]), 'currencyID' => options[:currency] || currency(options[:activation][:amount])
79
+ xml.tag! 'n2:FailedInitAmountAction', options[:activation][:failed_action] unless options[:activation][:failed_action] # 'ContinueOnFailure/CancelOnFailure'
80
+ xml.tag! 'n2:MaxFailedPayments', options[:activation][:max_failed_payments].to_s unless options[:activation][:max_failed_payments].nil?
81
+ end
82
+ end
83
+
84
+ # WARNING: trial option not tested
85
+ unless options[:trial].nil?
86
+ xml.tag! 'n2:TrialPeriod' do
87
+ frequency, period = get_pay_period(options[:trial][:periodicity])
88
+ xml.tag! 'n2:BillingFrequency', frequency.to_s
89
+ xml.tag! 'n2:BillingPeriod', period
90
+ xml.tag! 'n2:Amount', amount(options[:trial][:amount]), 'currencyID' => options[:currency] || currency(options[:trial][:amount])
91
+ xml.tag! 'n2:TotalBillingCycles', options[:trial][:total_payments].to_s
92
+ end
93
+ end
94
+
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ elsif action == :modify
101
+ xml.tag! 'UpdateRecurringPaymentsProfileReq', 'xmlns' => PAYPAL_NAMESPACE do
102
+ xml.tag! 'UpdateRecurringPaymentsProfileRequest', 'xmlns:n2' => EBAY_NAMESPACE do
103
+ xml.tag! 'n2:Version', 50.0 # API_VERSION # must be >= 50.0
104
+ xml.tag! 'n2:UpdateRecurringPaymentsProfileRequestDetails' do
105
+
106
+ xml.tag! 'n2:ProfileID', options[:profile_id]
107
+ xml.tag! 'n2:Note', options[:note] unless options[:note].nil?
108
+ xml.tag! 'n2:Description', options[:description] unless options[:description].nil? # <= 127 single-byte alphanumeric characters!!!
109
+
110
+ # SubscriberName (optional)
111
+ # SubscriberShippingAddress (optional)
112
+ # ProfileReference (optional) = The merchant’s own unique reference or invoice number.
113
+ xml.tag! 'n2:AdditionalBillingCycles', options[:additional_payments].to_s unless options[:additional_payments].nil?
114
+ xml.tag! 'n2:Amount', amount(money), 'currencyID' => options[:currency] || currency(money) unless money.nil?
115
+ # ShippingAmount (optional)
116
+ # TaxAmount (optional)
117
+ # OutStandingBalance (optional)
118
+ # The current past due or outstanding amount for this profile. You can only
119
+ # decrease the outstanding amount—it cannot be increased.
120
+ # ? AutoBillOutstandingAmount (optional) = NoAutoBill / AddToNextBilling
121
+ # ? MaxFailedPayments (optional) = The number of failed payments allowed before the profile is automatically suspended.
122
+
123
+ yield xml # put card information : CreditCardDetails
124
+ # Only enter credit card details for recurring payments with direct payments.
125
+ # Credit card billing address is optional, but if you update any of the address
126
+ # fields, you must enter all of them. For example, if you want to update the
127
+ # street address, you must specify all of the address fields listed in
128
+ # CreditCardDetailsType, not just the field for the street address.
129
+ end
130
+ end
131
+ end
132
+
133
+ elsif action == :cancel
134
+ xml.tag! 'ManageRecurringPaymentsProfileStatusReq', 'xmlns' => PAYPAL_NAMESPACE do
135
+ xml.tag! 'ManageRecurringPaymentsProfileStatusRequest', 'xmlns:n2' => EBAY_NAMESPACE do
136
+ xml.tag! 'n2:Version', 50.0
137
+ xml.tag! 'n2:ManageRecurringPaymentsProfileStatusRequestDetails' do
138
+ xml.tag! 'n2:ProfileID', options[:profile_id]
139
+ xml.tag! 'n2:Action', 'Cancel'
140
+ xml.tag! 'n2:Note', options[:note] unless options[:note].nil?
141
+ end
142
+ end
143
+ end
144
+
145
+ elsif action == :inquiry
146
+ xml.tag! 'GetRecurringPaymentsProfileDetailsReq', 'xmlns' => PAYPAL_NAMESPACE do
147
+ xml.tag! 'GetRecurringPaymentsProfileDetailsRequest', 'xmlns:n2' => EBAY_NAMESPACE do
148
+ xml.tag! 'n2:Version', 50.0
149
+ xml.tag! 'ProfileID', options[:profile_id]
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ def format_date(dat)
156
+ case dat.class.to_s
157
+ when 'Date' then return dat.strftime('%FT%T')
158
+ when 'Time' then return dat.getgm.strftime('%FT%T')
159
+ when 'String' then return dat
160
+ end
161
+ end
162
+
163
+ def format_unit(unit)
164
+ requires!({:data => unit}, [:data, 'Week', 'SemiMonth', 'Month', 'Year'])
165
+ unit.to_s.downcase.capitalize
166
+ end
167
+
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+
3
+ gem 'activemerchant'
4
+ require 'active_merchant'
5
+
6
+ #TODO: Autodiscover new libs from vendor and add them to load path
7
+ $: << File.dirname(__FILE__) + "/../../vendor/money-1.7.1/lib"
8
+ require "money"
9
+
10
+ gem 'activesupport'
11
+ require 'active_support/core_ext/string/inflections'
12
+ class String # :nodoc:
13
+ include ActiveSupport::CoreExtensions::String::Inflections
14
+ end
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/utils'
2
+ require File.dirname(__FILE__) + '/recurring_billing'
3
+ require File.dirname(__FILE__) + '/am_extensions'
4
+
5
+ Dir[File.dirname(__FILE__) + '/gateways/*.rb'].each{|g| require g}
@@ -0,0 +1,103 @@
1
+ module RecurringBilling
2
+ class AuthorizeNetGateway < RecurringBillingGateway
3
+ include Utils
4
+
5
+ def code
6
+ :authorize_net
7
+ end
8
+
9
+ def name
10
+ 'Authorize.net'
11
+ end
12
+
13
+ # Check if update is possible using specified arguments
14
+ def correct_update?(billing_id, amount, card, payment_options, recurring_options)
15
+ if !recurring_options.nil? && recurring_options.length > 0
16
+ raise StandardError, 'Cannot update recurring options at #{name} gateway'
17
+ end
18
+ return true
19
+ end
20
+
21
+ # Make an update using gateway-specific actions
22
+ def update_specific(billing_id, amount, card, payment_options, recurring_options)
23
+ options = compile_options(amount, card, payment_options, recurring_options)
24
+ options[:amount] = amount
25
+ options[:subscription_id] = billing_id
26
+ (@last_response = @gateway.update_recurring(options)).success?
27
+ end
28
+
29
+ # Create payment using gateway-specific actions
30
+ def create_specific(amount, card, payment_options, recurring_options)
31
+ @last_response = @gateway.recurring(amount, card, compile_options(amount, card, payment_options, recurring_options))
32
+ return @last_response.authorization if @last_response.success?
33
+ nil
34
+ end
35
+
36
+ # Cancel the subscription
37
+ def delete_specific(billing_id)
38
+ (@last_response = @gateway.cancel_recurring(billing_id)).success?
39
+ end
40
+
41
+ # Get ready-to-send options hash
42
+ def compile_options(amount, card, payment_options, recurring_options)
43
+ new_options = {}
44
+ if !recurring_options.nil? && !recurring_options.empty?
45
+ requires!(recurring_options, :start_date, :interval)
46
+ requires!(recurring_options, :occurrences) unless recurring_options.has_key?(:end_date)
47
+ requires!(recurring_options, :end_date) unless recurring_options.has_key?(:occurrences)
48
+ transformed_dates = (recurring_options[:occurrences]) ?
49
+ transform_dates(recurring_options[:start_date], recurring_options[:interval], recurring_options[:occurrences], nil) :
50
+ transform_dates(recurring_options[:start_date], recurring_options[:interval], nil, recurring_options[:end_date])
51
+
52
+ new_options = {:interval => transformed_dates[:interval], :duration => transformed_dates[:duration]}
53
+ end
54
+
55
+ billing_address = payment_options.has_key?(:billing_address) ? payment_options[:billing_address] : {}
56
+ if (!billing_address.has_key?(:last_name) || billing_address[:last_name].empty?) && card
57
+ billing_address[:last_name] = card.last_name
58
+ billing_address[:first_name] = card.first_name
59
+ end
60
+
61
+
62
+ new_options[:billing_address] = billing_address
63
+ new_options[:subscription_name] = payment_options[:subscription_name] if payment_options.has_key?(:subscription_name)
64
+ new_options[:order] = payment_options[:order] if payment_options.has_key?(:order)
65
+
66
+ return new_options
67
+
68
+ end
69
+
70
+ #Transform dates to Authorize.net-recognizable format
71
+ def transform_dates(start_date, interval, occurrences, end_date)
72
+
73
+ raise ArgumentError, 'Either number of occurences OR end date should be specified' if (!occurrences.nil? && !end_date.nil?) || ((occurrences.nil? && end_date.nil?))
74
+ raise ArgumentError, 'Payment cycle start date ({#start_date}) should be less than or equal to end date ({#end_date})' if !end_date.nil? && (start_date>end_date)
75
+ raise ArgumentError, 'Number of payment occurrences should be a positive integer)' if !occurrences.nil? && (occurrences <= 0)
76
+
77
+ i_length, i_unit = parse_interval(interval)
78
+
79
+ if i_length == 0.5 && (i_unit != :y)
80
+ raise ArgumentError, "Semi- interval is not supported to this units (#{i_unit.to_s})"
81
+ end
82
+
83
+ new_interval = case i_unit
84
+ when :d then {:length=>i_length, :unit=>:days}
85
+ when :w then {:length=>i_length*7, :unit=>:days}
86
+ when :m then {:length=>i_length, :unit=>:months}
87
+ when :y then {:length=>i_length*12, :unit=>:months}
88
+ end
89
+ if !occurrences.nil?
90
+ return {:interval=>new_interval, :duration=>{:start_date=>start_date, :occurrences=>occurrences}}
91
+ else
92
+ if new_interval[:unit] == :days
93
+ new_occurrences = 1 + ((end_date - start_date)/new_interval[:length]).to_i
94
+ elsif new_interval[:unit] == :months
95
+ new_occurrences = 1 + (months_between(end_date, start_date)/new_interval[:length]).to_i
96
+ end
97
+ return {:interval=>new_interval, :duration=>{:start_date=>start_date, :occurrences=>new_occurrences}}
98
+ end
99
+ end
100
+
101
+
102
+ end
103
+ end
@@ -0,0 +1,124 @@
1
+ module RecurringBilling
2
+ class PaypalGateway < RecurringBillingGateway
3
+ include Utils
4
+
5
+ # Returns :paypal
6
+ def code
7
+ :paypal
8
+ end
9
+
10
+ # Returns 'PayPal Website Payments Pro (US)'
11
+ def name
12
+ 'PayPal Website Payments Pro (US)'
13
+ end
14
+
15
+ # Checks whether passed parameters of requested recurring payment conform to specification
16
+ def correct_create?(amount, card, payment_options, recurring_options)
17
+ raise ArgumentError, 'Ammount must be defined and more than zero' if amount.nil? || amount.zero?
18
+ raise ArgumentError, 'Card is mandatory' if card.nil? # must be object of CreditCard class
19
+ raise ArgumentError, 'Subscription name is mandatory' if payment_options[:subscription_name].to_s.empty?
20
+ raise ArgumentError, 'Starting date is mandatory' if recurring_options[:start_date].to_s.empty?
21
+ raise ArgumentError, 'Interval is mandatory' if recurring_options[:interval].to_s.empty?
22
+ # end_date and occurrences - both can be ommited
23
+ return true
24
+ end
25
+
26
+
27
+ # Checks if update is possible using specified arguments
28
+ def correct_update?(billing_id, amount, card, payment_options, recurring_options)
29
+ raise ArgumentError, 'Billing ID is mandatory' if billing_id.to_s.empty?
30
+ raise ArgumentError, 'Starting date cannot be updated' if !recurring_options[:start_date].to_s.empty?
31
+ raise ArgumentError, 'Interval cannot be updated' if !recurring_options[:interval].to_s.empty?
32
+
33
+ if !(recurring_options[:end_date].to_s.empty? && recurring_options[:occurrences].to_s.empty?)
34
+ raise NotImplementedError, 'Cannot shift the end of recurring payment'
35
+ # it is made via "AdditionalBillingCycles", so we have to know previous data
36
+ end
37
+ return true
38
+ end
39
+
40
+ # Create payment using gateway-specific actions
41
+ def create_specific(amount, card, payment_options, recurring_options)
42
+ @last_response = @gateway.recurring(amount, card, convert_options(payment_options, recurring_options))
43
+ return @last_response.params['profile_id'] if @last_response.success?
44
+ nil
45
+ end
46
+
47
+ # Make an update using gateway-specific actions
48
+ def update_specific(billing_id, amount, card, payment_options, recurring_options)
49
+ options = convert_options(payment_options, recurring_options)
50
+ options[:profile_id] = billing_id
51
+ (@last_response = @gateway.recurring(amount, card, options)).success?
52
+ end
53
+
54
+ # Cancel the subscription
55
+ # TODO: Add :note parameter to API to enable it in update and cancel
56
+ def delete_specific(billing_id)
57
+ (@last_response = @gateway.cancel_recurring(billing_id, {})).success?
58
+ end
59
+
60
+ # TODO: Unify result parameters names and values
61
+ def inquiry_specific(billing_id)
62
+ @last_response = @gateway.inquiry_recurring(billing_id)
63
+ result = @last_response.params.clone
64
+
65
+ result.each do |k,v|
66
+ if k =~ /(^number_|_count$|_cycles(_|$)|_payments$|_frequency$|_month$|_year$)/
67
+ result[k] = v.to_i
68
+ elsif k =~ /(_date|^timestamp)$/
69
+ result[k] = DateTime.parse(v)
70
+ elsif (k =~ /(_|^)amount(_paid)?$/ && k != 'auto_bill_outstanding_amount') || k =~ /_balance$/
71
+ currency = result[k+'_currency_id']
72
+ result[k] = Money.new(v.to_f*100, currency=currency) # dollars => cents
73
+ elsif k =~ /_(status|period|card_type)$/
74
+ result[k] = v.downcase
75
+ end
76
+ end
77
+
78
+ result['profile_status'] =~ /^(.*)Profile$/i
79
+ result['profile_status'] = $1 # active | pending | cancelled | suspended | expired
80
+
81
+ return result.reject {|k,v| k =~ /_currency_id$/}
82
+ end
83
+
84
+
85
+ def convert_options(payment_options, recurring_options)
86
+ options = {}
87
+ options[:billing_address] = payment_options[:billing_address] if !payment_options[:billing_address].nil?
88
+ options[:description] = payment_options[:subscription_name]
89
+ options[:starting_at] = recurring_options[:start_date]
90
+ options[:total_payments] = recurring_options[:occurrences] if !recurring_options[:occurrences].nil?
91
+ options[:interval] = convert_interval(recurring_options[:interval]) if !recurring_options[:interval].nil? # absent for update
92
+ options[:currency] = payment_options[:currency] if !payment_options[:currency].nil?
93
+ #options[:note] = payment_options[:note] if !payment_options[:note].nil?
94
+ return options
95
+ end
96
+
97
+
98
+ def convert_interval(interval)
99
+ i_length, i_unit = parse_interval(interval)
100
+
101
+ if i_length == 0.5 && ![:m,:y].include?(i_unit)
102
+ raise ArgumentError, "Semi- interval is not supported to this units (#{i_unit.to_s})"
103
+ end
104
+
105
+ if [i_length, i_unit] == [0.5, :m]
106
+ return {:length => 1, :unit => 'SemiMonth'}
107
+ elsif [i_length, i_unit] == [0.5, :y]
108
+ i_length, i_unit = [6, :m]
109
+ end
110
+
111
+ return {:length => i_length, :unit => convert_unit(i_unit)}
112
+ end
113
+
114
+ def convert_unit(unit)
115
+ return case unit
116
+ when :d then 'Day'
117
+ when :w then 'Week'
118
+ when :m then 'Month'
119
+ when :y then 'Year'
120
+ end
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,130 @@
1
+ require File.dirname(__FILE__) + '/dependencies'
2
+
3
+ # RecurringBilling module provides common API for managing recurring billing operations
4
+ # via remote gateway. All manipulations are done through instances of RecurringBillingGateway
5
+ # and its descendants (though direct use of that class descendants is discouraged).
6
+ #
7
+ # Please see RecurringBillingGateway for more detailed reference.
8
+ module RecurringBilling
9
+
10
+ #:include:recurring_billing.rdoc
11
+ #:include:recurring_billing_extension.rdoc
12
+ class RecurringBillingGateway
13
+ include ActiveMerchant::RequiresParameters
14
+ attr_reader :last_response
15
+
16
+ # Returns code that is used to identify the gateway
17
+ def code
18
+ raise NotImplementedError, 'Method is virtual'
19
+ end
20
+
21
+ # Returns gateway name
22
+ def name
23
+ raise NotImplementedError, 'Method is virtual'
24
+ end
25
+
26
+ # Creates a new recurring billing gateway
27
+ def initialize(options)#:nodoc:
28
+ @gateway = ::ActiveMerchant::Billing::Base.gateway(code).new(
29
+ :login => options[:login],
30
+ :password => options[:password],
31
+ :test => options[:is_test].nil? ? false : options[:is_test], # false by default
32
+ :signature => options[:signature]
33
+ )
34
+ @last_response = nil
35
+ end
36
+
37
+ # Creates a recurring payment
38
+ def create(amount, card, payment_options={}, recurring_options={})
39
+ if correct_create?(amount, card, payment_options, recurring_options)
40
+ create_specific(amount, card, payment_options, recurring_options)
41
+ end
42
+ end
43
+
44
+ # Updates a recurring payment
45
+ def update(billing_id, amount=nil, card=nil, payment_options={}, recurring_options={})
46
+ if correct_update?(billing_id, amount, card, payment_options, recurring_options)
47
+ update_specific(billing_id, amount, card, payment_options, recurring_options)
48
+ end
49
+ end
50
+
51
+ # Deletes a recurring payment
52
+ def delete(billing_id)
53
+ delete_specific(billing_id)
54
+ end
55
+
56
+ # Asks for status of recurring payment
57
+ def inquiry(billing_id)
58
+ inquiry_specific(billing_id)
59
+ end
60
+
61
+ class << self
62
+ # Converts single options hash into hash of parameters used by create|update methods
63
+ #
64
+ # :amount or :billing_amount => amount
65
+ # :card => card
66
+ # :subscription_name, :billing_address, :order, :taxes_amount_included => payment_options
67
+ # :start_date, :interval, :end_date, :trial_end, :occurrences, :trial_occurrences => recurring_options
68
+ def separate_create_update_params_from_options(options)
69
+ payment_options, recurring_options = {}, {}
70
+ amount = options[:billing_amount] unless amount = options[:amount]
71
+ card = options[:card]
72
+ options.each do |k,v|
73
+ payment_options[k] = v if [:subscription_name, :billing_address, :order, :taxes_amount_included].include?(k)
74
+ recurring_options[k] = v if [:start_date, :interval, :end_date, :trial_end, :occurrences, :trial_occurrences, :trial_days, :pay_on_day_x].include?(k)
75
+ end
76
+
77
+ return {:amount => amount, :card => card, :payment_options => payment_options, :recurring_options => recurring_options}
78
+ end
79
+
80
+
81
+ # Returns an instance of RecurringBillingGateway for selected gateway
82
+ #
83
+ # options <= hash of :gateway, :login, :password, :is_test(optional), :signature(optional)
84
+ def get_instance(options)
85
+ raise ArgumentError, ':gateway key is required' unless options.has_key?(:gateway)
86
+
87
+ gateway = RecurringBilling.const_get("#{options[:gateway].to_s.downcase}_gateway".camelize)
88
+ gateway.new(options)
89
+ end
90
+ end
91
+
92
+ ###
93
+ protected
94
+ # Checks whether requested change can be done via simple update (or recreate needed)
95
+ def correct_update?(billing_id, amount, card, payment_options, recurring_options)
96
+ raise NotImplementedError, 'Method is virtual'
97
+ end
98
+
99
+ # Make an update using gateway-specific actions
100
+ def update_specific(billing_id, amount, card, payment_options, recurring_options)
101
+ raise NotImplementedError, 'Method is virtual'
102
+ end
103
+ # Checks whether passed parameters of requested recurring payment conform to specification
104
+ def correct_create?(amount, card, payment_options, recurring_options)
105
+ raise ArgumentError, 'Card must be of ActiveMerchant::Billing::CreditCard' unless card.is_a?(ActiveMerchant::Billing::CreditCard)
106
+ if (!recurring_options) || !(recurring_options.has_key?(:end_date) || recurring_options.has_key?(:occurrences))
107
+ raise StandardError, 'Either payments'' end date or number of payment occurences should be set'
108
+ end
109
+ return true
110
+ end
111
+
112
+ # Creates a recurring payment using gateway-specific actions (virtual)
113
+ def create_specific(amount, card, payment_options, recurring_options)
114
+ raise NotImplementedError, 'Method is virtual'
115
+ end
116
+
117
+ # Deletes a recurring payment
118
+ def delete_specific(billing_id)
119
+ raise NotImplementedError, 'Method is virtual'
120
+ end
121
+
122
+ # Inquires status of given subscription profile on payment gateway.
123
+ def inquiry_specific(billing_id)
124
+ raise NotImplementedError, 'Method is virtual'
125
+ end
126
+
127
+ end
128
+ end
129
+
130
+ require File.dirname(__FILE__) + "/gateways"