defmodule Cannery.Ammo do @moduledoc """ The Ammo context. """ import CanneryWeb.Gettext import Ecto.Query, warn: false alias Cannery.{Accounts.User, Containers, Repo} alias Cannery.Containers.{Container, ContainerTag, Tag} alias Cannery.{ActivityLog, ActivityLog.ShotRecord} alias Cannery.Ammo.{Pack, Type} alias Ecto.{Changeset, Queryable} @pack_create_limit 10_000 @pack_preloads [:type] @type_preloads [:packs] @type list_types_option :: {:search, String.t() | nil} | {:class, Type.class() | :all} @type list_types_options :: [list_types_option()] @doc """ Returns the list of types. ## Examples iex> list_types(%User{id: 123}) [%Type{}, ...] iex> list_types(%User{id: 123}, search: "cool", class: :shotgun) [%Type{name: "My cool type", class: :shotgun}, ...] """ @spec list_types(User.t()) :: [Type.t()] @spec list_types(User.t(), list_types_options()) :: [Type.t()] def list_types(%User{id: user_id}, opts \\ []) do from(t in Type, as: :t, where: t.user_id == ^user_id, preload: ^@type_preloads ) |> list_types_class(Keyword.get(opts, :class, :all)) |> list_types_search(Keyword.get(opts, :search)) |> Repo.all() end @spec list_types_search(Queryable.t(), search :: String.t() | nil) :: Queryable.t() defp list_types_search(query, search) when search in ["", nil], do: query |> order_by([t: t], t.name) defp list_types_search(query, search) when search |> is_binary() do trimmed_search = String.trim(search) query |> where( [t: t], fragment( "? @@ websearch_to_tsquery('english', ?)", t.search, ^trimmed_search ) ) |> order_by( [t: t], { :desc, fragment( "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", t.search, ^trimmed_search ) } ) end @spec list_types_class(Queryable.t(), Type.class() | :all) :: Queryable.t() defp list_types_class(query, class) when class in [:rifle, :pistol, :shotgun], do: query |> where([t: t], t.class == ^class) defp list_types_class(query, _all), do: query @doc """ Returns a count of types. ## Examples iex> get_types_count!(%User{id: 123}) 3 """ @spec get_types_count!(User.t()) :: integer() def get_types_count!(%User{id: user_id}) do Repo.one( from t in Type, where: t.user_id == ^user_id, select: count(t.id), distinct: true ) || 0 end @doc """ Gets a single type. Raises `Ecto.NoResultsError` if the type does not exist. ## Examples iex> get_type!(123, %User{id: 123}) %Type{} iex> get_type!(456, %User{id: 123}) ** (Ecto.NoResultsError) """ @spec get_type!(Type.id(), User.t()) :: Type.t() def get_type!(id, %User{id: user_id}) do Repo.one!( from t in Type, where: t.id == ^id, where: t.user_id == ^user_id, preload: ^@type_preloads ) end @doc """ Gets the average cost of a type from packs with price information. ## Examples iex> get_average_cost( ...> %Type{id: 123, user_id: 456}, ...> %User{id: 456} ...> ) 1.50 """ @spec get_average_cost(Type.t(), User.t()) :: float() | nil def get_average_cost(%Type{id: type_id} = type, user) do [type] |> get_average_costs(user) |> Map.get(type_id) end @doc """ Gets the average cost of types from packs with price information for multiple types. ## Examples iex> get_average_costs( ...> [%Type{id: 123, user_id: 456}], ...> %User{id: 456} ...> ) 1.50 """ @spec get_average_costs([Type.t()], User.t()) :: %{optional(Type.id()) => float()} def get_average_costs(types, %User{id: user_id}) do type_ids = types |> Enum.map(fn %Type{id: type_id, user_id: ^user_id} -> type_id end) sg_total_query = from sr in ShotRecord, where: not (sr.count |> is_nil()), group_by: sr.pack_id, select: %{pack_id: sr.pack_id, total: sum(sr.count)} Repo.all( from p in Pack, as: :pack, left_join: sg_query in subquery(sg_total_query), on: p.id == sg_query.pack_id, where: p.type_id in ^type_ids, group_by: p.type_id, where: not (p.price_paid |> is_nil()), select: {p.type_id, sum(p.price_paid) / sum(p.count + coalesce(sg_query.total, 0))} ) |> Map.new() end @type get_round_count_option :: {:type_id, Type.id() | nil} | {:container_id, Container.id()} @type get_round_count_options :: [get_round_count_option()] @doc """ Gets the total number of rounds for a type ## Examples iex> get_round_count(%User{id: 456}, type_id: 123) 35 iex> get_round_count(%User{id: 456}, container_id: 123) 25 """ @spec get_round_count(User.t()) :: non_neg_integer() @spec get_round_count(User.t(), get_round_count_options()) :: non_neg_integer() def get_round_count(%User{id: user_id}, opts \\ []) do from(p in Pack, as: :p, where: p.user_id == ^user_id, select: sum(p.count), distinct: true ) |> get_round_count_type_id(Keyword.get(opts, :type_id)) |> get_round_count_container_id(Keyword.get(opts, :container_id)) |> Repo.one() || 0 end @spec get_round_count_type_id(Queryable.t(), Type.id() | nil) :: Queryable.t() defp get_round_count_type_id(query, type_id) when type_id |> is_binary() do query |> where([p: p], p.type_id == ^type_id) end defp get_round_count_type_id(query, _nil), do: query @spec get_round_count_container_id(Queryable.t(), Container.id() | nil) :: Queryable.t() defp get_round_count_container_id(query, container_id) when container_id |> is_binary() do query |> where([p: p], p.container_id == ^container_id) end defp get_round_count_container_id(query, _nil), do: query @type get_grouped_round_count_option :: {:types, [Type.t()] | nil} | {:containers, [Container.t()] | nil} | {:group_by, atom()} @type get_grouped_round_count_options :: [get_grouped_round_count_option()] @doc """ Gets the total number of rounds for multiple types ## Examples iex> get_grouped_round_count( ...> %User{id: 456}, ...> group_by: :type_id, ...> types: [%Type{id: 123, user_id: 456}] ...> ) %{123 => 35} """ @spec get_grouped_round_count(User.t(), get_grouped_round_count_options()) :: %{optional(Type.id() | Container.id()) => non_neg_integer()} def get_grouped_round_count(%User{id: user_id}, opts) do from(p in Pack, as: :p, where: p.user_id == ^user_id ) |> get_grouped_round_count_filter_ids( Keyword.fetch!(opts, :group_by), Keyword.get(opts, :types) ) |> get_grouped_round_count_filter_ids( Keyword.fetch!(opts, :group_by), Keyword.get(opts, :containers) ) |> get_grouped_round_count_group_by(Keyword.fetch!(opts, :group_by)) |> Repo.all() |> Map.new() end @spec get_grouped_round_count_group_by(Queryable.t(), atom()) :: Queryable.t() defp get_grouped_round_count_group_by(query, group_key) when group_key |> is_atom() do query |> group_by([p: p], field(p, ^group_key)) |> select([p: p], {field(p, ^group_key), sum(p.count)}) end @spec get_grouped_round_count_filter_ids( Queryable.t(), atom(), [Type.t()] | [Container.t()] | nil ) :: Queryable.t() defp get_grouped_round_count_filter_ids(query, group_key, items) when items |> is_list() do item_ids = items |> Enum.map(fn %{id: id} -> id end) query |> where([p: p], field(p, ^group_key) in ^item_ids) end defp get_grouped_round_count_filter_ids(query, _group_key, _nil), do: query @doc """ Gets the total number of ammo ever bought for a type ## Examples iex> get_historical_count( ...> %Type{id: 123, user_id: 456}, ...> %User{id: 456} ...> ) 5 """ @spec get_historical_count(Type.t(), User.t()) :: non_neg_integer() def get_historical_count(%Type{id: type_id} = type, user) do [type] |> get_historical_counts(user) |> Map.get(type_id, 0) end @doc """ Gets the total number of ammo ever bought for multiple types ## Examples iex> get_historical_counts( ...> [%Type{id: 123, user_id: 456}], ...> %User{id: 456} ...> ) %{123 => 5} """ @spec get_historical_counts([Type.t()], User.t()) :: %{optional(Type.id()) => non_neg_integer()} def get_historical_counts(types, %User{id: user_id} = user) do used_counts = ActivityLog.get_grouped_used_counts(user, types: types, group_by: :type_id) round_counts = get_grouped_round_count(user, types: types, group_by: :type_id) types |> Enum.filter(fn %Type{id: type_id, user_id: ^user_id} -> Map.has_key?(used_counts, type_id) or Map.has_key?(round_counts, type_id) end) |> Map.new(fn %{id: type_id} -> historical_count = Map.get(used_counts, type_id, 0) + Map.get(round_counts, type_id, 0) {type_id, historical_count} end) end @doc """ Creates a type. ## Examples iex> create_type(%{field: value}, %User{id: 123}) {:ok, %Type{}} iex> create_type(%{field: bad_value}, %User{id: 123}) {:error, %Changeset{}} """ @spec create_type(attrs :: map(), User.t()) :: {:ok, Type.t()} | {:error, Type.changeset()} def create_type(attrs \\ %{}, %User{} = user) do %Type{} |> Type.create_changeset(user, attrs) |> Repo.insert() |> case do {:ok, type} -> {:ok, type |> preload_type()} {:error, changeset} -> {:error, changeset} end end @spec preload_type(Type.t()) :: Type.t() @spec preload_type([Type.t()]) :: [Type.t()] defp preload_type(type_or_types) do type_or_types |> Repo.preload(@type_preloads) end @doc """ Updates a type. ## Examples iex> update_type(type, %{field: new_value}, %User{id: 123}) {:ok, %Type{}} iex> update_type(type, %{field: bad_value}, %User{id: 123}) {:error, %Changeset{}} """ @spec update_type(Type.t(), attrs :: map(), User.t()) :: {:ok, Type.t()} | {:error, Type.changeset()} def update_type(%Type{user_id: user_id} = type, attrs, %User{id: user_id}) do type |> Type.update_changeset(attrs) |> Repo.update() |> case do {:ok, type} -> {:ok, type |> preload_type()} {:error, changeset} -> {:error, changeset} end end @doc """ Deletes a type. ## Examples iex> delete_type(type, %User{id: 123}) {:ok, %Type{}} iex> delete_type(type, %User{id: 123}) {:error, %Changeset{}} """ @spec delete_type(Type.t(), User.t()) :: {:ok, Type.t()} | {:error, Type.changeset()} def delete_type(%Type{user_id: user_id} = type, %User{id: user_id}) do type |> Repo.delete() |> case do {:ok, type} -> {:ok, type |> preload_type()} {:error, changeset} -> {:error, changeset} end end @doc """ Deletes a type. ## Examples iex> delete_type!(type, %User{id: 123}) %Type{} """ @spec delete_type!(Type.t(), User.t()) :: Type.t() def delete_type!(type, user) do {:ok, type} = delete_type(type, user) type end # Packs @type list_packs_option :: {:type_id, Type.id()} | {:container_id, Container.id()} | {:class, Type.class() | :all} | {:show_used, boolean() | nil} | {:search, String.t() | nil} | {:staged, boolean() | nil} @type list_packs_options :: [list_packs_option()] @doc """ Returns the list of packs for a user and type. ## Examples iex> list_packs(%User{id: 456}) [%Pack{}, ...] iex> list_packs( ...> %User{id: 456}, ...> show_used: true, ...> type_id: 123, ...> container_id: 789, ...> search: "something", ...> staged: true ...> ) [%Pack{}, %Pack{}, ...] """ @spec list_packs(User.t()) :: [Pack.t()] @spec list_packs(User.t(), list_packs_options()) :: [Pack.t()] def list_packs(%User{id: user_id}, opts \\ []) do from(p in Pack, as: :p, join: t in assoc(p, :type), on: p.user_id == t.user_id, as: :t, join: c in Container, on: p.container_id == c.id, on: p.user_id == c.user_id, as: :c, where: p.user_id == c.user_id, left_join: ct in ContainerTag, on: c.id == ct.container_id, left_join: tag in Tag, on: ct.tag_id == tag.id, on: p.user_id == tag.user_id, as: :tag, where: p.user_id == ^user_id, distinct: p.id, preload: ^@pack_preloads ) |> list_packs_search(Keyword.get(opts, :search)) |> list_packs_class(Keyword.get(opts, :class, :all)) |> list_packs_show_used(Keyword.get(opts, :show_used)) |> list_packs_staged(Keyword.get(opts, :staged)) |> list_packs_container_id(Keyword.get(opts, :container_id)) |> list_packs_type_id(Keyword.get(opts, :type_id)) |> Repo.all() end @spec list_packs_search(Queryable.t(), search :: String.t() | nil) :: Queryable.t() defp list_packs_search(query, search) when search in ["", nil], do: query defp list_packs_search(query, search) when search |> is_binary() do trimmed_search = String.trim(search) query |> where( [p: p, t: t, c: c, tag: tag], fragment( "? @@ websearch_to_tsquery('english', ?)", p.search, ^trimmed_search ) or fragment( "? @@ websearch_to_tsquery('english', ?)", t.search, ^trimmed_search ) or fragment( "? @@ websearch_to_tsquery('english', ?)", c.search, ^trimmed_search ) or fragment( "? @@ websearch_to_tsquery('english', ?)", tag.search, ^trimmed_search ) ) |> order_by( [p: p], desc: fragment( "ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)", p.search, ^trimmed_search ) ) end @spec list_packs_class(Queryable.t(), Type.class() | :all) :: Queryable.t() defp list_packs_class(query, class) when class in [:rifle, :pistol, :shotgun], do: query |> where([t: t], t.class == ^class) defp list_packs_class(query, _all), do: query @spec list_packs_show_used(Queryable.t(), show_used :: boolean() | nil) :: Queryable.t() defp list_packs_show_used(query, true), do: query defp list_packs_show_used(query, _false), do: query |> where([p: p], not (p.count == 0)) @spec list_packs_container_id(Queryable.t(), Container.id() | nil) :: Queryable.t() defp list_packs_container_id(query, container_id) when container_id |> is_binary(), do: query |> where([p: p], p.container_id == ^container_id) defp list_packs_container_id(query, _nil), do: query @spec list_packs_type_id(Queryable.t(), Type.id() | nil) :: Queryable.t() defp list_packs_type_id(query, type_id) when type_id |> is_binary(), do: query |> where([p: p], p.type_id == ^type_id) defp list_packs_type_id(query, _nil), do: query @spec list_packs_staged(Queryable.t(), staged :: boolean() | nil) :: Queryable.t() defp list_packs_staged(query, staged) when staged |> is_boolean(), do: query |> where([p: p], p.staged == ^staged) defp list_packs_staged(query, _nil), do: query @type get_packs_count_option :: {:container_id, Container.id() | nil} | {:type_id, Type.id() | nil} | {:show_used, :only_used | boolean() | nil} @type get_packs_count_options :: [get_packs_count_option()] @doc """ Returns a count of packs. ## Examples iex> get_packs_count(%User{id: 123}) 3 iex> get_packs_count(%User{id: 123}, show_used: true) 4 iex> get_packs_count(%User{id: 123}, container_id: 456) 1 iex> get_packs_count(%User{id: 123}, type_id: 456) 2 """ @spec get_packs_count(User.t()) :: integer() @spec get_packs_count(User.t(), get_packs_count_options()) :: integer() def get_packs_count(%User{id: user_id}, opts \\ []) do from(p in Pack, as: :p, where: p.user_id == ^user_id, select: count(p.id), distinct: true ) |> get_packs_count_show_used(Keyword.get(opts, :show_used)) |> get_packs_count_container_id(Keyword.get(opts, :container_id)) |> get_packs_count_type_id(Keyword.get(opts, :type_id)) |> Repo.one() || 0 end @spec get_packs_count_show_used(Queryable.t(), show_used :: :only_used | boolean() | nil) :: Queryable.t() defp get_packs_count_show_used(query, true), do: query defp get_packs_count_show_used(query, :only_used), do: query |> where([p: p], p.count == 0) defp get_packs_count_show_used(query, _false), do: query |> where([p: p], p.count > 0) @spec get_packs_count_type_id(Queryable.t(), Type.id() | nil) :: Queryable.t() defp get_packs_count_type_id(query, type_id) when type_id |> is_binary(), do: query |> where([p: p], p.type_id == ^type_id) defp get_packs_count_type_id(query, _nil), do: query @spec get_packs_count_container_id(Queryable.t(), Container.id() | nil) :: Queryable.t() defp get_packs_count_container_id(query, container_id) when container_id |> is_binary(), do: query |> where([p: p], p.container_id == ^container_id) defp get_packs_count_container_id(query, _nil), do: query @type get_grouped_packs_count_opt :: {:group_by, atom()} | {:containers, [Container.t()] | nil} | {:types, [Type.t()] | nil} | {:show_used, :only_used | boolean() | nil} @type get_grouped_packs_counts_opts :: [get_grouped_packs_count_opt()] @doc """ Returns the count of packs for multiple types. ## Examples iex> get_grouped_packs_count( ...> %User{id: 456}, ...> group_by: :type_id, ...> types: [%Type{id: 123, user_id: 456}] ...> ) 3 iex> get_grouped_packs_count( ...> %User{id: 456}, ...> group_by: :type_id, ...> types: [%Type{id: 123, user_id: 456}], ...> show_used: true ...> ) 5 iex> get_grouped_packs_count( ...> %User{id: 456}, ...> group_by: :type_id, ...> types: [%Type{id: 123, user_id: 456}], ...> show_used: :only_used ...> ) 2 iex> get_grouped_packs_count( ...> %User{id: 456}, ...> group_by: :container_id, ...> containers: [%Container{id: 123, user_id: 456}] ...> ) 7 """ @spec get_grouped_packs_count(User.t(), get_grouped_packs_counts_opts()) :: %{optional(Type.id() | Container.id()) => non_neg_integer()} def get_grouped_packs_count(%User{id: user_id}, opts) do from(p in Pack, as: :p, where: p.user_id == ^user_id ) |> get_grouped_packs_count_group_by(Keyword.fetch!(opts, :group_by)) |> get_grouped_packs_count_filter_ids( Keyword.fetch!(opts, :group_by), Keyword.get(opts, :types) ) |> get_grouped_packs_count_filter_ids( Keyword.fetch!(opts, :group_by), Keyword.get(opts, :containers) ) |> get_grouped_packs_count_show_used(Keyword.get(opts, :show_used)) |> Repo.all() |> Map.new() end @spec get_grouped_packs_count_group_by(Queryable.t(), :type_id | :container_id) :: Queryable.t() defp get_grouped_packs_count_group_by(query, group_key) when group_key |> is_atom() do query |> group_by([p: p], field(p, ^group_key)) |> select([p: p], {field(p, ^group_key), count(p.id)}) end @spec get_grouped_packs_count_filter_ids( Queryable.t(), :type_id | :container_id, [Type.t()] | [Container.t()] | nil ) :: Queryable.t() defp get_grouped_packs_count_filter_ids(query, group_key, items) when items |> is_list() do item_ids = items |> Enum.map(fn %{id: id} -> id end) query |> where([p: p], field(p, ^group_key) in ^item_ids) end defp get_grouped_packs_count_filter_ids(query, _filter_key, _nil), do: query @spec get_grouped_packs_count_show_used( Queryable.t(), show_used :: :only_used | boolean() | nil ) :: Queryable.t() defp get_grouped_packs_count_show_used(query, true), do: query defp get_grouped_packs_count_show_used(query, :only_used) do query |> where([p: p], p.count == 0) end defp get_grouped_packs_count_show_used(query, _false) do query |> where([p: p], not (p.count == 0)) end @doc """ Gets a single pack. Raises `KeyError` if the pack does not exist. ## Examples iex> get_pack!(123, %User{id: 123}) %Pack{} iex> get_pack!(456, %User{id: 123}) ** (KeyError) """ @spec get_pack!(Pack.id(), User.t()) :: Pack.t() def get_pack!(id, user) do [id] |> get_packs(user) |> Map.fetch!(id) end @doc """ Gets a group of packs by their ID. ## Examples iex> get_packs([123, 456], %User{id: 123}) %{123 => %Pack{}, 456 => %Pack{}} """ @spec get_packs([Pack.id()], User.t()) :: %{optional(Pack.id()) => Pack.t()} def get_packs(ids, %User{id: user_id}) do Repo.all( from p in Pack, where: p.id in ^ids, where: p.user_id == ^user_id, preload: ^@pack_preloads, select: {p.id, p} ) |> Map.new() end @doc """ Calculates the percentage remaining of a pack out of 100 ## Examples iex> get_percentage_remaining( ...> %Pack{id: 123, count: 5, user_id: 456}, ...> %User{id: 456} ...> ) 100 """ @spec get_percentage_remaining(Pack.t(), User.t()) :: non_neg_integer() def get_percentage_remaining(%Pack{id: pack_id} = pack, user) do [pack] |> get_percentages_remaining(user) |> Map.fetch!(pack_id) end @doc """ Calculates the percentages remaining of multiple packs out of 100 ## Examples iex> get_percentages_remaining( ...> [%Pack{id: 123, count: 5, user_id: 456}], ...> %User{id: 456} ...> ) %{123 => 100} """ @spec get_percentages_remaining([Pack.t()], User.t()) :: %{optional(Pack.id()) => non_neg_integer()} def get_percentages_remaining(packs, %User{id: user_id} = user) do original_counts = get_original_counts(packs, user) packs |> Map.new(fn %Pack{id: pack_id, count: count, user_id: ^user_id} -> percentage = case count do 0 -> 0 count -> round(count / Map.fetch!(original_counts, pack_id) * 100) end {pack_id, percentage} end) end @doc """ Gets the original count for a pack ## Examples iex> get_original_count( ...> %Pack{id: 123, count: 5, user_id: 456}, ...> %User{id: 456} ...> ) 5 """ @spec get_original_count(Pack.t(), User.t()) :: non_neg_integer() def get_original_count(%Pack{id: pack_id} = pack, current_user) do [pack] |> get_original_counts(current_user) |> Map.fetch!(pack_id) end @doc """ Gets the original counts for multiple packs ## Examples iex> get_original_counts( ...> [%Pack{id: 123, count: 5, user_id: 456}], ...> %User{id: 456} ...> ) %{123 => 5} """ @spec get_original_counts([Pack.t()], User.t()) :: %{optional(Pack.id()) => non_neg_integer()} def get_original_counts(packs, %User{id: user_id} = current_user) do used_counts = ActivityLog.get_grouped_used_counts(current_user, packs: packs, group_by: :pack_id) packs |> Map.new(fn %Pack{id: pack_id, count: count, user_id: ^user_id} -> {pack_id, count + Map.get(used_counts, pack_id, 0)} end) end @doc """ Calculates the CPR for a single pack ## Examples iex> get_cpr( ...> %Pack{id: 123, price_paid: 5, count: 5, user_id: 456}, ...> %User{id: 456} ...> ) 1 """ @spec get_cpr(Pack.t(), User.t()) :: float() | nil def get_cpr(%Pack{id: pack_id} = pack, user) do [pack] |> get_cprs(user) |> Map.get(pack_id) end @doc """ Calculates the CPR for multiple packs ## Examples iex> get_cprs( ...> [%Pack{id: 123, price_paid: 5, count: 5, user_id: 456}], ...> %User{id: 456} ...> ) %{123 => 1} """ @spec get_cprs([Pack.t()], User.t()) :: %{optional(Pack.id()) => float()} def get_cprs(packs, %User{id: user_id} = current_user) do original_counts = get_original_counts(packs, current_user) packs |> Enum.reject(fn %Pack{price_paid: price_paid, user_id: ^user_id} -> price_paid |> is_nil() end) |> Map.new(fn %{id: pack_id, price_paid: price_paid} -> {pack_id, calculate_cpr(price_paid, Map.fetch!(original_counts, pack_id))} end) end @spec calculate_cpr(price_paid :: float() | nil, count :: integer()) :: float() | nil defp calculate_cpr(nil, _count), do: nil defp calculate_cpr(_price_paid, 0), do: nil defp calculate_cpr(price_paid, total_count), do: price_paid / total_count @doc """ Creates multiple packs at once. ## Examples iex> create_packs(%{field: value}, 3, %User{id: 123}) {:ok, {3, [%Pack{}]}} iex> create_packs(%{field: bad_value}, 3, %User{id: 123}) {:error, %Changeset{}} """ @spec create_packs(attrs :: map(), multiplier :: non_neg_integer(), User.t()) :: {:ok, {count :: non_neg_integer(), [Pack.t()] | nil}} | {:error, Pack.changeset()} def create_packs(attrs, multiplier, %User{} = user) do attrs |> Map.new(fn {k, v} -> {to_string(k), v} end) |> do_create_packs(multiplier, user) end defp do_create_packs( %{"type_id" => type_id, "container_id" => container_id} = attrs, multiplier, user ) when multiplier >= 1 and multiplier <= @pack_create_limit and type_id |> is_binary() and container_id |> is_binary() do now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) changesets = Enum.map(1..multiplier, fn _count -> %Pack{} |> Pack.create_changeset( get_type!(type_id, user), Containers.get_container!(container_id, user), user, attrs ) end) if changesets |> Enum.all?(fn %{valid?: valid} -> valid end) do {count, inserted_packs} = Repo.insert_all( Pack, changesets |> Enum.map(fn changeset -> changeset |> Map.get(:changes) |> Map.merge(%{inserted_at: now, updated_at: now}) end), returning: true ) {:ok, {count, inserted_packs |> preload_pack()}} else changesets |> Enum.reject(fn %{valid?: valid} -> valid end) |> List.first() |> Changeset.apply_action(:insert) end end defp do_create_packs( %{"type_id" => type_id, "container_id" => container_id} = attrs, _multiplier, user ) when is_binary(type_id) and is_binary(container_id) do changeset = %Pack{} |> Pack.create_changeset( get_type!(type_id, user), Containers.get_container!(container_id, user), user, attrs ) |> Changeset.add_error(:multiplier, dgettext("errors", "Invalid multiplier")) {:error, changeset} end defp do_create_packs(invalid_attrs, _multiplier, user) do {:error, %Pack{} |> Pack.create_changeset(nil, nil, user, invalid_attrs)} end @spec preload_pack(Pack.t()) :: Pack.t() @spec preload_pack([Pack.t()]) :: [Pack.t()] defp preload_pack(pack_or_packs) do pack_or_packs |> Repo.preload(@pack_preloads) end @doc """ Updates a pack. ## Examples iex> update_pack(pack, %{field: new_value}, %User{id: 123}) {:ok, %Pack{}} iex> update_pack(pack, %{field: bad_value}, %User{id: 123}) {:error, %Changeset{}} """ @spec update_pack(Pack.t(), attrs :: map(), User.t()) :: {:ok, Pack.t()} | {:error, Pack.changeset()} def update_pack( %Pack{user_id: user_id} = pack, attrs, %User{id: user_id} = user ) do pack |> Pack.update_changeset(attrs, user) |> Repo.update() |> case do {:ok, pack} -> {:ok, pack |> preload_pack()} {:error, changeset} -> {:error, changeset} end end @doc """ Deletes a pack. ## Examples iex> delete_pack(pack, %User{id: 123}) {:ok, %Pack{}} iex> delete_pack(pack, %User{id: 123}) {:error, %Changeset{}} """ @spec delete_pack(Pack.t(), User.t()) :: {:ok, Pack.t()} | {:error, Pack.changeset()} def delete_pack(%Pack{user_id: user_id} = pack, %User{id: user_id}) do pack |> Repo.delete() |> case do {:ok, pack} -> {:ok, pack |> preload_pack()} {:error, changeset} -> {:error, changeset} end end @doc """ Deletes a pack. ## Examples iex> delete_pack!(pack, %User{id: 123}) %Pack{} """ @spec delete_pack!(Pack.t(), User.t()) :: Pack.t() def delete_pack!(pack, user) do {:ok, pack} = delete_pack(pack, user) pack end end