Current section

Files

Jump to
cybersource_sdk lib cybersource-sdk client.ex
Raw

lib/cybersource-sdk/client.ex

defmodule CyberSourceSDK.Client do
@moduledoc """
This Client module handle all HTTPS requests to the CyberSource server. It
takes some parameters and convert to HTTPS requests.
It support the following payments:
* Android Pay
* Apple Pay
It supports the following requests:
* Authorization
* Capture
* Refund
"""
import SweetXml
alias CyberSourceSDK.Helper
use GenServer
def init(args) do
{:ok, args}
end
def start_link do
GenServer.start_link(__MODULE__, {}, name: :cybersource_sdk_client)
end
@doc """
Create an authorization payment
For a normal account, bill_to is mandatory. If you ask CyberSource for a
relaxed AVS check, bill_to can be optional.
## Parameters
- price: Float that represents the price to be charged to the user.
- merchant_reference_code: String that represents the order. Normally you should pass an unique identifier like `order_id`.
- card_type: String with the name of card type, like VISA, MASTERCARD, etc.
- encrypted_payment: String that must be in Base64 received by Apple/Android payment system.
- bill_to: Structure generated by `CyberSourceSDK.bill_to()`. (Optional)
- worker: Atom with name of the structure in configurations to be use. (Optional)
## Example
Without `bill_to` and `worker` parameters
```
authorize(32.0, "1234", "VISA", "oJ8IOx6SA9HNncxzpS9akm32n+DSAJH==")
```
With `bill_to` parameter
```
bill_to = CyberSourceSDK.bill_to("John", "Doe", "Marylane Street", "34", "New York", "Hong Kong", "john@example.com")
authorize(32.0, "1234", "VISA", "oJ8IOx6SA9HNncxzpS9akm32n+DSAJH==", bill_to)
```
"""
def authorize(
price,
merchant_reference_code,
card_type,
encrypted_payment,
bill_to \\ [],
worker \\ :merchant
)
def authorize(price, merchant_reference_code, card_type, encrypted_payment, bill_to, worker)
when is_float(price) do
case validate_merchant_reference_code(merchant_reference_code) do
{:error, reason} ->
{:error, reason}
merchant_reference_code_validated ->
case Helper.check_payment_type(encrypted_payment) do
{:ok, :apple_pay} ->
pay_with_apple_pay(
price,
merchant_reference_code_validated,
card_type,
encrypted_payment,
bill_to,
worker
)
{:ok, :android_pay} ->
pay_with_android_pay(
price,
merchant_reference_code_validated,
card_type,
encrypted_payment,
bill_to,
worker
)
{:error, reason} ->
{:error, reason}
end
end
end
def authorize(_, _, _, _, _, _) do
{:error, :price_needs_to_be_float}
end
@doc """
Capture authorization on user credit card
## Parameters
- order_id: Unique number to identify the purchase.
- request_params: Base64 of a JSON with `request_id` and `request_token` from authorization request.
- items: An array of map containing the following values: `id`, `unit_price` and `quantity`. Example: ```%{id: id, unit_price: unit_price, quantity: quantity}```
- worker: Merchant atom to use (setup in configurations).
## Result
On successful return the result will be:
```
{:ok, object}
```
"""
def capture(order_id, request_params, items \\ [], worker \\ :merchant) do
case Helper.json_from_base64(request_params) do
{:ok, %{request_id: request_id, request_token: _request_token}} ->
merchant_configuration = get_configuration_params(worker)
if length(merchant_configuration) > 0 do
replace_params =
get_configuration_params(worker) ++
[request_id: request_id, reference_id: order_id] ++ [items: items]
EEx.eval_file(get_template("capture_request.xml"), assigns: replace_params)
|> call
else
Helper.invalid_merchant_configuration()
end
{:error, message} ->
{:error, message}
end
end
@doc """
Remove authorization on user credit card
## Parameters
- order_id: Unique number to identify the purchase.
- amount: Price (value) to refund.
- request_params: Base64 of a JSON with `request_id` and `request_token` from authorization request.
- items: An array of map containing the following values: `id`, `unit_price` and `quantity`. Example: ```%{id: id, unit_price: unit_price, quantity: quantity}```
- worker: Merchant atom to use (setup in configurations)
## Example
```
refund("1234", 23435465442432, items)
```
"""
def refund(order_id, amount, request_params, items \\ [], worker \\ :merchant) do
case Helper.json_from_base64(request_params) do
{:ok, %{request_id: request_id, request_token: _request_token}} ->
merchant_configuration = get_configuration_params(worker)
if length(merchant_configuration) > 0 do
replace_params =
get_configuration_params(worker) ++
[request_id: request_id, reference_id: order_id, total_amount: amount] ++
[items: items]
EEx.eval_file(get_template("refund_request.xml"), assigns: replace_params)
|> call
else
Helper.invalid_merchant_configuration()
end
{:error, message} ->
{:error, message}
end
end
@doc """
A void cancels a capture or credit request that you submitted to CyberSource. A
transaction can be voided only when CyberSource has not already submitted the capture
or credit request to your processor. CyberSource usually submits capture and credit
requests to your processor once a day, so your window for successfully voiding a capture
or credit request is small. CyberSource declines your void request when the capture or
credit request has already been sent to the processor
"""
def void(order_id, request_params, worker \\ :merchant) do
case Helper.json_from_base64(request_params) do
{:ok, %{request_id: request_id, request_token: _request_token}} ->
merchant_configuration = get_configuration_params(worker)
if length(merchant_configuration) > 0 do
replace_params =
get_configuration_params(worker) ++ [request_id: request_id, reference_id: order_id]
EEx.eval_file(get_template("void_request.xml"), assigns: replace_params)
|> call
else
Helper.invalid_merchant_configuration()
end
{:error, message} ->
{:error, message}
end
end
@doc """
When your request for a credit is successful, the issuing bank for the credit
card takes money out of your merchant bank account and returns it to the customer.
It usually takes two to four days for your acquiring bank to transfer funds
from your merchant bank account.
"""
def credit(order_id, amount, reason, request_params, worker \\ :merchant) do
case Helper.json_from_base64(request_params) do
{:ok, %{request_id: request_id, request_token: _request_token}} ->
merchant_configuration = get_configuration_params(worker)
if length(merchant_configuration) > 0 do
replace_params =
get_configuration_params(worker) ++
[
request_id: request_id,
reference_id: order_id,
total_amount: amount,
refund_reason: reason
]
EEx.eval_file(get_template("credit_request.xml"), assigns: replace_params)
|> call
else
Helper.invalid_merchant_configuration()
end
{:error, message} ->
{:error, message}
end
end
@doc """
Make a request to pay with Android Pay
Returns `{:ok, response_object}` , `{:error, :card_type_not_found` or
`{:error, response_code}`
"""
def pay_with_android_pay(
price,
merchant_reference_code,
card_type,
encrypted_payment,
bill_to \\ [],
worker \\ :merchant
) do
case get_card_type(card_type) do
nil ->
{:error, :card_type_not_found}
card_type ->
merchant_configuration = get_configuration_params(worker)
if length(merchant_configuration) > 0 do
replace_params =
get_configuration_params(worker) ++
get_payment_params(merchant_reference_code, price, encrypted_payment, card_type) ++
bill_to
EEx.eval_file(get_template("android_pay_request.xml"), assigns: replace_params)
|> call
else
Helper.invalid_merchant_configuration()
end
end
end
@doc """
Make a request to pay with Apple Pay
Returns `{:ok, response_object}` , `{:error, :card_type_not_found` or
`{:error, response_code}`
"""
def pay_with_apple_pay(
price,
merchant_reference_code,
card_type,
encrypted_payment,
bill_to \\ [],
worker \\ :merchant
) do
case get_card_type(card_type) do
nil ->
{:error, :card_type_not_found}
card_type ->
merchant_configuration = get_configuration_params(worker)
if length(merchant_configuration) > 0 do
replace_params =
CyberSourceSDK.Client.get_configuration_params(worker) ++
CyberSourceSDK.Client.get_payment_params(
merchant_reference_code,
price,
encrypted_payment,
card_type
) ++ bill_to
EEx.eval_file(get_template("apple_pay_request.xml"), assigns: replace_params)
|> call()
else
Helper.invalid_merchant_configuration()
end
end
end
# Define path of request templates
defp get_template(filename) do
Path.join(__DIR__, "/requests/" <> filename <> ".eex")
end
# Get Payment parameters
@spec get_payment_params(String.t(), float(), String.t(), String.t()) :: list()
def get_payment_params(order_id, price, encrypted_token, card_type) do
[
reference_id: order_id,
total_amount: price,
encrypted_payment_data: encrypted_token,
card_type: card_type
]
end
@spec get_card_type(String.t()) :: String.t() | nil
defp get_card_type(card_type) do
case card_type do
"VISA" -> "001"
"MASTERCARD" -> "002"
"AMEX" -> "003"
"DISCOVER" -> "004"
"JCB" -> nil
_ -> nil
end
end
@spec get_configuration_params(atom()) :: list()
def get_configuration_params(worker) do
merchant_configuration = Application.get_env(:cybersource_sdk, worker)
if !is_nil(merchant_configuration) do
[
merchant_id: Map.get(merchant_configuration, :id),
transaction_key: Map.get(merchant_configuration, :transaction_key),
currency: Map.get(merchant_configuration, :currency),
client_library: "CyberSourceSDK Elixir #{Application.spec(:cybersource_sdk, :vsn)}"
]
else
[]
end
end
# Make HTTPS request
@spec call(String.t()) :: {:ok, map()} | {:error, String.t()} | {:error, :unknown_response}
defp call(xml_body) do
endpoint = Application.get_env(:cybersource_sdk, :endpoint)
timeout = Application.get_env(:cybersource_sdk, :timeout, 8000)
case HTTPoison.post(
endpoint,
xml_body,
[{"Content-Type", "application/xml"}],
timeout: timeout
) do
{:ok, %HTTPoison.Response{body: response_body}} ->
parse_response(response_body)
|> handle_response
{:error, %HTTPoison.Error{id: _, reason: reason}} ->
{:error, reason}
end
end
defp validate_merchant_reference_code(merchant_reference_code) do
cond do
String.valid?(merchant_reference_code) && String.length(merchant_reference_code) ->
merchant_reference_code
is_integer(merchant_reference_code) ->
Integer.to_string(merchant_reference_code)
true ->
{:error, :invalid_order_id}
end
end
# Parse response from CyberSource
@spec parse_response(String.t()) :: map()
def parse_response(xml) do
xml
|> xmap(
merchantReferenceCode:
~x"//soap:Envelope/soap:Body/c:replyMessage/c:merchantReferenceCode/text()"os,
requestID: ~x"//soap:Envelope/soap:Body/c:replyMessage/c:requestID/text()"oi,
decision: ~x"//soap:Envelope/soap:Body/c:replyMessage/c:decision/text()"os,
reasonCode: ~x"//soap:Envelope/soap:Body/c:replyMessage/c:reasonCode/text()"oi,
requestToken: ~x"//soap:Envelope/soap:Body/c:replyMessage/c:requestToken/text()"os,
ccAuthReply: [
~x".//c:ccAuthReply"o,
reasonCode: ~x"./c:reasonCode/text()"i,
amount: ~x"./c:amount/text()"of
],
ccCaptureReply: [
~x".//c:ccCaptureReply"o,
reasonCode: ~x"./c:reasonCode/text()"i,
amount: ~x"./c:amount/text()"of,
requestDateTime: ~x"./c:requestDateTime/text()"so,
reconciliationID: ~x"./c:reconciliationID/text()"io
],
ccAuthReversalReply: [
~x".//c:ccAuthReversalReply"o,
reasonCode: ~x"./c:reasonCode/text()"i
],
originalTransaction: [
~x".//c:originalTransaction"o,
amount: ~x"./c:amount/text()"of,
reasonCode: ~x"./c:reasonCode/text()"i
],
voidReply: [
~x".//c:voidReply"o,
reasonCode: ~x"./c:reasonCode/text()"i,
amount: ~x"./c:amount/text()"of,
requestDateTime: ~x"./c:requestDateTime/text()"so,
currency: ~x"./c:currency/text()"so
],
ccCreditReply: [
~x".//c:ccCreditReply"o,
reasonCode: ~x"./c:reasonCode/text()"i,
requestDateTime: ~x"./c:requestDateTime/text()"so,
amount: ~x"./c:amount/text()"of,
reconciliationID: ~x"./c:reconciliationID/text()"so,
purchasingLevel3Enabled: ~x"./c:purchasingLevel3Enabled/text()"so,
enhancedDataEnabled: ~x"./c:enhancedDataEnabled/text()"so,
authorizationXID: ~x"./c:authorizationXID/text()"so,
forwardCode: ~x"./c:forwardCode/text()"so,
ownerMerchantID: ~x"./c:ownerMerchantID/text()"so,
reconciliationReferenceNumber: ~x"./c:reconciliationReferenceNumber/text()"so
],
fault: [
~x"//soap:Envelope/soap:Body/soap:Fault"o,
faultCode: ~x"./faultcode/text()"s,
faultString: ~x"./faultstring/text()"s
]
)
end
@spec handle_response(map()) ::
{:ok, map()} | {:error, String.t()} | {:error, :unknown_response}
defp handle_response(response) do
cond do
response.decision != "" ->
case response.decision do
"ACCEPT" -> {:ok, response}
"REJECT" -> {:error, response.reasonCode}
"ERROR" -> {:error, response.reasonCode}
end
response.fault.faultCode != "" ->
{:error, "#{response.fault.faultCode} - #{response.fault.faultString}"}
true ->
{:error, :unknown_response}
end
end
end