I’m having trouble making a associated save. I have a Payment object that sometimes have a associated ExternalAuthorization object.
- They are related trought the external_authorization_id column,
- The migration where created as below:
defmodule Payment.Repo.Migrations.CreateExternalAuthorization do
use Ecto.Migration
def up do
# Credit Card
create table(:external_authorization) do
#add(:id, type: :uuid, primary_key: true)
#add(:payment_id, references("payment"))
add(:cavv, :string, size: 255)
add(:xid, :string, size: 255)
add(:eci, :string, size: 4)
add(:version, :string, size: 4)
add(:reference_id, :string, size: 255)
add(:acquirer_id, references("acquirer"))
timestamps(inserted_at: :created_at, updated_at: :updated_at, type: :timestamptz)
end
alter table(:payment) do
add(:external_authorization_id, references("external_authorization"))
end
end
def down do
alter table(:payment) do
remove(:external_authorization_id)
end
end
end
The associations are linked in the files below:
gateway_web/lib/controllers/pay_controller.ex
defmodule GatewayWeb.PayController do
@moduledoc """
Handle new Payments
"""
use GatewayWeb, :controller
import GatewayWeb, only: [render_payment: 2]
alias Gateway.{
Acquirer,
Payload,
Payment
}
alias Gateway.Pay.{
Antifraud,
BankBillet,
CreditCard,
ExternalAuthorization,
PayPal
}
alias Gateway.Actions.Analyse.Bureau
alias GatewayWeb.FallbackController
action_fallback(FallbackController)
@doc """
Convert the params in Payload struct before execute the actions
"""
def action(conn, _) do
with {:ok, payload} <- Payload.from_params(conn.params) do
acquirer = Acquirer.init(payload.acquirer)
args = [conn, acquirer, payload, conn.private[:jwt]]
apply(__MODULE__, action_name(conn), args)
end
end
@doc """
CreditCard payment with Antifraud
"""
def pay(conn, acquirer, %{antifraud: antifraud, credit_card: credit_card} = payload, auth)
when not is_nil(antifraud) and not is_nil(credit_card) do
with {:ok, payment} <- Payment.with_antifraud(payload, auth),
{:ok, updated} <- Bureau.analyse(payment, payload, auth),
{:ok, payment} <- Antifraud.pay(acquirer, payload, updated) do
render_payment(conn, payment)
end
end
@doc """
Payment with BankBillet
"""
def pay(conn, acquirer, %{bank_billet: bb} = payload, auth) when not is_nil(bb) do
with {:ok, payment} <- Payment.with_bank_billet(payload, auth),
{:ok, updated} <- BankBillet.pay(payment, acquirer) do
render_payment(conn, updated)
end
end
@doc """
Payment with PayPal
"""
def pay(conn, acquirer, %{paypal: paypal} = payload, auth) when not is_nil(paypal) do
with {:ok, payment} <- Payment.with_paypal(payload, auth),
{:ok, payment} <- PayPal.pay(payment, payload, acquirer, auth) do
render_payment(conn, payment)
end
end
@doc """
Payment with CreditCard
"""
def pay(conn, acquirer, payload, auth) do
with {:ok, payment} <- Payment.with_credit_card(payload, auth),
{:ok, upload} <- CreditCard.pay(payment, payload, acquirer) do
render_payment(conn, upload)
end
end
end
gateway/lib/payload.ex
defmodule Gateway.Payload do
@moduledoc """
Schema of the incoming request body data
"""
use Ecto.Schema
import Ecto.Changeset
import Gateway.Gettext
alias Ecto.UUID
alias Gateway.Payload.{
Antifraud,
BankBillet,
CreditCard,
ExternalAuthorization,
Customer,
Items,
PayPal
}
@default_acquirer Application.get_env(:gateway, :acquirers)[:default]
@bank_billet_acquirer Application.get_env(:gateway, :acquirers)[:bank_billet_default]
@default_currency Application.get_env(:gateway, :currencies)[:default]
@primary_key false
embedded_schema do
field(:external_id, :string)
field(:amount, :integer)
field(:interest, :integer)
field(:acquirer_config, UUID)
field(:currency, :string, default: @default_currency)
field(:acquirer, :string)
field(:capture, :boolean, default: false)
field(:soft_descriptor, :string)
field(:postback_url, :string)
embeds_one(:customer, Customer)
embeds_one(:credit_card, CreditCard)
embeds_one(:external_authorization, ExternalAuthorization)
embeds_one(:bank_billet, BankBillet)
embeds_one(:paypal, PayPal)
embeds_one(:antifraud, Antifraud)
embeds_many(:items, Items)
end
@required_fields ~w(
external_id
amount
interest
)a
@optional_fields ~w(
soft_descriptor
acquirer
acquirer_config
capture
currency
postback_url
)a
@doc """
Format data from a map into a Payload
"""
def from_params(data) when is_map(data) do
acquirers = Application.get_env(:gateway, :acquirers)[:valid]
currencies = Application.get_env(:gateway, :currencies)[:valid]
%__MODULE__{}
|> cast(data, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
# Sanitize data
|> update_change(:external_id, &String.trim/1)
|> update_change(:acquirer, &String.trim/1)
|> update_change(:acquirer, &String.downcase/1)
|> update_change(:acquirer_config, &String.trim/1)
|> update_change(:currency, &String.trim/1)
|> update_change(:soft_descriptor, &replace_special_chars/1)
|> update_change(:currency, &String.upcase/1)
# Validate data
|> validate_inclusion(:acquirer, acquirers)
|> validate_inclusion(:currency, currencies)
|> validate_number(:amount, greater_than: 0)
|> validate_number(:interest, greater_than_or_equal_to: 0)
|> validate_length(:soft_descriptor, max: 13)
|> validate_url(:postback_url)
# Embeded data
|> cast_embed(:bank_billet)
|> cast_embed(:credit_card)
|> cast_embed(:external_authorization)
|> cast_embed(:paypal)
|> cast_embed(:antifraud)
|> cast_embed(:customer, required: true)
|> cast_embed(:items, required: true)
|> apply_aquirer()
|> apply_action(:insert)
end
defp replace_special_chars(field) do
field
|> String.normalize(:nfd)
|> String.replace(~r/[^A-z\s]/u, "")
|> String.trim()
end
defp apply_aquirer(changeset) do
acquirer = get_field(changeset, :acquirer, nil)
bank_billet = get_field(changeset, :bank_billet, nil)
cond do
not is_nil(acquirer) ->
changeset
is_nil(bank_billet) ->
changeset
|> put_change(:acquirer, @default_acquirer)
true ->
changeset
|> put_change(:acquirer, @bank_billet_acquirer)
end
end
defp validate_url(changeset, field) do
url = get_field(changeset, field, nil)
if is_nil(url) do
changeset
else
case :http_uri.parse(url) do
{:ok, _} ->
changeset
{:error, _} ->
changeset
|> add_error("postback_url", dgettext("errors", "invalid postback url format"))
end
end
end
end
apps/payment/lib/new.ex
defmodule Payment.New do
@moduledoc false
alias Ecto.{
Changeset,
Multi
}
alias Payment.Repo
alias Payment.Schema.{
Acquirer,
Antifraud,
AntifraudData,
BankBillet,
CreditCard,
ExternalAuthorization,
CreditCardTokenHistory,
Currency,
Customer,
Item,
PayPal,
QRCode,
Status,
StatusHistory,
Type
}
def with_bank_billet(payload, config, auth) do
bank_billet = payload.bank_billet
customer = payload.customer
Multi.new()
|> Multi.run(:customer, &customer(&1, customer, auth))
|> Multi.run(:payment, &new_payment(&1, Type.bank_billet(), payload, auth, config))
|> Multi.run(:history, &add_history/1)
|> Multi.run(:bank_billet, &bank_billet(&1, bank_billet))
|> Multi.run(:associated, &payment_method(&1))
|> insert([
:acquirer_config,
:bank_billet,
:status,
customer: :address
])
end
def with_qrcode(payload, config, auth) do
Multi.new()
|> Multi.run(:customer, &customer(&1, payload.customer, auth))
|> Multi.run(:payment, &new_payment(&1, Type.qrcode(), payload, auth, config))
|> Multi.run(:qrcode, &qrcode(&1))
|> Multi.run(:history, &add_history/1)
|> Multi.run(:associated, &payment_method(&1))
|> insert([
:acquirer_config,
:details,
:qrcode,
:status,
customer: :address
])
end
def with_credit_card(payload, config, auth) do
credit_card = payload.credit_card
installments = credit_card.installments
customer = payload.customer
external_authorization = payload.external_authorization
Multi.new()
|> Multi.run(:customer, &customer(&1, customer, auth))
|> Multi.run(:payment, &new_payment(&1, Type.credit_card(), payload, auth, config))
|> Multi.run(:history, &add_history/1)
|> Multi.run(:credit_card, &credit_card(&1, credit_card))
|> Multi.run(:external_authorization, &external_authorization(&1, external_authorization))
|> Multi.run(:associated, &payment_method(&1, installments))
|> insert([
:acquirer_config,
:status,
:credit_card,
:external_authorization,
customer: :address,
credit_card: :credit_card_token
])
|> credit_card_history()
|> loaded_virtual_fields(payload)
end
defp external_authorization(%{payment: _payment}, external_authorization) do
data = %{
cavv: external_authorization.cavv,
xid: external_authorization.xid,
eci: external_authorization.eci,
version: external_authorization.version,
reference_id: external_authorization.reference_id,
acquirer_id: 1
}
external_authorization
|> Map.from_struct()
|> Map.delete(:token)
|> Map.merge(data)
|> ExternalAuthorization.new()
|> Repo.insert()
end
def with_antifraud(payload, config, antifraud, auth) do
credit_card = payload.credit_card
installments = credit_card.installments
customer = payload.customer
Multi.new()
|> Multi.run(:customer, &customer(&1, customer, auth))
|> Multi.run(:payment, &new_payment(&1, Type.credit_card(), payload, auth, config, antifraud))
|> Multi.run(:antifraud_data, &antifraud_data(&1, payload.antifraud))
|> Multi.run(:history, &add_history/1)
|> Multi.run(:credit_card, &credit_card(&1, credit_card))
|> Multi.run(:associated, &payment_method(&1, installments))
|> insert([
:acquirer,
:acquirer_config,
:antifraud,
:antifraud_config,
:antifraud_data,
:currency,
:items,
:status,
:credit_card,
customer: :address,
credit_card: :credit_card_token
])
|> credit_card_history()
|> loaded_virtual_fields(payload)
end
def with_paypal(payload, config, auth) do
paypal = payload.paypal
installments = paypal.installments
customer = payload.customer
Multi.new()
|> Multi.run(:customer, &customer(&1, customer, auth))
|> Multi.run(:payment, &new_payment(&1, Type.paypal(), payload, auth, config))
|> Multi.run(:history, &add_history/1)
|> Multi.run(:paypal, &paypal(&1, paypal))
|> Multi.run(:associated, &payment_method(&1, installments))
|> insert([
:acquirer_config,
:status,
:paypal,
customer: :address
])
end
defp insert(multi, preload) do
case multi |> Repo.transaction() do
{:ok, transaction} ->
{:ok, Repo.preload(transaction.associated, preload)}
{:error, reason} ->
{:error, reason}
{:error, _, reason, _} ->
{:error, reason}
end
end
defp customer(_, customer, auth) do
customer
|> Customer.get_or_create(auth.company_id)
|> Repo.insert_or_update()
end
defp new_payment(%{customer: customer}, type, payload, auth, config) do
data = %{
capture: payload.capture,
type_id: type,
status_id: Status.get_id(:started),
company_id: auth.company_id,
currency_id: Currency.get_id(payload.currency),
customer_id: customer.id,
acquirer_id: Acquirer.get_id(payload.acquirer),
antifraud_id: Antifraud.get_id(payload.antifraud),
application_id: Integer.to_string(auth.application_id),
acquirer_config_id: config.id
}
do_new_payment(payload, data)
end
defp new_payment(%{customer: customer}, type, payload, auth, config, fraud_config) do
data = %{
capture: payload.capture,
type_id: type,
status_id: Status.get_id(:started),
company_id: auth.company_id,
currency_id: Currency.get_id(payload.currency),
customer_id: customer.id,
acquirer_id: Acquirer.get_id(payload.acquirer),
antifraud_id: Antifraud.get_id(payload.antifraud),
application_id: Integer.to_string(auth.application_id),
acquirer_config_id: config.id,
antifraud_config_id: fraud_config.id
}
do_new_payment(payload, data)
end
defp do_new_payment(payload, data) do
items = Item.from_struct_list(payload.items)
payload
|> Map.from_struct()
|> Map.merge(data)
|> Payment.new()
|> Changeset.put_assoc(:items, items)
|> Repo.insert()
end
defp add_history(%{payment: payment}) do
payment
|> StatusHistory.new()
|> Repo.insert()
end
defp credit_card_history({:error, reason}), do: {:error, reason}
defp credit_card_history({:ok, %{credit_card: credit_card} = payment}) do
with {:ok, _} <- CreditCardTokenHistory.new(credit_card) do
{:ok, payment}
end
end
# Credit_card payment with token
defp credit_card(%{payment: payment, customer: customer}, %{token: token} = credit_card)
when not is_nil(token) do
CreditCard.get_card_token(
payment.acquirer_id,
customer.id,
credit_card.token
)
end
defp credit_card(%{payment: payment}, credit_card) do
credit_card_numbers = CreditCard.numbers(credit_card.number)
data = %{
expiration: CreditCard.get_expiration(credit_card.expiration),
masked: credit_card_numbers.masked,
customer_id: payment.customer_id,
acquirer_id: payment.acquirer_id,
first_six: credit_card_numbers.first_six,
last_four: credit_card_numbers.last_four
}
credit_card
|> Map.from_struct()
|> Map.delete(:token)
|> Map.merge(data)
|> CreditCard.get_or_create()
|> Repo.insert_or_update()
end
defp paypal(%{payment: payment}, paypal) do
data = %{
customer_id: payment.customer_id,
acquirer_id: payment.acquirer_id
}
paypal
|> Map.from_struct()
|> Map.merge(data)
|> PayPal.new()
|> Repo.insert()
end
defp loaded_virtual_fields({:error, reason}, _), do: {:error, reason}
defp loaded_virtual_fields(
{:ok, %{credit_card: %{credit_card_token: token}} = payment},
payload
)
when not is_nil(token) do
cvv =
if is_nil(payload.credit_card.cvv) do
token.cvv
else
payload.credit_card.cvv
end
credit_card = %{payment.credit_card | number: token.number, cvv: cvv}
{:ok, %{payment | credit_card: credit_card}}
end
defp loaded_virtual_fields({:ok, payment}, payload) do
credit_card = %{
payment.credit_card
| number: payload.credit_card.number,
cvv: payload.credit_card.cvv
}
{:ok, %{payment | credit_card: credit_card}}
end
defp bank_billet(_, %{expiration: expiration} = bank_billet) do
bank_billet
|> Map.from_struct()
|> Map.put(:expiration, BankBillet.get_expiration(expiration))
|> BankBillet.new()
|> Repo.insert()
end
defp qrcode(_) do
%{}
|> Map.put(:expiration, QRCode.get_expiration())
|> QRCode.new()
|> Repo.insert()
end
defp payment_method(%{payment: payment, credit_card: credit_card}, installments) do
payment
|> Payment.changeset(%{})
|> Changeset.put_change(:credit_card_id, credit_card.id)
|> Changeset.put_change(:installments, installments)
|> Repo.update()
end
defp payment_method(%{payment: payment, paypal: paypal}, installments) do
payment
|> Payment.changeset(%{})
|> Changeset.put_change(:paypal_id, paypal.id)
|> Changeset.put_change(:installments, installments)
|> Repo.update()
end
defp payment_method(%{payment: payment, bank_billet: bank_billet}) do
payment
|> Payment.changeset(%{})
|> Changeset.put_change(:bank_billet_id, bank_billet.id)
|> Repo.update()
end
defp payment_method(%{payment: payment, qrcode: qrcode}) do
payment
|> Payment.changeset(%{})
|> Changeset.put_change(:qrcode_id, qrcode.id)
|> Repo.update()
end
defp antifraud_data(%{payment: payment}, antifraud) do
payment
|> AntifraudData.new(antifraud)
|> Repo.insert()
end
end
apps/payment/lib/payment.ex
defmodule Gateway.Payment do
@moduledoc """
Gateway payment module handle different payment types workflows
"""
alias Payment.Schema.{
AcquirerConfig,
AntifraudConfig
}
alias Payment.New
def with_antifraud(payload, auth) do
{:ok, antifraud} = AntifraudConfig.get_config(payload, auth.company_id)
New.with_antifraud(payload, acquirer_config(payload, auth), antifraud, auth)
end
def with_bank_billet(payload, auth),
do: New.with_bank_billet(payload, acquirer_config(payload, auth), auth)
def with_credit_card(payload, auth),
do: New.with_credit_card(payload, acquirer_config(payload, auth), auth)
def with_qrcode(payload, auth),
do: New.with_qrcode(payload, acquirer_config(payload, auth), auth)
def with_paypal(payload, auth),
do: do_create(payload, acquirer_config(payload, auth), auth)
defp do_create(payload, %{keys: %{"manual_review" => true}} = config, auth) do
New.with_paypal(%{payload | capture: false}, config, auth)
end
defp do_create(payload, config, auth), do: New.with_paypal(payload, config, auth)
defp acquirer_config(payload, %{company_id: company_id}) do
with {:ok, config} <- AcquirerConfig.get_config(payload, company_id) do
config
end
end
end
apps/payment/lib/schema.ex
defmodule Payment.Schema do
@moduledoc """
Payment Schema
"""
def schema do
quote do
use Ecto.Schema
import Ecto
import Ecto.Changeset
import Ecto.Query
alias Payment.Repo
alias Payment.Schema.{
Acquirer,
AcquirerConfig,
Antifraud,
AntifraudConfig,
AntifraudData,
AntifraudDecision,
BankBillet,
CaptureHistory,
CreditCard,
ExternalAuthorization,
CreditCardToken,
CreditCardTokenHistory,
Currency,
Customer,
CustomerAddress,
DeclinedReason,
Details,
Item,
PayPal,
PostbackHistory,
QRCode,
RefundHistory,
Status,
StatusHistory,
Type
}
end
end
@doc false
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end
apps/gateway/lib/payload/external_authorization.ex
defmodule Gateway.Payload.ExternalAuthorization do
@moduledoc """
CreditCard data in the incoming request body
"""
use Ecto.Schema
#alias Ecto.UUID
import Ecto.Changeset
#import Gateway.Gettext
@primary_key false
embedded_schema do
field(:cavv, :string)
field(:xid, :string)
field(:eci, :string)
field(:version, :string)
field(:reference_id, :string)
end
@required_fields ~w(cavv xid eci version reference_id)a
@optional_fields ~w()a
# @doc false
# def changeset(struct, %{"token" => token} = data) when not is_nil(token) do
# struct
# |> cast(data, @required_token_fields ++ @optional_token_fields)
# |> validate_required(@required_token_fields)
# # Sanitize data
# |> update_change(:cvv, &String.trim/1)
# |> update_change(:token, &String.trim/1)
# # Validate Data
# |> validate_binary_token()
# |> validate_number(:installments, greater_than_or_equal_to: 1)
# |> validate_number(:installments, less_than_or_equal_to: @installments)
# end
def changeset(struct, data) do
struct
|> cast(data, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
# Sanitize data
|> update_change(:cavv, &String.trim/1)
|> update_change(:xid, &String.trim/1)
|> update_change(:eci, &String.trim/1)
|> update_change(:version, &String.trim/1)
|> update_change(:reference_id, &String.trim/1)
end
# # Remove non digits
# defp clean_nondigit_char(field) do
# Regex.replace(~r/\D/, field, "")
# end
end
apps/payment/lib/schema/external_authorization.ex
defmodule Payment.Schema.ExternalAuthorization do
@moduledoc """
CreditCardToken Schema
"""
use Payment.Schema, :schema
alias Ecto.Kms
@primary_key {:id, :id, autogenerate: true}
schema "external_authorization" do
field(:cavv, Kms)
field(:xid, Kms)
field(:eci, Kms)
field(:version, Kms)
field(:reference_id, Kms)
#belongs_to(:payment, Payment)
belongs_to(:acquirer, Acquirer)
timestamps(inserted_at: :created_at, type: :utc_datetime)
end
@required_fields ~w(cavv
xid
eci
version
reference_id)a
@optional_fields ~w(acquirer_id)a
@doc false
def changeset(%__MODULE__{} = external_authorization, attrs) do
external_authorization
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
end
@doc false
def new(attrs) do
__MODULE__.changeset(%__MODULE__{}, attrs)
end
def to_map(external_authorization) when is_nil(external_authorization), do: nil
def to_map(%__MODULE__{} = external_authorization) do
%{
cavv: external_authorization.cavv,
xid: external_authorization.xid,
eci: external_authorization.eci,
version: external_authorization.version,
reference_id: external_authorization.reference_id,
acquirer: 1
}
end
end
The payload I’m sending via Api is below:
{
"acquirer": "cielo",
"externalId": "c05b23d2-fc67-11e7-96f5-d7fc82149a94",
"company_id" : 1,
"amount": 100,
"interest": 0,
"creditCard": {
"installments": 1,
"number": "5555555555555555",
"holder": "Teste",
"expiration": "02/2028",
"cvv": "Teste",
"save": false
},
"externalAuthorization": {
"cavv": "Y2FyZGluYWxjb21tZXJjZWF1dGg=",
"xid": "Y2FyZGluYWxjb21tZXJjZWF1dGg=",
"eci": "05",
"version": "2",
"reference_id": "e473919f-a33d-4cb7-b172-1c0e826025f7"
},
"customer": {
"externalId": "297229",
"name": "Nome Teste",
"document": "123.345.321-92",
"email": "teste@teste.com",
"phone": "(11) 99999-9999",
"gender": "male",
"address": {
"street": "teste",
"number": "444",
"complement": "655",
"zipcode": "04563-060",
"city": "São Paulo",
"state": "SP",
"country": "BR",
"district": "Brooklin"
}
},
"items": [
{
"sku": "378283",
"type": "ticket",
"name": "teste",
"unitPrice": 500,
"quantity": 1
}
]
}
Obs: ExternalAuthorization is optional
I’m receiving the error: {{%ArgumentError{message: “unknown field external_authorization_id
. Only fields, embeds and associations (except :through ones) are supported in changesets”}.