Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions apps/bankingAPI/lib/bankingAPI/accounts/schemas/account.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
defmodule BankingAPI.Accounts.Schemas.Account do
@moduledoc """
The entity of Account.
Account entity.

1 user (id) - N accounts (FK user_id)
1 account (FK user_id) -> 1 user (id)
"""
use Ecto.Schema

import Ecto.Changeset

alias BankingAPI.Users.Schemas.User

@derive {Jason.Encoder, except: [:__meta__]}
@required [:amount]

@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "accounts" do
belongs_to(:user, User)
field(:account_number, :integer, read_after_writes: true)
field(:amount, :integer)

timestamps()
end

def changeset(model \\ %__MODULE__{}, params) do
model
|> cast(params, @required)
|> validate_required(@required)
|> validate_number(:amount, greater_than_or_equal_to: 0)
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule BankingAPI.Users.Inputs.Create do
@moduledoc """
Input data for calling insert_new_user/1.
Input data for calling create/1.
"""
use Ecto.Schema

Expand Down
7 changes: 5 additions & 2 deletions apps/bankingAPI/lib/bankingAPI/users/schemas/user.ex
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
defmodule BankingAPI.Users.Schemas.User do
@moduledoc """
The user of an account
The User
"""
use Ecto.Schema

import Ecto.Changeset

@required [:name, :email]
alias BankingAPI.Accounts.Schemas.Account

@derive {Jason.Encoder, except: [:__meta__]}
@required [:name, :email]

@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "users" do
field(:name, :string)
field(:email, :string)
has_one(:account, Account)

timestamps()
end

def changeset(model \\ %__MODULE__{}, params) do
model
|> cast(params, @required)
|> cast_assoc(:account, with: &Account.changeset/2)
|> validate_required(@required)
|> validate_length(:name, min: 3)
|> validate_format(:email, ~r/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/)
Expand Down
12 changes: 4 additions & 8 deletions apps/bankingAPI/lib/bankingAPI/users/users.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ defmodule BankingAPI.Users do
@moduledoc """
Domain public functions for users context
"""
require Logger

alias BankingAPI.Repo
alias BankingAPI.Users.Inputs
alias BankingAPI.Users.Schemas.User

require Logger

@doc """
Given a VALID changeset it attempts to insert a new user.
It might fail due to email unique index and we transform that return
into an error tuple.
"""
@spec create(Inputs.Create.t()) ::
{:ok, User.t()} | {:error, Ecto.Changeset.t() | :email_conflict}
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
def create(%Inputs.Create{} = input) do
Logger.info("Inserting new user")

params = %{name: input.name, email: input.email}
new_account = %{amount: 1000}
params = %{name: input.name, email: input.email, account: new_account}

params
|> User.changeset()
Expand All @@ -29,10 +29,6 @@ defmodule BankingAPI.Users do
Logger.info("User successfully inserted", email: inspect(user.email))
{:ok, user}

{:error, %{errors: [email: {"has already been taken", _}]}} ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Osh, esse caso de erro ainda pode acontecer, não?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provavelmente o erro vai estourar no controller qnd acontecer

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Siimm, vou refazer o controller namoral agora

Logger.info("Email already taken")
{:error, :email_conflict}

{:error, changeset} ->
Logger.error("Error while inserting new user", error: inspect(changeset))
{:error, changeset}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ defmodule BankingAPI.Repo.Migrations.CreateUser do
def change do
create table(:users, primary_key: false) do
add(:id, :uuid, primary_key: true)
add(:name, :string)
add(:email, :string)
add(:name, :string, null: false)
add(:email, :string, null: false)

timestamps()
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,25 @@ defmodule BankingAPI.Repo.Migrations.CreateAccount do
def change do
create table(:accounts, primary_key: false) do
add(:id, :uuid, primary_key: true)
add(:user_id, references(:users, type: :uuid))
add(:amount, :integer)
add(:user_id, references(:users, type: :uuid), null: false)
add(:account_number, :serial, null: false)
add(:amount, :integer, null: false)

timestamps()
end

create(
constraint(:accounts, "account_number_must_be_between_10000_and_99999",
check: "account_number >= 10000 and account_number <= 99999"
)
)

create(unique_index(:accounts, [:account_number]))

execute "ALTER SEQUENCE accounts_account_number_seq START with 10000 RESTART"
Copy link
Collaborator

@renanlage renanlage May 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tem um problema em expor numeros sequenciais publicamente q eh revelar coisas como: quantidade de contas no banco e velocidade em que elas são abertas. No geral, é preferível usar números aleatórios mas não precisa mudar

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eu pensei nisso, mas fiquei com duvida de como faria isso na aplicação. Pois se gerar um número randomico que já exista no banco, ele iria ficar tentando várias vezes até conseguir um número vago (isso quando tiver muitos registros de account number). Tem alguma sugestão para isso?

Copy link
Collaborator

@renanlage renanlage May 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hoje acontece conflito e a gente retenta e tem resolvido so far. Com 6 digitos são 1 milhão de contas, demora atéo conflito virar um problema.

Mas temos uma todo pra resolver isso inclusive. O jeito que pensei é gerar tds os numeros possiveis (até x digitos) e subtrair os existentes numa query (da pra fzr no postgres essa geracao e subtracao), cachear esses numeros e ir tirando aleatoriamente da lista. O cache pode durar alguns minutos pra nao precisar ir no banco toda vez gerar essa lista. Se o cache for numa storage externa tipo redis nem precisa ir no banco de novo, só usar ele pra armazenar essa pool de contas disponiveis

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

É nois, Vou tentar gerar um novo concorrente de peso aqui então.


create(constraint(:accounts, "ammount_must_be_0_or_positive", check: "amount >= 0"))

create(index(:accounts, [:user_id]))
end
end
128 changes: 116 additions & 12 deletions apps/bankingAPI/test/users_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,129 @@ defmodule BankingAPI.UsersTest do
alias BankingAPI.Users.Schemas.User

describe "create/1" do
test "successfully create an user and account (with amount 1000) using valid input" do
email = "#{Ecto.UUID.generate()}@email.com"
name = "abc"

create_input = %Inputs.Create{
name: name,
email: email
}

assert {:ok,
%User{
id: user_id,
name: ^name,
email: ^email,
inserted_at: %NaiveDateTime{},
updated_at: %NaiveDateTime{},
account: %{amount: 1000}
} = expected_user} = Users.create(create_input)

assert expected_user == Repo.get(User, user_id) |> Repo.preload(:account)

assert [expected_user] == Repo.all(User) |> Repo.preload(:account)
end

@tag capture_log: true
test "fail if try to create user with empty name" do
email = "#{Ecto.UUID.generate()}@email.com"
name = ""

create_input = %Inputs.Create{
name: name,
email: email
}

assert {:error,
%{valid?: false, errors: [name: {"can't be blank", [validation: :required]}]}} =
Users.create(create_input)

assert [] == Repo.all(User)
end

@tag capture_log: true
test "fail if try to create user with empty email" do
email = ""
name = "abc"

create_input = %Inputs.Create{
name: name,
email: email
}

assert {:error,
%{valid?: false, errors: [email: {"can't be blank", [validation: :required]}]}} =
Users.create(create_input)

assert [] == Repo.all(User)
end

@tag capture_log: true
test "fail if try to create user with name having two characters" do
email = "#{Ecto.UUID.generate()}@email.com"
name = "ab"

create_input = %Inputs.Create{
name: name,
email: email
}

assert {:error,
%{
valid?: false,
errors: [
name:
{"should be at least %{count} character(s)",
[{:count, 3}, {:validation, :length}, {:kind, :min}, {:type, :string}]}
]
}} = Users.create(create_input)

assert [] == Repo.all(User)
end

@tag capture_log: true
test "fail if email is already taken" do
email = "taken@email.com"
Repo.insert!(%User{email: email})
test "fail if try to create user with invalid email" do
email = "abs.email.com"
name = "abc"

create_input = %Inputs.Create{
name: name,
email: email
}

assert {:error, :email_conflict} ==
Users.create(%Inputs.Create{name: "abc", email: email})
assert {:error,
%{
valid?: false,
errors: [email: {"has invalid format", [validation: :format]}]
}} = Users.create(create_input)

assert [] == Repo.all(User)
end

test "successfully create an user with valid input" do
@tag capture_log: true
test "fail if try to create 2 users with same email" do
email = "#{Ecto.UUID.generate()}@email.com"
name = "abc"

create_input = %Inputs.Create{
name: name,
email: email
}

assert {:ok, first_user} = Users.create(create_input)

assert {:ok, %User{name: "random name", email: ^email, inserted_at: %NaiveDateTime{}, updated_at: %NaiveDateTime{}} = user} =
Users.create(%Inputs.Create{
name: "random name",
email: email
})
assert {:error,
%{
valid?: false,
errors: [
email:
{"has already been taken",
[constraint: :unique, constraint_name: "users_email_index"]}
]
}} = Users.create(create_input)

assert user == Repo.get_by(User, email: email)
assert [first_user] == Repo.all(User) |> Repo.preload(:account)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,67 @@ defmodule BankingAPIWeb.UserController do
alias BankingAPI.Users.Inputs

alias BankingAPIWeb.InputValidation
alias BankingAPIWeb.UserView

def create(conn, params) do
with {:ok, input} <- InputValidation.cast_and_apply(params, Inputs.Create),
{:ok, user} <- Users.create(input) do
send_json(conn, 200, user)
# send_json(conn, 200, user)
conn
|> put_status(200)
|> put_view(UserView)
|> render("show.json", %{
user: user
})
else
{:error, %Ecto.Changeset{}} ->
msg = %{type: "bad_input", description: "Invalid input"}
send_json(conn, 400, msg)

{:error, :email_conflict} ->
msg = %{type: "conflict", description: "Email already taken"}
{:error,
%Ecto.Changeset{
errors: [
email:
{"has already been taken",
[constraint: :unique, constraint_name: "users_email_index"]}
]
}} ->
msg = %{type: "unprocessable_entity", description: "Email already taken"}
send_json(conn, 422, msg)

{:error,
%Ecto.Changeset{
errors: [
name:
{"should be at least %{count} character(s)",
[count: 3, validation: :length, kind: :min, type: :string]}
]
}} ->
msg = %{type: "length_required", description: "Name must have 3 digits or more"}
send_json(conn, 411, msg)

{:error,
%Ecto.Changeset{
errors: [
email_confirmation: {"does not match confirmation", [validation: :confirmation]}
]
}} ->
msg = %{type: "precondition_failed", description: "E-mail confirmations does not match"}
send_json(conn, 412, msg)

{:error, %Ecto.Changeset{errors: [email: {"has invalid format", [validation: :format]}]}} ->
msg = %{type: "precondition_failed", description: "E-mail has invalid format"}
send_json(conn, 412, msg)

{:error,
%Ecto.Changeset{
errors: [
name: {"can't be blank", [validation: :required]},
email: {"can't be blank", [validation: :required]}
]
}} ->
msg = %{type: "precondition_required", description: "Name or Email can not be blank"}
send_json(conn, 428, msg)

{:error, %Ecto.Changeset{}} ->
msg = %{type: "internal_server_error", description: "Internal Server Error"}
send_json(conn, 500, msg)
end
end

Expand Down
14 changes: 14 additions & 0 deletions apps/bankingAPI_web/lib/bankingAPI_web/views/user_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule BankingAPIWeb.UserView do
@moduledoc """
User view
"""
use BankingAPIWeb, :view

def render("show.json", %{user: user}) do
%{
name: user.name,
email: user.email,
account: %{account_number: user.account.account_number, amount: user.account.amount}
}
end
end
Loading