killbill-cybersource 4.0.2 → 4.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9b493be89fec342f29d8b2301b33df6d1fb2242f
4
- data.tar.gz: af046593004ae23ac977ea4c818df2fc53620e5d
3
+ metadata.gz: bf0b374a0672d53db036f9f39c45d663c5478b71
4
+ data.tar.gz: f7244811da2b9e5c2dae307c84e919b3886a1692
5
5
  SHA512:
6
- metadata.gz: 1f76a9a57ab6780e4bfefc3deb1429a7d86cfba8efb70bc45d0ac058557c36edf0912cdd6a87209dd699ba0bfd4fbcbbfbfaeb46a1ce6a4447aca02bbff59b5a
7
- data.tar.gz: fd5086b9920e5cc9b734b094b94952e428b37b19d41f9bc4879fec8f3051ed85812aedd9440a94ca6eec5da944c507275ddd6747a19e17806b55212b9443fdee
6
+ metadata.gz: 23bd9c1612dfa4bc4c2af62fb090865a64ab1e8ef2acbd8bb7d6ad9ead71da4b22814c0fe08cabf1b50a362d9e25528909fe475a696924477e124fc8c30d14a3
7
+ data.tar.gz: 3989c693a1c021c9ec0186638bbaeb78154c007884be21b5a237281948cb5f49016b5c15ab93ac9f6f988d3f28cb60af84eb99cd764393f6d3edcbf2b92e609d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- killbill-cybersource (4.0.2)
4
+ killbill-cybersource (4.0.3)
5
5
  actionpack (~> 4.1.0)
6
6
  actionview (~> 4.1.0)
7
7
  activemerchant (~> 1.48.0)
@@ -65,7 +65,7 @@ GEM
65
65
  diff-lcs (1.1.3)
66
66
  equalizer (0.0.11)
67
67
  erubis (2.7.0)
68
- ethon (0.8.1)
68
+ ethon (0.9.0)
69
69
  ffi (>= 1.3.0)
70
70
  ffi (1.9.10-java)
71
71
  i18n (0.7.0)
data/NEWS CHANGED
@@ -1,3 +1,16 @@
1
+ 4.0.3
2
+ Add support for business rules on Apple Pay
3
+ You can now specify commerce_indicator as a plugin property to override the commerceIndicator value
4
+ You can now specify force_validation=true as a plugin property to trigger $1 CC validation in case
5
+ the processor used doesn't support $0 auth for that card
6
+ Disable costly duplicate checks by default when triggering a payment if the reporting API is configured
7
+ - Set check_for_duplicates: true in your configuration to enable them
8
+ - No change in the GET path (UNDEFINED transactions will be fixed if possible)
9
+ Fix XML parsing of responses (authorization reversal errors weren't parsed correctly)
10
+ Improve categorization of error codes to return the right transaction status
11
+ Set clientLibrary to 'Kill Bill' and clientLibraryVersion to the plugin version
12
+ Change cybersource_responses.message to text
13
+
1
14
  4.0.2
2
15
  Add support for auth reversal after voiding a capture
3
16
  Add support for ignore_avs and ignore_cvv properties
data/README.md CHANGED
@@ -130,29 +130,32 @@ curl -v \
130
130
  Plugin properties
131
131
  -----------------
132
132
 
133
- | Key | Description |
134
- | ---------------------------: | ----------------------------------------------------------------- |
135
- | skip_gw | If true, skip the call to CyberSource |
136
- | payment_processor_account_id | Config entry name of the merchant account to use |
137
- | external_key_as_order_id | If true, set the payment external key as the CyberSource order id |
138
- | ignore_avs | If true, ignore the results of AVS checking |
139
- | ignore_cvv | If true, ignore the results of CVN checking |
140
- | cc_first_name | Credit card holder first name |
141
- | cc_last_name | Credit card holder last name |
142
- | cc_type | Credit card brand |
143
- | cc_expiration_month | Credit card expiration month |
144
- | cc_expiration_year | Credit card expiration year |
145
- | cc_verification_value | CVC/CVV/CVN |
146
- | email | Purchaser email |
147
- | address1 | Billing address first line |
148
- | address2 | Billing address second line |
149
- | city | Billing address city |
150
- | zip | Billing address zip code |
151
- | state | Billing address state |
152
- | country | Billing address country |
153
- | eci | Network tokenization attribute |
154
- | payment_cryptogram | Network tokenization attribute |
155
- | transaction_id | Network tokenization attribute |
156
- | payment_instrument_name | ApplePay tokenization attribute |
157
- | payment_network | ApplePay tokenization attribute |
158
- | transaction_identifier | ApplePay tokenization attribute |
133
+ | Key | Description |
134
+ | ---------------------------: | ------------------------------------------------------------------------|
135
+ | skip_gw | If true, skip the call to CyberSource |
136
+ | payment_processor_account_id | Config entry name of the merchant account to use |
137
+ | external_key_as_order_id | If true, set the payment external key as the CyberSource order id |
138
+ | ignore_avs | If true, ignore the results of AVS checking |
139
+ | ignore_cvv | If true, ignore the results of CVN checking |
140
+ | cc_first_name | Credit card holder first name |
141
+ | cc_last_name | Credit card holder last name |
142
+ | cc_type | Credit card brand |
143
+ | cc_expiration_month | Credit card expiration month |
144
+ | cc_expiration_year | Credit card expiration year |
145
+ | cc_verification_value | CVC/CVV/CVN |
146
+ | email | Purchaser email |
147
+ | address1 | Billing address first line |
148
+ | address2 | Billing address second line |
149
+ | city | Billing address city |
150
+ | zip | Billing address zip code |
151
+ | state | Billing address state |
152
+ | country | Billing address country |
153
+ | commerce_indicator | Override the commerce indicator field |
154
+ | eci | Network tokenization attribute |
155
+ | payment_cryptogram | Network tokenization attribute |
156
+ | transaction_id | Network tokenization attribute |
157
+ | payment_instrument_name | ApplePay tokenization attribute |
158
+ | payment_network | ApplePay tokenization attribute |
159
+ | transaction_identifier | ApplePay tokenization attribute |
160
+ | force_validation | If true, trigger a non-$0 auth to validate cards not supporting $0 auth |
161
+ | force_validation_amount | Amount to use when force_validation is set |
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.0.2
1
+ 4.0.3
data/cybersource.yml CHANGED
@@ -7,6 +7,7 @@
7
7
  # :merchantID: <%= ENV['MERCHANT_ID'] %>
8
8
  # :username: <%= ENV['OD_USERNAME'] %>
9
9
  # :password: <%= ENV['OD_PASSWORD'] %>
10
+ # :check_for_duplicates: true
10
11
 
11
12
  :database:
12
13
  # SQLite (development)
data/db/ddl.sql CHANGED
@@ -57,7 +57,7 @@ CREATE TABLE `cybersource_responses` (
57
57
  `kb_payment_transaction_id` varchar(255) DEFAULT NULL,
58
58
  `transaction_type` varchar(255) DEFAULT NULL,
59
59
  `payment_processor_account_id` varchar(255) DEFAULT NULL,
60
- `message` varchar(255) DEFAULT NULL,
60
+ `message` text DEFAULT NULL,
61
61
  `authorization` varchar(255) DEFAULT NULL,
62
62
  `fraud_review` tinyint(1) DEFAULT NULL,
63
63
  `test` tinyint(1) DEFAULT NULL,
@@ -0,0 +1,11 @@
1
+ class EnlargeMessage < ActiveRecord::Migration
2
+
3
+ def change
4
+ reversible do |dir|
5
+ change_table :cybersource_responses do |t|
6
+ dir.up { t.change :message, :text }
7
+ dir.down { t.change :message, :string }
8
+ end
9
+ end
10
+ end
11
+ end
data/db/schema.rb CHANGED
@@ -58,7 +58,7 @@ ActiveRecord::Schema.define(:version => 20140410153635) do
58
58
  t.string "kb_payment_transaction_id"
59
59
  t.string "transaction_type"
60
60
  t.string "payment_processor_account_id"
61
- t.string "message"
61
+ t.text "message"
62
62
  t.string "authorization"
63
63
  t.boolean "fraud_review"
64
64
  t.boolean "test"
@@ -31,7 +31,18 @@ module Killbill #:nodoc:
31
31
  add_required_options(kb_account_id, properties, options, context)
32
32
 
33
33
  properties = merge_properties(properties, options)
34
- super(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
34
+ auth_response = super(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
35
+
36
+ # Error 234 is "A problem exists with your CyberSource merchant configuration", most likely the processor used doesn't support $0 auth for this card type
37
+ if auth_response.gateway_error_code == '234' && to_cents(amount, currency) == 0
38
+ h_props = properties_to_hash(properties)
39
+ if ::Killbill::Plugin::ActiveMerchant::Utils.normalized(h_props, :force_validation)
40
+ force_validation_amount = (::Killbill::Plugin::ActiveMerchant::Utils.normalized(h_props, :force_validation_amount) || 1).to_f
41
+ auth_response = force_validation(auth_response, kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, force_validation_amount, currency, properties, context)
42
+ end
43
+ end
44
+
45
+ auth_response
35
46
  end
36
47
 
37
48
  def capture_payment(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
@@ -257,18 +268,31 @@ module Killbill #:nodoc:
257
268
  super
258
269
 
259
270
  merchant_reference_code = options[:order_id]
260
- report = get_report(merchant_reference_code, kb_transaction.created_date, options, context)
271
+ report = get_report_for_kb_transaction(merchant_reference_code, kb_transaction, options, context)
261
272
  return nil if report.nil? || report.empty?
262
273
 
263
- logger.info "Skipping gateway call for existing transaction #{kb_transaction.id}, merchant reference code #{merchant_reference_code}"
274
+ logger.info "Skipping gateway call for existing kb_transaction_id='#{kb_transaction.id}', merchant_reference_code='#{merchant_reference_code}'"
264
275
  options[:skip_gw] = true
265
276
  rescue => e
266
- logger.warn "Error checking for duplicate payment: #{e.message}"
277
+ logger.warn "Error checking for duplicate payment for merchant_reference_code='#{merchant_reference_code}'\n#{e.backtrace.join("\n")}"
278
+ end
279
+
280
+ # Duplicate check
281
+ def get_report_for_kb_transaction(merchant_reference_code, kb_transaction, options, context)
282
+ report_api = get_report_api(options, context)
283
+ return nil if report_api.nil? || !report_api.check_for_duplicates?
284
+ # kb_transaction is a Utils::LazyEvaluator, delay evaluation as much as possible
285
+ get_single_transaction_report(report_api, merchant_reference_code, kb_transaction.created_date)
267
286
  end
268
287
 
288
+ # Janitor path
269
289
  def get_report(merchant_reference_code, date, options, context)
270
- report_api = get_report_api(context.tenant_id)
271
- return nil if report_api.nil? || options[:skip_gw]
290
+ report_api = get_report_api(options, context)
291
+ return nil if report_api.nil?
292
+ get_single_transaction_report(report_api, merchant_reference_code, date)
293
+ end
294
+
295
+ def get_single_transaction_report(report_api, merchant_reference_code, date)
272
296
  report_api.single_transaction_report(merchant_reference_code, date.strftime('%Y%m%d'))
273
297
  end
274
298
 
@@ -287,12 +311,57 @@ module Killbill #:nodoc:
287
311
  ::Killbill::Plugin::ActiveMerchant::Utils.normalize_property(properties, 'ignore_cvv')
288
312
  end
289
313
 
290
- def get_report_api(kb_tenant_id)
291
- gateway = lookup_gateway(:on_demand, kb_tenant_id)
292
- CyberSourceOnDemand.new(gateway, logger)
293
- rescue
314
+ def get_report_api(options, context)
315
+ return nil if options[:skip_gw] || options[:bypass_duplicate_check]
316
+ cybersource_config = config(context.tenant_id)[:cybersource]
317
+ return nil unless cybersource_config.is_a?(Array)
318
+ on_demand_config = cybersource_config.find { |c| c[:account_id].to_s == 'on_demand' }
319
+ return nil if on_demand_config.nil?
320
+ CyberSourceOnDemand.new(on_demand_config, logger)
321
+ rescue => e
322
+ @logger.warn("Unexpected exception while looking-up reporting API for kb_tenant_id='#{context.tenant_id}'\n#{e.backtrace.join("\n")}")
294
323
  nil
295
324
  end
325
+
326
+ # TODO: should this eventually be hardened and extracted into the base framework?
327
+ def force_validation(auth_response, kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
328
+ # Trigger a non-$0 auth
329
+ new_auth_response = nil
330
+ begin
331
+ # If duplicate checks are enabled, we need to bypass them (since a transaction for that merchant reference code was already attempted)
332
+ properties << build_property(:bypass_duplicate_check, true)
333
+ new_auth_response = authorize_payment(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
334
+ rescue => e
335
+ # Note: state might be broken here (potentially two responses with the same kb_payment_transaction_id)
336
+ @logger.warn("Unexpected exception while forcing validation for kb_payment_id='#{kb_payment_id}', kb_payment_transaction_id='#{kb_payment_transaction_id}'\n#{e.backtrace.join("\n")}")
337
+ return auth_response
338
+ end
339
+
340
+ # Void it right away on success (make sure we didn't skip the gateway call too)
341
+ if new_auth_response.status == :PROCESSED && !new_auth_response.first_payment_reference_id.blank?
342
+ begin
343
+ void_payment(kb_account_id, kb_payment_id, SecureRandom.uuid, kb_payment_method_id, properties, context)
344
+ rescue => e
345
+ @logger.warn("Unexpected exception while voiding forced validation for kb_payment_id='#{kb_payment_id}', kb_payment_transaction_id='#{kb_payment_transaction_id}'\n#{e.backtrace.join("\n")}")
346
+ end
347
+ end
348
+
349
+ # Finally, clean up the state of the original (failed) auth
350
+ cybersource_response_id = find_value_from_properties(auth_response.properties, 'cybersourceResponseId')
351
+ if cybersource_response_id.nil?
352
+ @logger.warn "Unable to find cybersourceResponseId matching failed authorization for kb_payment_id='#{kb_payment_id}', kb_payment_transaction_id='#{kb_payment_transaction_id}'"
353
+ else
354
+ response = CybersourceResponse.find_by(:id => cybersource_response_id)
355
+ if response.nil?
356
+ @logger.warn "Unable to find response matching failed authorization for kb_payment_id='#{kb_payment_id}', kb_payment_transaction_id='#{kb_payment_transaction_id}'"
357
+ else
358
+ # Change the kb_payment_transaction_id to avoid confusing Kill Bill (there is no transaction row to update since the call wasn't successful)
359
+ response.update(:kb_payment_transaction_id => SecureRandom.uuid)
360
+ end
361
+ end
362
+
363
+ new_auth_response
364
+ end
296
365
  end
297
366
  end
298
367
  end
@@ -3,17 +3,20 @@ module Killbill #:nodoc:
3
3
  # See http://apps.cybersource.com/library/documentation/dev_guides/Reporting_Developers_Guide/reporting_dg.pdf
4
4
  class CyberSourceOnDemand
5
5
 
6
- @@live_url = 'https://ebc.cybersource.com/ebc/Query'
7
- @@test_url = 'https://ebctest.cybersource.com/ebctest/Query'
6
+ # For convenience, re-use the ActiveMerchant connection code, as the configuration is global currently
7
+ # (see https://github.com/killbill/killbill-plugin-framework-ruby/issues/47)
8
+ include ActiveMerchant::PostsData
8
9
 
9
- def initialize(gateway, logger)
10
- @gateway = gateway
10
+ def initialize(config, logger)
11
+ @config = config
11
12
  @logger = logger
13
+
14
+ configure_connection
12
15
  end
13
16
 
14
17
  def single_transaction_report(merchant_reference_code, target_date)
15
18
  params = {
16
- :merchantID => @gateway.config[:merchantID],
19
+ :merchantID => @config[:merchantID],
17
20
  :merchantReferenceNumber => merchant_reference_code,
18
21
  :targetDate => target_date,
19
22
  :type => 'transaction',
@@ -23,14 +26,50 @@ module Killbill #:nodoc:
23
26
 
24
27
  headers = {
25
28
  # Don't use symbols or it will confuse Net/HTTP
26
- 'Authorization' => 'Basic ' + Base64.encode64("#{@gateway.config[:username]}:#{@gateway.config[:password]}").chomp
29
+ 'Authorization' => 'Basic ' + Base64.encode64("#{@config[:username]}:#{@config[:password]}").chomp
27
30
  }
28
31
 
29
32
  data = URI.encode_www_form(params)
30
- endpoint = @gateway.test? ? @@test_url : @@live_url
31
33
 
32
34
  # Will raise ResponseError if the response code is > 300
33
- CyberSourceOnDemandTransactionReport.new(@gateway.ssl_post(endpoint, data, headers), @logger)
35
+ CyberSourceOnDemandTransactionReport.new(ssl_post(endpoint, data, headers), @logger)
36
+ end
37
+
38
+ def check_for_duplicates?
39
+ @config[:check_for_duplicates] == true
40
+ end
41
+
42
+ private
43
+
44
+ def endpoint
45
+ @config[:test] == false ? live_url : test_url
46
+ end
47
+
48
+ def live_url
49
+ @config[:live_url] || 'https://ebc.cybersource.com/ebc/Query'
50
+ end
51
+
52
+ def test_url
53
+ @config[:test_url] || 'https://ebctest.cybersource.com/ebctest/Query'
54
+ end
55
+
56
+ def configure_connection
57
+ if @config[:log_file]
58
+ self.wiredump_device = File.open(@config[:log_file], 'w')
59
+ else
60
+ log_method = @config[:quiet] ? :debug : :info
61
+ self.wiredump_device = ::Killbill::Plugin::ActiveMerchant::Utils::KBWiredumpDevice.new(@logger, log_method)
62
+ end
63
+ self.wiredump_device.sync = true
64
+
65
+ self.open_timeout = @config[:open_timeout] unless @config[:open_timeout].nil?
66
+ self.read_timeout = @config[:read_timeout] unless @config[:read_timeout].nil?
67
+ self.retry_safe = @config[:retry_safe] unless @config[:retry_safe].nil?
68
+ self.ssl_strict = @config[:ssl_strict] unless @config[:ssl_strict].nil?
69
+ self.ssl_version = @config[:ssl_version] unless @config[:ssl_version].nil?
70
+ self.max_retries = @config[:max_retries] unless @config[:max_retries].nil?
71
+ self.proxy_address = @config[:proxy_address] unless @config[:proxy_address].nil?
72
+ self.proxy_port = @config[:proxy_port] unless @config[:proxy_port].nil?
34
73
  end
35
74
 
36
75
  class CyberSourceOnDemandTransactionReport
@@ -1,7 +1,18 @@
1
1
  module ActiveMerchant
2
2
  module Billing
3
+
4
+ KB_PLUGIN_VERSION = Gem.loaded_specs['killbill-cybersource'].version.version rescue nil
5
+
3
6
  class CyberSourceGateway
4
7
 
8
+ def initialize(options = {})
9
+ super
10
+
11
+ # Add missing response codes
12
+ @@response_codes[:r104] = 'The merchant reference code for this authorization request matches the merchant reference code of another authorization request that you sent within the past 15 minutes.'
13
+ @@response_codes[:r110] = 'Only a partial amount was approved'
14
+ end
15
+
5
16
  # Add support for CreditCard objects
6
17
  def build_credit_request(money, creditcard_or_reference, options)
7
18
  xml = Builder::XmlMarkup.new :indent => 2
@@ -14,6 +25,67 @@ module ActiveMerchant
14
25
  xml.target!
15
26
  end
16
27
 
28
+ # Add support for commerceIndicator override
29
+ def add_auth_service(xml, payment_method, options)
30
+ if network_tokenization?(payment_method)
31
+ add_network_tokenization(xml, payment_method, options)
32
+ else
33
+ xml.tag! 'ccAuthService', {'run' => 'true'} do
34
+ # Let CyberSource figure it out otherwise (internet is the default unless tokens are used)
35
+ xml.tag!("commerceIndicator", options[:commerce_indicator]) unless options[:commerce_indicator].blank?
36
+ end
37
+ end
38
+ end
39
+
40
+ # Changes:
41
+ # * Add support for commerceIndicator override
42
+ # * Don't set paymentNetworkToken (needs to be set after businessRules)
43
+ def add_network_tokenization(xml, payment_method, options)
44
+ return unless network_tokenization?(payment_method)
45
+
46
+ case card_brand(payment_method).to_sym
47
+ when :visa
48
+ xml.tag! 'ccAuthService', {'run' => 'true'} do
49
+ xml.tag!("cavv", payment_method.payment_cryptogram)
50
+ xml.tag!("commerceIndicator", options[:commerce_indicator] || "vbv")
51
+ xml.tag!("xid", payment_method.payment_cryptogram)
52
+ end
53
+ when :mastercard
54
+ xml.tag! 'ucaf' do
55
+ xml.tag!("authenticationData", payment_method.payment_cryptogram)
56
+ xml.tag!("collectionIndicator", "2")
57
+ end
58
+ xml.tag! 'ccAuthService', {'run' => 'true'} do
59
+ xml.tag!("commerceIndicator", options[:commerce_indicator] || "spa")
60
+ end
61
+ when :american_express
62
+ cryptogram = Base64.decode64(payment_method.payment_cryptogram)
63
+ xml.tag! 'ccAuthService', {'run' => 'true'} do
64
+ xml.tag!("cavv", Base64.encode64(cryptogram[0...20]))
65
+ xml.tag!("commerceIndicator", options[:commerce_indicator] || "aesk")
66
+ xml.tag!("xid", Base64.encode64(cryptogram[20...40]))
67
+ end
68
+ end
69
+ end
70
+
71
+ # Changes:
72
+ # * Enable business rules for Apple Pay
73
+ # * Set paymentNetworkToken if needed (a bit of a hack to do it here, but it avoids having to override too much code)
74
+ def add_business_rules_data(xml, payment_method, options)
75
+ prioritized_options = [options, @options]
76
+
77
+ xml.tag! 'businessRules' do
78
+ xml.tag!('ignoreAVSResult', 'true') if extract_option(prioritized_options, :ignore_avs)
79
+ xml.tag!('ignoreCVResult', 'true') if extract_option(prioritized_options, :ignore_cvv)
80
+ end
81
+
82
+ if network_tokenization?(payment_method)
83
+ xml.tag! 'paymentNetworkToken' do
84
+ xml.tag!('transactionType', "1")
85
+ end
86
+ end
87
+ end
88
+
17
89
  # See https://github.com/killbill/killbill-cybersource-plugin/issues/4
18
90
  def commit(request, options)
19
91
  request = build_request(request, options)
@@ -24,14 +96,23 @@ module ActiveMerchant
24
96
  end
25
97
  response = parse(raw_response)
26
98
 
99
+ # Remove namespace when unnecessary (ActiveMerchant and our original code expect it that way)
100
+ response.keys.each do |k|
101
+ _, actual_key = k.to_s.split('_', 2)
102
+ if !actual_key.nil? && !response.has_key?(actual_key)
103
+ response[actual_key] = response[k]
104
+ response.delete(k)
105
+ end
106
+ end
107
+
27
108
  success = response[:decision] == 'ACCEPT'
28
- authorization = success ? [options[:order_id], response[:requestID], response[:requestToken]].compact.join(";") : nil
109
+ authorization = success ? [options[:order_id], response[:requestID], response[:requestToken]].compact.join(';') : nil
29
110
 
30
- if response[:faultcode] == 'wsse:FailedCheck'
31
- message = { :exception_message => response[:message], :payment_plugin_status => :CANCELED }.to_json
32
- else
33
- message = @@response_codes[('r' + response[:reasonCode]).to_sym] rescue response[:message]
111
+ message = nil
112
+ if response[:reasonCode].blank? && (response[:faultcode] == 'wsse:FailedCheck' || response[:faultcode] == 'wsse:InvalidSecurity' || response[:faultcode] == 'soap:Client' || response[:faultcode] == 'c:ServerError')
113
+ message = {:exception_message => response[:message], :payment_plugin_status => :CANCELED}.to_json
34
114
  end
115
+ message ||= @@response_codes[('r' + response[:reasonCode].to_s).to_sym] || response[:message]
35
116
 
36
117
  Response.new(success, message, response,
37
118
  :test => test?,
@@ -40,6 +121,31 @@ module ActiveMerchant
40
121
  :cvv_result => response[:cvCode]
41
122
  )
42
123
  end
124
+
125
+ def add_merchant_data(xml, options)
126
+ xml.tag! 'merchantID', @options[:login]
127
+ xml.tag! 'merchantReferenceCode', options[:order_id]
128
+ xml.tag! 'clientLibrary' ,'Kill Bill'
129
+ xml.tag! 'clientLibraryVersion', KB_PLUGIN_VERSION
130
+ xml.tag! 'clientEnvironment' , RUBY_PLATFORM
131
+ end
132
+
133
+ def parse_element(reply, node)
134
+ if node.has_elements?
135
+ node.elements.each { |e| parse_element(reply, e) }
136
+ else
137
+ # The original ActiveMerchant implementation clobbers top level fields with values from the children
138
+ # Instead, always namespace the keys for children (cleanup is needed afterwards, see above)
139
+ is_top_level = node.parent.name == 'replyMessage' || node.parent.name == 'Fault'
140
+ key = node.name.to_sym
141
+ unless is_top_level
142
+ parent = node.parent.name + (node.parent.attributes['id'] ? '_' + node.parent.attributes['id'] : '')
143
+ key = (parent + '_' + node.name).to_sym
144
+ end
145
+ reply[key] = node.text
146
+ end
147
+ return reply
148
+ end
43
149
  end
44
150
  end
45
151
  end
@@ -6,6 +6,9 @@ module Killbill #:nodoc:
6
6
 
7
7
  has_one :cybersource_transaction
8
8
 
9
+ UNDEFINED_ERROR_CODES = [ 151, 152, 250 ]
10
+ CANCELED_ERROR_CODES = [ 101, 102, 104, 150, 207, 232, 234, 235, 236, 237, 238, 239, 240, 241, 243, 246, 247, 254 ]
11
+
9
12
  def self.from_response(api_call, kb_account_id, kb_payment_id, kb_payment_transaction_id, transaction_type, payment_processor_account_id, kb_tenant_id, response, extra_params = {}, model = ::Killbill::Cybersource::CybersourceResponse)
10
13
  super(api_call,
11
14
  kb_account_id,
@@ -98,8 +101,23 @@ module Killbill #:nodoc:
98
101
 
99
102
  t_info_plugin.properties << create_plugin_property('cybersourceResponseId', id)
100
103
 
104
+ set_correct_status(t_info_plugin)
105
+
101
106
  t_info_plugin
102
107
  end
108
+
109
+ def set_correct_status(t_info_plugin)
110
+ # Respect the existing status if the payment was successful, if overridden or if there is no error code
111
+ return if success || message.strip.start_with?('{') || gateway_error_code.blank?
112
+
113
+ if CANCELED_ERROR_CODES.include?(gateway_error_code.to_i)
114
+ t_info_plugin.status = :CANCELED
115
+ elsif UNDEFINED_ERROR_CODES.include?(gateway_error_code.to_i)
116
+ t_info_plugin.status = :UNDEFINED
117
+ else
118
+ t_info_plugin.status = :ERROR
119
+ end
120
+ end
103
121
  end
104
122
  end
105
123
  end
data/pom.xml CHANGED
@@ -25,7 +25,7 @@
25
25
  <groupId>org.kill-bill.billing.plugin.ruby</groupId>
26
26
  <artifactId>cybersource-plugin</artifactId>
27
27
  <packaging>pom</packaging>
28
- <version>4.0.2</version>
28
+ <version>4.0.3</version>
29
29
  <name>cybersource-plugin</name>
30
30
  <url>http://github.com/killbill/killbill-cybersource-plugin</url>
31
31
  <description>Plugin for accessing Cybersource as a payment gateway</description>
@@ -24,6 +24,26 @@ describe Killbill::Cybersource::PaymentPlugin do
24
24
  end
25
25
  end
26
26
 
27
+ let(:expected_successful_params) do
28
+ {
29
+ :params_merchant_reference_code => 'b0a6cf9aa07f1a8495f89c364bbd6a9a',
30
+ :params_request_id => '2004333231260008401927',
31
+ :params_decision => 'ACCEPT',
32
+ :params_reason_code => '100',
33
+ :params_request_token => 'Afvvj7Ke2Fmsbq0wHFE2sM6R4GAptYZ0jwPSA+R9PhkyhFTb0KRjoE4+ynthZrG6tMBwjAtT',
34
+ :params_currency => 'USD',
35
+ :params_amount => '1.00',
36
+ :params_authorization_code => '123456',
37
+ :params_avs_code => 'Y',
38
+ :params_avs_code_raw => 'Y',
39
+ :params_cv_code => 'M',
40
+ :params_authorized_date_time => '2008-01-15T21:42:03Z',
41
+ :params_processor_response => '00',
42
+ :params_reconciliation_id => 'ABCDEF',
43
+ :params_subscription_id => 'XXYYZZ'
44
+ }
45
+ end
46
+
27
47
  it 'should start and stop correctly' do
28
48
  @plugin.stop_plugin
29
49
  end
@@ -54,8 +74,8 @@ describe Killbill::Cybersource::PaymentPlugin do
54
74
  request_body.should_not match('<ignoreCVResult>')
55
75
  successful_purchase_response
56
76
  end
57
- purchase
58
- purchase(:PROCESSED, [build_property('ignore_avs', 'false'), build_property('ignore_cvv', 'false')])
77
+ purchase_with_token(:PROCESSED, [], expected_successful_params)
78
+ purchase_with_token(:PROCESSED, [build_property('ignore_avs', 'false'), build_property('ignore_cvv', 'false')], expected_successful_params)
59
79
  end
60
80
 
61
81
  it 'ignores AVS and CVN' do
@@ -64,7 +84,7 @@ describe Killbill::Cybersource::PaymentPlugin do
64
84
  request_body.should match('<ignoreCVResult>')
65
85
  successful_purchase_response
66
86
  end
67
- purchase(:PROCESSED, [build_property('ignore_avs', 'true'), build_property('ignore_cvv', 'true')])
87
+ purchase_with_token(:PROCESSED, [build_property('ignore_avs', 'true'), build_property('ignore_cvv', 'true')], expected_successful_params)
68
88
  end
69
89
 
70
90
  it 'ignores AVS but not CVN' do
@@ -73,8 +93,8 @@ describe Killbill::Cybersource::PaymentPlugin do
73
93
  request_body.should_not match('<ignoreCVResult>')
74
94
  successful_purchase_response
75
95
  end
76
- purchase(:PROCESSED, [build_property('ignore_avs', 'true')])
77
- purchase(:PROCESSED, [build_property('ignore_avs', 'true'), build_property('ignore_cvv', 'false')])
96
+ purchase_with_token(:PROCESSED, [build_property('ignore_avs', 'true')], expected_successful_params)
97
+ purchase_with_token(:PROCESSED, [build_property('ignore_avs', 'true'), build_property('ignore_cvv', 'false')], expected_successful_params)
78
98
  end
79
99
 
80
100
  it 'ignores CVN but not AVS' do
@@ -83,8 +103,45 @@ describe Killbill::Cybersource::PaymentPlugin do
83
103
  request_body.should match('<ignoreCVResult>')
84
104
  successful_purchase_response
85
105
  end
86
- purchase(:PROCESSED, [build_property('ignore_cvv', 'true')])
87
- purchase(:PROCESSED, [build_property('ignore_avs', 'false'), build_property('ignore_cvv', 'true')])
106
+ purchase_with_token(:PROCESSED, [build_property('ignore_cvv', 'true')], expected_successful_params)
107
+ purchase_with_token(:PROCESSED, [build_property('ignore_avs', 'false'), build_property('ignore_cvv', 'true')], expected_successful_params)
108
+ end
109
+ end
110
+
111
+ context 'Override parameters' do
112
+
113
+ it 'has a default commerceIndicator' do
114
+ ::ActiveMerchant::Billing::CyberSourceGateway.any_instance.stub(:ssl_post) do |host, request_body|
115
+ request_body.should_not match('<commerceIndicator>')
116
+ successful_purchase_response
117
+ end
118
+ purchase_with_token(:PROCESSED, [], expected_successful_params)
119
+ end
120
+
121
+ it 'can override commerceIndicator for card-on-file' do
122
+ ::ActiveMerchant::Billing::CyberSourceGateway.any_instance.stub(:ssl_post) do |host, request_body|
123
+ request_body.should match('<commerceIndicator>recurring</commerceIndicator>')
124
+ successful_purchase_response
125
+ end
126
+ purchase_with_card(:PROCESSED, [build_property('commerce_indicator', 'recurring')], expected_successful_params)
127
+ end
128
+
129
+ it 'has a default commerceIndicator for Apple Pay' do
130
+ ::ActiveMerchant::Billing::CyberSourceGateway.any_instance.stub(:ssl_post) do |host, request_body|
131
+ request_body.should_not match('<commerceIndicator>internet</commerceIndicator>')
132
+ request_body.should match('<commerceIndicator>vbv</commerceIndicator>')
133
+ successful_purchase_response
134
+ end
135
+ purchase_with_network_tokenization(:PROCESSED, [], expected_successful_params)
136
+ end
137
+
138
+ it 'can override commerceIndicator for Apple Pay' do
139
+ ::ActiveMerchant::Billing::CyberSourceGateway.any_instance.stub(:ssl_post) do |host, request_body|
140
+ request_body.should_not match('<commerceIndicator>vbv</commerceIndicator>')
141
+ request_body.should match('<commerceIndicator>internet</commerceIndicator>')
142
+ successful_purchase_response
143
+ end
144
+ purchase_with_network_tokenization(:PROCESSED, [build_property('commerce_indicator', 'internet')], expected_successful_params)
88
145
  end
89
146
  end
90
147
 
@@ -92,17 +149,24 @@ describe Killbill::Cybersource::PaymentPlugin do
92
149
 
93
150
  it 'handles expired passwords as CANCELED transactions' do
94
151
  ::ActiveMerchant::Billing::CyberSourceGateway.any_instance.stub(:ssl_post).and_return(password_expired_response)
95
- purchase(:CANCELED).gateway_error.should == 'wsse:FailedCheck: Security Data : Merchant password has expired.'
152
+ purchase_with_token(:CANCELED).gateway_error.should == 'wsse:FailedCheck: Security Data : Merchant password has expired.'
96
153
  end
97
154
 
98
155
  it 'handles bad passwords as CANCELED transactions' do
99
156
  ::ActiveMerchant::Billing::CyberSourceGateway.any_instance.stub(:ssl_post).and_return(bad_password_response)
100
- purchase(:CANCELED).gateway_error.should == 'wsse:FailedCheck: Security Data : UsernameToken authentication failed.'
157
+ purchase_with_token(:CANCELED).gateway_error.should == 'wsse:FailedCheck: Security Data : UsernameToken authentication failed.'
101
158
  end
102
159
 
103
160
  it 'handles unsuccessful authorizations as ERROR transactions' do
104
161
  ::ActiveMerchant::Billing::CyberSourceGateway.any_instance.stub(:ssl_post).and_return(unsuccessful_authorization_response)
105
- purchase(:ERROR).gateway_error.should == 'Invalid account number'
162
+ purchase_with_token(:ERROR).gateway_error.should == 'Invalid account number'
163
+ end
164
+
165
+ it 'parses correctly authorization reversal errors' do
166
+ ::ActiveMerchant::Billing::CyberSourceGateway.any_instance.stub(:ssl_post).and_return(unsuccessful_auth_reversal_response)
167
+ payment_response = purchase_with_token(:CANCELED)
168
+ payment_response.gateway_error.should == 'One or more fields contains invalid data'
169
+ payment_response.gateway_error_code.should == '102'
106
170
  end
107
171
  end
108
172
 
@@ -125,16 +189,43 @@ describe Killbill::Cybersource::PaymentPlugin do
125
189
  t.destroy! unless t.nil?
126
190
  end
127
191
 
128
- def purchase(expected_status = :PROCESSED, properties = [])
129
- kb_payment_id = SecureRandom.uuid
130
- kb_payment = @plugin.kb_apis.proxied_services[:payment_api].add_payment(kb_payment_id)
131
- kb_transaction_id = kb_payment.transactions[0].id
192
+ def purchase_with_card(expected_status = :PROCESSED, properties = [], expected_params = {})
193
+ properties << build_property('email', 'foo@bar.com')
194
+ properties << build_property('cc_number', '4111111111111111')
132
195
 
196
+ purchase(expected_status, properties, expected_params)
197
+ end
198
+
199
+ def purchase_with_token(expected_status = :PROCESSED, properties = [], expected_params = {})
133
200
  properties << build_property('email', 'foo@bar.com')
134
201
  properties << build_property('token', '1234')
135
202
 
203
+ purchase(expected_status, properties, expected_params)
204
+ end
205
+
206
+ def purchase_with_network_tokenization(expected_status = :PROCESSED, properties = [], expected_params = {})
207
+ properties << build_property('email', 'foo@bar.com')
208
+ properties << build_property('cc_number', '4111111111111111')
209
+ properties << build_property('brand', 'visa')
210
+ properties << build_property('eci', '05')
211
+ properties << build_property('payment_cryptogram', '111111111100cryptogram')
212
+
213
+ purchase(expected_status, properties, expected_params)
214
+ end
215
+
216
+ def purchase(expected_status = :PROCESSED, properties = [], expected_params = {})
217
+ kb_payment_id = SecureRandom.uuid
218
+ kb_payment = @plugin.kb_apis.proxied_services[:payment_api].add_payment(kb_payment_id)
219
+ kb_transaction_id = kb_payment.transactions[0].id
220
+
136
221
  payment_response = @plugin.purchase_payment(SecureRandom.uuid, kb_payment_id, kb_transaction_id, SecureRandom.uuid, BigDecimal.new('100'), 'USD', properties, build_call_context)
137
222
  payment_response.status.should eq(expected_status), payment_response.gateway_error
223
+
224
+ gw_response = Killbill::Cybersource::CybersourceResponse.last
225
+ expected_params.each do |k, v|
226
+ gw_response.send(k.to_sym).should == v
227
+ end
228
+
138
229
  payment_response
139
230
  end
140
231
 
@@ -142,7 +233,7 @@ describe Killbill::Cybersource::PaymentPlugin do
142
233
  <<-XML
143
234
  <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
144
235
  <soap:Header>
145
- <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-2636690"><wsu:Created>2008-01-15T21:42:03.343Z</wsu:Created></wsu:Timestamp></wsse:Security></soap:Header><soap:Body><c:replyMessage xmlns:c="urn:schemas-cybersource-com:transaction-data-1.26"><c:merchantReferenceCode>b0a6cf9aa07f1a8495f89c364bbd6a9a</c:merchantReferenceCode><c:requestID>2004333231260008401927</c:requestID><c:decision>ACCEPT</c:decision><c:reasonCode>100</c:reasonCode><c:requestToken>Afvvj7Ke2Fmsbq0wHFE2sM6R4GAptYZ0jwPSA+R9PhkyhFTb0KRjoE4+ynthZrG6tMBwjAtT</c:requestToken><c:purchaseTotals><c:currency>USD</c:currency></c:purchaseTotals><c:ccAuthReply><c:reasonCode>100</c:reasonCode><c:amount>1.00</c:amount><c:authorizationCode>123456</c:authorizationCode><c:avsCode>Y</c:avsCode><c:avsCodeRaw>Y</c:avsCodeRaw><c:cvCode>M</c:cvCode><c:cvCodeRaw>M</c:cvCodeRaw><c:authorizedDateTime>2008-01-15T21:42:03Z</c:authorizedDateTime><c:processorResponse>00</c:processorResponse><c:authFactorCode>U</c:authFactorCode></c:ccAuthReply></c:replyMessage></soap:Body></soap:Envelope>
236
+ <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-2636690"><wsu:Created>2008-01-15T21:42:03.343Z</wsu:Created></wsu:Timestamp></wsse:Security></soap:Header><soap:Body><c:replyMessage xmlns:c="urn:schemas-cybersource-com:transaction-data-1.26"><c:merchantReferenceCode>b0a6cf9aa07f1a8495f89c364bbd6a9a</c:merchantReferenceCode><c:requestID>2004333231260008401927</c:requestID><c:decision>ACCEPT</c:decision><c:reasonCode>100</c:reasonCode><c:requestToken>Afvvj7Ke2Fmsbq0wHFE2sM6R4GAptYZ0jwPSA+R9PhkyhFTb0KRjoE4+ynthZrG6tMBwjAtT</c:requestToken><c:purchaseTotals><c:currency>USD</c:currency></c:purchaseTotals><c:ccAuthReply><c:reasonCode>100</c:reasonCode><c:amount>1.00</c:amount><c:authorizationCode>123456</c:authorizationCode><c:avsCode>Y</c:avsCode><c:avsCodeRaw>Y</c:avsCodeRaw><c:cvCode>M</c:cvCode><c:cvCodeRaw>M</c:cvCodeRaw><c:authorizedDateTime>2008-01-15T21:42:03Z</c:authorizedDateTime><c:processorResponse>00</c:processorResponse><c:reconciliationID>ABCDEF</c:reconciliationID><c:authFactorCode>U</c:authFactorCode></c:ccAuthReply><c:paySubscriptionCreateReply><c:reasonCode>100</c:reasonCode><c:subscriptionID>XXYYZZ</c:subscriptionID></c:paySubscriptionCreateReply></c:replyMessage></soap:Body></soap:Envelope>
146
237
  XML
147
238
  end
148
239
 
@@ -169,4 +260,12 @@ describe Killbill::Cybersource::PaymentPlugin do
169
260
  <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-28121162"><wsu:Created>2008-01-15T21:50:41.580Z</wsu:Created></wsu:Timestamp></wsse:Security></soap:Header><soap:Body><c:replyMessage xmlns:c="urn:schemas-cybersource-com:transaction-data-1.26"><c:merchantReferenceCode>a1efca956703a2a5037178a8a28f7357</c:merchantReferenceCode><c:requestID>2004338415330008402434</c:requestID><c:decision>REJECT</c:decision><c:reasonCode>231</c:reasonCode><c:requestToken>Afvvj7KfIgU12gooCFE2/DanQIApt+G1OgTSA+R9PTnyhFTb0KRjgFY+ynyIFNdoKKAghwgx</c:requestToken><c:ccAuthReply><c:reasonCode>231</c:reasonCode></c:ccAuthReply></c:replyMessage></soap:Body></soap:Envelope>
170
261
  XML
171
262
  end
263
+
264
+ def unsuccessful_auth_reversal_response
265
+ <<-XML
266
+ <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
267
+ <soap:Header>
268
+ <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="Timestamp-28121162"><wsu:Created>2008-01-15T21:50:41.580Z</wsu:Created></wsu:Timestamp></wsse:Security></soap:Header><soap:Body><c:replyMessage xmlns:c="urn:schemas-cybersource-com:transaction-data-1.26"><c:merchantReferenceCode>a1efca956703a2a5037178a8a28f7357</c:merchantReferenceCode><c:requestID>2004338415330008402434</c:requestID><c:decision>REJECT</c:decision><c:reasonCode>102</c:reasonCode><c:requestToken>Afvvj7KfIgU12gooCFE2/DanQIApt+G1OgTSA+R9PTnyhFTb0KRjgFY+ynyIFNdoKKAghwgx</c:requestToken><c:ccAuthReversalReply><c:reasonCode>102</c:reasonCode></c:ccAuthReversalReply><c:originalTransaction><c:amount>0.00</c:amount><c:reasonCode>100</c:reasonCode></c:originalTransaction></c:replyMessage></soap:Body></soap:Envelope>
269
+ XML
270
+ end
172
271
  end
@@ -21,18 +21,19 @@ describe Killbill::Cybersource::PaymentPlugin do
21
21
  @amount = BigDecimal.new('100')
22
22
  @currency = 'USD'
23
23
 
24
- kb_payment_id = SecureRandom.uuid
25
- 1.upto(6) do
26
- @kb_payment = @plugin.kb_apis.proxied_services[:payment_api].add_payment(kb_payment_id)
27
- end
24
+ @kb_payment = setup_kb_payment(6)
28
25
  end
29
26
 
30
27
  after(:each) do
31
28
  @plugin.stop_plugin
32
29
  end
33
30
 
31
+ let(:report_api) do
32
+ @plugin.get_report_api({}, @call_context)
33
+ end
34
+
34
35
  let(:with_report_api) do
35
- @plugin.get_report_api(@call_context.tenant_id).present?
36
+ report_api.present?
36
37
  end
37
38
 
38
39
  it 'should be able to charge a Credit Card directly and calls should be idempotent' do
@@ -43,12 +44,9 @@ describe Killbill::Cybersource::PaymentPlugin do
43
44
  Killbill::Cybersource::CybersourceTransaction.all.size.should == 0
44
45
 
45
46
  payment_response = @plugin.purchase_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, properties, @call_context)
46
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
47
- payment_response.amount.should == @amount
48
- payment_response.transaction_type.should == :PURCHASE
47
+ check_response(payment_response, @amount, :PURCHASE, :PROCESSED, 'Successful transaction', '100')
49
48
  payment_response.first_payment_reference_id.should_not be_nil
50
49
  payment_response.second_payment_reference_id.should_not be_nil
51
- payment_response.gateway_error_code.should_not be_nil
52
50
 
53
51
  responses = Killbill::Cybersource::CybersourceResponse.all
54
52
  responses.size.should == 2
@@ -60,8 +58,8 @@ describe Killbill::Cybersource::PaymentPlugin do
60
58
  transactions.size.should == 1
61
59
  transactions[0].api_call.should == 'purchase'
62
60
 
63
- # Skip the rest of the test if the report API isn't configured
64
- break unless with_report_api
61
+ # Skip the rest of the test if the report API isn't configured to check for duplicates
62
+ break unless with_report_api && report_api.check_for_duplicates?
65
63
 
66
64
  payment_response = @plugin.purchase_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
67
65
  payment_response.amount.should == @amount
@@ -88,9 +86,77 @@ describe Killbill::Cybersource::PaymentPlugin do
88
86
  transactions[1].txn_id.should be_nil
89
87
  end
90
88
 
89
+ it 'should be able to verify a Credit Card' do
90
+ # Valid card
91
+ properties = build_pm_properties
92
+ kb_payment = setup_kb_payment(2)
93
+ payment_response = @plugin.authorize_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, @pm.kb_payment_method_id, 0, @currency, properties, @call_context)
94
+ check_response(payment_response, 0, :AUTHORIZE, :PROCESSED, 'Successful transaction', '100')
95
+ payment_response.first_payment_reference_id.should_not be_nil
96
+ payment_response.second_payment_reference_id.should_not be_nil
97
+
98
+ # Note that you won't be able to void the $0 auth
99
+ payment_response = @plugin.void_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[1].id, @pm.kb_payment_method_id, @properties, @call_context)
100
+ check_response(payment_response, nil, :VOID, :CANCELED, 'One or more fields contains invalid data', '102')
101
+
102
+ # Invalid card
103
+ # See http://www.cybersource.com/developers/getting_started/test_and_manage/simple_order_api/HTML/General_testing_info/soapi_general_test.html
104
+ properties = build_pm_properties(nil, { :cc_exp_year => 1998 })
105
+ kb_payment = setup_kb_payment
106
+ payment_response = @plugin.authorize_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, @pm.kb_payment_method_id, 0, @currency, properties, @call_context)
107
+ check_response(payment_response, nil, :AUTHORIZE, :ERROR, 'Expired card', '202')
108
+ payment_response.first_payment_reference_id.should_not be_nil
109
+ payment_response.second_payment_reference_id.should be_nil
110
+
111
+ # Discover card (doesn't support $0 auth on Paymentech)
112
+ # See http://www.cybersource.com/developers/other_resources/quick_references/test_cc_numbers/
113
+ properties = build_pm_properties(nil, { :cc_number => '6011111111111117', :cc_type => :discover })
114
+ kb_payment = setup_kb_payment
115
+ payment_response = @plugin.authorize_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, @pm.kb_payment_method_id, 0, @currency, properties, @call_context)
116
+ check_response(payment_response, nil, :AUTHORIZE, :CANCELED, 'A problem exists with your CyberSource merchant configuration', '234')
117
+ payment_response.first_payment_reference_id.should_not be_nil
118
+ payment_response.second_payment_reference_id.should be_nil
119
+ # Verify the GET path
120
+ transaction_info_plugins = @plugin.get_payment_info(@pm.kb_account_id, kb_payment.id, @properties, @call_context)
121
+ transaction_info_plugins.size.should == 1
122
+ transaction_info_plugins.first.transaction_type.should eq(:AUTHORIZE)
123
+ transaction_info_plugins.first.status.should eq(:CANCELED)
124
+
125
+ # Force the validation on Discover
126
+ properties << build_property('force_validation', 'true')
127
+ kb_payment = setup_kb_payment
128
+ payment_response = @plugin.authorize_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, @pm.kb_payment_method_id, 0, @currency, properties, @call_context)
129
+ check_response(payment_response, 1, :AUTHORIZE, :PROCESSED, 'Successful transaction', '100')
130
+ payment_response.first_payment_reference_id.should_not be_nil
131
+ payment_response.second_payment_reference_id.should_not be_nil
132
+ # Verify the GET path
133
+ transaction_info_plugins = @plugin.get_payment_info(@pm.kb_account_id, kb_payment.id, @properties, @call_context)
134
+ transaction_info_plugins.size.should == 3
135
+ transaction_info_plugins[0].transaction_type.should eq(:AUTHORIZE)
136
+ transaction_info_plugins[0].status.should eq(:CANCELED)
137
+ transaction_info_plugins[0].kb_transaction_payment_id.should_not eq(kb_payment.transactions[0].id)
138
+ transaction_info_plugins[1].transaction_type.should eq(:AUTHORIZE)
139
+ transaction_info_plugins[1].status.should eq(:PROCESSED)
140
+ transaction_info_plugins[1].kb_transaction_payment_id.should eq(kb_payment.transactions[0].id)
141
+ transaction_info_plugins[2].transaction_type.should eq(:VOID)
142
+ transaction_info_plugins[2].status.should eq(:PROCESSED)
143
+ transaction_info_plugins[2].kb_transaction_payment_id.should_not eq(kb_payment.transactions[0].id)
144
+ end
145
+
146
+ it 'should be able to bypass AVS and CVV rules with Apple Pay' do
147
+ properties = build_pm_properties(nil,
148
+ {
149
+ :payment_cryptogram => 'EHuWW9PiBkWvqE5juRwDzAUFBAk=',
150
+ :ignore_avs => true,
151
+ :ignore_cvv => true
152
+ })
153
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, properties, @call_context)
154
+ check_response(payment_response, @amount, :PURCHASE, :PROCESSED, 'Successful transaction', '100')
155
+ end
156
+
91
157
  it 'should be able to fix UNDEFINED payments' do
92
158
  payment_response = @plugin.purchase_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
93
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
159
+ check_response(payment_response, @amount, :PURCHASE, :PROCESSED, 'Successful transaction', '100')
94
160
 
95
161
  # Force a transition to :UNDEFINED
96
162
  Killbill::Cybersource::CybersourceTransaction.last.delete
@@ -149,71 +215,51 @@ describe Killbill::Cybersource::PaymentPlugin do
149
215
 
150
216
  it 'should be able to charge and refund' do
151
217
  payment_response = @plugin.purchase_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
152
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
153
- payment_response.amount.should == @amount
154
- payment_response.transaction_type.should == :PURCHASE
218
+ check_response(payment_response, @amount, :PURCHASE, :PROCESSED, 'Successful transaction', '100')
155
219
 
156
220
  # Try a full refund
157
221
  refund_response = @plugin.refund_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[1].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
158
- refund_response.status.should eq(:PROCESSED), refund_response.gateway_error
159
- refund_response.amount.should == @amount
160
- refund_response.transaction_type.should == :REFUND
222
+ check_response(refund_response, @amount, :REFUND, :PROCESSED, 'Successful transaction', '100')
161
223
  end
162
224
 
163
225
  it 'should be able to auth, capture and refund' do
164
226
  payment_response = @plugin.authorize_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
165
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
166
- payment_response.amount.should == @amount
167
- payment_response.transaction_type.should == :AUTHORIZE
227
+ check_response(payment_response, @amount, :AUTHORIZE, :PROCESSED, 'Successful transaction', '100')
168
228
 
169
229
  # Try multiple partial captures
170
230
  partial_capture_amount = BigDecimal.new('10')
171
231
  1.upto(3) do |i|
172
232
  payment_response = @plugin.capture_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[i].id, @pm.kb_payment_method_id, partial_capture_amount, @currency, @properties, @call_context)
173
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
174
- payment_response.amount.should == partial_capture_amount
175
- payment_response.transaction_type.should == :CAPTURE
233
+ check_response(payment_response, partial_capture_amount, :CAPTURE, :PROCESSED, 'Successful transaction', '100')
176
234
  end
177
235
 
178
236
  # Try a partial refund
179
237
  refund_response = @plugin.refund_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[4].id, @pm.kb_payment_method_id, partial_capture_amount, @currency, @properties, @call_context)
180
- refund_response.status.should eq(:PROCESSED), refund_response.gateway_error
181
- refund_response.amount.should == partial_capture_amount
182
- refund_response.transaction_type.should == :REFUND
238
+ check_response(refund_response, partial_capture_amount, :REFUND, :PROCESSED, 'Successful transaction', '100')
183
239
 
184
240
  # Try to capture again
185
241
  payment_response = @plugin.capture_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[5].id, @pm.kb_payment_method_id, partial_capture_amount, @currency, @properties, @call_context)
186
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
187
- payment_response.amount.should == partial_capture_amount
188
- payment_response.transaction_type.should == :CAPTURE
242
+ check_response(payment_response, partial_capture_amount, :CAPTURE, :PROCESSED, 'Successful transaction', '100')
189
243
  end
190
244
 
191
245
  it 'should be able to auth and void' do
192
246
  payment_response = @plugin.authorize_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
193
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
194
- payment_response.amount.should == @amount
195
- payment_response.transaction_type.should == :AUTHORIZE
247
+ check_response(payment_response, @amount, :AUTHORIZE, :PROCESSED, 'Successful transaction', '100')
196
248
 
197
249
  payment_response = @plugin.void_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[1].id, @pm.kb_payment_method_id, @properties, @call_context)
198
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
199
- payment_response.transaction_type.should == :VOID
250
+ check_response(payment_response, nil, :VOID, :PROCESSED, 'Successful transaction', '100')
200
251
  end
201
252
 
202
253
  it 'should be able to auth, partial capture and void' do
203
254
  payment_response = @plugin.authorize_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
204
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
205
- payment_response.amount.should == @amount
206
- payment_response.transaction_type.should == :AUTHORIZE
255
+ check_response(payment_response, @amount, :AUTHORIZE, :PROCESSED, 'Successful transaction', '100')
207
256
 
208
257
  partial_capture_amount = BigDecimal.new('10')
209
258
  payment_response = @plugin.capture_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[1].id, @pm.kb_payment_method_id, partial_capture_amount, @currency, @properties, @call_context)
210
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
211
- payment_response.amount.should == partial_capture_amount
212
- payment_response.transaction_type.should == :CAPTURE
259
+ check_response(payment_response, partial_capture_amount, :CAPTURE, :PROCESSED, 'Successful transaction', '100')
213
260
 
214
261
  payment_response = @plugin.void_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[2].id, @pm.kb_payment_method_id, @properties, @call_context)
215
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
216
- payment_response.transaction_type.should == :VOID
262
+ check_response(payment_response, nil, :VOID, :PROCESSED, 'Successful transaction', '100')
217
263
  Killbill::Cybersource::CybersourceResponse.last.params_amount.should == '10.00'
218
264
 
219
265
  # From the CyberSource documentation:
@@ -221,25 +267,217 @@ describe Killbill::Cybersource::PaymentPlugin do
221
267
  # your processor supports authorization reversal after void as described in "Authorization Reversal After Void," page 39, CyberSource recommends that you request an authorization reversal
222
268
  # to release the hold on the unused credit card funds.
223
269
  payment_response = @plugin.void_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[3].id, @pm.kb_payment_method_id, @properties, @call_context)
224
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
225
- payment_response.transaction_type.should == :VOID
270
+ check_response(payment_response, nil, :VOID, :PROCESSED, 'Successful transaction', '100')
226
271
  Killbill::Cybersource::CybersourceResponse.last.params_amount.should == '100.00'
227
272
  end
228
273
 
229
274
  it 'should be able to credit' do
230
275
  payment_response = @plugin.credit_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
231
- payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
232
- payment_response.amount.should == @amount
233
- payment_response.transaction_type.should == :CREDIT
276
+ check_response(payment_response, @amount, :CREDIT, :PROCESSED, 'Successful transaction', '100')
234
277
  end
235
278
 
236
279
  # See https://github.com/killbill/killbill-cybersource-plugin/issues/4
237
- it 'handles errors gracefully' do
280
+ it 'handles 500 errors gracefully' do
238
281
  properties_with_no_expiration_year = build_pm_properties
239
282
  cc_exp_year = properties_with_no_expiration_year.find { |prop| prop.key == 'ccExpirationYear' }
240
283
  cc_exp_year.value = nil
241
284
 
242
- payment_response = @plugin.purchase_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, SecureRandom.uuid, @amount, @currency, properties_with_no_expiration_year, @call_context)
243
- payment_response.status.should eq(:ERROR), payment_response.gateway_error
285
+ kb_payment = setup_kb_payment
286
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, SecureRandom.uuid, @amount, @currency, properties_with_no_expiration_year, @call_context)
287
+ check_response(payment_response, nil, :PURCHASE, :CANCELED, '{"exception_message":"soap:Client: \\nXML parse error.\\n","payment_plugin_status":"CANCELED"}', nil)
288
+ end
289
+
290
+ # See http://www.cybersource.com/developers/getting_started/test_and_manage/simple_order_api/HTML/General_testing_info/soapi_general_test.html
291
+ it 'sets the correct transaction status' do
292
+ properties = build_pm_properties
293
+
294
+ kb_payment = setup_kb_payment
295
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, SecureRandom.uuid, -1, @currency, properties, @call_context)
296
+ check_response(payment_response, nil, :PURCHASE, :CANCELED, 'One or more fields contains invalid data', '102')
297
+
298
+ kb_payment = setup_kb_payment
299
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, SecureRandom.uuid, 100000000000, @currency, properties, @call_context)
300
+ check_response(payment_response, nil, :PURCHASE, :CANCELED, 'One or more fields contains invalid data', '102')
301
+
302
+ kb_payment = setup_kb_payment
303
+ bogus_properties = build_pm_properties(nil, {:cc_number => '4111111111111112'})
304
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, SecureRandom.uuid, @amount, @currency, bogus_properties, @call_context)
305
+ check_response(payment_response, nil, :PURCHASE, :ERROR, 'Invalid account number', '231')
306
+
307
+ kb_payment = setup_kb_payment
308
+ bogus_properties = build_pm_properties(nil, {:cc_number => '412345678912345678914'})
309
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, SecureRandom.uuid, @amount, @currency, bogus_properties, @call_context)
310
+ check_response(payment_response, nil, :PURCHASE, :ERROR, 'Invalid account number', '231')
311
+
312
+ kb_payment = setup_kb_payment
313
+ bogus_properties = build_pm_properties(nil, {:cc_exp_month => '13'})
314
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, SecureRandom.uuid, @amount, @currency, bogus_properties, @call_context)
315
+ check_response(payment_response, nil, :PURCHASE, :CANCELED, 'One or more fields contains invalid data', '102')
316
+ end
317
+
318
+ context 'Processors' do
319
+
320
+ # See http://www.cybersource.com/developers/getting_started/test_and_manage/simple_order_api/HTML/Paymentech/soapi_ptech_err.html
321
+ it 'handles Chase Paymentech Solutions errors' do
322
+ properties = build_pm_properties
323
+
324
+ %w(000 236 248 265 266 267 301 519 769 902 905 906).each do |expected_processor_response|
325
+ kb_payment = setup_kb_payment
326
+ amount = 2000 + expected_processor_response.to_i
327
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, SecureRandom.uuid, amount, @currency, properties, @call_context)
328
+ check_response(payment_response, nil, :PURCHASE, :CANCELED, 'General failure', '150', expected_processor_response)
329
+ end
330
+
331
+ %w(239 241 249 833).each do |expected_processor_response|
332
+ kb_payment = setup_kb_payment
333
+ amount = 2000 + expected_processor_response.to_i
334
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, SecureRandom.uuid, amount, @currency, properties, @call_context)
335
+ check_response(payment_response, nil, :PURCHASE, :CANCELED, 'A problem exists with your CyberSource merchant configuration', '234', expected_processor_response)
336
+ end
337
+
338
+ {'201' => '231',
339
+ '202' => '233',
340
+ '203' => '233',
341
+ # Disable most of the checks by default (test lasts for 7 minutes otherwise)
342
+ =begin
343
+ '204' => '233',
344
+ '205' => '233',
345
+ '218' => '233',
346
+ '219' => '233',
347
+ '220' => '233',
348
+ '225' => '233',
349
+ '227' => '233',
350
+ '231' => '233',
351
+ '233' => '233',
352
+ '234' => '233',
353
+ '238' => '233',
354
+ '243' => '233',
355
+ '244' => '233',
356
+ '245' => '233',
357
+ '246' => '233',
358
+ '247' => '233',
359
+ '253' => '233',
360
+ '257' => '233',
361
+ '258' => '233',
362
+ '261' => '233',
363
+ '263' => '233',
364
+ '264' => '233',
365
+ '268' => '233',
366
+ '269' => '203',
367
+ '270' => '203',
368
+ '271' => '203',
369
+ '273' => '203',
370
+ '275' => '203',
371
+ '302' => '210',
372
+ '303' => '203',
373
+ '304' => '231',
374
+ '401' => '201',
375
+ '402' => '201',
376
+ '501' => '205',
377
+ '502' => '205',
378
+ '503' => '209',
379
+ '505' => '203',
380
+ '508' => '203',
381
+ '509' => '204',
382
+ '510' => '203',
383
+ '521' => '204',
384
+ '522' => '202',
385
+ '523' => '233',
386
+ '524' => '211',
387
+ '530' => '203',
388
+ '531' => '211',
389
+ '540' => '203',
390
+ '541' => '205',
391
+ '542' => '203',
392
+ '543' => '203',
393
+ '544' => '203',
394
+ '545' => '203',
395
+ '546' => '203',
396
+ '547' => '233',
397
+ '548' => '233',
398
+ '549' => '203',
399
+ '550' => '203',
400
+ '551' => '233',
401
+ '560' => '203',
402
+ '561' => '203',
403
+ '562' => '203',
404
+ '563' => '203',
405
+ '564' => '203',
406
+ '567' => '203',
407
+ '570' => '203',
408
+ '571' => '203',
409
+ '572' => '203',
410
+ '591' => '231',
411
+ '592' => '203',
412
+ '594' => '203',
413
+ '595' => '208',
414
+ '596' => '205',
415
+ '597' => '233',
416
+ '602' => '233',
417
+ '603' => '233',
418
+ '605' => '233',
419
+ '606' => '208',
420
+ '607' => '233',
421
+ '610' => '231',
422
+ '617' => '203',
423
+ '719' => '203',
424
+ '740' => '233',
425
+ '741' => '233',
426
+ '742' => '233',
427
+ '747' => '233',
428
+ '750' => '233',
429
+ '751' => '233',
430
+ '752' => '233',
431
+ '753' => '233',
432
+ '754' => '233',
433
+ '755' => '233',
434
+ '756' => '233',
435
+ '757' => '233',
436
+ '758' => '233',
437
+ '759' => '233',
438
+ '760' => '233',
439
+ '763' => '233',
440
+ '764' => '233',
441
+ '765' => '233',
442
+ '766' => '233',
443
+ '767' => '233',
444
+ '768' => '233',
445
+ '802' => '203',
446
+ '806' => '203',
447
+ =end
448
+ '811' => '209',
449
+ '813' => '203',
450
+ '825' => '231',
451
+ '834' => '203',
452
+ '903' => '203',
453
+ '904' => '203'}.each do |expected_processor_response, expected_reason_code|
454
+ kb_payment = setup_kb_payment
455
+ amount = 2000 + expected_processor_response.to_i
456
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, kb_payment.id, kb_payment.transactions[0].id, SecureRandom.uuid, amount, @currency, properties, @call_context)
457
+ expected_error = ::ActiveMerchant::Billing::CyberSourceGateway.class_variable_get(:@@response_codes)[('r' + expected_reason_code).to_sym]
458
+ check_response(payment_response, nil, :PURCHASE, :ERROR, expected_error, expected_reason_code, expected_processor_response)
459
+ end
460
+ end
461
+ end
462
+
463
+ private
464
+
465
+ def check_response(payment_response, amount, transaction_type, expected_status, expected_error, expected_error_code, expected_processor_response = nil)
466
+ payment_response.amount.should == amount
467
+ payment_response.transaction_type.should == transaction_type
468
+ payment_response.status.should eq(expected_status), payment_response.gateway_error
469
+
470
+ gw_response = Killbill::Cybersource::CybersourceResponse.last
471
+ gw_response.gateway_error.should == expected_error
472
+ gw_response.gateway_error_code.should == expected_error_code
473
+ gw_response.params_processor_response.should == expected_processor_response unless expected_processor_response.nil?
474
+ end
475
+
476
+ def setup_kb_payment(nb_transactions=1, kb_payment_id=SecureRandom.uuid)
477
+ kb_payment = nil
478
+ 1.upto(nb_transactions) do
479
+ kb_payment = @plugin.kb_apis.proxied_services[:payment_api].add_payment(kb_payment_id)
480
+ end
481
+ kb_payment
244
482
  end
245
483
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: killbill-cybersource
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.2
4
+ version: 4.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kill Bill core team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-22 00:00:00.000000000 Z
11
+ date: 2016-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: killbill
@@ -283,6 +283,7 @@ files:
283
283
  - config.ru
284
284
  - cybersource.yml
285
285
  - db/ddl.sql
286
+ - db/migrate/20162519092522_enlarge_message.rb
286
287
  - db/schema.rb
287
288
  - killbill-cybersource.gemspec
288
289
  - killbill.properties
@@ -324,7 +325,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
324
325
  version: '0'
325
326
  requirements: []
326
327
  rubyforge_project:
327
- rubygems_version: 2.1.9
328
+ rubygems_version: 2.4.6
328
329
  signing_key:
329
330
  specification_version: 4
330
331
  summary: Plugin to use Cybersource as a gateway.