diff --git a/CHANGELOG b/CHANGELOG
index 32c4e68ea39..aab6e7ce323 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -174,6 +174,8 @@
* Worldpay: Add support for deafult ECI value [aenand] #5126
* DLocal: Update stored credentials [sinourain] #5112
* NMI: Add NTID override [yunnydang] #5134
+* Cybersource Rest: Support L2/L3 data [aenand] #5117
+* Worldpay: Support L2/L3 data [aenand] #5117
== Version 1.135.0 (August 24, 2023)
* PaymentExpress: Correct endpoints [steveh] #4827
diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb
index 0a5e65711c8..fd0e30c5232 100644
--- a/lib/active_merchant/billing/gateways/cyber_source_rest.rb
+++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb
@@ -106,6 +106,23 @@ def scrub(transcript)
private
+ def add_level_2_data(post, options)
+ return unless options[:purchase_order_number]
+
+ post[:orderInformation][:invoiceDetails] ||= {}
+ post[:orderInformation][:invoiceDetails][:purchaseOrderNumber] = options[:purchase_order_number]
+ end
+
+ def add_level_3_data(post, options)
+ return unless options[:line_items]
+
+ post[:orderInformation][:lineItems] = options[:line_items]
+ post[:processingInformation][:purchaseLevel] = '3'
+ post[:orderInformation][:shipping_details] = { shipFromPostalCode: options[:ships_from_postal_code] }
+ post[:orderInformation][:amountDetails] ||= {}
+ post[:orderInformation][:amountDetails][:discountAmount] = options[:discount_amount]
+ end
+
def add_three_ds(post, payment_method, options)
return unless three_d_secure = options[:three_d_secure]
@@ -149,6 +166,8 @@ def build_auth_request(amount, payment, options)
add_partner_solution_id(post)
add_stored_credentials(post, payment, options)
add_three_ds(post, payment, options)
+ add_level_2_data(post, options)
+ add_level_3_data(post, options)
end.compact
end
@@ -477,7 +496,8 @@ def add_sec_code(post, options)
def add_invoice_number(post, options)
return unless options[:invoice_number].present?
- post[:orderInformation][:invoiceDetails] = { invoiceNumber: options[:invoice_number] }
+ post[:orderInformation][:invoiceDetails] ||= {}
+ post[:orderInformation][:invoiceDetails][:invoiceNumber] = options[:invoice_number]
end
def add_partner_solution_id(post)
diff --git a/lib/active_merchant/billing/gateways/worldpay.rb b/lib/active_merchant/billing/gateways/worldpay.rb
index 8405d805250..5793c9f7891 100644
--- a/lib/active_merchant/billing/gateways/worldpay.rb
+++ b/lib/active_merchant/billing/gateways/worldpay.rb
@@ -260,9 +260,8 @@ def add_level_two_and_three_data(xml, amount, data)
xml.invoiceReferenceNumber data[:invoice_reference_number] if data.include?(:invoice_reference_number)
xml.customerReference data[:customer_reference] if data.include?(:customer_reference)
xml.cardAcceptorTaxId data[:card_acceptor_tax_id] if data.include?(:card_acceptor_tax_id)
-
{
- sales_tax: 'salesTax',
+ tax_amount: 'salesTax',
discount_amount: 'discountAmount',
shipping_amount: 'shippingAmount',
duty_amount: 'dutyAmount'
@@ -270,53 +269,37 @@ def add_level_two_and_three_data(xml, amount, data)
next unless data.include?(key)
xml.tag! tag do
- data_amount = data[key].symbolize_keys
- add_amount(xml, data_amount[:amount].to_i, data_amount)
+ add_amount(xml, data[key].to_i, data)
end
end
- xml.discountName data[:discount_name] if data.include?(:discount_name)
- xml.discountCode data[:discount_code] if data.include?(:discount_code)
-
- add_date_element(xml, 'shippingDate', data[:shipping_date]) if data.include?(:shipping_date)
-
- if data.include?(:shipping_courier)
- xml.shippingCourier(
- data[:shipping_courier][:priority],
- data[:shipping_courier][:tracking_number],
- data[:shipping_courier][:name]
- )
- end
-
add_optional_data_level_two_and_three(xml, data)
- if data.include?(:item) && data[:item].kind_of?(Array)
- data[:item].each { |item| add_items_into_level_three_data(xml, item.symbolize_keys) }
- elsif data.include?(:item)
- add_items_into_level_three_data(xml, data[:item].symbolize_keys)
- end
+ data[:line_items].each { |item| add_line_items_into_level_three_data(xml, item.symbolize_keys, data) } if data.include?(:line_items)
end
- def add_items_into_level_three_data(xml, item)
+ def add_line_items_into_level_three_data(xml, item, data)
xml.item do
xml.description item[:description] if item[:description]
xml.productCode item[:product_code] if item[:product_code]
xml.commodityCode item[:commodity_code] if item[:commodity_code]
xml.quantity item[:quantity] if item[:quantity]
-
- {
- unit_cost: 'unitCost',
- item_total: 'itemTotal',
- item_total_with_tax: 'itemTotalWithTax',
- item_discount_amount: 'itemDiscountAmount',
- tax_amount: 'taxAmount'
- }.each do |key, tag|
- next unless item.include?(key)
-
- xml.tag! tag do
- data_amount = item[key].symbolize_keys
- add_amount(xml, data_amount[:amount].to_i, data_amount)
- end
+ xml.unitCost do
+ add_amount(xml, item[:unit_cost], data)
+ end
+ xml.unitOfMeasure item[:unit_of_measure] || 'each'
+ xml.itemTotal do
+ sub_total_amount = item[:quantity].to_i * (item[:unit_cost].to_i - item[:discount_amount].to_i)
+ add_amount(xml, sub_total_amount, data)
+ end
+ xml.itemTotalWithTax do
+ add_amount(xml, item[:total_amount], data)
+ end
+ xml.itemDiscountAmount do
+ add_amount(xml, item[:discount_amount], data)
+ end
+ xml.taxAmount do
+ add_amount(xml, item[:tax_amount], data)
end
end
end
@@ -326,7 +309,7 @@ def add_optional_data_level_two_and_three(xml, data)
xml.destinationPostalCode data[:destination_postal_code] if data.include?(:destination_postal_code)
xml.destinationCountryCode data[:destination_country_code] if data.include?(:destination_country_code)
add_date_element(xml, 'orderDate', data[:order_date].symbolize_keys) if data.include?(:order_date)
- xml.taxExempt data[:tax_exempt] if data.include?(:tax_exempt)
+ xml.taxExempt data[:tax_amount].to_i > 0 ? 'false' : 'true'
end
def order_tag_attributes(options)
@@ -562,10 +545,10 @@ def add_date_element(xml, name, date)
end
def add_amount(xml, money, options)
- currency = options[:currency] || currency(money)
+ currency = options[:currency] || currency(money.to_i)
amount_hash = {
- :value => localized_amount(money, currency),
+ :value => localized_amount(money.to_i, currency),
'currencyCode' => currency,
'exponent' => currency_exponent(currency)
}
diff --git a/test/remote/gateways/remote_cyber_source_rest_test.rb b/test/remote/gateways/remote_cyber_source_rest_test.rb
index a08307d376a..ce6107356cc 100644
--- a/test/remote/gateways/remote_cyber_source_rest_test.rb
+++ b/test/remote/gateways/remote_cyber_source_rest_test.rb
@@ -585,4 +585,49 @@ def test_successful_authorize_with_3ds2_mastercard
auth = @gateway.authorize(@amount, @master_card, @options)
assert_success auth
end
+
+ def test_successful_purchase_with_level_2_data
+ response = @gateway.purchase(@amount, @visa_card, @options.merge({ purchase_order_number: '13829012412' }))
+ assert_success response
+ assert response.test?
+ assert_equal 'AUTHORIZED', response.message
+ assert_nil response.params['_links']['capture']
+ end
+
+ def test_successful_purchase_with_level_2_and_3_data
+ options = {
+ purchase_order_number: '6789',
+ discount_amount: '150',
+ ships_from_postal_code: '90210',
+ line_items: [
+ {
+ productName: 'Product Name',
+ kind: 'debit',
+ quantity: 10,
+ unitPrice: '9.5000',
+ totalAmount: '95.00',
+ taxAmount: '5.00',
+ discountAmount: '0.00',
+ productCode: '54321',
+ commodityCode: '98765'
+ },
+ {
+ productName: 'Other Product Name',
+ kind: 'debit',
+ quantity: 1,
+ unitPrice: '2.5000',
+ totalAmount: '90.00',
+ taxAmount: '2.00',
+ discountAmount: '1.00',
+ productCode: '54322',
+ commodityCode: '98766'
+ }
+ ]
+ }
+ assert response = @gateway.purchase(@amount, @visa_card, @options.merge(options))
+ assert_success response
+ assert response.test?
+ assert_equal 'AUTHORIZED', response.message
+ assert_nil response.params['_links']['capture']
+ end
end
diff --git a/test/remote/gateways/remote_worldpay_test.rb b/test/remote/gateways/remote_worldpay_test.rb
index adc9f4551d4..07540d478e7 100644
--- a/test/remote/gateways/remote_worldpay_test.rb
+++ b/test/remote/gateways/remote_worldpay_test.rb
@@ -59,11 +59,8 @@ def setup
invoice_reference_number: 'INV12233565',
customer_reference: 'CUST00000101',
card_acceptor_tax_id: 'VAT1999292',
- sales_tax: {
- amount: '20',
- exponent: '2',
- currency: 'USD'
- }
+ tax_amount: '20',
+ ship_from_postal_code: '43245'
}
}
@@ -71,58 +68,32 @@ def setup
level_3_data: {
customer_reference: 'CUST00000102',
card_acceptor_tax_id: 'VAT1999285',
- sales_tax: {
- amount: '20',
- exponent: '2',
- currency: 'USD'
- },
- discount_amount: {
- amount: '1',
- exponent: '2',
- currency: 'USD'
- },
- shipping_amount: {
- amount: '50',
- exponent: '2',
- currency: 'USD'
- },
- duty_amount: {
- amount: '20',
- exponent: '2',
- currency: 'USD'
- },
- item: {
+ tax_amount: '20',
+ discount_amount: '1',
+ shipping_amount: '50',
+ duty_amount: '20',
+ line_items: [{
description: 'Laptop 14',
product_code: 'LP00125',
commodity_code: 'COM00125',
quantity: '2',
- unit_cost: {
- amount: '1500',
- exponent: '2',
- currency: 'USD'
- },
+ unit_cost: '1500',
unit_of_measure: 'each',
- item_total: {
- amount: '3000',
- exponent: '2',
- currency: 'USD'
- },
- item_total_with_tax: {
- amount: '3500',
- exponent: '2',
- currency: 'USD'
- },
- item_discount_amount: {
- amount: '200',
- exponent: '2',
- currency: 'USD'
- },
- tax_amount: {
- amount: '500',
- exponent: '2',
- currency: 'USD'
- }
- }
+ discount_amount: '200',
+ tax_amount: '500',
+ total_amount: '3300'
+ },
+ {
+ description: 'Laptop 15',
+ product_code: 'LP00125',
+ commodity_code: 'COM00125',
+ quantity: '2',
+ unit_cost: '1500',
+ unit_of_measure: 'each',
+ discount_amount: '200',
+ tax_amount: '500',
+ total_amount: '3300'
+ }]
}
}
@@ -705,7 +676,7 @@ def test_successful_purchase_with_level_two_fields
end
def test_successful_purchase_with_level_two_fields_and_sales_tax_zero
- @level_two_data[:level_2_data][:sales_tax][:amount] = 0
+ @level_two_data[:level_2_data][:tax_amount] = 0
assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_two_data))
assert_success response
assert_equal true, response.params['ok']
@@ -720,12 +691,13 @@ def test_successful_purchase_with_level_three_fields
end
def test_unsuccessful_purchase_level_three_data_without_item_mastercard
- @level_three_data[:level_3_data][:item] = {}
+ @level_three_data[:level_3_data][:line_items] = [{
+ }]
@credit_card.brand = 'master'
assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_three_data))
assert_failure response
assert_equal response.error_code, '2'
- assert_equal response.params['error'].gsub(/\"+/, ''), 'The content of element type item is incomplete, it must match (description,productCode?,commodityCode?,quantity?,unitCost?,unitOfMeasure?,itemTotal?,itemTotalWithTax?,itemDiscountAmount?,taxAmount?,categories?,pageURL?,imageURL?).'
+ assert_equal response.params['error'].gsub(/\"+/, ''), 'The content of element type item must match (description,productCode?,commodityCode?,quantity?,unitCost?,unitOfMeasure?,itemTotal?,itemTotalWithTax?,itemDiscountAmount?,itemTaxRate?,lineDiscountIndicator?,itemLocalTaxRate?,itemLocalTaxAmount?,taxAmount?,categories?,pageURL?,imageURL?).'
end
def test_successful_purchase_with_level_two_and_three_fields
diff --git a/test/unit/gateways/cyber_source_rest_test.rb b/test/unit/gateways/cyber_source_rest_test.rb
index 8105b78c8ec..6548b7b8722 100644
--- a/test/unit/gateways/cyber_source_rest_test.rb
+++ b/test/unit/gateways/cyber_source_rest_test.rb
@@ -491,6 +491,55 @@ def test_adds_application_id_as_partner_solution_id
CyberSourceRestGateway.application_id = nil
end
+ def test_purchase_with_level_2_data
+ stub_comms do
+ @gateway.authorize(100, @credit_card, @options.merge({ purchase_order_number: '13829012412' }))
+ end.check_request do |_endpoint, data, _headers|
+ request = JSON.parse(data)
+ assert_equal '13829012412', request['orderInformation']['invoiceDetails']['purchaseOrderNumber']
+ end.respond_with(successful_purchase_response)
+ end
+
+ def test_purchase_with_level_3_data
+ options = {
+ purchase_order_number: '6789',
+ discount_amount: '150',
+ ships_from_postal_code: '90210',
+ line_items: [
+ {
+ productName: 'Product Name',
+ kind: 'debit',
+ quantity: 10,
+ unitPrice: '9.5000',
+ totalAmount: '95.00',
+ taxAmount: '5.00',
+ discountAmount: '0.00',
+ productCode: '54321',
+ commodityCode: '98765'
+ },
+ {
+ productName: 'Other Product Name',
+ kind: 'debit',
+ quantity: 1,
+ unitPrice: '2.5000',
+ totalAmount: '90.00',
+ taxAmount: '2.00',
+ discountAmount: '1.00',
+ productCode: '54322',
+ commodityCode: '98766'
+ }
+ ]
+ }
+ stub_comms do
+ @gateway.authorize(100, @credit_card, @options.merge(options))
+ end.check_request do |_endpoint, data, _headers|
+ request = JSON.parse(data)
+ assert_equal '3', request['processingInformation']['purchaseLevel']
+ assert_equal '150', request['orderInformation']['amountDetails']['discountAmount']
+ assert_equal '90210', request['orderInformation']['shipping_details']['shipFromPostalCode']
+ end.respond_with(successful_purchase_response)
+ end
+
private
def parse_signature(signature)
diff --git a/test/unit/gateways/worldpay_test.rb b/test/unit/gateways/worldpay_test.rb
index e35ef845cfd..8e7e7d325d6 100644
--- a/test/unit/gateways/worldpay_test.rb
+++ b/test/unit/gateways/worldpay_test.rb
@@ -89,11 +89,7 @@ def setup
invoice_reference_number: 'INV12233565',
customer_reference: 'CUST00000101',
card_acceptor_tax_id: 'VAT1999292',
- sales_tax: {
- amount: '20',
- exponent: '2',
- currency: 'USD'
- },
+ tax_amount: '20',
ship_from_postal_code: '43245',
destination_postal_code: '54545',
destination_country_code: 'CO',
@@ -101,8 +97,7 @@ def setup
day_of_month: Date.today.day,
month: Date.today.month,
year: Date.today.year
- },
- tax_exempt: 'false'
+ }
}
}
@@ -110,58 +105,34 @@ def setup
level_3_data: {
customer_reference: 'CUST00000102',
card_acceptor_tax_id: 'VAT1999285',
- sales_tax: {
- amount: '20',
- exponent: '2',
- currency: 'USD'
- },
- discount_amount: {
- amount: '1',
- exponent: '2',
- currency: 'USD'
- },
- shipping_amount: {
- amount: '50',
- exponent: '2',
- currency: 'USD'
- },
- duty_amount: {
- amount: '20',
- exponent: '2',
- currency: 'USD'
- },
- item: {
+ tax_amount: '20',
+ discount_amount: '1',
+ shipping_amount: '50',
+ duty_amount: '20',
+ line_items: [{
description: 'Laptop 14',
product_code: 'LP00125',
commodity_code: 'COM00125',
quantity: '2',
- unit_cost: {
- amount: '1500',
- exponent: '2',
- currency: 'USD'
- },
+ unit_cost: '1500',
unit_of_measure: 'each',
- item_total: {
- amount: '3000',
- exponent: '2',
- currency: 'USD'
- },
- item_total_with_tax: {
- amount: '3500',
- exponent: '2',
- currency: 'USD'
- },
- item_discount_amount: {
- amount: '200',
- exponent: '2',
- currency: 'USD'
- },
- tax_amount: {
- amount: '500',
- exponent: '2',
- currency: 'USD'
- }
- }
+ item_discount_amount: '200',
+ discount_amount: '0',
+ tax_amount: '500',
+ total_amount: '4000'
+ },
+ {
+ description: 'Laptop 15',
+ product_code: 'LP00120',
+ commodity_code: 'COM00125',
+ quantity: '2',
+ unit_cost: '1000',
+ unit_of_measure: 'each',
+ item_discount_amount: '200',
+ tax_amount: '500',
+ discount_amount: '0',
+ total_amount: '3000'
+ }]
}
}
end
@@ -442,12 +413,30 @@ def test_transaction_with_level_two_data
assert_match %r(INV12233565), data
assert_match %r(CUST00000101), data
assert_match %r(VAT1999292), data
- assert_match %r(), data.gsub(/\s+/, '')
+ assert_match %r(), data.gsub(/\s+/, '')
assert_match %r(43245), data
assert_match %r(54545), data
assert_match %r(CO), data
assert_match %r(false), data
- assert_match %r(), data.gsub(/\s+/, '')
+ assert_match %r(), data.gsub(/\s+/, '')
+ end.respond_with(successful_authorize_response)
+ assert_success response
+ end
+
+ def test_transaction_with_level_two_data_without_tax
+ @level_two_data[:level_2_data][:tax_amount] = 0
+ options = @options.merge(@level_two_data)
+ response = stub_comms do
+ @gateway.authorize(@amount, @credit_card, options)
+ end.check_request do |_endpoint, data, _headers|
+ assert_match %r(INV12233565), data
+ assert_match %r(CUST00000101), data
+ assert_match %r(VAT1999292), data
+ assert_match %r(), data.gsub(/\s+/, '')
+ assert_match %r(43245), data
+ assert_match %r(54545), data
+ assert_match %r(CO), data
+ assert_match %r(true), data
assert_match %r(), data.gsub(/\s+/, '')
end.respond_with(successful_authorize_response)
assert_success response
@@ -460,11 +449,11 @@ def test_transaction_with_level_three_data
end.check_request do |_endpoint, data, _headers|
assert_match %r(CUST00000102), data
assert_match %r(VAT1999285), data
- assert_match %r(), data.gsub(/\s+/, '')
- assert_match %r(), data.gsub(/\s+/, '')
- assert_match %r(), data.gsub(/\s+/, '')
- assert_match %r(), data.gsub(/\s+/, '')
- assert_match %r(- Laptop14LP00125COM001252
), data.gsub(/\s+/, '')
+ assert_match %r(), data.gsub(/\s+/, '')
+ assert_match %r(), data.gsub(/\s+/, '')
+ assert_match %r(), data.gsub(/\s+/, '')
+ assert_match %r(), data.gsub(/\s+/, '')
+ assert_match %r(- Laptop14LP00125COM001252each
- Laptop15LP00120COM001252each
), data.gsub(/\s+/, '')
end.respond_with(successful_authorize_response)
assert_success response
end