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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Jarfile +5 -5
  4. data/NEWS +4 -0
  5. data/README.md +45 -8
  6. data/VERSION +1 -1
  7. data/generators/active_merchant/active_merchant_generator.rb +38 -0
  8. data/generators/active_merchant/templates/.gitignore.rb +36 -0
  9. data/generators/active_merchant/templates/.travis.yml.rb +19 -0
  10. data/generators/active_merchant/templates/Gemfile.rb +3 -0
  11. data/generators/active_merchant/templates/Jarfile.rb +6 -0
  12. data/generators/active_merchant/templates/LICENSE.rb +201 -0
  13. data/generators/active_merchant/templates/NEWS.rb +2 -0
  14. data/generators/active_merchant/templates/Rakefile.rb +30 -0
  15. data/generators/active_merchant/templates/VERSION.rb +1 -0
  16. data/generators/active_merchant/templates/config.ru.rb +4 -0
  17. data/generators/active_merchant/templates/config.yml.rb +13 -0
  18. data/generators/active_merchant/templates/db/ddl.sql.rb +64 -0
  19. data/generators/active_merchant/templates/db/schema.rb +64 -0
  20. data/generators/active_merchant/templates/killbill.properties.rb +3 -0
  21. data/generators/active_merchant/templates/lib/api.rb +119 -0
  22. data/generators/active_merchant/templates/lib/application.rb +84 -0
  23. data/generators/active_merchant/templates/lib/models/payment_method.rb +22 -0
  24. data/generators/active_merchant/templates/lib/models/response.rb +22 -0
  25. data/generators/active_merchant/templates/lib/models/transaction.rb +11 -0
  26. data/generators/active_merchant/templates/lib/plugin.rb +23 -0
  27. data/generators/active_merchant/templates/lib/private_api.rb +6 -0
  28. data/generators/active_merchant/templates/lib/views/form.erb +8 -0
  29. data/generators/active_merchant/templates/plugin.gemspec.rb +48 -0
  30. data/generators/active_merchant/templates/pom.xml.rb +44 -0
  31. data/generators/active_merchant/templates/release.sh.rb +41 -0
  32. data/generators/active_merchant/templates/spec/base_plugin_spec.rb +30 -0
  33. data/generators/active_merchant/templates/spec/integration_spec.rb +31 -0
  34. data/generators/active_merchant/templates/spec/spec_helper.rb +24 -0
  35. data/generators/killbill_generator.rb +38 -0
  36. data/killbill.gemspec +10 -2
  37. data/lib/killbill/gen/api/block.rb +82 -0
  38. data/lib/killbill/gen/api/direct_payment.rb +176 -0
  39. data/lib/killbill/gen/api/direct_payment_api.rb +329 -0
  40. data/lib/killbill/gen/api/direct_payment_transaction.rb +156 -0
  41. data/lib/killbill/gen/api/fixed.rb +63 -0
  42. data/lib/killbill/gen/api/invoice_item.rb +7 -1
  43. data/lib/killbill/gen/api/invoice_item_formatter.rb +7 -1
  44. data/lib/killbill/gen/api/invoice_user_api.rb +18 -136
  45. data/lib/killbill/gen/api/migration_plan.rb +6 -6
  46. data/lib/killbill/gen/api/payment_api.rb +216 -54
  47. data/lib/killbill/gen/api/payment_method_plugin.rb +3 -3
  48. data/lib/killbill/gen/api/plan.rb +6 -6
  49. data/lib/killbill/gen/api/plan_phase.rb +16 -23
  50. data/lib/killbill/gen/api/plugin_property.rb +71 -0
  51. data/lib/killbill/gen/api/recurring.rb +63 -0
  52. data/lib/killbill/gen/api/require_gen.rb +10 -1
  53. data/lib/killbill/gen/api/static_catalog.rb +8 -1
  54. data/lib/killbill/gen/api/tier.rb +77 -0
  55. data/lib/killbill/gen/api/tiered_block.rb +88 -0
  56. data/lib/killbill/gen/api/usage.rb +111 -0
  57. data/lib/killbill/gen/api/usage_user_api.rb +59 -3
  58. data/lib/killbill/gen/plugin-api/billing_address.rb +85 -0
  59. data/lib/killbill/gen/plugin-api/customer.rb +73 -0
  60. data/lib/killbill/gen/plugin-api/hosted_payment_page_descriptor_fields.rb +145 -0
  61. data/lib/killbill/gen/plugin-api/hosted_payment_page_form_descriptor.rb +80 -0
  62. data/lib/killbill/gen/plugin-api/hosted_payment_page_notification.rb +129 -0
  63. data/lib/killbill/gen/plugin-api/payment_info_plugin.rb +20 -1
  64. data/lib/killbill/gen/plugin-api/payment_plugin_api.rb +358 -39
  65. data/lib/killbill/gen/plugin-api/refund_info_plugin.rb +20 -1
  66. data/lib/killbill/gen/plugin-api/require_gen.rb +3 -0
  67. data/lib/killbill/helpers/active_merchant.rb +21 -0
  68. data/lib/killbill/helpers/active_merchant/active_record.rb +17 -0
  69. data/lib/killbill/helpers/active_merchant/active_record/models/helpers.rb +25 -0
  70. data/lib/killbill/helpers/active_merchant/active_record/models/payment_method.rb +195 -0
  71. data/lib/killbill/helpers/active_merchant/active_record/models/response.rb +178 -0
  72. data/lib/killbill/helpers/active_merchant/active_record/models/streamy_result_set.rb +35 -0
  73. data/lib/killbill/helpers/active_merchant/active_record/models/transaction.rb +63 -0
  74. data/lib/killbill/helpers/active_merchant/configuration.rb +54 -0
  75. data/lib/killbill/helpers/active_merchant/core_ext.rb +41 -0
  76. data/lib/killbill/helpers/active_merchant/gateway.rb +35 -0
  77. data/lib/killbill/helpers/active_merchant/killbill_spec_helper.rb +117 -0
  78. data/lib/killbill/helpers/active_merchant/payment_plugin.rb +365 -0
  79. data/lib/killbill/helpers/active_merchant/private_payment_plugin.rb +119 -0
  80. data/lib/killbill/helpers/active_merchant/properties.rb +20 -0
  81. data/lib/killbill/helpers/active_merchant/sinatra.rb +30 -0
  82. data/lib/killbill/helpers/active_merchant/utils.rb +23 -0
  83. data/lib/killbill/payment.rb +22 -10
  84. data/script/generate +15 -0
  85. data/spec/killbill/helpers/payment_method_spec.rb +101 -0
  86. data/spec/killbill/helpers/response_spec.rb +74 -0
  87. data/spec/killbill/helpers/test_schema.rb +57 -0
  88. data/spec/killbill/helpers/utils_spec.rb +22 -0
  89. data/spec/killbill/payment_plugin_api_spec.rb +23 -18
  90. data/spec/killbill/payment_plugin_spec.rb +11 -10
  91. data/spec/killbill/payment_test.rb +9 -9
  92. data/spec/spec_helper.rb +8 -3
  93. 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