From 466ebb773dc56210b3e98bf2804866f9a8997936 Mon Sep 17 00:00:00 2001 From: Matheus Buss Date: Wed, 3 Aug 2022 22:32:10 -0300 Subject: [PATCH 1/7] First implementation of shop orders --- lib/cambiatus/orders.ex | 321 ++++++++++++++++++ lib/cambiatus/orders/item.ex | 34 ++ lib/cambiatus/orders/order.ex | 29 ++ lib/cambiatus/shop/order.ex | 13 +- .../20220803140956_rename_orders_to_items.exs | 7 + .../20220803194923_create_orders.exs | 26 ++ .../20220803194940_repurpose_items.exs | 25 ++ test/cambiatus/orders_test.exs | 144 ++++++++ 8 files changed, 591 insertions(+), 8 deletions(-) create mode 100644 lib/cambiatus/orders.ex create mode 100644 lib/cambiatus/orders/item.ex create mode 100644 lib/cambiatus/orders/order.ex create mode 100644 priv/repo/migrations/20220803140956_rename_orders_to_items.exs create mode 100644 priv/repo/migrations/20220803194923_create_orders.exs create mode 100644 priv/repo/migrations/20220803194940_repurpose_items.exs create mode 100644 test/cambiatus/orders_test.exs diff --git a/lib/cambiatus/orders.ex b/lib/cambiatus/orders.ex new file mode 100644 index 00000000..76625309 --- /dev/null +++ b/lib/cambiatus/orders.ex @@ -0,0 +1,321 @@ +defmodule Cambiatus.Orders do + @moduledoc """ + The Orders context. + """ + + import Ecto.Query, warn: false + alias Cambiatus.Repo + + alias Cambiatus.Orders.Order + + @doc """ + Returns the list of orders. + + ## Examples + + iex> list_orders() + [%Order{}, ...] + + """ + def list_orders do + Repo.all(Order) + end + + @doc """ + Gets a single order. + + Returns {:error, "No order exists with the id: {id}} if the Order does not exist. + + ## Examples + + iex> get_order(123) + {:ok, %Order{}} + + iex> get_order(456) + {:error, "Could not find order with id: {id}} + + """ + def get_order(id) do + case Repo.get(Order, id) do + nil -> + {:error, "No order exists with the id: #{id}"} + + val -> + {:ok, val} + end + end + + @doc """ + Gets a single order. + + Raises `Ecto.NoResultsError` if the Order does not exist. + + ## Examples + + iex> get_order!(123) + %Order{} + + iex> get_order!(456) + ** (Ecto.NoResultsError) + + """ + def get_order!(id), do: Repo.get!(Order, id) + + @doc """ + Gets a single order. + + Returns {:error, "No order exists with the id: {id}} if the Order does not exist. + + ## Examples + + iex> get_order(123) + {:ok, %Order{}} + + iex> get_order(456) + {:error, "Could not find order with id: {id}} + + """ + + # TODO: Add current_user to query + def get_shopping_cart() do + Order + |> where([o], o.status == "cart") + |> Repo.all() + |> case do + [] -> + {:error, "User has no shopping cart"} + + [value] -> + {:ok, value} + + _ -> + {:error, "User has more than one shopping cart"} + end + end + + @doc """ + Creates an order. + + ## Examples + + iex> create_order(%{field: value}) + {:ok, %Order{}} + + iex> create_order(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_order(attrs \\ %{}) do + %Order{} + |> Order.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Creates an order. + + ## Examples + + iex> create_order!(%{field: value}) + %Order{} + + iex> create_order!(%{field: bad_value}) + Error + + """ + def create_order!(attrs \\ %{status: "cart", payment_method: "eos", total: 0}) do + %Order{} + |> Order.changeset(attrs) + |> Repo.insert!() + end + + @doc """ + Updates a order. + + ## Examples + + iex> update_order(order, %{field: new_value}) + {:ok, %Order{}} + + iex> update_order(order, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_order(%Order{} = order, attrs) do + order + |> Order.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a order. + + ## Examples + + iex> delete_order(order) + {:ok, %Order{}} + + iex> delete_order(order) + {:error, %Ecto.Changeset{}} + + """ + def delete_order(%Order{} = order) do + Repo.delete(order) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking order changes. + + ## Examples + + iex> change_order(order) + %Ecto.Changeset{data: %Order{}} + + """ + def change_order(%Order{} = order, attrs \\ %{}) do + Order.changeset(order, attrs) + end + + alias Cambiatus.Orders.Item + + @doc """ + Returns the list of items. + + ## Examples + + iex> list_items() + [%Item{}, ...] + + """ + def list_items do + Repo.all(Item) + end + + @doc """ + Gets a single item. + + Returns {:error, "No item exists with the id: {id}} if the Item does not exist. + + ## Examples + + iex> get_item(123) + {:ok, %Item{}} + + iex> get_item(456) + {:error, "Could not find item with id: {id}} + + """ + def get_item(id) do + case Repo.get(Item, id) do + nil -> + {:error, "No item exists with the id: #{id}"} + + val -> + {:ok, val} + end + end + + @doc """ + Gets a single item. + + Raises `Ecto.NoResultsError` if the Item does not exist. + + ## Examples + + iex> get_item!(123) + %Item{} + + iex> get_item!(456) + ** (Ecto.NoResultsError) + + """ + def get_item!(id), do: Repo.get!(Item, id) + + @doc """ + Creates a item. + + ## Examples + + iex> create_item(%{field: value}) + {:ok, %Item{}} + + iex> create_item(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_item(attrs \\ %{}) + + def create_item(%{order_id: id} = attrs) do + %Item{} + |> Item.changeset(attrs) + |> Repo.insert() + end + + def create_item(attrs) do + case get_shopping_cart() do + {:error, "User has no shopping cart"} -> + cart_id = + create_order!() + |> Map.get(:id) + + attrs + |> Map.put(:order_id, cart_id) + |> create_item() + + {:ok, order} -> + attrs + |> Map.put(:order_id, order.id) + |> create_item() + + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Updates a item. + + ## Examples + + iex> update_item(item, %{field: new_value}) + {:ok, %Item{}} + + iex> update_item(item, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_item(%Item{} = item, attrs) do + item + |> Item.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a item. + + ## Examples + + iex> delete_item(item) + {:ok, %Item{}} + + iex> delete_item(item) + {:error, %Ecto.Changeset{}} + + """ + def delete_item(%Item{} = item) do + Repo.delete(item) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking item changes. + + ## Examples + + iex> change_item(item) + %Ecto.Changeset{data: %Item{}} + + """ + def change_item(%Item{} = item, attrs \\ %{}) do + Item.changeset(item, attrs) + end +end diff --git a/lib/cambiatus/orders/item.ex b/lib/cambiatus/orders/item.ex new file mode 100644 index 00000000..b86e965c --- /dev/null +++ b/lib/cambiatus/orders/item.ex @@ -0,0 +1,34 @@ +defmodule Cambiatus.Orders.Item do + use Ecto.Schema + import Ecto.Changeset + + alias Cambiatus.Shop.Product + alias Cambiatus.Accounts.User + alias Cambiatus.Orders.Order + + schema "items" do + field(:units, :integer) + field(:unit_price, :float) + field(:status, :string) + field(:shipping, :string) + + field(:created_block, :integer) + field(:created_tx, :string) + field(:created_eos_account, :string) + + belongs_to(:product, Product) + belongs_to(:order, Order) + + timestamps() + end + + @required_fields ~w(units unit_price status)a + @optional_fields ~w(shipping inserted_at updated_at)a + + @doc false + def changeset(item, attrs) do + item + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/cambiatus/orders/order.ex b/lib/cambiatus/orders/order.ex new file mode 100644 index 00000000..21301849 --- /dev/null +++ b/lib/cambiatus/orders/order.ex @@ -0,0 +1,29 @@ +defmodule Cambiatus.Orders.Order do + use Ecto.Schema + import Ecto.Changeset + + alias Cambiatus.Accounts.User + alias Cambiatus.Orders.Item + + schema "orders" do + field(:payment_method, :string) + field(:total, :float) + field(:status, :string) + + belongs_to(:buyer, User, references: :account, type: :string) + + has_many(:items, Item, on_delete: :delete_all) + + timestamps() + end + + @required_fields ~w(payment_method total status)a + @optional_fields ~w(inserted_at updated_at)a + + @doc false + def changeset(order, attrs) do + order + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/cambiatus/shop/order.ex b/lib/cambiatus/shop/order.ex index 06c99fd6..a11ad4f9 100755 --- a/lib/cambiatus/shop/order.ex +++ b/lib/cambiatus/shop/order.ex @@ -8,16 +8,13 @@ defmodule Cambiatus.Shop.Order do alias Cambiatus.{Accounts.User, Shop.Product} schema "orders" do - field(:amount, :float) - field(:units, :integer) + field(:payment_method, :string) + field(:total, :float) + field(:status, :string) - field(:created_block, :integer) - field(:created_tx, :string) - field(:created_eos_account, :string) + field(:updated_at, :utc_datetime) field(:created_at, :utc_datetime) - belongs_to(:product, Product) - belongs_to(:from, User, references: :account, type: :string) - belongs_to(:to, User, references: :account, type: :string) + belongs_to(:buyer_id, User, references: :account, type: :string) end end diff --git a/priv/repo/migrations/20220803140956_rename_orders_to_items.exs b/priv/repo/migrations/20220803140956_rename_orders_to_items.exs new file mode 100644 index 00000000..2219c47d --- /dev/null +++ b/priv/repo/migrations/20220803140956_rename_orders_to_items.exs @@ -0,0 +1,7 @@ +defmodule Cambiatus.Repo.Migrations.RenameOrdersToItems do + use Ecto.Migration + + def change do + rename(table(:orders), to: table(:items)) + end +end diff --git a/priv/repo/migrations/20220803194923_create_orders.exs b/priv/repo/migrations/20220803194923_create_orders.exs new file mode 100644 index 00000000..e7ca7599 --- /dev/null +++ b/priv/repo/migrations/20220803194923_create_orders.exs @@ -0,0 +1,26 @@ +defmodule Cambiatus.Repo.Migrations.CreateOrders do + use Ecto.Migration + + def change do + create table(:orders) do + add(:buyer_id, references(:users, column: :account, type: :string)) + + add(:payment_method, :payment_method, + default: "eos", + null: false, + comment: "Payment method used, typed with the integrations we got" + ) + + add(:total, :float, comment: "Order total") + + # TODO: Elaborate shipping and status + add(:status, :string, + null: false, + default: "Pending confirmation", + comment: "This is still a placeholder" + ) + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20220803194940_repurpose_items.exs b/priv/repo/migrations/20220803194940_repurpose_items.exs new file mode 100644 index 00000000..415071e9 --- /dev/null +++ b/priv/repo/migrations/20220803194940_repurpose_items.exs @@ -0,0 +1,25 @@ +defmodule Cambiatus.Repo.Migrations.RepurposeItems do + use Ecto.Migration + + def change do + rename(table(:items), :from_id, to: :seller_id) + rename(table(:items), :amount, to: :unit_price) + + # TODO: Transfer "to_id" from items table to orders table as buyer_id + + alter table(:items) do + add(:order_id, references(:orders, on_delete: :delete_all)) + + # TODO: Elaborate shipping and status + add(:shipping, :string, null: true, comment: "This is still a placeholder") + + add(:status, :string, + null: false, + default: "Pending confirmation", + comment: "This is still a placeholder" + ) + + timestamps() + end + end +end diff --git a/test/cambiatus/orders_test.exs b/test/cambiatus/orders_test.exs new file mode 100644 index 00000000..041c548a --- /dev/null +++ b/test/cambiatus/orders_test.exs @@ -0,0 +1,144 @@ +defmodule Cambiatus.OrdersTest do + use Cambiatus.DataCase + + alias Cambiatus.Orders + + describe "orders" do + alias Cambiatus.Orders.Order + + @valid_attrs %{ + payment_method: "eos", + total: 10.2, + status: "cart" + } + @update_attrs %{ + payment_method: "paypal", + total: 560, + status: "pending payment" + } + @invalid_attrs %{ + payment_method: nil, + total: nil, + status: nil + } + + def order_fixture(attrs \\ %{}) do + {:ok, order} = + attrs + |> Enum.into(@valid_attrs) + |> Orders.create_order() + + order + end + + test "list_orders/0 returns all orders" do + order = order_fixture() + assert Orders.list_orders() == [order] + end + + test "get_order!/1 returns the order with given id" do + order = order_fixture() + assert Orders.get_order!(order.id) == order + end + + test "create_order/1 with valid data creates a order" do + assert {:ok, %Order{} = order} = Orders.create_order(@valid_attrs) + end + + test "create_order/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Orders.create_order(@invalid_attrs) + end + + test "update_order/2 with valid data updates the order" do + order = order_fixture() + assert {:ok, %Order{} = order} = Orders.update_order(order, @update_attrs) + end + + test "update_order/2 with invalid data returns error changeset" do + order = order_fixture() + assert {:error, %Ecto.Changeset{}} = Orders.update_order(order, @invalid_attrs) + assert order == Orders.get_order!(order.id) + end + + test "delete_order/1 deletes the order" do + order = order_fixture() + assert {:ok, %Order{}} = Orders.delete_order(order) + assert_raise Ecto.NoResultsError, fn -> Orders.get_order!(order.id) end + end + + test "change_order/1 returns a order changeset" do + order = order_fixture() + assert %Ecto.Changeset{} = Orders.change_order(order) + end + end + + describe "items" do + alias Cambiatus.Orders.Item + + @valid_attrs %{ + units: 2, + unit_price: 5.2, + status: "pending" + } + @update_attrs %{ + units: 60, + unit_price: 2, + status: "in transport", + shipping: "chariot" + } + @invalid_attrs %{ + units: nil, + unit_price: nil, + status: nil + } + + def item_fixture(attrs \\ %{}) do + {:ok, item} = + attrs + |> Enum.into(@valid_attrs) + |> Orders.create_item() + + item + end + + test "list_items/0 returns all items" do + item = item_fixture() + assert Orders.list_items() == [item] + end + + test "get_item!/1 returns the item with given id" do + item = item_fixture() + assert Orders.get_item!(item.id) == item + end + + test "create_item/1 with valid data creates a item" do + assert {:ok, %Item{} = item} = Orders.create_item(@valid_attrs) + end + + test "create_item/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Orders.create_item(@invalid_attrs) + end + + test "update_item/2 with valid data updates the item" do + item = item_fixture() + assert {:ok, %Item{} = item} = Orders.update_item(item, @update_attrs) + end + + test "update_item/2 with invalid data returns error changeset" do + item = item_fixture() + assert {:error, %Ecto.Changeset{}} = Orders.update_item(item, @invalid_attrs) + assert item == Orders.get_item!(item.id) + end + + test "delete_item/1 deletes the item" do + item = item_fixture() + assert {:ok, %Item{}} = Orders.delete_item(item) + assert_raise Ecto.NoResultsError, fn -> Orders.get_item!(item.id) end + end + + test "change_item/1 returns a item changeset" do + item = item_fixture() + assert %Ecto.Changeset{} = Orders.change_item(item) + end + end +end From 5c1c4f36d8bb59295b1610ab10bcc2c03d3ed3f5 Mon Sep 17 00:00:00 2001 From: Matheus da Cunha Buss Date: Thu, 4 Aug 2022 19:41:04 -0300 Subject: [PATCH 2/7] Created order and item factories --- test/support/factory.ex | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/support/factory.ex b/test/support/factory.ex index 9c5ac1fc..6f353fe3 100755 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -44,6 +44,11 @@ defmodule Cambiatus.Factory do PushSubscription } + alias Cambiatus.Orders.{ + Order, + Item + } + alias Cambiatus.Payments.Contribution alias Cambiatus.Repo alias Cambiatus.Shop.{Category, Product, ProductImage, ProductCategory} @@ -432,4 +437,24 @@ defmodule Cambiatus.Factory do category: build(:category) } end + + def order_factory do + %Order{ + payment_method: Enum.random([:paypal, :bitcoin, :ethereum, :eos]), + total: :rand.uniform(), + status: "pending", + buyer: build(:user) + } + end + + def item_factory do + %Item{ + units: Enum.random(1..10), + unit_price: Enum.random(1..50), + status: "cart", + shipping: "chariot", + product: build(:product), + order: build(:order) + } + end end From 13f778f6aab31bc5abd350c046e9afbce6af2de1 Mon Sep 17 00:00:00 2001 From: Matheus da Cunha Buss Date: Thu, 11 Aug 2022 10:01:56 -0300 Subject: [PATCH 3/7] Created shopping cart and vinculated order to buyer --- lib/cambiatus/orders.ex | 29 +++++++------- lib/cambiatus/orders/item.ex | 20 +++++++++- lib/cambiatus/orders/order.ex | 18 ++++++++- test/cambiatus/orders_test.exs | 69 +++++++++++++++++++++++++--------- 4 files changed, 101 insertions(+), 35 deletions(-) diff --git a/lib/cambiatus/orders.ex b/lib/cambiatus/orders.ex index 76625309..68ef1826 100644 --- a/lib/cambiatus/orders.ex +++ b/lib/cambiatus/orders.ex @@ -7,6 +7,7 @@ defmodule Cambiatus.Orders do alias Cambiatus.Repo alias Cambiatus.Orders.Order + alias Cambiatus.Shop.Product @doc """ Returns the list of orders. @@ -77,9 +78,11 @@ defmodule Cambiatus.Orders do """ # TODO: Add current_user to query - def get_shopping_cart() do + def get_shopping_cart(buyer) do Order |> where([o], o.status == "cart") + |> join(:left, [o], b in assoc(o, :buyer)) + |> where([o, b], b.account == ^buyer.account) |> Repo.all() |> case do [] -> @@ -130,8 +133,8 @@ defmodule Cambiatus.Orders do end @doc """ - Updates a order. - + Updates an order. + ## Examples iex> update_order(order, %{field: new_value}) @@ -245,21 +248,19 @@ defmodule Cambiatus.Orders do """ def create_item(attrs \\ %{}) - def create_item(%{order_id: id} = attrs) do + def create_item(%{order_id: _id} = attrs) do %Item{} |> Item.changeset(attrs) |> Repo.insert() end - def create_item(attrs) do - case get_shopping_cart() do + def create_item(%{buyer: buyer} = attrs) do + case get_shopping_cart(buyer) do {:error, "User has no shopping cart"} -> - cart_id = - create_order!() - |> Map.get(:id) + cart = create_order!() attrs - |> Map.put(:order_id, cart_id) + |> Map.put(:order_id, cart.id) |> create_item() {:ok, order} -> @@ -273,8 +274,8 @@ defmodule Cambiatus.Orders do end @doc """ - Updates a item. - + Updates an item. + ## Examples iex> update_item(item, %{field: new_value}) @@ -291,8 +292,8 @@ defmodule Cambiatus.Orders do end @doc """ - Deletes a item. - + Deletes an item. + ## Examples iex> delete_item(item) diff --git a/lib/cambiatus/orders/item.ex b/lib/cambiatus/orders/item.ex index b86e965c..42fe4958 100644 --- a/lib/cambiatus/orders/item.ex +++ b/lib/cambiatus/orders/item.ex @@ -5,6 +5,8 @@ defmodule Cambiatus.Orders.Item do alias Cambiatus.Shop.Product alias Cambiatus.Accounts.User alias Cambiatus.Orders.Order + alias Cambiatus.Orders + alias Cambiatus.Repo schema "items" do field(:units, :integer) @@ -23,7 +25,7 @@ defmodule Cambiatus.Orders.Item do end @required_fields ~w(units unit_price status)a - @optional_fields ~w(shipping inserted_at updated_at)a + @optional_fields ~w(shipping inserted_at updated_at product_id order_id)a @doc false def changeset(item, attrs) do @@ -31,4 +33,20 @@ defmodule Cambiatus.Orders.Item do |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) end + + def validate_item_stock(%{id: id} = changeset) do + with {:ok, item} <- Orders.get_item(id), + item <- Repo.preload(item, :product), + product <- Map.get(item, :product) do + case product.track_stock do + false -> + changeset + + true -> + if product.units >= item.units, + do: changeset, + else: add_error(changeset, :units, "Not enough product in stock") + end + end + end end diff --git a/lib/cambiatus/orders/order.ex b/lib/cambiatus/orders/order.ex index 21301849..36eba124 100644 --- a/lib/cambiatus/orders/order.ex +++ b/lib/cambiatus/orders/order.ex @@ -4,9 +4,14 @@ defmodule Cambiatus.Orders.Order do alias Cambiatus.Accounts.User alias Cambiatus.Orders.Item + alias Cambiatus.Orders schema "orders" do - field(:payment_method, :string) + field(:payment_method, Ecto.Enum, + values: [:paypal, :bitcoin, :ethereum, :eos], + default: :paypal + ) + field(:total, :float) field(:status, :string) @@ -18,12 +23,21 @@ defmodule Cambiatus.Orders.Order do end @required_fields ~w(payment_method total status)a - @optional_fields ~w(inserted_at updated_at)a + @optional_fields ~w(inserted_at updated_at buyer_id)a @doc false def changeset(order, attrs) do order |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) + |> validate_checkout() + end + + def validate_checkout(changeset) do + with order_id <- get_field(changeset, :id), + {:ok, order} <- Orders.get_order(order_id) do + if Map.get(order, :status) == "cart" and get_field(changeset, :status) == "checkout" do + end + end end end diff --git a/test/cambiatus/orders_test.exs b/test/cambiatus/orders_test.exs index 041c548a..38a219ca 100644 --- a/test/cambiatus/orders_test.exs +++ b/test/cambiatus/orders_test.exs @@ -41,7 +41,7 @@ defmodule Cambiatus.OrdersTest do assert Orders.get_order!(order.id) == order end - test "create_order/1 with valid data creates a order" do + test "create_order/1 with valid data creates an order" do assert {:ok, %Order{} = order} = Orders.create_order(@valid_attrs) end @@ -66,10 +66,28 @@ defmodule Cambiatus.OrdersTest do assert_raise Ecto.NoResultsError, fn -> Orders.get_order!(order.id) end end - test "change_order/1 returns a order changeset" do + test "change_order/1 returns an order changeset" do order = order_fixture() assert %Ecto.Changeset{} = Orders.change_order(order) end + + test "deleting an order deletes all items associated to it" do + order = insert(:order) + item1 = insert(:item, %{order: order}) + item2 = insert(:item, %{order: order}) + item3 = insert(:item, %{order: order}) + + order = Repo.preload(order, :items) + + assert Enum.count(order.items) == 3 + + Orders.delete_order(order) + + assert Orders.get_order(order.id) == {:error, "No order exists with the id: #{order.id}"} + assert Orders.get_item(item1.id) == {:error, "No item exists with the id: #{item1.id}"} + assert Orders.get_item(item2.id) == {:error, "No item exists with the id: #{item2.id}"} + assert Orders.get_item(item3.id) == {:error, "No item exists with the id: #{item3.id}"} + end end describe "items" do @@ -101,44 +119,59 @@ defmodule Cambiatus.OrdersTest do item end - test "list_items/0 returns all items" do - item = item_fixture() + setup do + %{user: insert(:user)} + end + + test "list_items/0 returns all items", %{user: user} do + item = item_fixture(%{buyer: user}) assert Orders.list_items() == [item] end - test "get_item!/1 returns the item with given id" do - item = item_fixture() + test "get_item!/1 returns the item with given id", %{user: user} do + item = item_fixture(%{buyer: user}) assert Orders.get_item!(item.id) == item end - test "create_item/1 with valid data creates a item" do - assert {:ok, %Item{} = item} = Orders.create_item(@valid_attrs) + test "create_item/1 with valid data creates an item", %{user: user} do + assert {:ok, %Item{} = item} = Orders.create_item(Map.merge(@valid_attrs, %{buyer: user})) end - test "create_item/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Orders.create_item(@invalid_attrs) + test "create_item/1 with invalid data returns error changeset", %{user: user} do + assert {:error, %Ecto.Changeset{}} = + Orders.create_item(Map.merge(@invalid_attrs, %{buyer: user})) end - test "update_item/2 with valid data updates the item" do - item = item_fixture() + test "update_item/2 with valid data updates the item", %{user: user} do + item = item_fixture(%{buyer: user}) assert {:ok, %Item{} = item} = Orders.update_item(item, @update_attrs) end - test "update_item/2 with invalid data returns error changeset" do - item = item_fixture() + test "update_item/2 with invalid data returns error changeset", %{user: user} do + item = item_fixture(%{buyer: user}) assert {:error, %Ecto.Changeset{}} = Orders.update_item(item, @invalid_attrs) assert item == Orders.get_item!(item.id) end - test "delete_item/1 deletes the item" do - item = item_fixture() + test "delete_item/1 deletes the item", %{user: user} do + item = item_fixture(%{buyer: user}) assert {:ok, %Item{}} = Orders.delete_item(item) assert_raise Ecto.NoResultsError, fn -> Orders.get_item!(item.id) end end - test "change_item/1 returns a item changeset" do - item = item_fixture() + test "change_item/1 returns an item changeset", %{user: user} do + item = item_fixture(%{buyer: user}) assert %Ecto.Changeset{} = Orders.change_item(item) end + + test "add item to existing cart", %{user: user} do + cart = insert(:order, %{status: "cart", buyer: user}) + + item = + item_fixture(%{buyer: user}) + |> Repo.preload(:order) + + assert item.order.id == cart.id + end end end From f5db0520e1921470af646407468dbb80fb2e41b0 Mon Sep 17 00:00:00 2001 From: Matheus Buss Date: Mon, 15 Aug 2022 15:27:50 -0300 Subject: [PATCH 4/7] Added validation to check if order has items --- lib/cambiatus/orders.ex | 18 ++++++++++++------ lib/cambiatus/orders/item.ex | 1 - lib/cambiatus/orders/order.ex | 21 +++++++++++++++------ lib/cambiatus/shop/order.ex | 2 +- test/cambiatus/orders_test.exs | 17 +++++++++++++++++ 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/lib/cambiatus/orders.ex b/lib/cambiatus/orders.ex index 68ef1826..52c78a00 100644 --- a/lib/cambiatus/orders.ex +++ b/lib/cambiatus/orders.ex @@ -7,7 +7,6 @@ defmodule Cambiatus.Orders do alias Cambiatus.Repo alias Cambiatus.Orders.Order - alias Cambiatus.Shop.Product @doc """ Returns the list of orders. @@ -134,7 +133,7 @@ defmodule Cambiatus.Orders do @doc """ Updates an order. - + ## Examples iex> update_order(order, %{field: new_value}) @@ -151,7 +150,7 @@ defmodule Cambiatus.Orders do end @doc """ - Deletes a order. + Deletes an order. ## Examples @@ -179,6 +178,13 @@ defmodule Cambiatus.Orders do Order.changeset(order, attrs) end + def has_items?(%Order{} = order) do + order + |> Repo.preload(:items) + |> Map.get(:items) + |> Enum.any?() + end + alias Cambiatus.Orders.Item @doc """ @@ -235,7 +241,7 @@ defmodule Cambiatus.Orders do def get_item!(id), do: Repo.get!(Item, id) @doc """ - Creates a item. + Creates an item. ## Examples @@ -275,7 +281,7 @@ defmodule Cambiatus.Orders do @doc """ Updates an item. - + ## Examples iex> update_item(item, %{field: new_value}) @@ -293,7 +299,7 @@ defmodule Cambiatus.Orders do @doc """ Deletes an item. - + ## Examples iex> delete_item(item) diff --git a/lib/cambiatus/orders/item.ex b/lib/cambiatus/orders/item.ex index 42fe4958..6ca7a930 100644 --- a/lib/cambiatus/orders/item.ex +++ b/lib/cambiatus/orders/item.ex @@ -3,7 +3,6 @@ defmodule Cambiatus.Orders.Item do import Ecto.Changeset alias Cambiatus.Shop.Product - alias Cambiatus.Accounts.User alias Cambiatus.Orders.Order alias Cambiatus.Orders alias Cambiatus.Repo diff --git a/lib/cambiatus/orders/order.ex b/lib/cambiatus/orders/order.ex index 36eba124..2f2027c0 100644 --- a/lib/cambiatus/orders/order.ex +++ b/lib/cambiatus/orders/order.ex @@ -30,14 +30,23 @@ defmodule Cambiatus.Orders.Order do order |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - |> validate_checkout() + |> validate_checkout(order) end - def validate_checkout(changeset) do - with order_id <- get_field(changeset, :id), - {:ok, order} <- Orders.get_order(order_id) do - if Map.get(order, :status) == "cart" and get_field(changeset, :status) == "checkout" do - end + def validate_checkout(changeset, order) do + if Map.get(order, :status) == "cart" and get_field(changeset, :status) == "checkout" do + changeset + |> order_has_items(order) + else + changeset + end + end + + def order_has_items(changeset, order) do + if Orders.has_items?(order) do + changeset + else + add_error(changeset, :items, "Orders has no items") end end end diff --git a/lib/cambiatus/shop/order.ex b/lib/cambiatus/shop/order.ex index a11ad4f9..a7b9b81a 100755 --- a/lib/cambiatus/shop/order.ex +++ b/lib/cambiatus/shop/order.ex @@ -5,7 +5,7 @@ defmodule Cambiatus.Shop.Order do use Ecto.Schema - alias Cambiatus.{Accounts.User, Shop.Product} + alias Cambiatus.Accounts.User schema "orders" do field(:payment_method, :string) diff --git a/test/cambiatus/orders_test.exs b/test/cambiatus/orders_test.exs index 38a219ca..1c63399a 100644 --- a/test/cambiatus/orders_test.exs +++ b/test/cambiatus/orders_test.exs @@ -88,6 +88,23 @@ defmodule Cambiatus.OrdersTest do assert Orders.get_item(item2.id) == {:error, "No item exists with the id: #{item2.id}"} assert Orders.get_item(item3.id) == {:error, "No item exists with the id: #{item3.id}"} end + + test "valid checkout" do + order = insert(:order, status: "cart") + item = insert(:item, %{order: order}) + + order = Repo.preload(order, :items) + + assert {:ok, %{order | status: "checkout"}} == + Orders.update_order(order, %{status: "checkout"}) + end + + test "refute checkout without items" do + order = insert(:order, %{items: [], status: "cart"}) + + assert {:error, changeset} = Orders.update_order(order, %{status: "checkout"}) + assert [items: {"Orders has no items", []}] == changeset.errors + end end describe "items" do From 7116ab41e99f2ed6f8b966b133d7cdf8440b7e61 Mon Sep 17 00:00:00 2001 From: Matheus Buss Date: Mon, 15 Aug 2022 17:28:39 -0300 Subject: [PATCH 5/7] Added item validation to orders --- lib/cambiatus/orders/item.ex | 26 +++++++++++++++++--------- lib/cambiatus/orders/order.ex | 24 ++++++++++++++++++++++++ lib/cambiatus/shop/product.ex | 1 + test/cambiatus/orders_test.exs | 17 ++++++++++++++++- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/lib/cambiatus/orders/item.ex b/lib/cambiatus/orders/item.ex index 6ca7a930..47ac2dc5 100644 --- a/lib/cambiatus/orders/item.ex +++ b/lib/cambiatus/orders/item.ex @@ -33,18 +33,26 @@ defmodule Cambiatus.Orders.Item do |> validate_required(@required_fields) end - def validate_item_stock(%{id: id} = changeset) do - with {:ok, item} <- Orders.get_item(id), - item <- Repo.preload(item, :product), + @spec changeset(Changeset.t(), Item.t()) :: Ecto.Changeset.t() + def validate_item(changeset, item \\ %{}) do + item = item || Orders.get_item!(fetch_change!(changeset, :id)) + + changeset + |> validate_item_stock(item) + end + + def validate_item_stock(changeset, item) do + with item <- Repo.preload(item, :product), product <- Map.get(item, :product) do - case product.track_stock do - false -> + case {product.track_stock, product.units >= item.units} do + {true, true} -> changeset - true -> - if product.units >= item.units, - do: changeset, - else: add_error(changeset, :units, "Not enough product in stock") + {true, false} -> + add_error(changeset, :units, "Not enough product in stock") + + {false, _} -> + changeset end end end diff --git a/lib/cambiatus/orders/order.ex b/lib/cambiatus/orders/order.ex index 2f2027c0..dafb984c 100644 --- a/lib/cambiatus/orders/order.ex +++ b/lib/cambiatus/orders/order.ex @@ -2,6 +2,8 @@ defmodule Cambiatus.Orders.Order do use Ecto.Schema import Ecto.Changeset + alias Cambiatus.Repo + alias Cambiatus.Accounts.User alias Cambiatus.Orders.Item alias Cambiatus.Orders @@ -33,10 +35,12 @@ defmodule Cambiatus.Orders.Order do |> validate_checkout(order) end + # TODO: Validate other fields(shipping, payment, total) def validate_checkout(changeset, order) do if Map.get(order, :status) == "cart" and get_field(changeset, :status) == "checkout" do changeset |> order_has_items(order) + |> validate_items(order) else changeset end @@ -49,4 +53,24 @@ defmodule Cambiatus.Orders.Order do add_error(changeset, :items, "Orders has no items") end end + + def validate_items(changeset, order) do + order + |> Repo.preload(:items) + |> Map.get(:items) + |> Enum.reduce(changeset, fn item, changeset -> + case Item.validate_item(%Ecto.Changeset{errors: []}, item) do + %{errors: []} -> + changeset + + error -> + add_error( + changeset, + :items, + "Item with id #{item.id} is invalid", + reason: error.errors + ) + end + end) + end end diff --git a/lib/cambiatus/shop/product.ex b/lib/cambiatus/shop/product.ex index 33999a84..125bca8f 100755 --- a/lib/cambiatus/shop/product.ex +++ b/lib/cambiatus/shop/product.ex @@ -36,6 +36,7 @@ defmodule Cambiatus.Shop.Product do has_many(:product_categories, ProductCategory, on_replace: :delete) has_many(:categories, through: [:product_categories, :category]) + has_many(:items, Cambiatus.Orders.Item) end @required_fields ~w(community_id title description price track_stock)a diff --git a/test/cambiatus/orders_test.exs b/test/cambiatus/orders_test.exs index 1c63399a..37b1474b 100644 --- a/test/cambiatus/orders_test.exs +++ b/test/cambiatus/orders_test.exs @@ -91,7 +91,8 @@ defmodule Cambiatus.OrdersTest do test "valid checkout" do order = insert(:order, status: "cart") - item = insert(:item, %{order: order}) + product = insert(:product, %{track_stock: true, units: 50}) + item = insert(:item, %{order: order, product: product, units: 2}) order = Repo.preload(order, :items) @@ -105,6 +106,20 @@ defmodule Cambiatus.OrdersTest do assert {:error, changeset} = Orders.update_order(order, %{status: "checkout"}) assert [items: {"Orders has no items", []}] == changeset.errors end + + test "refute checkout with an item out of stock" do + order = insert(:order, %{status: "cart"}) + product = insert(:product, %{track_stock: true, units: 4}) + item = insert(:item, %{order: order, product: product, units: 6}) + + assert {:error, changeset} = Orders.update_order(order, %{status: "checkout"}) + + assert [ + items: + {"Item with id #{item.id} is invalid", + [{:reason, [units: {"Not enough product in stock", []}]}]} + ] == changeset.errors + end end describe "items" do From 66c77b194097b8334e92626634ee508de50a2460 Mon Sep 17 00:00:00 2001 From: Matheus Buss Date: Mon, 15 Aug 2022 17:59:49 -0300 Subject: [PATCH 6/7] Moved items and orders to the shop context --- lib/cambiatus/orders.ex | 4 +- lib/cambiatus/orders/order.ex | 76 ------ lib/cambiatus/shop.ex | 324 ++++++++++++++++++++++++- lib/cambiatus/{orders => shop}/item.ex | 8 +- lib/cambiatus/shop/order.ex | 72 +++++- lib/cambiatus/shop/product.ex | 8 +- test/cambiatus/orders_test.exs | 209 ---------------- test/cambiatus/shop_test.exs | 204 ++++++++++++++++ test/support/factory.ex | 7 +- 9 files changed, 602 insertions(+), 310 deletions(-) delete mode 100644 lib/cambiatus/orders/order.ex rename lib/cambiatus/{orders => shop}/item.ex (84%) mode change 100755 => 100644 lib/cambiatus/shop/order.ex delete mode 100644 test/cambiatus/orders_test.exs diff --git a/lib/cambiatus/orders.ex b/lib/cambiatus/orders.ex index 52c78a00..de1f3d25 100644 --- a/lib/cambiatus/orders.ex +++ b/lib/cambiatus/orders.ex @@ -6,7 +6,7 @@ defmodule Cambiatus.Orders do import Ecto.Query, warn: false alias Cambiatus.Repo - alias Cambiatus.Orders.Order + alias Cambiatus.Shop.Order @doc """ Returns the list of orders. @@ -185,7 +185,7 @@ defmodule Cambiatus.Orders do |> Enum.any?() end - alias Cambiatus.Orders.Item + alias Cambiatus.Shop.Item @doc """ Returns the list of items. diff --git a/lib/cambiatus/orders/order.ex b/lib/cambiatus/orders/order.ex deleted file mode 100644 index dafb984c..00000000 --- a/lib/cambiatus/orders/order.ex +++ /dev/null @@ -1,76 +0,0 @@ -defmodule Cambiatus.Orders.Order do - use Ecto.Schema - import Ecto.Changeset - - alias Cambiatus.Repo - - alias Cambiatus.Accounts.User - alias Cambiatus.Orders.Item - alias Cambiatus.Orders - - schema "orders" do - field(:payment_method, Ecto.Enum, - values: [:paypal, :bitcoin, :ethereum, :eos], - default: :paypal - ) - - field(:total, :float) - field(:status, :string) - - belongs_to(:buyer, User, references: :account, type: :string) - - has_many(:items, Item, on_delete: :delete_all) - - timestamps() - end - - @required_fields ~w(payment_method total status)a - @optional_fields ~w(inserted_at updated_at buyer_id)a - - @doc false - def changeset(order, attrs) do - order - |> cast(attrs, @required_fields ++ @optional_fields) - |> validate_required(@required_fields) - |> validate_checkout(order) - end - - # TODO: Validate other fields(shipping, payment, total) - def validate_checkout(changeset, order) do - if Map.get(order, :status) == "cart" and get_field(changeset, :status) == "checkout" do - changeset - |> order_has_items(order) - |> validate_items(order) - else - changeset - end - end - - def order_has_items(changeset, order) do - if Orders.has_items?(order) do - changeset - else - add_error(changeset, :items, "Orders has no items") - end - end - - def validate_items(changeset, order) do - order - |> Repo.preload(:items) - |> Map.get(:items) - |> Enum.reduce(changeset, fn item, changeset -> - case Item.validate_item(%Ecto.Changeset{errors: []}, item) do - %{errors: []} -> - changeset - - error -> - add_error( - changeset, - :items, - "Item with id #{item.id} is invalid", - reason: error.errors - ) - end - end) - end -end diff --git a/lib/cambiatus/shop.ex b/lib/cambiatus/shop.ex index 9f2d8ed2..671d80eb 100644 --- a/lib/cambiatus/shop.ex +++ b/lib/cambiatus/shop.ex @@ -178,10 +178,6 @@ defmodule Cambiatus.Shop do |> Repo.aggregate(:count, :id) end - def get_order(id) do - Repo.get(Order, id) - end - @doc """ Returns the list of categories. @@ -429,4 +425,324 @@ defmodule Cambiatus.Shop do def change_category(%Category{} = category, attrs \\ %{}) do Category.changeset(category, attrs) end + + alias Cambiatus.Shop.Order + + @doc """ + Returns the list of orders. + + ## Examples + + iex> list_orders() + [%Order{}, ...] + + """ + def list_orders do + Repo.all(Order) + end + + @doc """ + Gets a single order. + + Returns {:error, "No order exists with the id: {id}} if the Order does not exist. + + ## Examples + + iex> get_order(123) + {:ok, %Order{}} + + iex> get_order(456) + {:error, "Could not find order with id: {id}} + + """ + def get_order(id) do + case Repo.get(Order, id) do + nil -> + {:error, "No order exists with the id: #{id}"} + + val -> + {:ok, val} + end + end + + @doc """ + Gets a single order. + + Raises `Ecto.NoResultsError` if the Order does not exist. + + ## Examples + + iex> get_order!(123) + %Order{} + + iex> get_order!(456) + ** (Ecto.NoResultsError) + + """ + def get_order!(id), do: Repo.get!(Order, id) + + @doc """ + Gets a single order. + + Returns {:error, "No order exists with the id: {id}} if the Order does not exist. + + ## Examples + + iex> get_order(123) + {:ok, %Order{}} + + iex> get_order(456) + {:error, "Could not find order with id: {id}} + + """ + + # TODO: Add current_user to query + def get_shopping_cart(buyer) do + Order + |> where([o], o.status == "cart") + |> join(:left, [o], b in assoc(o, :buyer)) + |> where([o, b], b.account == ^buyer.account) + |> Repo.all() + |> case do + [] -> + {:error, "User has no shopping cart"} + + [value] -> + {:ok, value} + + _ -> + {:error, "User has more than one shopping cart"} + end + end + + @doc """ + Creates an order. + + ## Examples + + iex> create_order(%{field: value}) + {:ok, %Order{}} + + iex> create_order(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_order(attrs \\ %{}) do + %Order{} + |> Order.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Creates an order. + + ## Examples + + iex> create_order!(%{field: value}) + %Order{} + + iex> create_order!(%{field: bad_value}) + Error + + """ + def create_order!(attrs \\ %{status: "cart", payment_method: "eos", total: 0}) do + %Order{} + |> Order.changeset(attrs) + |> Repo.insert!() + end + + @doc """ + Updates an order. + + ## Examples + + iex> update_order(order, %{field: new_value}) + {:ok, %Order{}} + + iex> update_order(order, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_order(%Order{} = order, attrs) do + order + |> Order.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes an order. + + ## Examples + + iex> delete_order(order) + {:ok, %Order{}} + + iex> delete_order(order) + {:error, %Ecto.Changeset{}} + + """ + def delete_order(%Order{} = order) do + Repo.delete(order) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking order changes. + + ## Examples + + iex> change_order(order) + %Ecto.Changeset{data: %Order{}} + + """ + def change_order(%Order{} = order, attrs \\ %{}) do + Order.changeset(order, attrs) + end + + def has_items?(%Order{} = order) do + order + |> Repo.preload(:items) + |> Map.get(:items) + |> Enum.any?() + end + + alias Cambiatus.Shop.Item + + @doc """ + Returns the list of items. + + ## Examples + + iex> list_items() + [%Item{}, ...] + + """ + def list_items do + Repo.all(Item) + end + + @doc """ + Gets a single item. + + Returns {:error, "No item exists with the id: {id}} if the Item does not exist. + + ## Examples + + iex> get_item(123) + {:ok, %Item{}} + + iex> get_item(456) + {:error, "Could not find item with id: {id}} + + """ + def get_item(id) do + case Repo.get(Item, id) do + nil -> + {:error, "No item exists with the id: #{id}"} + + val -> + {:ok, val} + end + end + + @doc """ + Gets a single item. + + Raises `Ecto.NoResultsError` if the Item does not exist. + + ## Examples + + iex> get_item!(123) + %Item{} + + iex> get_item!(456) + ** (Ecto.NoResultsError) + + """ + def get_item!(id), do: Repo.get!(Item, id) + + @doc """ + Creates an item. + + ## Examples + + iex> create_item(%{field: value}) + {:ok, %Item{}} + + iex> create_item(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_item(attrs \\ %{}) + + def create_item(%{order_id: _id} = attrs) do + %Item{} + |> Item.changeset(attrs) + |> Repo.insert() + end + + def create_item(%{buyer: buyer} = attrs) do + case get_shopping_cart(buyer) do + {:error, "User has no shopping cart"} -> + cart = create_order!() + + attrs + |> Map.put(:order_id, cart.id) + |> create_item() + + {:ok, order} -> + attrs + |> Map.put(:order_id, order.id) + |> create_item() + + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Updates an item. + + ## Examples + + iex> update_item(item, %{field: new_value}) + {:ok, %Item{}} + + iex> update_item(item, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_item(%Item{} = item, attrs) do + item + |> Item.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes an item. + + ## Examples + + iex> delete_item(item) + {:ok, %Item{}} + + iex> delete_item(item) + {:error, %Ecto.Changeset{}} + + """ + def delete_item(%Item{} = item) do + Repo.delete(item) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking item changes. + + ## Examples + + iex> change_item(item) + %Ecto.Changeset{data: %Item{}} + + """ + def change_item(%Item{} = item, attrs \\ %{}) do + Item.changeset(item, attrs) + end end diff --git a/lib/cambiatus/orders/item.ex b/lib/cambiatus/shop/item.ex similarity index 84% rename from lib/cambiatus/orders/item.ex rename to lib/cambiatus/shop/item.ex index 47ac2dc5..a0eadf6d 100644 --- a/lib/cambiatus/orders/item.ex +++ b/lib/cambiatus/shop/item.ex @@ -1,9 +1,13 @@ -defmodule Cambiatus.Orders.Item do +defmodule Cambiatus.Shop.Item do + @moduledoc """ + This module holds the data structure that represents an instance of a `Cambiatus.CShop.Item` use it to + build and validate changesets for operating on an item + """ use Ecto.Schema import Ecto.Changeset alias Cambiatus.Shop.Product - alias Cambiatus.Orders.Order + alias Cambiatus.Shop.Order alias Cambiatus.Orders alias Cambiatus.Repo diff --git a/lib/cambiatus/shop/order.ex b/lib/cambiatus/shop/order.ex old mode 100755 new mode 100644 index a7b9b81a..bc7fc283 --- a/lib/cambiatus/shop/order.ex +++ b/lib/cambiatus/shop/order.ex @@ -1,20 +1,80 @@ defmodule Cambiatus.Shop.Order do @moduledoc """ - This module represents an Order. + This module holds the data structure that represents an instance of a `Cambiatus.Shop.Order` use it to + build and validate changesets for operating on an order """ - use Ecto.Schema + import Ecto.Changeset + + alias Cambiatus.Repo alias Cambiatus.Accounts.User + alias Cambiatus.Shop.Item + alias Cambiatus.Orders schema "orders" do - field(:payment_method, :string) + field(:payment_method, Ecto.Enum, + values: [:paypal, :bitcoin, :ethereum, :eos], + default: :paypal + ) + field(:total, :float) field(:status, :string) - field(:updated_at, :utc_datetime) - field(:created_at, :utc_datetime) + belongs_to(:buyer, User, references: :account, type: :string) + + has_many(:items, Item, on_delete: :delete_all) + + timestamps() + end + + @required_fields ~w(payment_method total status)a + @optional_fields ~w(inserted_at updated_at buyer_id)a + + @doc false + def changeset(order, attrs) do + order + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + |> validate_checkout(order) + end + + # TODO: Validate other fields(shipping, payment, total) + def validate_checkout(changeset, order) do + if Map.get(order, :status) == "cart" and get_field(changeset, :status) == "checkout" do + changeset + |> order_has_items(order) + |> validate_items(order) + else + changeset + end + end + + def order_has_items(changeset, order) do + if Orders.has_items?(order) do + changeset + else + add_error(changeset, :items, "Order has no items") + end + end + + def validate_items(changeset, order) do + order + |> Repo.preload(:items) + |> Map.get(:items) + |> Enum.reduce(changeset, fn item, changeset -> + case Item.validate_item(%Ecto.Changeset{errors: []}, item) do + %{errors: []} -> + changeset - belongs_to(:buyer_id, User, references: :account, type: :string) + error -> + add_error( + changeset, + :items, + "Item with id #{item.id} is invalid", + reason: error.errors + ) + end + end) end end diff --git a/lib/cambiatus/shop/product.ex b/lib/cambiatus/shop/product.ex index 125bca8f..22f78699 100755 --- a/lib/cambiatus/shop/product.ex +++ b/lib/cambiatus/shop/product.ex @@ -1,6 +1,6 @@ defmodule Cambiatus.Shop.Product do @moduledoc """ - This module holds the data structure that represents an instance of a `Cambiatus.Commune.Product` use it to + This module holds the data structure that represents an instance of a `Cambiatus.Shop.Product` use it to build and validate changesets for operating on a product """ use Ecto.Schema @@ -11,7 +11,7 @@ defmodule Cambiatus.Shop.Product do alias Cambiatus.{Accounts.User, Repo} alias Cambiatus.Commune.Community alias Cambiatus.Shop - alias Cambiatus.Shop.{Category, Order, Product, ProductCategory, ProductImage} + alias Cambiatus.Shop.{Category, Item, Product, ProductCategory, ProductImage} schema "products" do field(:title, :string) @@ -27,8 +27,6 @@ defmodule Cambiatus.Shop.Product do belongs_to(:creator, User, references: :account, type: :string) belongs_to(:community, Community, references: :symbol, type: :string) - has_many(:orders, Order, foreign_key: :product_id) - has_many(:images, ProductImage, on_replace: :delete, on_delete: :delete_all @@ -36,7 +34,7 @@ defmodule Cambiatus.Shop.Product do has_many(:product_categories, ProductCategory, on_replace: :delete) has_many(:categories, through: [:product_categories, :category]) - has_many(:items, Cambiatus.Orders.Item) + has_many(:items, Item) end @required_fields ~w(community_id title description price track_stock)a diff --git a/test/cambiatus/orders_test.exs b/test/cambiatus/orders_test.exs deleted file mode 100644 index 37b1474b..00000000 --- a/test/cambiatus/orders_test.exs +++ /dev/null @@ -1,209 +0,0 @@ -defmodule Cambiatus.OrdersTest do - use Cambiatus.DataCase - - alias Cambiatus.Orders - - describe "orders" do - alias Cambiatus.Orders.Order - - @valid_attrs %{ - payment_method: "eos", - total: 10.2, - status: "cart" - } - @update_attrs %{ - payment_method: "paypal", - total: 560, - status: "pending payment" - } - @invalid_attrs %{ - payment_method: nil, - total: nil, - status: nil - } - - def order_fixture(attrs \\ %{}) do - {:ok, order} = - attrs - |> Enum.into(@valid_attrs) - |> Orders.create_order() - - order - end - - test "list_orders/0 returns all orders" do - order = order_fixture() - assert Orders.list_orders() == [order] - end - - test "get_order!/1 returns the order with given id" do - order = order_fixture() - assert Orders.get_order!(order.id) == order - end - - test "create_order/1 with valid data creates an order" do - assert {:ok, %Order{} = order} = Orders.create_order(@valid_attrs) - end - - test "create_order/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Orders.create_order(@invalid_attrs) - end - - test "update_order/2 with valid data updates the order" do - order = order_fixture() - assert {:ok, %Order{} = order} = Orders.update_order(order, @update_attrs) - end - - test "update_order/2 with invalid data returns error changeset" do - order = order_fixture() - assert {:error, %Ecto.Changeset{}} = Orders.update_order(order, @invalid_attrs) - assert order == Orders.get_order!(order.id) - end - - test "delete_order/1 deletes the order" do - order = order_fixture() - assert {:ok, %Order{}} = Orders.delete_order(order) - assert_raise Ecto.NoResultsError, fn -> Orders.get_order!(order.id) end - end - - test "change_order/1 returns an order changeset" do - order = order_fixture() - assert %Ecto.Changeset{} = Orders.change_order(order) - end - - test "deleting an order deletes all items associated to it" do - order = insert(:order) - item1 = insert(:item, %{order: order}) - item2 = insert(:item, %{order: order}) - item3 = insert(:item, %{order: order}) - - order = Repo.preload(order, :items) - - assert Enum.count(order.items) == 3 - - Orders.delete_order(order) - - assert Orders.get_order(order.id) == {:error, "No order exists with the id: #{order.id}"} - assert Orders.get_item(item1.id) == {:error, "No item exists with the id: #{item1.id}"} - assert Orders.get_item(item2.id) == {:error, "No item exists with the id: #{item2.id}"} - assert Orders.get_item(item3.id) == {:error, "No item exists with the id: #{item3.id}"} - end - - test "valid checkout" do - order = insert(:order, status: "cart") - product = insert(:product, %{track_stock: true, units: 50}) - item = insert(:item, %{order: order, product: product, units: 2}) - - order = Repo.preload(order, :items) - - assert {:ok, %{order | status: "checkout"}} == - Orders.update_order(order, %{status: "checkout"}) - end - - test "refute checkout without items" do - order = insert(:order, %{items: [], status: "cart"}) - - assert {:error, changeset} = Orders.update_order(order, %{status: "checkout"}) - assert [items: {"Orders has no items", []}] == changeset.errors - end - - test "refute checkout with an item out of stock" do - order = insert(:order, %{status: "cart"}) - product = insert(:product, %{track_stock: true, units: 4}) - item = insert(:item, %{order: order, product: product, units: 6}) - - assert {:error, changeset} = Orders.update_order(order, %{status: "checkout"}) - - assert [ - items: - {"Item with id #{item.id} is invalid", - [{:reason, [units: {"Not enough product in stock", []}]}]} - ] == changeset.errors - end - end - - describe "items" do - alias Cambiatus.Orders.Item - - @valid_attrs %{ - units: 2, - unit_price: 5.2, - status: "pending" - } - @update_attrs %{ - units: 60, - unit_price: 2, - status: "in transport", - shipping: "chariot" - } - @invalid_attrs %{ - units: nil, - unit_price: nil, - status: nil - } - - def item_fixture(attrs \\ %{}) do - {:ok, item} = - attrs - |> Enum.into(@valid_attrs) - |> Orders.create_item() - - item - end - - setup do - %{user: insert(:user)} - end - - test "list_items/0 returns all items", %{user: user} do - item = item_fixture(%{buyer: user}) - assert Orders.list_items() == [item] - end - - test "get_item!/1 returns the item with given id", %{user: user} do - item = item_fixture(%{buyer: user}) - assert Orders.get_item!(item.id) == item - end - - test "create_item/1 with valid data creates an item", %{user: user} do - assert {:ok, %Item{} = item} = Orders.create_item(Map.merge(@valid_attrs, %{buyer: user})) - end - - test "create_item/1 with invalid data returns error changeset", %{user: user} do - assert {:error, %Ecto.Changeset{}} = - Orders.create_item(Map.merge(@invalid_attrs, %{buyer: user})) - end - - test "update_item/2 with valid data updates the item", %{user: user} do - item = item_fixture(%{buyer: user}) - assert {:ok, %Item{} = item} = Orders.update_item(item, @update_attrs) - end - - test "update_item/2 with invalid data returns error changeset", %{user: user} do - item = item_fixture(%{buyer: user}) - assert {:error, %Ecto.Changeset{}} = Orders.update_item(item, @invalid_attrs) - assert item == Orders.get_item!(item.id) - end - - test "delete_item/1 deletes the item", %{user: user} do - item = item_fixture(%{buyer: user}) - assert {:ok, %Item{}} = Orders.delete_item(item) - assert_raise Ecto.NoResultsError, fn -> Orders.get_item!(item.id) end - end - - test "change_item/1 returns an item changeset", %{user: user} do - item = item_fixture(%{buyer: user}) - assert %Ecto.Changeset{} = Orders.change_item(item) - end - - test "add item to existing cart", %{user: user} do - cart = insert(:order, %{status: "cart", buyer: user}) - - item = - item_fixture(%{buyer: user}) - |> Repo.preload(:order) - - assert item.order.id == cart.id - end - end -end diff --git a/test/cambiatus/shop_test.exs b/test/cambiatus/shop_test.exs index d23d41e4..12c078be 100644 --- a/test/cambiatus/shop_test.exs +++ b/test/cambiatus/shop_test.exs @@ -554,4 +554,208 @@ defmodule Cambiatus.ShopTest do Enum.map(updated_root_categories, &Map.take(&1, [:id, :position])) end end + + describe "orders" do + alias Cambiatus.Shop.Order + + @valid_attrs %{ + payment_method: "eos", + total: 10.2, + status: "cart" + } + @update_attrs %{ + payment_method: "paypal", + total: 560, + status: "pending payment" + } + @invalid_attrs %{ + payment_method: nil, + total: nil, + status: nil + } + + def order_fixture(attrs \\ %{}) do + {:ok, order} = + attrs + |> Enum.into(@valid_attrs) + |> Shop.create_order() + + order + end + + test "list_orders/0 returns all orders" do + order = order_fixture() + assert Shop.list_orders() == [order] + end + + test "get_order!/1 returns the order with given id" do + order = order_fixture() + assert Shop.get_order!(order.id) == order + end + + test "create_order/1 with valid data creates an order" do + assert {:ok, %Order{} = order} = Shop.create_order(@valid_attrs) + end + + test "create_order/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Shop.create_order(@invalid_attrs) + end + + test "update_order/2 with valid data updates the order" do + order = order_fixture() + assert {:ok, %Order{} = order} = Shop.update_order(order, @update_attrs) + end + + test "update_order/2 with invalid data returns error changeset" do + order = order_fixture() + assert {:error, %Ecto.Changeset{}} = Shop.update_order(order, @invalid_attrs) + assert order == Shop.get_order!(order.id) + end + + test "delete_order/1 deletes the order" do + order = order_fixture() + assert {:ok, %Order{}} = Shop.delete_order(order) + assert_raise Ecto.NoResultsError, fn -> Shop.get_order!(order.id) end + end + + test "change_order/1 returns an order changeset" do + order = order_fixture() + assert %Ecto.Changeset{} = Shop.change_order(order) + end + + test "deleting an order deletes all items associated to it" do + order = insert(:order) + item1 = insert(:item, %{order: order}) + item2 = insert(:item, %{order: order}) + item3 = insert(:item, %{order: order}) + + order = Repo.preload(order, :items) + + assert Enum.count(order.items) == 3 + + Shop.delete_order(order) + + assert Shop.get_order(order.id) == {:error, "No order exists with the id: #{order.id}"} + assert Shop.get_item(item1.id) == {:error, "No item exists with the id: #{item1.id}"} + assert Shop.get_item(item2.id) == {:error, "No item exists with the id: #{item2.id}"} + assert Shop.get_item(item3.id) == {:error, "No item exists with the id: #{item3.id}"} + end + + test "valid checkout" do + order = insert(:order, status: "cart") + product = insert(:product, %{track_stock: true, units: 50}) + item = insert(:item, %{order: order, product: product, units: 2}) + + order = Repo.preload(order, :items) + + assert {:ok, %{order | status: "checkout"}} == + Shop.update_order(order, %{status: "checkout"}) + end + + test "refute checkout without items" do + order = insert(:order, %{items: [], status: "cart"}) + + assert {:error, changeset} = Shop.update_order(order, %{status: "checkout"}) + assert [items: {"Order has no items", []}] == changeset.errors + end + + test "refute checkout with an item out of stock" do + order = insert(:order, %{status: "cart"}) + product = insert(:product, %{track_stock: true, units: 4}) + item = insert(:item, %{order: order, product: product, units: 6}) + + assert {:error, changeset} = Shop.update_order(order, %{status: "checkout"}) + + assert [ + items: + {"Item with id #{item.id} is invalid", + [{:reason, [units: {"Not enough product in stock", []}]}]} + ] == changeset.errors + end + end + + describe "items" do + alias Cambiatus.Shop.Item + + @valid_attrs %{ + units: 2, + unit_price: 5.2, + status: "pending" + } + @update_attrs %{ + units: 60, + unit_price: 2, + status: "in transport", + shipping: "chariot" + } + @invalid_attrs %{ + units: nil, + unit_price: nil, + status: nil + } + + def item_fixture(attrs \\ %{}) do + {:ok, item} = + attrs + |> Enum.into(@valid_attrs) + |> Shop.create_item() + + item + end + + setup do + %{user: insert(:user)} + end + + test "list_items/0 returns all items", %{user: user} do + item = item_fixture(%{buyer: user}) + assert Shop.list_items() == [item] + end + + test "get_item!/1 returns the item with given id", %{user: user} do + item = item_fixture(%{buyer: user}) + assert Shop.get_item!(item.id) == item + end + + test "create_item/1 with valid data creates an item", %{user: user} do + assert {:ok, %Item{} = item} = Shop.create_item(Map.merge(@valid_attrs, %{buyer: user})) + end + + test "create_item/1 with invalid data returns error changeset", %{user: user} do + assert {:error, %Ecto.Changeset{}} = + Shop.create_item(Map.merge(@invalid_attrs, %{buyer: user})) + end + + test "update_item/2 with valid data updates the item", %{user: user} do + item = item_fixture(%{buyer: user}) + assert {:ok, %Item{} = item} = Shop.update_item(item, @update_attrs) + end + + test "update_item/2 with invalid data returns error changeset", %{user: user} do + item = item_fixture(%{buyer: user}) + assert {:error, %Ecto.Changeset{}} = Shop.update_item(item, @invalid_attrs) + assert item == Shop.get_item!(item.id) + end + + test "delete_item/1 deletes the item", %{user: user} do + item = item_fixture(%{buyer: user}) + assert {:ok, %Item{}} = Shop.delete_item(item) + assert_raise Ecto.NoResultsError, fn -> Shop.get_item!(item.id) end + end + + test "change_item/1 returns an item changeset", %{user: user} do + item = item_fixture(%{buyer: user}) + assert %Ecto.Changeset{} = Shop.change_item(item) + end + + test "add item to existing cart", %{user: user} do + cart = insert(:order, %{status: "cart", buyer: user}) + + item = + item_fixture(%{buyer: user}) + |> Repo.preload(:order) + + assert item.order.id == cart.id + end + end end diff --git a/test/support/factory.ex b/test/support/factory.ex index 6f353fe3..a256dda8 100755 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -44,14 +44,9 @@ defmodule Cambiatus.Factory do PushSubscription } - alias Cambiatus.Orders.{ - Order, - Item - } - alias Cambiatus.Payments.Contribution alias Cambiatus.Repo - alias Cambiatus.Shop.{Category, Product, ProductImage, ProductCategory} + alias Cambiatus.Shop.{Category, Product, ProductImage, ProductCategory, Order, Item} alias Cambiatus.Social.{ News, From 3f1bff6241bdfadeb7bae1119f8509a76ba07ad5 Mon Sep 17 00:00:00 2001 From: Matheus Buss Date: Tue, 16 Aug 2022 10:03:55 -0300 Subject: [PATCH 7/7] Removed add_current user to query TODO's --- lib/cambiatus/orders.ex | 1 - lib/cambiatus/shop.ex | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/cambiatus/orders.ex b/lib/cambiatus/orders.ex index de1f3d25..fbb5c84c 100644 --- a/lib/cambiatus/orders.ex +++ b/lib/cambiatus/orders.ex @@ -76,7 +76,6 @@ defmodule Cambiatus.Orders do """ - # TODO: Add current_user to query def get_shopping_cart(buyer) do Order |> where([o], o.status == "cart") diff --git a/lib/cambiatus/shop.ex b/lib/cambiatus/shop.ex index 671d80eb..74b2f536 100644 --- a/lib/cambiatus/shop.ex +++ b/lib/cambiatus/shop.ex @@ -496,7 +496,6 @@ defmodule Cambiatus.Shop do """ - # TODO: Add current_user to query def get_shopping_cart(buyer) do Order |> where([o], o.status == "cart")