diff --git a/lib/rocketpay.ex b/lib/rocketpay.ex index 424b93c..b66383d 100644 --- a/lib/rocketpay.ex +++ b/lib/rocketpay.ex @@ -1,9 +1,11 @@ defmodule Rocketpay do alias Rocketpay.Users.Create, as: UserCreate - alias Rocketpay.Accouns.Deposit + alias Rocketpay.Accounts.{Deposit, Withdraw} defdelegate create_user(params), to: UserCreate, as: :call defdelegate deposit(params), to: Deposit, as: :call + defdelegate withdraw(params), to: Withdraw, as: :call + end diff --git a/lib/rocketpay/account.ex b/lib/rocketpay/account.ex index 54fabea..52e0260 100644 --- a/lib/rocketpay/account.ex +++ b/lib/rocketpay/account.ex @@ -15,8 +15,8 @@ defmodule Rocketpay.Account do timestamps() end - def changeset(params) do - %__MODULE__{} + def changeset(struct \\ %__MODULE__{}, params) do + struct |> cast(params, @required_params) |> validate_required(@required_params) |> check_constraint(:balance, name: :balance_must_be_positive_or_zero) diff --git a/lib/rocketpay/accounts/deposit.ex b/lib/rocketpay/accounts/deposit.ex index b64ee3f..c535fed 100644 --- a/lib/rocketpay/accounts/deposit.ex +++ b/lib/rocketpay/accounts/deposit.ex @@ -1,45 +1,17 @@ defmodule Rocketpay.Accounts.Deposit do - alias Ecto.Multi + alias Rocketpay.Accounts.Operation + alias Rocketpay.Repo - alias Rocketpay.{Account, Repo} - - def call(%{"id" => id, "value" => value}) do - Multi.new() - |> Multi.run(:account, fn repo, _changes -> get_account(repo, id) end) - |> Multi.run(:update_balance, fn repo, %{account: account} -> - update_balance(repo, account, value) - end) + def call(params) do + params + |> Operation.call(:deposit) + |> run_transaction() end - defp get_account(repo, id) do - case repo.get(Account, id) do - nil -> {:error, "Account, not found!"} - account -> {:ok, account} + defp run_transaction(multi) do + case Repo.transaction(multi) do + {:error, _opration, reason, _changes} -> {:error, reason} + {:ok, %{update_balance: account}} -> {:ok, account} end end - - defp update_balance(repo, account, value) do - account - |> sum_values(value) - |> update_account(repo) - end - - defp sum_values(%Account{balance: balance}, value) do - value - |>Decimal.cast() - |> handle_cast(balance) - end - - defp handle_cast({:ok, value}, balance), do: Decimal.add(value, balance) - defp handle_cast(:error, _balanace), do: {:error, "Invalid deposit value!"} - - defp update_account({:error, _reason} = error, _repo), do: error - - defp update_account(value, repo) do - params = %{balance: value} - - params - |> Account.changeset() - |> repo.update() - end end diff --git a/lib/rocketpay/accounts/operation.ex b/lib/rocketpay/accounts/operation.ex new file mode 100644 index 0000000..aa9f14c --- /dev/null +++ b/lib/rocketpay/accounts/operation.ex @@ -0,0 +1,54 @@ +defmodule Rocketpay.Accounts.Operation do + alias Ecto.Multi + + alias Rocketpay.{Account, Repo} + + def call(%{"id" => id, "value" => value}, operation) do + operation_name = account_operation_name(operation) + + Multi.new() + |> Multi.run(operation_name, fn repo, _changes -> get_account(repo, id) end) + |> Multi.run(:update_balance, fn repo, changes -> + account = Map.get(changes, operation_name) + update_balance(repo, account, value, operation) + end) + end + + defp get_account(repo, id) do + case repo.get(Account, id) do + nil -> {:error, "Account not found!"} + account -> {:ok, account} + end + end + + defp update_balance(repo, account, value, operation) do + account + |> operation(value, operation) + |> update_account(repo, account) + end + + defp operation(%Account{balance: balance}, value, operation) do + value + |>Decimal.cast() + |> handle_cast(balance, operation) + end + + defp handle_cast({:ok, value}, balance, :deposit), do: Decimal.add(balance, value) + defp handle_cast({:ok, value}, balance, :withdraw), do: Decimal.sub(balance, value) + defp handle_cast(:error, _balanace, operation), do: {:error, "Invalid #{operation} value!"} + + defp update_account({:error, _reason} = error, _repo, _account), do: error + + defp update_account(value, repo, account) do + params = %{balance: value} + + account + |> Account.changeset(params) + |> repo.update() + end + + defp account_operation_name(operation) do + "account_#{Atom.to_string(operation)}" + |> String.to_atom() + end +end diff --git a/lib/rocketpay/accounts/transaction.ex b/lib/rocketpay/accounts/transaction.ex new file mode 100644 index 0000000..348716f --- /dev/null +++ b/lib/rocketpay/accounts/transaction.ex @@ -0,0 +1,26 @@ +defmodule Rocketpay.Accounts.Transaction do + alias Ecto.Multi + alias Rocketpay.Repo + alias Rocketpay.Accounts.Operation + + def call(%{"from" => from_id, "to" => to_id, "value" => value}) do + withdraw_params = build_params(from_id, value) + deposit_params = build_params(to_id, value) + + Multi.new() + |> Multi.merge(fn _changes -> Operation.call(withdraw_params, :withdraw) end) + |> Multi.merge(fn _changes -> Operation.call(deposit_params, :deposit) end) + |> run_transaction() + end + + defp build_params(id, value), do: %{"id" => id, "value" => value} + + defp run_transaction(multi) do + case Repo.transaction(multi) do + {:error, _opration, reason, _changes} -> + {:error, reason} + {:ok, %{deposit: to_account, withdraw: from_account}} -> + {:ok, %{to_account: to_account, from_account: from_account}} + end + end +end diff --git a/lib/rocketpay/accounts/withdraw.ex b/lib/rocketpay/accounts/withdraw.ex new file mode 100644 index 0000000..f593f29 --- /dev/null +++ b/lib/rocketpay/accounts/withdraw.ex @@ -0,0 +1,17 @@ +defmodule Rocketpay.Accounts.Withdraw do + alias Rocketpay.Accounts.Operation + alias Rocketpay.Repo + + def call(params) do + params + |> Operation.call(:withdraw) + |> run_transaction() + end + + defp run_transaction(multi) do + case Repo.transaction(multi) do + {:error, _opration, reason, _changes} -> {:error, reason} + {:ok, %{update_balance: account}} -> {:ok, account} + end + end +end diff --git a/lib/rocketpay_web/controllers/accounts_controller.ex b/lib/rocketpay_web/controllers/accounts_controller.ex index 52797b9..43a42e8 100644 --- a/lib/rocketpay_web/controllers/accounts_controller.ex +++ b/lib/rocketpay_web/controllers/accounts_controller.ex @@ -14,6 +14,10 @@ defmodule RocketpayWeb.AccountsController do end def withdraw(conn, params) do - + with {:ok, %Account{} = account} <- Rocketpay.withdraw(params) do + conn + |> put_status(:ok) + |> render("update.json", account: account) + end end end diff --git a/lib/rocketpay_web/views/accounts_view.ex b/lib/rocketpay_web/views/accounts_view.ex new file mode 100644 index 0000000..4665d6c --- /dev/null +++ b/lib/rocketpay_web/views/accounts_view.ex @@ -0,0 +1,18 @@ +defmodule RocketpayWeb.AccountsView do + alias Rocketpay.Account + + def render("update.json", %{ + account: %Account{ + id: account_id, + balance: balance + } + }) do + %{ + message: "Ballance changed successfully", + account: %{ + id: account_id, + balance: balance + } + } + end +end diff --git a/lib/rocketpay_web/views/error_view.ex b/lib/rocketpay_web/views/error_view.ex index 1c26277..7190077 100644 --- a/lib/rocketpay_web/views/error_view.ex +++ b/lib/rocketpay_web/views/error_view.ex @@ -20,6 +20,10 @@ defmodule RocketpayWeb.ErrorView do %{message: translate_errors(changeset)} end + def render("400.json", %{result: message}) do + %{message: message} + end + defp translate_errors(changeset) do traverse_errors(changeset, fn {msg, opts} -> Enum.reduce(opts, msg, fn {key, value}, acc -> diff --git a/mix.exs b/mix.exs index 6fa67d1..3d38788 100644 --- a/mix.exs +++ b/mix.exs @@ -44,7 +44,8 @@ defmodule Rocketpay.MixProject do {:jason, "~> 1.0"}, {:plug_cowboy, "~> 2.0"}, {:credo, "~> 1.5", only: [:dev, :test], runtime: false}, - {:bcrypt_elixir, "~> 2.0"} + {:bcrypt_elixir, "~> 2.0"}, + {:decimal, "~> 2.0"} ] end