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,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
|
+
|