Current section
Files
Jump to
Current section
Files
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}} ->
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
{: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}} ->
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
{: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 ->
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
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, request_id, bill_to \\ [], worker \\ :merchant) do
case get_card_type(card_type) do
nil ->
{:error, :card_type_not_found}
card_type ->
replace_params = get_configuration_params(worker)
++ get_payment_params(merchant_reference_code, price, encrypted_payment, card_type)
++ [request_id: request_id]
++ bill_to
EEx.eval_file(get_template("apple_pay_request.xml"), assigns: replace_params)
|> call()
end
end
# Define path of request templates
defp get_template(filename) do
Path.join(__DIR__, "/requests/" <> filename <> ".eex")
end
# Get Payment parameters
defp 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"
"AMERICAN EXPRESS" -> "003"
"DISCOVER" -> "004"
"JCB" -> nil
_ -> nil
end
end
defp 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)
# TODO: Check errors
{:ok, %HTTPoison.Response{body: response_body}} = HTTPoison.post endpoint, xml_body, [{"Content-Type", "application/xml"}]
parse_response(response_body)
|> handle_response
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()"f
],
ccCapctureReply: [
~x".//c:ccCapctureReply"o,
reasonCode: ~x"./c:reasonCode/text()"i,
amount: ~x"./c:amount/text()"f
],
ccAuthReversalReply: [
~x".//c:ccAuthReversalReply"o,
reasonCode: ~x"./c:reasonCode/text()"i
],
originalTransaction: [
~x".//c:originalTransaction"o,
amount: ~x"./c:amount/text()"f,
reasonCode: ~x"./c:reasonCode/text()"i
]
)
end
defp handle_response(response) do
case response.decision do
"ACCEPT" -> {:ok, response}
"REJECT" -> {:error, response.reasonCode}
end
end
end