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(Laptop14LP00125COM001252eachLaptop15LP00120COM001252each), data.gsub(/\s+/, '') end.respond_with(successful_authorize_response) assert_success response end