killbill 3.0.0 → 3.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Jarfile +5 -5
- data/NEWS +4 -0
- data/README.md +45 -8
- data/VERSION +1 -1
- data/generators/active_merchant/active_merchant_generator.rb +38 -0
- data/generators/active_merchant/templates/.gitignore.rb +36 -0
- data/generators/active_merchant/templates/.travis.yml.rb +19 -0
- data/generators/active_merchant/templates/Gemfile.rb +3 -0
- data/generators/active_merchant/templates/Jarfile.rb +6 -0
- data/generators/active_merchant/templates/LICENSE.rb +201 -0
- data/generators/active_merchant/templates/NEWS.rb +2 -0
- data/generators/active_merchant/templates/Rakefile.rb +30 -0
- data/generators/active_merchant/templates/VERSION.rb +1 -0
- data/generators/active_merchant/templates/config.ru.rb +4 -0
- data/generators/active_merchant/templates/config.yml.rb +13 -0
- data/generators/active_merchant/templates/db/ddl.sql.rb +64 -0
- data/generators/active_merchant/templates/db/schema.rb +64 -0
- data/generators/active_merchant/templates/killbill.properties.rb +3 -0
- data/generators/active_merchant/templates/lib/api.rb +119 -0
- data/generators/active_merchant/templates/lib/application.rb +84 -0
- data/generators/active_merchant/templates/lib/models/payment_method.rb +22 -0
- data/generators/active_merchant/templates/lib/models/response.rb +22 -0
- data/generators/active_merchant/templates/lib/models/transaction.rb +11 -0
- data/generators/active_merchant/templates/lib/plugin.rb +23 -0
- data/generators/active_merchant/templates/lib/private_api.rb +6 -0
- data/generators/active_merchant/templates/lib/views/form.erb +8 -0
- data/generators/active_merchant/templates/plugin.gemspec.rb +48 -0
- data/generators/active_merchant/templates/pom.xml.rb +44 -0
- data/generators/active_merchant/templates/release.sh.rb +41 -0
- data/generators/active_merchant/templates/spec/base_plugin_spec.rb +30 -0
- data/generators/active_merchant/templates/spec/integration_spec.rb +31 -0
- data/generators/active_merchant/templates/spec/spec_helper.rb +24 -0
- data/generators/killbill_generator.rb +38 -0
- data/killbill.gemspec +10 -2
- data/lib/killbill/gen/api/block.rb +82 -0
- data/lib/killbill/gen/api/direct_payment.rb +176 -0
- data/lib/killbill/gen/api/direct_payment_api.rb +329 -0
- data/lib/killbill/gen/api/direct_payment_transaction.rb +156 -0
- data/lib/killbill/gen/api/fixed.rb +63 -0
- data/lib/killbill/gen/api/invoice_item.rb +7 -1
- data/lib/killbill/gen/api/invoice_item_formatter.rb +7 -1
- data/lib/killbill/gen/api/invoice_user_api.rb +18 -136
- data/lib/killbill/gen/api/migration_plan.rb +6 -6
- data/lib/killbill/gen/api/payment_api.rb +216 -54
- data/lib/killbill/gen/api/payment_method_plugin.rb +3 -3
- data/lib/killbill/gen/api/plan.rb +6 -6
- data/lib/killbill/gen/api/plan_phase.rb +16 -23
- data/lib/killbill/gen/api/plugin_property.rb +71 -0
- data/lib/killbill/gen/api/recurring.rb +63 -0
- data/lib/killbill/gen/api/require_gen.rb +10 -1
- data/lib/killbill/gen/api/static_catalog.rb +8 -1
- data/lib/killbill/gen/api/tier.rb +77 -0
- data/lib/killbill/gen/api/tiered_block.rb +88 -0
- data/lib/killbill/gen/api/usage.rb +111 -0
- data/lib/killbill/gen/api/usage_user_api.rb +59 -3
- data/lib/killbill/gen/plugin-api/billing_address.rb +85 -0
- data/lib/killbill/gen/plugin-api/customer.rb +73 -0
- data/lib/killbill/gen/plugin-api/hosted_payment_page_descriptor_fields.rb +145 -0
- data/lib/killbill/gen/plugin-api/hosted_payment_page_form_descriptor.rb +80 -0
- data/lib/killbill/gen/plugin-api/hosted_payment_page_notification.rb +129 -0
- data/lib/killbill/gen/plugin-api/payment_info_plugin.rb +20 -1
- data/lib/killbill/gen/plugin-api/payment_plugin_api.rb +358 -39
- data/lib/killbill/gen/plugin-api/refund_info_plugin.rb +20 -1
- data/lib/killbill/gen/plugin-api/require_gen.rb +3 -0
- data/lib/killbill/helpers/active_merchant.rb +21 -0
- data/lib/killbill/helpers/active_merchant/active_record.rb +17 -0
- data/lib/killbill/helpers/active_merchant/active_record/models/helpers.rb +25 -0
- data/lib/killbill/helpers/active_merchant/active_record/models/payment_method.rb +195 -0
- data/lib/killbill/helpers/active_merchant/active_record/models/response.rb +178 -0
- data/lib/killbill/helpers/active_merchant/active_record/models/streamy_result_set.rb +35 -0
- data/lib/killbill/helpers/active_merchant/active_record/models/transaction.rb +63 -0
- data/lib/killbill/helpers/active_merchant/configuration.rb +54 -0
- data/lib/killbill/helpers/active_merchant/core_ext.rb +41 -0
- data/lib/killbill/helpers/active_merchant/gateway.rb +35 -0
- data/lib/killbill/helpers/active_merchant/killbill_spec_helper.rb +117 -0
- data/lib/killbill/helpers/active_merchant/payment_plugin.rb +365 -0
- data/lib/killbill/helpers/active_merchant/private_payment_plugin.rb +119 -0
- data/lib/killbill/helpers/active_merchant/properties.rb +20 -0
- data/lib/killbill/helpers/active_merchant/sinatra.rb +30 -0
- data/lib/killbill/helpers/active_merchant/utils.rb +23 -0
- data/lib/killbill/payment.rb +22 -10
- data/script/generate +15 -0
- data/spec/killbill/helpers/payment_method_spec.rb +101 -0
- data/spec/killbill/helpers/response_spec.rb +74 -0
- data/spec/killbill/helpers/test_schema.rb +57 -0
- data/spec/killbill/helpers/utils_spec.rb +22 -0
- data/spec/killbill/payment_plugin_api_spec.rb +23 -18
- data/spec/killbill/payment_plugin_spec.rb +11 -10
- data/spec/killbill/payment_test.rb +9 -9
- data/spec/spec_helper.rb +8 -3
- metadata +130 -5
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Killbill
|
|
2
|
+
module Plugin
|
|
3
|
+
module ActiveMerchant
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
require 'active_record'
|
|
6
|
+
|
|
7
|
+
# Closest from a streaming API as we can get with ActiveRecord
|
|
8
|
+
class StreamyResultSet
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
def initialize(limit, batch_size = 100, &delegate)
|
|
12
|
+
@limit = limit
|
|
13
|
+
@batch = [batch_size, limit].min
|
|
14
|
+
@delegate = delegate
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def each(&block)
|
|
18
|
+
(0..(@limit - @batch)).step(@batch) do |i|
|
|
19
|
+
result = @delegate.call(i, @batch)
|
|
20
|
+
block.call(result)
|
|
21
|
+
# Optimization: bail out if no more results
|
|
22
|
+
break if result.nil? || result.empty?
|
|
23
|
+
end if @batch > 0
|
|
24
|
+
# Make sure to return DB connections to the Pool
|
|
25
|
+
::ActiveRecord::Base.connection.close
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_a
|
|
29
|
+
super.to_a.flatten
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Killbill
|
|
2
|
+
module Plugin
|
|
3
|
+
module ActiveMerchant
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
require 'active_record'
|
|
6
|
+
|
|
7
|
+
class Transaction < ::ActiveRecord::Base
|
|
8
|
+
|
|
9
|
+
self.abstract_class = true
|
|
10
|
+
|
|
11
|
+
def self.authorization_from_kb_payment_id(kb_payment_id)
|
|
12
|
+
transaction_from_kb_payment_id :authorize, kb_payment_id, :single
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.charge_from_kb_payment_id(kb_payment_id)
|
|
16
|
+
transaction_from_kb_payment_id :charge, kb_payment_id, :single
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.refunds_from_kb_payment_id(kb_payment_id)
|
|
20
|
+
transaction_from_kb_payment_id :refund, kb_payment_id, :multiple
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.find_candidate_transaction_for_refund(kb_payment_id, amount_in_cents)
|
|
24
|
+
begin
|
|
25
|
+
do_find_candidate_transaction_for_refund :authorize, kb_payment_id, amount_in_cents
|
|
26
|
+
rescue
|
|
27
|
+
do_find_candidate_transaction_for_refund :charge, kb_payment_id, amount_in_cents
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.do_find_candidate_transaction_for_refund(api_call, kb_payment_id, amount_in_cents)
|
|
32
|
+
# Find one successful charge which amount is at least the amount we are trying to refund
|
|
33
|
+
transactions = where('amount_in_cents >= ? AND api_call = ? and kb_payment_id = ?', amount_in_cents, api_call, kb_payment_id)
|
|
34
|
+
raise "Unable to find transaction for payment #{kb_payment_id} and api_call #{api_call}" if transactions.size == 0
|
|
35
|
+
|
|
36
|
+
# We have candidates, but we now need to make sure we didn't refund more than for the specified amount
|
|
37
|
+
amount_refunded_in_cents = where('api_call = ? and kb_payment_id = ?', :refund, kb_payment_id)
|
|
38
|
+
.sum('amount_in_cents')
|
|
39
|
+
|
|
40
|
+
amount_left_to_refund_in_cents = -amount_refunded_in_cents
|
|
41
|
+
transactions.map { |transaction| amount_left_to_refund_in_cents += transaction.amount_in_cents }
|
|
42
|
+
raise "Amount #{amount_in_cents} too large to refund for payment #{kb_payment_id}" if amount_left_to_refund_in_cents < amount_in_cents
|
|
43
|
+
|
|
44
|
+
transactions.first
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def self.transaction_from_kb_payment_id(api_call, kb_payment_id, how_many)
|
|
50
|
+
transactions = where('api_call = ? and kb_payment_id = ?', api_call, kb_payment_id)
|
|
51
|
+
raise "Unable to find transaction id for payment #{kb_payment_id}" if transactions.empty?
|
|
52
|
+
if how_many == :single
|
|
53
|
+
raise "Kill Bill payment #{kb_payment_id} mapping to multiple plugin transactions" if transactions.size > 1
|
|
54
|
+
transactions[0]
|
|
55
|
+
else
|
|
56
|
+
transactions
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module Killbill
|
|
4
|
+
module Plugin
|
|
5
|
+
module ActiveMerchant
|
|
6
|
+
mattr_reader :config
|
|
7
|
+
mattr_reader :currency_conversions
|
|
8
|
+
mattr_reader :gateway
|
|
9
|
+
mattr_reader :initialized
|
|
10
|
+
mattr_reader :kb_apis
|
|
11
|
+
mattr_reader :logger
|
|
12
|
+
mattr_reader :test
|
|
13
|
+
|
|
14
|
+
def self.initialize!(gateway_builder, gateway_name, logger, config_file, kb_apis)
|
|
15
|
+
@@config = Properties.new(config_file)
|
|
16
|
+
@@config.parse!
|
|
17
|
+
|
|
18
|
+
@@currency_conversions = @@config[:currency_conversions]
|
|
19
|
+
@@kb_apis = kb_apis
|
|
20
|
+
@@test = @@config[gateway_name][:test]
|
|
21
|
+
|
|
22
|
+
@@gateway = Gateway.wrap(gateway_builder, @@config[gateway_name.to_sym])
|
|
23
|
+
|
|
24
|
+
@@logger = logger
|
|
25
|
+
@@logger.log_level = Logger::DEBUG if (@@config[:logger] || {})[:debug]
|
|
26
|
+
|
|
27
|
+
if defined?(JRUBY_VERSION)
|
|
28
|
+
begin
|
|
29
|
+
# See https://github.com/jruby/activerecord-jdbc-adapter/issues/302
|
|
30
|
+
require 'jdbc/mysql'
|
|
31
|
+
::Jdbc::MySQL.load_driver(:require) if ::Jdbc::MySQL.respond_to?(:load_driver)
|
|
32
|
+
rescue => e
|
|
33
|
+
@@logger.warn "Unable to load the JDBC driver: #{e}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
require 'active_record'
|
|
39
|
+
::ActiveRecord::Base.establish_connection(@@config[:database])
|
|
40
|
+
::ActiveRecord::Base.logger = @@logger
|
|
41
|
+
rescue => e
|
|
42
|
+
@@logger.warn "Unable to establish a database connection: #{e}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@@initialized = true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.converted_currency(currency)
|
|
49
|
+
currency_sym = currency.to_s.upcase.to_sym
|
|
50
|
+
@@currency_conversions && @@currency_conversions[currency_sym]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Thank you Rails!
|
|
2
|
+
class String
|
|
3
|
+
def camelize(uppercase_first_letter = true)
|
|
4
|
+
string = to_s
|
|
5
|
+
if uppercase_first_letter
|
|
6
|
+
string = string.sub(/^[a-z\d]*/) { $&.capitalize }
|
|
7
|
+
else
|
|
8
|
+
string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase }
|
|
9
|
+
end
|
|
10
|
+
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
|
|
11
|
+
string.gsub!('/', '::')
|
|
12
|
+
string
|
|
13
|
+
end unless respond_to?(:camelize)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Integer
|
|
17
|
+
def base(b)
|
|
18
|
+
self < b ? [self] : (self/b).base(b) + [self%b]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
require 'securerandom'
|
|
24
|
+
SecureRandom.uuid
|
|
25
|
+
rescue LoadError, NoMethodError
|
|
26
|
+
# See http://jira.codehaus.org/browse/JRUBY-6176
|
|
27
|
+
module SecureRandom
|
|
28
|
+
def self.uuid
|
|
29
|
+
ary = self.random_bytes(16).unpack("NnnnnN")
|
|
30
|
+
ary[2] = (ary[2] & 0x0fff) | 0x4000
|
|
31
|
+
ary[3] = (ary[3] & 0x3fff) | 0x8000
|
|
32
|
+
"%08x-%04x-%04x-%04x-%04x%08x" % ary
|
|
33
|
+
end unless respond_to?(:uuid)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Object
|
|
38
|
+
def blank?
|
|
39
|
+
respond_to?(:empty?) ? empty? : !self
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Killbill
|
|
2
|
+
module Plugin
|
|
3
|
+
module ActiveMerchant
|
|
4
|
+
require 'active_merchant'
|
|
5
|
+
|
|
6
|
+
class Gateway
|
|
7
|
+
def self.wrap(gateway_builder, config)
|
|
8
|
+
if config[:test]
|
|
9
|
+
::ActiveMerchant::Billing::Base.mode = :test
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
if config[:log_file]
|
|
13
|
+
::ActiveMerchant::Billing::Gateway.wiredump_device = File.open(config[:log_file], 'w')
|
|
14
|
+
::ActiveMerchant::Billing::Gateway.wiredump_device.sync = true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Gateway.new(gateway_builder.call(config))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(am_gateway)
|
|
21
|
+
@gateway = am_gateway
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Unfortunate name...
|
|
25
|
+
def capture(money, authorization, options = {})
|
|
26
|
+
@gateway.capture(money, authorization, options)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def method_missing(m, *args, &block)
|
|
30
|
+
@gateway.send(m, *args, &block)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Killbill
|
|
2
|
+
module Plugin
|
|
3
|
+
module ActiveMerchant
|
|
4
|
+
module RSpec
|
|
5
|
+
|
|
6
|
+
def create_payment_method(payment_method_model=::Killbill::Plugin::ActiveMerchant::ActiveRecord::PaymentMethod, kb_account_id=nil)
|
|
7
|
+
kb_payment_method_id = SecureRandom.uuid
|
|
8
|
+
|
|
9
|
+
if kb_account_id.nil?
|
|
10
|
+
kb_account_id = SecureRandom.uuid
|
|
11
|
+
|
|
12
|
+
# Create a new account
|
|
13
|
+
create_kb_account kb_account_id
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
account = @plugin.kb_apis.account_user_api.get_account_by_id(kb_account_id, @plugin.kb_apis.create_context)
|
|
17
|
+
|
|
18
|
+
# Generate a token
|
|
19
|
+
cc_number = '4242424242424242'
|
|
20
|
+
cc_first_name = 'John'
|
|
21
|
+
cc_last_name = 'Doe'
|
|
22
|
+
cc_type = 'Visa'
|
|
23
|
+
cc_exp_month = 12
|
|
24
|
+
cc_exp_year = 2017
|
|
25
|
+
cc_last_4 = 4242
|
|
26
|
+
address1 = '5, oakriu road'
|
|
27
|
+
address2 = 'apt. 298'
|
|
28
|
+
city = 'Gdio Foia'
|
|
29
|
+
state = 'FL'
|
|
30
|
+
zip = 49302
|
|
31
|
+
country = 'US'
|
|
32
|
+
cc_verification_value = 1234
|
|
33
|
+
|
|
34
|
+
properties = []
|
|
35
|
+
properties << create_pm_kv_info('ccNumber', cc_number)
|
|
36
|
+
properties << create_pm_kv_info('ccFirstName', cc_first_name)
|
|
37
|
+
properties << create_pm_kv_info('ccLastName', cc_last_name)
|
|
38
|
+
properties << create_pm_kv_info('ccType', cc_type)
|
|
39
|
+
properties << create_pm_kv_info('ccExpirationMonth', cc_exp_month)
|
|
40
|
+
properties << create_pm_kv_info('ccExpirationYear', cc_exp_year)
|
|
41
|
+
properties << create_pm_kv_info('ccLast4', cc_last_4)
|
|
42
|
+
properties << create_pm_kv_info('email', account.nil? ? nil : account.email)
|
|
43
|
+
properties << create_pm_kv_info('address1', address1)
|
|
44
|
+
properties << create_pm_kv_info('address2', address2)
|
|
45
|
+
properties << create_pm_kv_info('city', city)
|
|
46
|
+
properties << create_pm_kv_info('state', state)
|
|
47
|
+
properties << create_pm_kv_info('zip', zip)
|
|
48
|
+
properties << create_pm_kv_info('country', country)
|
|
49
|
+
properties << create_pm_kv_info('ccVerificationValue', cc_verification_value)
|
|
50
|
+
|
|
51
|
+
info = Killbill::Plugin::Model::PaymentMethodPlugin.new
|
|
52
|
+
info.properties = properties
|
|
53
|
+
payment_method = @plugin.add_payment_method(kb_account_id, kb_payment_method_id, info, true, nil)
|
|
54
|
+
|
|
55
|
+
pm = payment_method_model.from_kb_payment_method_id kb_payment_method_id
|
|
56
|
+
pm.should == payment_method
|
|
57
|
+
pm.kb_account_id.should == kb_account_id
|
|
58
|
+
pm.kb_payment_method_id.should == kb_payment_method_id
|
|
59
|
+
# Depends on the gateway
|
|
60
|
+
#pm.cc_first_name.should == cc_first_name + ' ' + cc_last_name
|
|
61
|
+
#pm.cc_last_name.should == cc_last_name
|
|
62
|
+
pm.cc_type.should == cc_type
|
|
63
|
+
pm.cc_exp_month.should == cc_exp_month
|
|
64
|
+
pm.cc_exp_year.should == cc_exp_year
|
|
65
|
+
#pm.cc_last_4.should == cc_last_4
|
|
66
|
+
pm.address1.should == address1
|
|
67
|
+
pm.address2.should == address2
|
|
68
|
+
pm.city.should == city
|
|
69
|
+
pm.state.should == state
|
|
70
|
+
pm.zip.should == zip.to_s
|
|
71
|
+
pm.country.should == country
|
|
72
|
+
|
|
73
|
+
pm
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def create_kb_account(kb_account_id)
|
|
77
|
+
external_key = Time.now.to_i.to_s + '-test'
|
|
78
|
+
email = external_key + '@tester.com'
|
|
79
|
+
|
|
80
|
+
account = ::Killbill::Plugin::Model::Account.new
|
|
81
|
+
account.id = kb_account_id
|
|
82
|
+
account.external_key = external_key
|
|
83
|
+
account.email = email
|
|
84
|
+
account.name = 'Integration spec'
|
|
85
|
+
account.currency = :USD
|
|
86
|
+
|
|
87
|
+
@account_api.accounts << account
|
|
88
|
+
|
|
89
|
+
return external_key, kb_account_id
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def create_pm_kv_info(key, value)
|
|
93
|
+
prop = ::Killbill::Plugin::Model::PaymentMethodKVInfo.new
|
|
94
|
+
prop.key = key
|
|
95
|
+
prop.value = value
|
|
96
|
+
prop
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class FakeJavaUserAccountApi
|
|
100
|
+
attr_accessor :accounts
|
|
101
|
+
|
|
102
|
+
def initialize
|
|
103
|
+
@accounts = []
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def get_account_by_id(id, context)
|
|
107
|
+
@accounts.find { |account| account.id == id.to_s }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def get_account_by_key(external_key, context)
|
|
111
|
+
@accounts.find { |account| account.external_key == external_key.to_s }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
module Killbill
|
|
2
|
+
module Plugin
|
|
3
|
+
module ActiveMerchant
|
|
4
|
+
require 'active_record'
|
|
5
|
+
require 'money'
|
|
6
|
+
|
|
7
|
+
class PaymentPlugin < ::Killbill::Plugin::Payment
|
|
8
|
+
|
|
9
|
+
def initialize(gateway_builder, identifier, payment_method_model, transaction_model, response_model)
|
|
10
|
+
super()
|
|
11
|
+
|
|
12
|
+
@gateway_builder = gateway_builder
|
|
13
|
+
@identifier = identifier
|
|
14
|
+
@payment_method_model = payment_method_model
|
|
15
|
+
@transaction_model = transaction_model
|
|
16
|
+
@response_model = response_model
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start_plugin
|
|
20
|
+
::Killbill::Plugin::ActiveMerchant.initialize! @gateway_builder,
|
|
21
|
+
@identifier.to_sym,
|
|
22
|
+
@logger,
|
|
23
|
+
"#{@conf_dir}/#{@identifier.to_s}.yml",
|
|
24
|
+
@kb_apis
|
|
25
|
+
|
|
26
|
+
super
|
|
27
|
+
|
|
28
|
+
@logger.info "#{@identifier} payment plugin started"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# return DB connections to the Pool if required
|
|
32
|
+
def after_request
|
|
33
|
+
::ActiveRecord::Base.connection.close
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def authorize_payment(kb_account_id, kb_payment_id, kb_payment_method_id, amount, currency, properties, context)
|
|
37
|
+
options = properties_to_hash(properties)
|
|
38
|
+
|
|
39
|
+
# Use Money to compute the amount in cents, as it depends on the currency (1 cent of BTC is 1 Satoshi, not 0.01 BTC)
|
|
40
|
+
amount_in_cents = Monetize.from_numeric(amount, currency).cents.to_i
|
|
41
|
+
|
|
42
|
+
# If the authorization was already made, just return the status (one auth per kb payment id)
|
|
43
|
+
transaction = @transaction_model.authorization_from_kb_payment_id(kb_payment_id) rescue nil
|
|
44
|
+
return transaction.send("#{@identifier}_response").to_payment_response(transaction) unless transaction.nil?
|
|
45
|
+
|
|
46
|
+
options[:order_id] ||= kb_payment_id
|
|
47
|
+
options[:currency] ||= currency.to_s.upcase
|
|
48
|
+
options[:description] ||= "Kill Bill authorization for #{kb_payment_id}"
|
|
49
|
+
|
|
50
|
+
# Retrieve the payment method
|
|
51
|
+
if options[:credit_card].blank?
|
|
52
|
+
pm = @payment_method_model.from_kb_payment_method_id(kb_payment_method_id)
|
|
53
|
+
payment_source = pm.token
|
|
54
|
+
else
|
|
55
|
+
payment_source = ::ActiveMerchant::Billing::CreditCard.new(options[:credit_card])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Go to the gateway
|
|
59
|
+
gw_response = gateway.authorize amount_in_cents, payment_source, options
|
|
60
|
+
response, transaction = save_response_and_transaction gw_response, :authorize, kb_payment_id, amount_in_cents, currency
|
|
61
|
+
|
|
62
|
+
response.to_payment_response(transaction)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def capture_payment(kb_account_id, kb_payment_id, kb_payment_method_id, amount, currency, properties, context)
|
|
66
|
+
options = properties_to_hash(properties)
|
|
67
|
+
|
|
68
|
+
# Use Money to compute the amount in cents, as it depends on the currency (1 cent of BTC is 1 Satoshi, not 0.01 BTC)
|
|
69
|
+
amount_in_cents = Monetize.from_numeric(amount, currency).cents.to_i
|
|
70
|
+
|
|
71
|
+
options[:order_id] ||= kb_payment_id
|
|
72
|
+
options[:currency] ||= currency.to_s.upcase
|
|
73
|
+
options[:description] ||= "Kill Bill capture for #{kb_payment_id}"
|
|
74
|
+
|
|
75
|
+
# Retrieve the authorization
|
|
76
|
+
authorization = @transaction_model.authorization_from_kb_payment_id(kb_payment_id).txn_id
|
|
77
|
+
|
|
78
|
+
# Go to the gateway
|
|
79
|
+
gw_response = gateway.capture amount_in_cents, authorization, options
|
|
80
|
+
response, transaction = save_response_and_transaction gw_response, :capture, kb_payment_id, amount_in_cents, currency
|
|
81
|
+
|
|
82
|
+
response.to_payment_response(transaction)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def void_payment(kb_account_id, kb_payment_id, kb_payment_method_id, properties, context)
|
|
86
|
+
options = properties_to_hash(properties)
|
|
87
|
+
options[:description] ||= "Kill Bill void for #{kb_payment_id}"
|
|
88
|
+
|
|
89
|
+
# Retrieve the authorization
|
|
90
|
+
authorization = @transaction_model.authorization_from_kb_payment_id(kb_payment_id).txn_id
|
|
91
|
+
|
|
92
|
+
# Go to the gateway
|
|
93
|
+
gw_response = gateway.void authorization, options
|
|
94
|
+
response, transaction = save_response_and_transaction gw_response, :void, kb_payment_id
|
|
95
|
+
|
|
96
|
+
response.to_payment_response(transaction)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def process_payment(kb_account_id, kb_payment_id, kb_payment_method_id, amount, currency, properties, context)
|
|
100
|
+
options = properties_to_hash(properties)
|
|
101
|
+
|
|
102
|
+
# Use Money to compute the amount in cents, as it depends on the currency (1 cent of BTC is 1 Satoshi, not 0.01 BTC)
|
|
103
|
+
amount_in_cents = Monetize.from_numeric(amount, currency).cents.to_i
|
|
104
|
+
|
|
105
|
+
# If the payment was already made, just return the status
|
|
106
|
+
transaction = @transaction_model.charge_from_kb_payment_id(kb_payment_id) rescue nil
|
|
107
|
+
return transaction.send("#{@identifier}_response").to_payment_response(transaction) unless transaction.nil?
|
|
108
|
+
|
|
109
|
+
options[:order_id] ||= kb_payment_id
|
|
110
|
+
options[:currency] ||= currency.to_s.upcase
|
|
111
|
+
options[:description] ||= "Kill Bill payment for #{kb_payment_id}"
|
|
112
|
+
|
|
113
|
+
# Retrieve the payment method
|
|
114
|
+
if options[:credit_card].blank?
|
|
115
|
+
pm = @payment_method_model.from_kb_payment_method_id(kb_payment_method_id)
|
|
116
|
+
payment_source = pm.token
|
|
117
|
+
else
|
|
118
|
+
payment_source = ::ActiveMerchant::Billing::CreditCard.new(options[:credit_card])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Go to the gateway
|
|
122
|
+
gw_response = gateway.purchase amount_in_cents, payment_source, options
|
|
123
|
+
response, transaction = save_response_and_transaction gw_response, :charge, kb_payment_id, amount_in_cents, currency
|
|
124
|
+
|
|
125
|
+
response.to_payment_response(transaction)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def process_refund(kb_account_id, kb_payment_id, amount, currency, properties, context)
|
|
129
|
+
options = properties_to_hash(properties)
|
|
130
|
+
|
|
131
|
+
# Use Money to compute the amount in cents, as it depends on the currency (1 cent of BTC is 1 Satoshi, not 0.01 BTC)
|
|
132
|
+
amount_in_cents = Monetize.from_numeric(amount, currency).cents.to_i
|
|
133
|
+
|
|
134
|
+
transaction = @transaction_model.find_candidate_transaction_for_refund(kb_payment_id, amount_in_cents)
|
|
135
|
+
|
|
136
|
+
# Go to the gateway
|
|
137
|
+
gw_response = gateway.refund amount_in_cents, transaction.txn_id, options
|
|
138
|
+
response, transaction = save_response_and_transaction gw_response, :refund, kb_payment_id, amount_in_cents, currency
|
|
139
|
+
|
|
140
|
+
response.to_refund_response(transaction)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def get_payment_info(kb_account_id, kb_payment_id, properties, context)
|
|
144
|
+
options = properties_to_hash(properties)
|
|
145
|
+
|
|
146
|
+
# We assume the payment is immutable in the Gateway and only look at our tables
|
|
147
|
+
transaction = @transaction_model.charge_from_kb_payment_id(kb_payment_id)
|
|
148
|
+
|
|
149
|
+
transaction.send("#{@identifier}_response").to_payment_response(transaction)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def get_refund_info(kb_account_id, kb_payment_id, properties, context)
|
|
153
|
+
options = properties_to_hash(properties)
|
|
154
|
+
|
|
155
|
+
# We assume the refund is immutable in the Gateway and only look at our tables
|
|
156
|
+
transactions = @transaction_model.refunds_from_kb_payment_id(kb_payment_id)
|
|
157
|
+
|
|
158
|
+
transactions.map { |t| t.send("#{@identifier}_response").to_refund_response(t) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def add_payment_method(kb_account_id, kb_payment_method_id, payment_method_props, set_default, properties, context)
|
|
162
|
+
options = properties_to_hash(properties)
|
|
163
|
+
options[:set_default] ||= set_default
|
|
164
|
+
|
|
165
|
+
# Registering a card or a token
|
|
166
|
+
cc_or_token = find_value_from_payment_method_props(payment_method_props, 'token') || find_value_from_payment_method_props(payment_method_props, 'cardId')
|
|
167
|
+
if cc_or_token.blank?
|
|
168
|
+
# Nope - real credit card
|
|
169
|
+
cc_or_token = ::ActiveMerchant::Billing::CreditCard.new(
|
|
170
|
+
:number => find_value_from_payment_method_props(payment_method_props, 'ccNumber'),
|
|
171
|
+
:brand => find_value_from_payment_method_props(payment_method_props, 'ccType'),
|
|
172
|
+
:month => find_value_from_payment_method_props(payment_method_props, 'ccExpirationMonth'),
|
|
173
|
+
:year => find_value_from_payment_method_props(payment_method_props, 'ccExpirationYear'),
|
|
174
|
+
:verification_value => find_value_from_payment_method_props(payment_method_props, 'ccVerificationValue'),
|
|
175
|
+
:first_name => find_value_from_payment_method_props(payment_method_props, 'ccFirstName'),
|
|
176
|
+
:last_name => find_value_from_payment_method_props(payment_method_props, 'ccLastName')
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
options[:billing_address] ||= {
|
|
181
|
+
:email => find_value_from_payment_method_props(payment_method_props, 'email'),
|
|
182
|
+
:address1 => find_value_from_payment_method_props(payment_method_props, 'address1'),
|
|
183
|
+
:address2 => find_value_from_payment_method_props(payment_method_props, 'address2'),
|
|
184
|
+
:city => find_value_from_payment_method_props(payment_method_props, 'city'),
|
|
185
|
+
:zip => find_value_from_payment_method_props(payment_method_props, 'zip'),
|
|
186
|
+
:state => find_value_from_payment_method_props(payment_method_props, 'state'),
|
|
187
|
+
:country => find_value_from_payment_method_props(payment_method_props, 'country')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# To make various gateway implementations happy...
|
|
191
|
+
options[:billing_address].each { |k,v| options[k] ||= v }
|
|
192
|
+
|
|
193
|
+
options[:order_id] ||= kb_payment_method_id
|
|
194
|
+
|
|
195
|
+
# Go to the gateway
|
|
196
|
+
gw_response = gateway.store cc_or_token, options
|
|
197
|
+
response, transaction = save_response_and_transaction gw_response, :add_payment_method
|
|
198
|
+
|
|
199
|
+
if response.success
|
|
200
|
+
payment_method = @payment_method_model.from_response(kb_account_id, kb_payment_method_id, cc_or_token, gw_response, options)
|
|
201
|
+
payment_method.save!
|
|
202
|
+
payment_method
|
|
203
|
+
else
|
|
204
|
+
raise response.message
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def delete_payment_method(kb_account_id, kb_payment_method_id, properties, context)
|
|
209
|
+
options = properties_to_hash(properties)
|
|
210
|
+
|
|
211
|
+
pm = @payment_method_model.from_kb_payment_method_id(kb_payment_method_id)
|
|
212
|
+
|
|
213
|
+
# Delete the card
|
|
214
|
+
if options[:customer_id]
|
|
215
|
+
gw_response = gateway.unstore(options[:customer_id], pm.token, options)
|
|
216
|
+
else
|
|
217
|
+
gw_response = gateway.unstore(pm.token, options)
|
|
218
|
+
end
|
|
219
|
+
response, transaction = save_response_and_transaction gw_response, :delete_payment_method
|
|
220
|
+
|
|
221
|
+
if response.success
|
|
222
|
+
@payment_method_model.mark_as_deleted! kb_payment_method_id
|
|
223
|
+
else
|
|
224
|
+
raise response.message
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def get_payment_method_detail(kb_account_id, kb_payment_method_id, properties, context)
|
|
229
|
+
options = properties_to_hash(properties)
|
|
230
|
+
@payment_method_model.from_kb_payment_method_id(kb_payment_method_id).to_payment_method_response
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def get_payment_methods(kb_account_id, refresh_from_gateway = false, properties, context)
|
|
234
|
+
options = properties_to_hash(properties)
|
|
235
|
+
@payment_method_model.from_kb_account_id(kb_account_id).collect { |pm| pm.to_payment_method_info_response }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def reset_payment_methods(kb_account_id, payment_methods, properties)
|
|
239
|
+
return if payment_methods.nil?
|
|
240
|
+
|
|
241
|
+
options = properties_to_hash(properties)
|
|
242
|
+
|
|
243
|
+
pms = @payment_method_model.from_kb_account_id(kb_account_id)
|
|
244
|
+
|
|
245
|
+
payment_methods.delete_if do |payment_method_info_plugin|
|
|
246
|
+
should_be_deleted = false
|
|
247
|
+
pms.each do |pm|
|
|
248
|
+
# Do pm and payment_method_info_plugin represent the same payment method?
|
|
249
|
+
if pm.external_payment_method_id == payment_method_info_plugin.external_payment_method_id
|
|
250
|
+
# Do we already have a kb_payment_method_id?
|
|
251
|
+
if pm.kb_payment_method_id == payment_method_info_plugin.payment_method_id
|
|
252
|
+
should_be_deleted = true
|
|
253
|
+
break
|
|
254
|
+
elsif pm.kb_payment_method_id.nil?
|
|
255
|
+
# We didn't have the kb_payment_method_id - update it
|
|
256
|
+
pm.kb_payment_method_id = payment_method_info_plugin.payment_method_id
|
|
257
|
+
should_be_deleted = pm.save
|
|
258
|
+
break
|
|
259
|
+
# Otherwise the same token points to 2 different kb_payment_method_id. This should never happen!
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
should_be_deleted
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# The remaining elements in payment_methods are not in our table (this should never happen?!)
|
|
268
|
+
payment_methods.each do |payment_method_info_plugin|
|
|
269
|
+
pm = @payment_method_model.create :kb_account_id => kb_account_id,
|
|
270
|
+
:kb_payment_method_id => payment_method_info_plugin.payment_method_id,
|
|
271
|
+
:token => payment_method_info_plugin.external_payment_method_id
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def search_payments(search_key, offset = 0, limit = 100, properties, context)
|
|
276
|
+
options = properties_to_hash(properties)
|
|
277
|
+
@response_model.search(search_key, offset, limit, :payment)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def search_refunds(search_key, offset = 0, limit = 100, properties, context)
|
|
281
|
+
options = properties_to_hash(properties)
|
|
282
|
+
@response_model.search(search_key, offset, limit, :refund)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def search_payment_methods(search_key, offset = 0, limit = 100, properties, context)
|
|
286
|
+
options = properties_to_hash(properties)
|
|
287
|
+
@payment_method_model.search(search_key, offset, limit)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def build_form_descriptor(kb_account_id, descriptor_fields, properties, context)
|
|
291
|
+
options = properties_to_hash(properties)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def process_notification(notification, properties, context)
|
|
295
|
+
options = properties_to_hash(properties)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Utilities
|
|
299
|
+
|
|
300
|
+
# Deprecated
|
|
301
|
+
def find_value_from_payment_method_props(payment_method_props, key)
|
|
302
|
+
find_value_from_properties(payment_method_props, key)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def find_value_from_properties(properties, key)
|
|
306
|
+
prop = (payment_method_props.properties.find { |kv| kv.key == key })
|
|
307
|
+
prop.nil? ? nil : prop.value
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def account_currency(kb_account_id)
|
|
311
|
+
account = @kb_apis.account_user_api.get_account_by_id(kb_account_id, @kb_apis.create_context)
|
|
312
|
+
account.currency
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def save_response_and_transaction(response, api_call, kb_payment_id=nil, amount_in_cents=0, currency=nil)
|
|
316
|
+
@logger.warn "Unsuccessful #{api_call}: #{response.message}" unless response.success?
|
|
317
|
+
|
|
318
|
+
# Save the response to our logs
|
|
319
|
+
response = @response_model.from_response(api_call, kb_payment_id, response)
|
|
320
|
+
response.save!
|
|
321
|
+
|
|
322
|
+
transaction = nil
|
|
323
|
+
txn_id = response.txn_id
|
|
324
|
+
if response.success and !kb_payment_id.blank? and !txn_id.blank?
|
|
325
|
+
# Record the transaction
|
|
326
|
+
transaction = response.send("create_#{@identifier}_transaction!",
|
|
327
|
+
:amount_in_cents => amount_in_cents,
|
|
328
|
+
:currency => currency,
|
|
329
|
+
:api_call => api_call,
|
|
330
|
+
:kb_payment_id => kb_payment_id,
|
|
331
|
+
:txn_id => txn_id)
|
|
332
|
+
|
|
333
|
+
@logger.debug "Recorded transaction: #{transaction.inspect}"
|
|
334
|
+
end
|
|
335
|
+
return response, transaction
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def gateway
|
|
339
|
+
::Killbill::Plugin::ActiveMerchant.gateway
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def properties_to_hash(properties, options = {})
|
|
343
|
+
merged = {}
|
|
344
|
+
properties.each do |p|
|
|
345
|
+
merged[p.key.to_sym] = p.value
|
|
346
|
+
end
|
|
347
|
+
merged.merge(options)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def merge_properties(properties, options)
|
|
351
|
+
merged = properties_to_hash(properties, options)
|
|
352
|
+
|
|
353
|
+
properties = []
|
|
354
|
+
merged.each do |k, v|
|
|
355
|
+
p = ::Killbill::Plugin::Model::PluginProperty.new
|
|
356
|
+
p.key = k
|
|
357
|
+
p.value = v
|
|
358
|
+
properties << p
|
|
359
|
+
end
|
|
360
|
+
properties
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|