killbill-cybersource 4.0.2 → 4.0.3

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