Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add L2/L3 data for cybersource rest and worldpay #5117

Merged
merged 6 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion lib/active_merchant/billing/gateways/cyber_source_rest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
63 changes: 23 additions & 40 deletions lib/active_merchant/billing/gateways/worldpay.rb
Original file line number Diff line number Diff line change
Expand Up @@ -260,63 +260,46 @@ 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',
DustinHaefele marked this conversation as resolved.
Show resolved Hide resolved
discount_amount: 'discountAmount',
shipping_amount: 'shippingAmount',
duty_amount: 'dutyAmount'
}.each do |key, tag|
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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old "each on a hardcoded hash" strategy checked to see if the param existed in item, at this next clause. That check appears to have been removed? is that cool?

Not for nothing but--while I see why you flattened the each out (there are two XML nodes that require special code) it's a little verbose. What would you think of keeping the original implementation for unitCost, itemTotalWithTax, itemDiscountAmount, and taxAmount... with less functionality, like I like how you moved to_i to the function that needs it... and then having special ones for itemTotal and unitOfMeasure

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to change this because Worldpay is particular about the order in which the xml attributes are added and i kept having to interrupt the hash to get the order "right"


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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
45 changes: 45 additions & 0 deletions test/remote/gateways/remote_cyber_source_rest_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
82 changes: 27 additions & 55 deletions test/remote/gateways/remote_worldpay_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,70 +59,41 @@ 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'
}
}

@level_three_data = {
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'
}]
}
}

Expand Down Expand Up @@ -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']
Expand All @@ -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
Expand Down
49 changes: 49 additions & 0 deletions test/unit/gateways/cyber_source_rest_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading