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,40 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
require 'money'
|
4
|
+
|
5
|
+
class SubscriptionManagementTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@smc = SubscriptionManagement
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_format_feature
|
12
|
+
feature = {'quantity' => 5, 'feature' => {'name'=> 'Quota', 'unit'=> 'Gigabyte'}}
|
13
|
+
assert_equal 'Quota: 5 Gigabytes', @smc.format_feature(feature)
|
14
|
+
feature = {'quantity' => 1, 'feature' => {'name'=> 'Quota', 'unit'=> 'Gigabyte'}}
|
15
|
+
assert_equal 'Quota: 1 Gigabyte', @smc.format_feature(feature)
|
16
|
+
feature = {'quantity' => 0, 'feature' => {'name'=> 'Quota', 'unit'=> 'Gigabyte'}}
|
17
|
+
assert_equal 'Quota: Unlimited', @smc.format_feature(feature)
|
18
|
+
feature = {'quantity' => 2, 'feature' => {'name'=> 'Users'}}
|
19
|
+
assert_equal 'Users: 2', @smc.format_feature(feature)
|
20
|
+
feature = {'feature' => {'name'=> 'SSL Encryption'}}
|
21
|
+
assert_equal 'SSL Encryption', @smc.format_feature(feature)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_format_periodicity
|
25
|
+
assert_equal 'twice a week', @smc.format_periodicity('0.5 w')
|
26
|
+
assert_equal 'each month', @smc.format_periodicity('1 m')
|
27
|
+
assert_equal 'each 2 years', @smc.format_periodicity('2 y')
|
28
|
+
assert_equal 'each 42 days', @smc.format_periodicity('42 d')
|
29
|
+
assert_raise ArgumentError do; x = @smc.format_periodicity('random'); end;
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_get_taxes_id
|
33
|
+
taxes = {"ca"=>{"country"=>"CA", "taxes"=>[{"tax"=>{"name"=>"Goods and Services Tax"}, "rate"=>0.05}], "state"=>"*"},
|
34
|
+
"us_ca"=>{"country"=>"US", "taxes"=>[{"tax"=>{"name"=>"Sample tax"}, "rate"=>0.2}], "state"=>"CA"}}
|
35
|
+
assert_equal 'ca', @smc.get_taxes_id(taxes, 'CA', 'ON')
|
36
|
+
assert_equal 'us_ca', @smc.get_taxes_id(taxes, 'US', 'CA')
|
37
|
+
assert_raise StandardError do; x = @smc.get_taxes_id(taxes, 'US', 'NY'); end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/tracker/README
ADDED
data/tracker/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require File.dirname(__FILE__) + '/tasks/schema'
|
3
|
+
|
4
|
+
require 'rake/testtask'
|
5
|
+
namespace :test do
|
6
|
+
|
7
|
+
Rake::TestTask.new(:remote_tracker) do |t|
|
8
|
+
t.pattern = 'test/remote/**/*_test.rb'
|
9
|
+
t.ruby_opts << '-rubygems'
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Run all remote tests"
|
14
|
+
task :remote => [:remote_tracker]
|
15
|
+
|
16
|
+
Rake::TestTask.new(:unit_tracker) do |t|
|
17
|
+
t.pattern = 'test/unit/**/*_test.rb'
|
18
|
+
t.ruby_opts << '-rubygems'
|
19
|
+
t.verbose = true
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Run all unit tests"
|
23
|
+
task :unit => [:unit_tracker]
|
24
|
+
|
25
|
+
task :rcov do
|
26
|
+
begin
|
27
|
+
require 'rcov/rcovtask'
|
28
|
+
Rcov::RcovTask.new do |t|
|
29
|
+
t.name = 'test'
|
30
|
+
t.libs << 'test'
|
31
|
+
t.test_files = FileList['test/**/**/*test.rb']
|
32
|
+
t.verbose = true
|
33
|
+
t.rcov_opts = ['-i', '^lib', '-x', 'recurring_billing']
|
34
|
+
end
|
35
|
+
rescue StandardError
|
36
|
+
# ignore missing rcov
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
File without changes
|
data/tracker/demo.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This is a smoke test for Tracker component
|
3
|
+
|
4
|
+
require 'tracker'
|
5
|
+
puts "Verifying models classes..."
|
6
|
+
Transaction
|
7
|
+
RecurringPaymentProfile
|
8
|
+
puts "Verifying DB..."
|
9
|
+
require 'test/connection'
|
10
|
+
Transaction.count
|
11
|
+
RecurringPaymentProfile.find :first
|
12
|
+
puts "All OK"
|
@@ -0,0 +1,134 @@
|
|
1
|
+
class RecurringPaymentProfile < ActiveRecord::Base
|
2
|
+
# We cannot just use `table_name_prefix = "tracker_"' because it uses broken
|
3
|
+
# cattr_accessor and applies to all ActiveRecord::Base descendants when
|
4
|
+
# tracker is loaded from Subscription Manager
|
5
|
+
def self.table_name_prefix
|
6
|
+
"tracker_"
|
7
|
+
end
|
8
|
+
has_many :transactions
|
9
|
+
|
10
|
+
# Returns single payment amount (sum of net amount and taxes)
|
11
|
+
def amount
|
12
|
+
self.net_amount+self.taxes_amount
|
13
|
+
end
|
14
|
+
|
15
|
+
def money
|
16
|
+
Money.new(self.amount, self.currency)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sets net amount
|
20
|
+
def net_money=(x_amount)
|
21
|
+
self[:net_amount] = x_amount.cents
|
22
|
+
if !self[:currency].nil? && x_amount.currency != self[:currency]
|
23
|
+
raise ArgumentError, 'Cannot change currency. Please, clean it first'
|
24
|
+
end
|
25
|
+
self[:currency] = x_amount.currency
|
26
|
+
end
|
27
|
+
|
28
|
+
# Sets taxes amount
|
29
|
+
def taxes_money=(x_amount)
|
30
|
+
self[:taxes_amount] = x_amount.cents
|
31
|
+
if !self[:currency].nil? && x_amount.currency != self[:currency]
|
32
|
+
raise ArgumentError, 'Cannot change currency. Please, clean it first'
|
33
|
+
end
|
34
|
+
self[:currency] = x_amount.currency
|
35
|
+
end
|
36
|
+
|
37
|
+
# Parses card and set related fields.
|
38
|
+
def parse_and_set_card(card, hint=nil)
|
39
|
+
self[:card_type] = card.type
|
40
|
+
number = card.number
|
41
|
+
default_hint = "#{card.first_name} #{card.last_name}, #{card.type.upcase}, #{mask_card_number(number)}, exp. #{card_exp_date(card)}"
|
42
|
+
self[:card_owner_memo] = (hint) ? hint : default_hint
|
43
|
+
end
|
44
|
+
|
45
|
+
# Masks card number (only last 4 digits are shown)
|
46
|
+
#
|
47
|
+
# Example:
|
48
|
+
# '031641616161' => 'XXXXXXXX6161'
|
49
|
+
def mask_card_number(number)
|
50
|
+
number.to_s.size < 5 ? number.to_s : (('X' * number.to_s[0..-5].length) + number.to_s[-4..-1])
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns formatted expiration date for given Card object
|
54
|
+
def card_exp_date(card)
|
55
|
+
'%04d-%02d' % [card.year, card.month]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns formatted amount for current record
|
59
|
+
def money_formatted
|
60
|
+
'%.2f %s' % [self.amount/100.00, self.currency.upcase]
|
61
|
+
end
|
62
|
+
|
63
|
+
def net_money_formatted
|
64
|
+
'%.2f %s' % [self.net_amount/100.00, self.currency.upcase]
|
65
|
+
end
|
66
|
+
|
67
|
+
def taxes_money_formatted
|
68
|
+
'%.2f %s' % [self.taxes_amount/100.00, self.currency.upcase]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Marks current profile as active and saves it
|
72
|
+
def set_profile_active_and_save!
|
73
|
+
self.status = 'active'
|
74
|
+
self.save
|
75
|
+
end
|
76
|
+
|
77
|
+
# Updates profile fields after it was UPDATEd via remote gateway
|
78
|
+
def set_profile_after_update!(amount, card, payment_options, recurring_options)
|
79
|
+
|
80
|
+
if amount
|
81
|
+
# Split amount into net/taxes
|
82
|
+
if payment_options.has_key?(:taxes_amount_included)
|
83
|
+
self.net_money = amount - payment_options[:taxes_amount_included]
|
84
|
+
self.taxes_money = payment_options[:taxes_amount_included]
|
85
|
+
payment_options.delete(:taxes_amount_included)
|
86
|
+
else
|
87
|
+
self.net_money = amount
|
88
|
+
self.taxes_amount = 0
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
self.parse_and_set_card(card) if card
|
93
|
+
self.subscription_name = payment_options[:subscription_name] if payment_options[:subscription_name]
|
94
|
+
unless (ro = recurring_options).empty?
|
95
|
+
self.pay_on_day_x = ro[:pay_on_day_x] unless ro[:pay_on_day_x].nil?
|
96
|
+
end
|
97
|
+
self.save
|
98
|
+
end
|
99
|
+
|
100
|
+
# Updates profile fields after it was INQUIREDd via remote gateway
|
101
|
+
def set_profile_after_inquiry!(result)
|
102
|
+
self.status = result['profile_status']
|
103
|
+
self.outstanding_balance = result['outstanding_balance'].cents
|
104
|
+
self.complete_payments_count = result['number_cycles_completed']
|
105
|
+
self.failed_payments_count = result['failed_payment_count']
|
106
|
+
self.remaining_payments_count = result['number_cycles_remaining']
|
107
|
+
self.last_synchronized_at = DateTime.now
|
108
|
+
self.save
|
109
|
+
end
|
110
|
+
|
111
|
+
# Updates given hash from current profile fields
|
112
|
+
def update_options_from_profile!(options)
|
113
|
+
options[:subscription_name] ||= self[:subscription_name]
|
114
|
+
options[:amount] ||= Money.new(self.amount, self[:currency])
|
115
|
+
options[:taxes_amount_included] ||= Money.new(self.taxes_amount, self[:currency])
|
116
|
+
options[:interval] ||= self[:periodicity]
|
117
|
+
options[:start_date] ||= (Date.new(self[:created_at].year,self[:created_at].month, self[:created_at].mday) + self[:trial_days].to_i)
|
118
|
+
unless options[:trial_days]
|
119
|
+
trial_days = options[:start_date] - Date.today
|
120
|
+
options[:trial_days] = trial_days if trial_days > 0
|
121
|
+
end
|
122
|
+
self[:complete_payments_count] = 0 unless self[:complete_payments_count].to_i > 0
|
123
|
+
options[:occurrences] = self[:total_payments_count] - self[:complete_payments_count]
|
124
|
+
options[:pay_on_day_x] ||= self[:pay_on_day_x]
|
125
|
+
end
|
126
|
+
|
127
|
+
# Marks profile as deleted
|
128
|
+
def mark_as_deleted!
|
129
|
+
self[:deleted_at] = Time.now
|
130
|
+
self[:status] = 'deleted'
|
131
|
+
self.save
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Transaction < ActiveRecord::Base
|
2
|
+
# We cannot just use `table_name_prefix = "tracker_"' because it uses broken
|
3
|
+
# cattr_accessor and applies to all ActiveRecord::Base descendants when
|
4
|
+
# tracker is loaded from Subscription Manager
|
5
|
+
def self.table_name_prefix
|
6
|
+
"tracker_"
|
7
|
+
end
|
8
|
+
|
9
|
+
belongs_to :recurring_payment_profile
|
10
|
+
|
11
|
+
def money
|
12
|
+
Money.new(self.amount, self.currency)
|
13
|
+
end
|
14
|
+
|
15
|
+
def money_formatted
|
16
|
+
'%.2f %s' % [self.amount/100.00, self.currency.upcase]
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../recurring_billing/lib/recurring_billing'
|
2
|
+
|
3
|
+
module RecurringBilling
|
4
|
+
class RecurringBillingGateway
|
5
|
+
|
6
|
+
# Create recurring payment AND store it
|
7
|
+
def create_with_persist(amount, card, payment_options={}, recurring_options={})
|
8
|
+
if payment_id = create_without_persist(amount, card, payment_options, recurring_options)
|
9
|
+
profile = RecurringPaymentProfile.new
|
10
|
+
profile.gateway_reference = payment_id
|
11
|
+
profile.gateway = code().to_s.upcase
|
12
|
+
profile.subscription_name = payment_options[:subscription_name]
|
13
|
+
|
14
|
+
# Split amount into net/taxes
|
15
|
+
if payment_options.has_key?(:taxes_amount_included)
|
16
|
+
profile.net_money = amount - payment_options[:taxes_amount_included]
|
17
|
+
profile.taxes_money = payment_options[:taxes_amount_included]
|
18
|
+
payment_options.delete(:taxes_amount_included)
|
19
|
+
else
|
20
|
+
profile.net_money = amount
|
21
|
+
profile.taxes_amount = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
profile.parse_and_set_card(card)
|
25
|
+
ro = recurring_options
|
26
|
+
profile.trial_days = trial_days = ro[:trial_days].nil? ? 0 : ro[:trial_days].to_i
|
27
|
+
profile.pay_on_day_x = ro[:pay_on_day_x] unless ro[:pay_on_day_x].nil?
|
28
|
+
start_date = ro[:start_date] - trial_days
|
29
|
+
if get_midnight(start_date) == get_midnight(DateTime.now)
|
30
|
+
profile.created_at = start_date
|
31
|
+
else
|
32
|
+
profile.created_at = get_midnight(start_date)
|
33
|
+
end
|
34
|
+
profile.periodicity = '%d %s' % parse_interval(ro[:interval])
|
35
|
+
if ro[:occurrences].nil?
|
36
|
+
profile.total_payments_count = get_occurrences(ro[:start_date] - trial_days, ro[:interval], ro[:end_date])
|
37
|
+
else
|
38
|
+
profile.total_payments_count = ro[:occurrences]
|
39
|
+
end
|
40
|
+
profile.set_profile_active_and_save!
|
41
|
+
return payment_id
|
42
|
+
end
|
43
|
+
end
|
44
|
+
alias_method_chain :create, :persist
|
45
|
+
|
46
|
+
# Update recurring payment AND its local info
|
47
|
+
def update_with_persist(billing_id, amount, card, payment_options={}, recurring_options={})
|
48
|
+
profile = RecurringPaymentProfile.find_by_gateway_reference(billing_id)
|
49
|
+
#TODO: change to custom error
|
50
|
+
raise StandardError, 'Cannot update a deleted profile (#{billing_id})' if profile.status == 'deleted'
|
51
|
+
|
52
|
+
if update_without_persist(billing_id, amount, card, payment_options, recurring_options)
|
53
|
+
profile.set_profile_after_update!(amount, card, payment_options, recurring_options)
|
54
|
+
return true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
alias_method_chain :update, :persist
|
58
|
+
|
59
|
+
# Inquire about recurring payment AND update its info
|
60
|
+
def inquiry_with_persist(billing_id)
|
61
|
+
result = inquiry_without_persist(billing_id)
|
62
|
+
RecurringPaymentProfile.find_by_gateway_reference(billing_id).set_profile_after_inquiry!(result)
|
63
|
+
return result
|
64
|
+
end
|
65
|
+
alias_method_chain :inquiry, :persist
|
66
|
+
|
67
|
+
# Cancel recurring payment AND update its info
|
68
|
+
def delete_with_persist(billing_id)
|
69
|
+
if delete_without_persist(billing_id)
|
70
|
+
RecurringPaymentProfile.find_by_gateway_reference(billing_id).mark_as_deleted!
|
71
|
+
return true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
alias_method_chain :delete, :persist
|
75
|
+
|
76
|
+
# Tells if update or recreate is needed
|
77
|
+
def can_update?(billing_id, options)
|
78
|
+
begin
|
79
|
+
options = self.class.separate_create_update_params_from_options(options)
|
80
|
+
can_update = correct_update?(billing_id, options[:amount], options[:card], options[:payment_options], options[:recurring_options])
|
81
|
+
rescue Exception
|
82
|
+
can_update = false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Updates or if updating is impossible, recreates profile with specified billing_id
|
87
|
+
def update_or_recreate(billing_id, options)
|
88
|
+
if can_update?(billing_id, options)
|
89
|
+
options_separated = self.class.separate_create_update_params_from_options(options)
|
90
|
+
update(billing_id, options_separated[:amount], options_separated[:card], options_separated[:payment_options], options_separated[:recurring_options])
|
91
|
+
return billing_id
|
92
|
+
else
|
93
|
+
RecurringPaymentProfile.find_by_gateway_reference(billing_id).update_options_from_profile!(options)
|
94
|
+
options_separated = self.class.separate_create_update_params_from_options(options)
|
95
|
+
correct_create?(options_separated[:amount], options_separated[:card], options_separated[:payment_options], options_separated[:recurring_options])
|
96
|
+
delete(billing_id)
|
97
|
+
return create(options_separated[:amount], options_separated[:card], options_separated[:payment_options], options_separated[:recurring_options])
|
98
|
+
end
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
== Tracker module methods
|
3
|
+
Tracker module provides payment profiles local storage capability for simple CRUD
|
4
|
+
API RecurringBilling is. As a result, every RecurringBilling action is accompanied by number of Tracker database operations.
|
5
|
+
Tracker auto-includes RecurringBilling
|
6
|
+
and replaces its create, update, inquiry and delete methods with its own extended implementations while re-aliasing old ones as
|
7
|
+
XXX_without_persist. The usual behavior of replacement methods is to execute corresponding old RecurringBilling method and,
|
8
|
+
on success, to store given or returned parameters into database. Extending the methods doesn't change the syntax. Also, several
|
9
|
+
new methods are added to RecurringBilling class.
|
10
|
+
|
11
|
+
=== Updating exising remote payment
|
12
|
+
The update method of RecurringBilling is limited by design: if updating payment information on remote gateway is impossible
|
13
|
+
with given set of options, update raises Exception (and quits). Given that the main reason of such behavior is that RecurringBilling
|
14
|
+
instance methods are isolated from each other payment-wise, Tracker combination of storage and gateway interaction provides an additional
|
15
|
+
option to handle such situations. If RecurringBilling returns an Exception (meaning update is impossible), the recurring payment on
|
16
|
+
gateway is cancelled and another, with updated options is created instead. For example, that way limitations on changing
|
17
|
+
:recurring_options could be overcame.
|
18
|
+
|
19
|
+
Two related methods are available for this feature. Both use payment gateway reference ID (+billing_id+) and combined options
|
20
|
+
hash (+options+) as parameters. Second parameter may be obtained from usual quad-element structure something like this:
|
21
|
+
options = {}
|
22
|
+
options[:amount] = amount
|
23
|
+
options[:card] = card
|
24
|
+
options.update(payment_options)
|
25
|
+
options.update(recurring_options)
|
26
|
+
To check if recurring payment could be updated by usual RecurringBilling means, can_update? method is used. This check is integrated into
|
27
|
+
update_or_recreate method that calls can_update? and then performs traditional update, or deletes and then re-creates the payment via gateway.
|
28
|
+
For example:
|
29
|
+
...
|
30
|
+
options = {:start_date => Date + 1337}
|
31
|
+
print 'Warning! The billing profile will be re-created} if gateway.can_update?(billing_id, options)
|
32
|
+
gateway.update_or_recreate(billing_id, options)
|
33
|
+
...
|
34
|
+
Please note that missing but required options for payment re-creation are calculated from database.
|
@@ -0,0 +1,66 @@
|
|
1
|
+
namespace :tracker do
|
2
|
+
desc 'Create Tracker database tables'
|
3
|
+
task :create_tables => :connection do
|
4
|
+
ActiveRecord::Base.connection.create_table :tracker_recurring_payment_profiles, :force => true do |t|
|
5
|
+
t.column :gateway_reference, :string, :unique => true
|
6
|
+
t.column :gateway, :string
|
7
|
+
t.column :subscription_name, :text
|
8
|
+
t.column :description, :text
|
9
|
+
t.column :currency, :string
|
10
|
+
t.column :net_amount, :integer, :null => false
|
11
|
+
t.column :taxes_amount, :integer, :null => false
|
12
|
+
t.column :outstanding_balance, :integer
|
13
|
+
t.column :total_payments_count, :integer
|
14
|
+
t.column :complete_payments_count, :integer
|
15
|
+
t.column :failed_payments_count, :integer
|
16
|
+
t.column :remaining_payments_count, :integer
|
17
|
+
t.column :periodicity, :string
|
18
|
+
t.column :trial_days, :integer, :default => 0
|
19
|
+
t.column :pay_on_day_x, :integer, :default => 0
|
20
|
+
t.column :status, :string
|
21
|
+
t.column :problem_status, :string
|
22
|
+
t.column :card_type, :string
|
23
|
+
t.column :card_owner_memo, :string
|
24
|
+
t.column :created_at, :datetime
|
25
|
+
t.column :updated_at, :datetime
|
26
|
+
t.column :deleted_at, :datetime
|
27
|
+
t.column :last_synchronized_at, :datetime
|
28
|
+
end
|
29
|
+
ActiveRecord::Base.connection.add_index :tracker_recurring_payment_profiles, [ :gateway ], :name => 'ix_tracker_recurring_payment_profiles_gateway'
|
30
|
+
ActiveRecord::Base.connection.add_index :tracker_recurring_payment_profiles, [ :gateway_reference ], :unique => true, :name => 'uix_tracker_recurring_payment_profiles_gateway_reference'
|
31
|
+
ActiveRecord::Base.connection.create_table :tracker_transactions, :force => true do |t|
|
32
|
+
t.column :recurring_payment_profile_id, :integer
|
33
|
+
t.column :gateway_reference, :string
|
34
|
+
t.column :currency, :string
|
35
|
+
t.column :amount, :integer
|
36
|
+
t.column :result_code, :string
|
37
|
+
t.column :result_text, :string
|
38
|
+
t.column :card_type, :string
|
39
|
+
t.column :card_owner_memo, :string
|
40
|
+
t.column :created_at, :datetime
|
41
|
+
t.column :recorded_at, :datetime
|
42
|
+
end
|
43
|
+
ActiveRecord::Base.connection.add_index :tracker_transactions, [ :recurring_payment_profile_id ]
|
44
|
+
end
|
45
|
+
|
46
|
+
desc 'Drop Tracker database tables'
|
47
|
+
task :drop_tables => :connection do
|
48
|
+
ActiveRecord::Base.connection.drop_table :tracker_recurring_payment_profiles
|
49
|
+
ActiveRecord::Base.connection.drop_table :tracker_transactions
|
50
|
+
end
|
51
|
+
|
52
|
+
# Use Rails connection when appropriate or fallback to local test db
|
53
|
+
task :connection do
|
54
|
+
connected = false
|
55
|
+
begin
|
56
|
+
begin
|
57
|
+
connected = true if ActiveRecord::Base.connection
|
58
|
+
rescue ActiveRecord::ConenctionNotEstablished
|
59
|
+
end
|
60
|
+
rescue NameError # ActiveRecord not loaded
|
61
|
+
end
|
62
|
+
require File.dirname(__FILE__) + "/../test/connection" unless connected
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|