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.
- 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"
|