killbill 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
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