servicemerchant 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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"