servicemerchant 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE.txt +20 -0
- data/README.txt +231 -0
- data/Rakefile +122 -0
- data/demo.rb +69 -0
- data/recurring_billing/lib/am_extensions.rb +1 -0
- data/recurring_billing/lib/am_extensions/paypal_extension.rb +170 -0
- data/recurring_billing/lib/dependencies.rb +14 -0
- data/recurring_billing/lib/gateways.rb +5 -0
- data/recurring_billing/lib/gateways/authorize_net.rb +103 -0
- data/recurring_billing/lib/gateways/paypal.rb +124 -0
- data/recurring_billing/lib/recurring_billing.rb +130 -0
- data/recurring_billing/lib/recurring_billing.rdoc +87 -0
- data/recurring_billing/lib/utils.rb +81 -0
- data/recurring_billing/test/fixtures.yml +33 -0
- data/recurring_billing/test/remote/authorize_net_test.rb +36 -0
- data/recurring_billing/test/remote/paypal_test.rb +46 -0
- data/recurring_billing/test/remote/recurring_billing_test.rb +41 -0
- data/recurring_billing/test/test_helper.rb +153 -0
- data/recurring_billing/test/unit/authorize_net_gateway_class_test.rb +42 -0
- data/recurring_billing/test/unit/paypal_gateway_class_test.rb +23 -0
- data/recurring_billing/test/unit/recurring_billing_gateway_class_test.rb +35 -0
- data/recurring_billing/test/unit/utils_test.rb +17 -0
- data/subscription_management/Rakefile +29 -0
- data/subscription_management/lib/models/subscription.rb +9 -0
- data/subscription_management/lib/models/subscription_profile.rb +4 -0
- data/subscription_management/lib/subscription_management.rb +326 -0
- data/subscription_management/samples/backpack.yml +101 -0
- data/subscription_management/samples/basecamp.yml +71 -0
- data/subscription_management/samples/brainkeeper.yml +90 -0
- data/subscription_management/samples/campfire.yml +74 -0
- data/subscription_management/samples/clickandpledge.yml +24 -0
- data/subscription_management/samples/demo.rb +19 -0
- data/subscription_management/samples/elm.yml +174 -0
- data/subscription_management/samples/freshbooks.yml +78 -0
- data/subscription_management/samples/highrise.yml +100 -0
- data/subscription_management/samples/presets.yml +10 -0
- data/subscription_management/samples/tariff.outline.yml +0 -0
- data/subscription_management/samples/taxes.yml +21 -0
- data/subscription_management/subscription_management.rb +7 -0
- data/subscription_management/tasks/schema.rb +50 -0
- data/subscription_management/test/connection.rb +10 -0
- data/subscription_management/test/remote/subscription_management_test.rb +112 -0
- data/subscription_management/test/test_helper.rb +84 -0
- data/subscription_management/test/unit/subscription_management_test.rb +40 -0
- data/tracker/README +12 -0
- data/tracker/Rakefile +40 -0
- data/tracker/db/migrations/empty-directory +0 -0
- data/tracker/demo.rb +12 -0
- data/tracker/lib/models/recurring_payment_profile.rb +134 -0
- data/tracker/lib/models/transaction.rb +19 -0
- data/tracker/lib/recurring_billing_extension.rb +103 -0
- data/tracker/lib/recurring_billing_extension.rdoc +34 -0
- data/tracker/tasks/schema.rb +66 -0
- data/tracker/test/connection.rb +10 -0
- data/tracker/test/recurring_payment_profile.rb +35 -0
- data/tracker/test/remote/authorize_net_test.rb +68 -0
- data/tracker/test/remote/paypal_test.rb +115 -0
- data/tracker/test/test_helper.rb +87 -0
- data/tracker/test/unit/recurring_payment_profile_test.rb +62 -0
- data/tracker/tracker.rb +10 -0
- data/vendor/money-1.7.1/MIT-LICENSE +20 -0
- data/vendor/money-1.7.1/README +75 -0
- data/vendor/money-1.7.1/lib/bank/no_exchange_bank.rb +9 -0
- data/vendor/money-1.7.1/lib/bank/variable_exchange_bank.rb +30 -0
- data/vendor/money-1.7.1/lib/money.rb +29 -0
- data/vendor/money-1.7.1/lib/money/core_extensions.rb +26 -0
- data/vendor/money-1.7.1/lib/money/money.rb +209 -0
- data/vendor/money-1.7.1/lib/support/cattr_accessor.rb +57 -0
- 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,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"
|