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 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 """
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
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
defp get_card_type(card_type) do
case card_type do
"VISA" -> "001"
"MASTERCARD" -> "002"
"AMEX" -> "003"
"DISCOVER" -> "004"
"JCB" -> nil
_ -> nil
end
end
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)
]
else
[]
end
end
# Make HTTPS request
defp call(xml_body) do
endpoint = Application.get_env(:cybersource_sdk, :endpoint)
case HTTPoison.post(endpoint, xml_body, [{"Content-Type", "application/xml"}]) 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
defp parse_response(xml) do
xml
|> xmap(
merchantReferenceCode:
~x"//soap:Envelope/soap:Body/c:replyMessage/c:merchantReferenceCode/text()"s,
requestID: ~x"//soap:Envelope/soap:Body/c:replyMessage/c:requestID/text()"i,
decision: ~x"//soap:Envelope/soap:Body/c:replyMessage/c:decision/text()"s,
reasonCode: ~x"//soap:Envelope/soap:Body/c:replyMessage/c:reasonCode/text()"i,
requestToken: ~x"//soap:Envelope/soap:Body/c:replyMessage/c:requestToken/text()"s,
ccAuthReply: [
~x".//c:ccAuthReply"o,
reasonCode: ~x"./c:reasonCode/text()"i,
amount: ~x"./c:amount/text()"of
],
ccCaptureReply: [
~x".//c:ccCapctureReply"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
]
)
end
defp handle_response(response) do
case response.decision do
"ACCEPT" -> {:ok, response}
"REJECT" -> {:error, response.reasonCode}
"ERROR" -> {:error, response.reasonCode}
end
end
end