Skip to content

Commit

Permalink
[feature] added the checkout resource
Browse files Browse the repository at this point in the history
  • Loading branch information
julioleitao committed Mar 15, 2019
1 parent 044e43f commit cbb9f71
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 4 deletions.
94 changes: 94 additions & 0 deletions apps/store_core/lib/store_core/contexts/checkouts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
defmodule StoreCore.Checkouts do
alias StoreCore.Promotions
alias StoreCore.Products
alias StoreCore.CheckoutProduct

defp to_checkout_products(products) do
products = products
|> Enum.map(fn params ->
checkout_product =%CheckoutProduct{}
|> CheckoutProduct.changeset(params)

if checkout_product.valid?() do
CheckoutProduct.apply(checkout_product)
else
[:errors, checkout_product.errors]
end
end)

errors = Enum.filter(products, fn product ->
case product do
[:errors, errors] -> errors
_ -> false
end
end)

if length(errors) != 0 do
{:errors, errors}
else
{:ok, products}
end
end


def retrieve_products(products) do
products = Enum.map(products, fn checkout_product ->
case Products.get(checkout_product.id) do
{:ok, product} ->
Map.put(checkout_product, :price, product.price)
error -> error
end
end)

IO.puts "verifica em caso de erro"
Enum.filter(products, fn product ->
case product do
{:error, reason} -> true
_ -> false
end
end)
|> IO.inspect


{:ok, products}
end

def preview(products) do
with {:ok, products} <- to_checkout_products(products),
{:ok, products} <- retrieve_products(products) do
total =
products
|> Enum.reduce(0, fn product, acc ->
price =
product
|> promotion_descount

acc + price
end)

{:ok, [total: total, products: products]}
end
end

defp promotion_descount(product) do
if (Map.has_key?(product, :promotion_id) and product.promotion_id == nil) or
not Map.has_key?(product, :promotion_id) do
product.price * product.quantity
else
{:ok, promo} = Promotions.get(product.promotion_id)
descount_from(product, promo.name)
end
end

def descount_from(%{price: price, quantity: quantity}, "Pague 1 Leve 2") do
numberOfProductsNotIncluded = rem(quantity, 2)
quantity = div(quantity - numberOfProductsNotIncluded, 2)
price * (quantity + numberOfProductsNotIncluded)
end

def descount_from(%{price: price, quantity: quantity}, "3 por 10 reais") do
numberOfProductsNotIncluded = rem(quantity, 3)
quantity = div(quantity - numberOfProductsNotIncluded, 3)
10 * quantity + numberOfProductsNotIncluded * price
end
end
19 changes: 18 additions & 1 deletion apps/store_core/lib/store_core/contexts/promotions.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
defmodule StoreCore.Promotions do
alias StoreCore.{Promotion, Repo}

def list, do: Promotion |> Repo.all
def list, do: Promotion |> Repo.all()

def get(id) do
case Repo.get(Promotion, id) do
nil -> {:error, :not_found}
promotion -> {:ok, promotion}
end
end

def filter_by_name(name) do
[promotion] =
list()
|> Enum.filter(fn promotion ->
promotion.name === name
end)

promotion
end
end
21 changes: 21 additions & 0 deletions apps/store_core/lib/store_core/schemas/checkout_product.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule StoreCore.CheckoutProduct do
use Ecto.Schema

import Ecto.Changeset

embedded_schema do
field :quantity, :integer
field :promotion_id, Ecto.UUID
end

@required [:id, :quantity]
@accepted [:promotion_id | @required]

def changeset(checkout_product, params \\ %{}) do
checkout_product
|> cast(params, @accepted)
|> validate_required(@required)
end

def apply(changeset), do: apply_changes(changeset)
end
4 changes: 3 additions & 1 deletion apps/store_core/lib/store_core/schemas/product.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ defmodule StoreCore.Product do
schema "products" do
field :name, :string
field :price, :integer
field :promotion_id, Ecto.UUID

timestamps()
end

@required_params [:name, :price]
@accepted_params [:promotion_id | @required_params]

def changeset(product, params \\ %{}) do
product
|> cast(params, @required_params)
|> cast(params, @accepted_params)
|> validate_required(@required_params)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ defmodule StoreCore.Repo.Migrations.CreatePromotions do
flush()

insert_promotions()

alter table(:products) do
add :promotion_id, references(:promotions, type: :uuid), null: true
end
end

defp insert_promotions() do
["Pague 1 Leve 2", "3 Por 10 reais"]
["Pague 1 Leve 2", "3 por 10 reais"]
|> Enum.map(fn name ->
%Promotion{name: name}
|> Promotion.changeset()
Expand Down
123 changes: 123 additions & 0 deletions apps/store_core/test/store_core/contexts/checkouts_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
defmodule StoreCore.CheckoutsTest do
use StoreCore.DataCase

alias StoreCore.{Checkouts, Product, Products, Promotions}

test "calculate a checkout without promo code" do
products =
[
%{name: "Lápis", price: 200, quantity: 1},
%{name: "Borracha", price: 100, quantity: 1}
]
|> Enum.map(fn product ->
{:ok, created_product} = Products.create(product)

product = %{product | name: nil}
product = %{product | price: nil}
Map.put(product, :id, created_product.id)
end)

assert [total: 300, products: ^products] = Checkouts.preview(products)
end

describe "calculate Pague 1 Leve 2 with" do
test "with one group" do
total =
[
%{name: "Lápis", price: 200, quantity: 2, promotion_id: pague_1_leve_2()},
%{name: "Borracha", price: 100, quantity: 1}
]
|> checkout_total

assert total === 300
end

test "with one group and one out from promotion" do
total =
[
%{name: "Lápis", price: 200, quantity: 3, promotion_id: pague_1_leve_2()},
%{name: "Borracha", price: 100, quantity: 1}
]
|> checkout_total

assert total === 500
end

test "with many groups" do
total =
[
%{name: "Lápis", price: 200, quantity: 7, promotion_id: pague_1_leve_2()},
%{name: "Borracha", price: 150, quantity: 3, promotion_id: pague_1_leve_2()}
]
|> checkout_total

assert total === 1100
end
end

describe "calculate 3 por 10 reais with" do
test "with one group" do
total =
[%{name: "Lápis", price: 200, quantity: 3, promotion_id: tres_por_10()}]
|> checkout_total

assert total === 10
end

test "with two groups" do
total =
[%{name: "Lápis", price: 200, quantity: 6, promotion_id: tres_por_10()}]
|> checkout_total

assert total === 20
end

test "with two groups and one out from promotion" do
total =
[%{name: "Lápis", price: 200, quantity: 7, promotion_id: tres_por_10()}]
|> checkout_total

assert total === 220
end
end

test "a mixed checkout" do
total =
[
# 700
%{name: "Ponta de lápis", price: 100, quantity: 7},
# 30
%{name: "Caixa de lápis", price: 650, quantity: 9, promotion_id: tres_por_10()},
# 300
%{name: "Borracha", price: 75, quantity: 7, promotion_id: pague_1_leve_2()},
# 320
%{name: "Pasta colegial", price: 300, quantity: 7, promotion_id: tres_por_10()},
# 200
%{name: "Caneta", price: 100, quantity: 3, promotion_id: pague_1_leve_2()},
# 1400
%{name: "Lápis", price: 200, quantity: 7}
]
|> checkout_total

assert total === 700 + 30 + 300 + 320 + 200 + 1400
end

defp tres_por_10, do: Promotions.filter_by_name("3 por 10 reais").id

defp pague_1_leve_2, do: Promotions.filter_by_name("Pague 1 Leve 2").id

defp checkout_total(products) do
[total: total, products: _] =
products
|> Enum.map(fn product ->
{:ok, created_product} = Products.create(product)
# ensure that the price it is retrieved from the backend
product = %{product | name: nil}
product = %{product | price: nil}
Map.put(product, :id, created_product.id)
end)
|> Checkouts.preview()

total
end
end
8 changes: 8 additions & 0 deletions apps/store_core/test/store_core/contexts/promotions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@ defmodule StoreCore.PromotionsTest do
test "list the default promotions" do
assert length(Promotions.list()) === 2
end

test "filter by name" do
name = "Pague 1 Leve 2"
promotion = Promotions.filter_by_name(name)

assert %{id: id, name: ^name} = promotion
assert promotion = Promotions.get(id)
end
end
21 changes: 21 additions & 0 deletions apps/store_web/lib/store_web/controllers/checkout_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule StoreWeb.CheckoutController do
use StoreWeb, :controller

alias StoreCore.Checkouts

action_fallback StoreWeb.FallbackController

def create(conn, %{"products" => products}) do
products = Enum.map(products, &convert_to_atom(&1))

with {:ok, checkout} <- Checkouts.preview(products) do
conn
|> put_status(:accepted)
|> render("show.json", checkout: checkout)
end
end

defp convert_to_atom(map) do
for {key, val} <- map, into: %{}, do: {String.to_atom(key), val}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ defmodule StoreWeb.FallbackController do
|> render("error.json", changeset: changeset)
end

def call(conn, {:errors, errors}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(StoreWeb.ChangesetView)
|> render("errors.json", errors: errors)
end


def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
Expand Down
1 change: 1 addition & 0 deletions apps/store_web/lib/store_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ defmodule StoreWeb.Router do

resources "/products", ProductController, only: [:index, :create, :show, :update, :delete]
get "/promotions", PromotionController, :index
post "/checkout", CheckoutController, :create
end
end
4 changes: 4 additions & 0 deletions apps/store_web/lib/store_web/views/changeset_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ defmodule StoreWeb.ChangesetView do
Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
end

def render("errors.json", %{errors: errors}) do
%{errors: ["Invalid entity"]}
end

def render("error.json", %{changeset: changeset}) do
%{errors: translate_errors(changeset)}
end
Expand Down
27 changes: 27 additions & 0 deletions apps/store_web/lib/store_web/views/checkout_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule StoreWeb.CheckoutView do
use StoreWeb, :view

alias StoreWeb.CheckoutView

def render("show.json", %{checkout: checkout}) do
%{data: render_one(checkout, CheckoutView, "checkout.json")}
end

def render("checkout.json", %{checkout: [total: total, products: products]}) do
%{
total: total,
products: products_to_view(products)
}
end

defp products_to_view(products) do
products
|> Enum.map(fn product ->
%{
id: product.id,
price: product.price,
quantity: product.quantity
}
end)
end
end
Loading

0 comments on commit cbb9f71

Please sign in to comment.