1204 lines
31 KiB
Elixir
1204 lines
31 KiB
Elixir
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.ShotGroup}
|
|
alias Cannery.Ammo.{AmmoType, Pack}
|
|
alias Ecto.{Changeset, Queryable}
|
|
|
|
@pack_create_limit 10_000
|
|
@pack_preloads [:ammo_type]
|
|
@ammo_type_preloads [:packs]
|
|
|
|
@doc """
|
|
Returns the list of ammo_types.
|
|
|
|
## Examples
|
|
|
|
iex> list_ammo_types(%User{id: 123}, :all)
|
|
[%AmmoType{}, ...]
|
|
|
|
iex> list_ammo_types("cool", %User{id: 123}, :shotgun)
|
|
[%AmmoType{name: "My cool ammo type", class: :shotgun}, ...]
|
|
|
|
"""
|
|
@spec list_ammo_types(User.t(), AmmoType.class() | :all) :: [AmmoType.t()]
|
|
@spec list_ammo_types(search :: nil | String.t(), User.t(), AmmoType.class() | :all) ::
|
|
[AmmoType.t()]
|
|
def list_ammo_types(search \\ nil, user, type)
|
|
|
|
def list_ammo_types(search, %{id: user_id}, type) do
|
|
from(at in AmmoType,
|
|
as: :at,
|
|
where: at.user_id == ^user_id,
|
|
preload: ^@ammo_type_preloads
|
|
)
|
|
|> list_ammo_types_filter_type(type)
|
|
|> list_ammo_types_filter_search(search)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec list_ammo_types_filter_search(Queryable.t(), search :: String.t() | nil) :: Queryable.t()
|
|
defp list_ammo_types_filter_search(query, search) when search in ["", nil],
|
|
do: query |> order_by([at: at], at.name)
|
|
|
|
defp list_ammo_types_filter_search(query, search) when search |> is_binary() do
|
|
trimmed_search = String.trim(search)
|
|
|
|
query
|
|
|> where(
|
|
[at: at],
|
|
fragment(
|
|
"? @@ websearch_to_tsquery('english', ?)",
|
|
at.search,
|
|
^trimmed_search
|
|
)
|
|
)
|
|
|> order_by(
|
|
[at: at],
|
|
{
|
|
:desc,
|
|
fragment(
|
|
"ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
|
|
at.search,
|
|
^trimmed_search
|
|
)
|
|
}
|
|
)
|
|
end
|
|
|
|
@spec list_ammo_types_filter_type(Queryable.t(), AmmoType.class() | :all) :: Queryable.t()
|
|
defp list_ammo_types_filter_type(query, :rifle),
|
|
do: query |> where([at: at], at.class == :rifle)
|
|
|
|
defp list_ammo_types_filter_type(query, :pistol),
|
|
do: query |> where([at: at], at.class == :pistol)
|
|
|
|
defp list_ammo_types_filter_type(query, :shotgun),
|
|
do: query |> where([at: at], at.class == :shotgun)
|
|
|
|
defp list_ammo_types_filter_type(query, _all), do: query
|
|
|
|
@doc """
|
|
Returns a count of ammo_types.
|
|
|
|
## Examples
|
|
|
|
iex> get_ammo_types_count!(%User{id: 123})
|
|
3
|
|
|
|
"""
|
|
@spec get_ammo_types_count!(User.t()) :: integer()
|
|
def get_ammo_types_count!(%User{id: user_id}) do
|
|
Repo.one(
|
|
from at in AmmoType,
|
|
where: at.user_id == ^user_id,
|
|
select: count(at.id),
|
|
distinct: true
|
|
) || 0
|
|
end
|
|
|
|
@doc """
|
|
Gets a single ammo_type.
|
|
|
|
Raises `Ecto.NoResultsError` if the Ammo type does not exist.
|
|
|
|
## Examples
|
|
|
|
iex> get_ammo_type!(123, %User{id: 123})
|
|
%AmmoType{}
|
|
|
|
iex> get_ammo_type!(456, %User{id: 123})
|
|
** (Ecto.NoResultsError)
|
|
|
|
"""
|
|
@spec get_ammo_type!(AmmoType.id(), User.t()) :: AmmoType.t()
|
|
def get_ammo_type!(id, %User{id: user_id}) do
|
|
Repo.one!(
|
|
from at in AmmoType,
|
|
where: at.id == ^id,
|
|
where: at.user_id == ^user_id,
|
|
preload: ^@ammo_type_preloads
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Gets the average cost of an ammo type from packs with price information.
|
|
|
|
## Examples
|
|
|
|
iex> get_average_cost_for_ammo_type(
|
|
...> %AmmoType{id: 123, user_id: 456},
|
|
...> %User{id: 456}
|
|
...> )
|
|
1.50
|
|
|
|
"""
|
|
@spec get_average_cost_for_ammo_type(AmmoType.t(), User.t()) :: float() | nil
|
|
def get_average_cost_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
|
|
[ammo_type]
|
|
|> get_average_cost_for_ammo_types(user)
|
|
|> Map.get(ammo_type_id)
|
|
end
|
|
|
|
@doc """
|
|
Gets the average cost of ammo types from packs with price information
|
|
for multiple ammo types.
|
|
|
|
## Examples
|
|
|
|
iex> get_average_cost_for_ammo_types(
|
|
...> [%AmmoType{id: 123, user_id: 456}],
|
|
...> %User{id: 456}
|
|
...> )
|
|
1.50
|
|
|
|
"""
|
|
@spec get_average_cost_for_ammo_types([AmmoType.t()], User.t()) ::
|
|
%{optional(AmmoType.id()) => float()}
|
|
def get_average_cost_for_ammo_types(ammo_types, %User{id: user_id}) do
|
|
ammo_type_ids =
|
|
ammo_types
|
|
|> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end)
|
|
|
|
sg_total_query =
|
|
from sg in ShotGroup,
|
|
where: not (sg.count |> is_nil()),
|
|
group_by: sg.pack_id,
|
|
select: %{pack_id: sg.pack_id, total: sum(sg.count)}
|
|
|
|
Repo.all(
|
|
from ag in Pack,
|
|
as: :pack,
|
|
left_join: sg_query in subquery(sg_total_query),
|
|
on: ag.id == sg_query.pack_id,
|
|
where: ag.ammo_type_id in ^ammo_type_ids,
|
|
group_by: ag.ammo_type_id,
|
|
where: not (ag.price_paid |> is_nil()),
|
|
select:
|
|
{ag.ammo_type_id, sum(ag.price_paid) / sum(ag.count + coalesce(sg_query.total, 0))}
|
|
)
|
|
|> Map.new()
|
|
end
|
|
|
|
@doc """
|
|
Gets the total number of rounds for an ammo type
|
|
|
|
## Examples
|
|
|
|
iex> get_round_count_for_ammo_type(
|
|
...> %AmmoType{id: 123, user_id: 456},
|
|
...> %User{id: 456}
|
|
...> )
|
|
35
|
|
|
|
"""
|
|
@spec get_round_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer()
|
|
def get_round_count_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
|
|
[ammo_type]
|
|
|> get_round_count_for_ammo_types(user)
|
|
|> Map.get(ammo_type_id, 0)
|
|
end
|
|
|
|
@doc """
|
|
Gets the total number of rounds for multiple ammo types
|
|
|
|
## Examples
|
|
|
|
iex> get_round_count_for_ammo_types(
|
|
...> [%AmmoType{id: 123, user_id: 456}],
|
|
...> %User{id: 456}
|
|
...> )
|
|
%{123 => 35}
|
|
|
|
"""
|
|
@spec get_round_count_for_ammo_types([AmmoType.t()], User.t()) ::
|
|
%{optional(AmmoType.id()) => non_neg_integer()}
|
|
def get_round_count_for_ammo_types(ammo_types, %User{id: user_id}) do
|
|
ammo_type_ids =
|
|
ammo_types
|
|
|> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end)
|
|
|
|
Repo.all(
|
|
from ag in Pack,
|
|
where: ag.ammo_type_id in ^ammo_type_ids,
|
|
where: ag.user_id == ^user_id,
|
|
group_by: ag.ammo_type_id,
|
|
select: {ag.ammo_type_id, sum(ag.count)}
|
|
)
|
|
|> Map.new()
|
|
end
|
|
|
|
@doc """
|
|
Gets the total number of ammo ever bought for an ammo type
|
|
|
|
## Examples
|
|
|
|
iex> get_historical_count_for_ammo_type(
|
|
...> %AmmoType{id: 123, user_id: 456},
|
|
...> %User{id: 456}
|
|
...> )
|
|
5
|
|
|
|
"""
|
|
@spec get_historical_count_for_ammo_type(AmmoType.t(), User.t()) :: non_neg_integer()
|
|
def get_historical_count_for_ammo_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
|
|
[ammo_type]
|
|
|> get_historical_count_for_ammo_types(user)
|
|
|> Map.get(ammo_type_id, 0)
|
|
end
|
|
|
|
@doc """
|
|
Gets the total number of ammo ever bought for multiple ammo types
|
|
|
|
## Examples
|
|
|
|
iex> get_historical_count_for_ammo_types(
|
|
...> [%AmmoType{id: 123, user_id: 456}],
|
|
...> %User{id: 456}
|
|
...> )
|
|
%{123 => 5}
|
|
|
|
"""
|
|
@spec get_historical_count_for_ammo_types([AmmoType.t()], User.t()) ::
|
|
%{optional(AmmoType.id()) => non_neg_integer()}
|
|
def get_historical_count_for_ammo_types(ammo_types, %User{id: user_id} = user) do
|
|
used_counts = ammo_types |> ActivityLog.get_used_count_for_ammo_types(user)
|
|
round_counts = ammo_types |> get_round_count_for_ammo_types(user)
|
|
|
|
ammo_types
|
|
|> Enum.filter(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} ->
|
|
Map.has_key?(used_counts, ammo_type_id) or Map.has_key?(round_counts, ammo_type_id)
|
|
end)
|
|
|> Map.new(fn %{id: ammo_type_id} ->
|
|
historical_count =
|
|
Map.get(used_counts, ammo_type_id, 0) + Map.get(round_counts, ammo_type_id, 0)
|
|
|
|
{ammo_type_id, historical_count}
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Creates a ammo_type.
|
|
|
|
## Examples
|
|
|
|
iex> create_ammo_type(%{field: value}, %User{id: 123})
|
|
{:ok, %AmmoType{}}
|
|
|
|
iex> create_ammo_type(%{field: bad_value}, %User{id: 123})
|
|
{:error, %Changeset{}}
|
|
|
|
"""
|
|
@spec create_ammo_type(attrs :: map(), User.t()) ::
|
|
{:ok, AmmoType.t()} | {:error, AmmoType.changeset()}
|
|
def create_ammo_type(attrs \\ %{}, %User{} = user) do
|
|
%AmmoType{}
|
|
|> AmmoType.create_changeset(user, attrs)
|
|
|> Repo.insert()
|
|
|> case do
|
|
{:ok, ammo_type} -> {:ok, ammo_type |> preload_ammo_type()}
|
|
{:error, changeset} -> {:error, changeset}
|
|
end
|
|
end
|
|
|
|
@spec preload_ammo_type(AmmoType.t()) :: AmmoType.t()
|
|
@spec preload_ammo_type([AmmoType.t()]) :: [AmmoType.t()]
|
|
defp preload_ammo_type(ammo_type_or_ammo_types) do
|
|
ammo_type_or_ammo_types |> Repo.preload(@ammo_type_preloads)
|
|
end
|
|
|
|
@doc """
|
|
Updates a ammo_type.
|
|
|
|
## Examples
|
|
|
|
iex> update_ammo_type(ammo_type, %{field: new_value}, %User{id: 123})
|
|
{:ok, %AmmoType{}}
|
|
|
|
iex> update_ammo_type(ammo_type, %{field: bad_value}, %User{id: 123})
|
|
{:error, %Changeset{}}
|
|
|
|
"""
|
|
@spec update_ammo_type(AmmoType.t(), attrs :: map(), User.t()) ::
|
|
{:ok, AmmoType.t()} | {:error, AmmoType.changeset()}
|
|
def update_ammo_type(%AmmoType{user_id: user_id} = ammo_type, attrs, %User{id: user_id}) do
|
|
ammo_type
|
|
|> AmmoType.update_changeset(attrs)
|
|
|> Repo.update()
|
|
|> case do
|
|
{:ok, ammo_type} -> {:ok, ammo_type |> preload_ammo_type()}
|
|
{:error, changeset} -> {:error, changeset}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Deletes a ammo_type.
|
|
|
|
## Examples
|
|
|
|
iex> delete_ammo_type(ammo_type, %User{id: 123})
|
|
{:ok, %AmmoType{}}
|
|
|
|
iex> delete_ammo_type(ammo_type, %User{id: 123})
|
|
{:error, %Changeset{}}
|
|
|
|
"""
|
|
@spec delete_ammo_type(AmmoType.t(), User.t()) ::
|
|
{:ok, AmmoType.t()} | {:error, AmmoType.changeset()}
|
|
def delete_ammo_type(%AmmoType{user_id: user_id} = ammo_type, %User{id: user_id}) do
|
|
ammo_type
|
|
|> Repo.delete()
|
|
|> case do
|
|
{:ok, ammo_type} -> {:ok, ammo_type |> preload_ammo_type()}
|
|
{:error, changeset} -> {:error, changeset}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Deletes a ammo_type.
|
|
|
|
## Examples
|
|
|
|
iex> delete_ammo_type!(ammo_type, %User{id: 123})
|
|
%AmmoType{}
|
|
|
|
"""
|
|
@spec delete_ammo_type!(AmmoType.t(), User.t()) :: AmmoType.t()
|
|
def delete_ammo_type!(ammo_type, user) do
|
|
{:ok, ammo_type} = delete_ammo_type(ammo_type, user)
|
|
ammo_type
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of packs for a user and type.
|
|
|
|
## Examples
|
|
|
|
iex> list_packs_for_type(
|
|
...> %AmmoType{id: 123, user_id: 456},
|
|
...> %User{id: 456}
|
|
...> )
|
|
[%Pack{}, ...]
|
|
|
|
iex> list_packs_for_type(
|
|
...> %AmmoType{id: 123, user_id: 456},
|
|
...> %User{id: 456},
|
|
...> true
|
|
...> )
|
|
[%Pack{}, %Pack{}, ...]
|
|
|
|
"""
|
|
@spec list_packs_for_type(AmmoType.t(), User.t()) :: [Pack.t()]
|
|
@spec list_packs_for_type(AmmoType.t(), User.t(), show_used :: boolean()) ::
|
|
[Pack.t()]
|
|
def list_packs_for_type(ammo_type, user, show_used \\ false)
|
|
|
|
def list_packs_for_type(
|
|
%AmmoType{id: ammo_type_id, user_id: user_id},
|
|
%User{id: user_id},
|
|
show_used
|
|
) do
|
|
from(ag in Pack,
|
|
as: :ag,
|
|
where: ag.ammo_type_id == ^ammo_type_id,
|
|
where: ag.user_id == ^user_id,
|
|
preload: ^@pack_preloads
|
|
)
|
|
|> list_packs_for_type_show_used(show_used)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec list_packs_for_type_show_used(Queryable.t(), show_used :: boolean()) ::
|
|
Queryable.t()
|
|
def list_packs_for_type_show_used(query, false),
|
|
do: query |> where([ag: ag], ag.count > 0)
|
|
|
|
def list_packs_for_type_show_used(query, _true), do: query
|
|
|
|
@doc """
|
|
Returns the list of packs for a user and container.
|
|
|
|
## Examples
|
|
|
|
iex> list_packs_for_container(
|
|
...> %Container{id: 123, user_id: 456},
|
|
...> :rifle,
|
|
...> %User{id: 456}
|
|
...> )
|
|
[%Pack{}, ...]
|
|
|
|
iex> list_packs_for_container(
|
|
...> %Container{id: 123, user_id: 456},
|
|
...> :all,
|
|
...> %User{id: 456}
|
|
...> )
|
|
[%Pack{}, %Pack{}, ...]
|
|
|
|
"""
|
|
@spec list_packs_for_container(
|
|
Container.t(),
|
|
AmmoType.t() | :all,
|
|
User.t()
|
|
) :: [Pack.t()]
|
|
def list_packs_for_container(
|
|
%Container{id: container_id, user_id: user_id},
|
|
type,
|
|
%User{id: user_id}
|
|
) do
|
|
from(ag in Pack,
|
|
as: :ag,
|
|
join: at in assoc(ag, :ammo_type),
|
|
as: :at,
|
|
where: ag.container_id == ^container_id,
|
|
where: ag.user_id == ^user_id,
|
|
where: ag.count > 0,
|
|
preload: ^@pack_preloads
|
|
)
|
|
|> list_packs_for_container_filter_type(type)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec list_packs_for_container_filter_type(Queryable.t(), AmmoType.class() | :all) ::
|
|
Queryable.t()
|
|
defp list_packs_for_container_filter_type(query, :rifle),
|
|
do: query |> where([at: at], at.class == :rifle)
|
|
|
|
defp list_packs_for_container_filter_type(query, :pistol),
|
|
do: query |> where([at: at], at.class == :pistol)
|
|
|
|
defp list_packs_for_container_filter_type(query, :shotgun),
|
|
do: query |> where([at: at], at.class == :shotgun)
|
|
|
|
defp list_packs_for_container_filter_type(query, _all), do: query
|
|
|
|
@doc """
|
|
Returns a count of packs.
|
|
|
|
## Examples
|
|
|
|
iex> get_packs_count!(%User{id: 123})
|
|
3
|
|
|
|
iex> get_packs_count!(%User{id: 123}, true)
|
|
4
|
|
|
|
"""
|
|
@spec get_packs_count!(User.t()) :: integer()
|
|
@spec get_packs_count!(User.t(), show_used :: boolean()) :: integer()
|
|
def get_packs_count!(%User{id: user_id}, show_used \\ false) do
|
|
from(ag in Pack,
|
|
as: :ag,
|
|
where: ag.user_id == ^user_id,
|
|
select: count(ag.id),
|
|
distinct: true
|
|
)
|
|
|> get_packs_count_show_used(show_used)
|
|
|> Repo.one() || 0
|
|
end
|
|
|
|
@spec get_packs_count_show_used(Queryable.t(), show_used :: boolean()) :: Queryable.t()
|
|
defp get_packs_count_show_used(query, false),
|
|
do: query |> where([ag: ag], ag.count > 0)
|
|
|
|
defp get_packs_count_show_used(query, _true), do: query
|
|
|
|
@doc """
|
|
Returns the count of packs for an ammo type.
|
|
|
|
## Examples
|
|
|
|
iex> get_packs_count_for_type(
|
|
...> %AmmoType{id: 123, user_id: 456},
|
|
...> %User{id: 456}
|
|
...> )
|
|
3
|
|
|
|
iex> get_packs_count_for_type(
|
|
...> %AmmoType{id: 123, user_id: 456},
|
|
...> %User{id: 456},
|
|
...> true
|
|
...> )
|
|
5
|
|
|
|
"""
|
|
@spec get_packs_count_for_type(AmmoType.t(), User.t()) :: non_neg_integer()
|
|
@spec get_packs_count_for_type(AmmoType.t(), User.t(), show_used :: boolean()) ::
|
|
non_neg_integer()
|
|
def get_packs_count_for_type(
|
|
%AmmoType{id: ammo_type_id} = ammo_type,
|
|
user,
|
|
show_used \\ false
|
|
) do
|
|
[ammo_type]
|
|
|> get_packs_count_for_types(user, show_used)
|
|
|> Map.get(ammo_type_id, 0)
|
|
end
|
|
|
|
@doc """
|
|
Returns the count of packs for multiple ammo types.
|
|
|
|
## Examples
|
|
|
|
iex> get_packs_count_for_types(
|
|
...> [%AmmoType{id: 123, user_id: 456}],
|
|
...> %User{id: 456}
|
|
...> )
|
|
3
|
|
|
|
iex> get_packs_count_for_types(
|
|
...> [%AmmoType{id: 123, user_id: 456}],
|
|
...> %User{id: 456},
|
|
...> true
|
|
...> )
|
|
5
|
|
|
|
"""
|
|
@spec get_packs_count_for_types([AmmoType.t()], User.t()) ::
|
|
%{optional(AmmoType.id()) => non_neg_integer()}
|
|
@spec get_packs_count_for_types([AmmoType.t()], User.t(), show_used :: boolean()) ::
|
|
%{optional(AmmoType.id()) => non_neg_integer()}
|
|
def get_packs_count_for_types(ammo_types, %User{id: user_id}, show_used \\ false) do
|
|
ammo_type_ids =
|
|
ammo_types
|
|
|> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end)
|
|
|
|
from(ag in Pack,
|
|
as: :ag,
|
|
where: ag.user_id == ^user_id,
|
|
where: ag.ammo_type_id in ^ammo_type_ids,
|
|
group_by: ag.ammo_type_id,
|
|
select: {ag.ammo_type_id, count(ag.id)}
|
|
)
|
|
|> get_packs_count_for_types_maybe_show_used(show_used)
|
|
|> Repo.all()
|
|
|> Map.new()
|
|
end
|
|
|
|
@spec get_packs_count_for_types_maybe_show_used(Queryable.t(), show_used :: boolean()) ::
|
|
Queryable.t()
|
|
defp get_packs_count_for_types_maybe_show_used(query, true), do: query
|
|
|
|
defp get_packs_count_for_types_maybe_show_used(query, _false) do
|
|
query |> where([ag: ag], not (ag.count == 0))
|
|
end
|
|
|
|
@doc """
|
|
Returns the count of used packs for an ammo type.
|
|
|
|
## Examples
|
|
|
|
iex> get_used_packs_count_for_type(
|
|
...> %AmmoType{id: 123, user_id: 456},
|
|
...> %User{id: 456}
|
|
...> )
|
|
3
|
|
|
|
"""
|
|
@spec get_used_packs_count_for_type(AmmoType.t(), User.t()) :: non_neg_integer()
|
|
def get_used_packs_count_for_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
|
|
[ammo_type]
|
|
|> get_used_packs_count_for_types(user)
|
|
|> Map.get(ammo_type_id, 0)
|
|
end
|
|
|
|
@doc """
|
|
Returns the count of used packs for multiple ammo types.
|
|
|
|
## Examples
|
|
|
|
iex> get_used_packs_count_for_types(
|
|
...> [%AmmoType{id: 123, user_id: 456}],
|
|
...> %User{id: 456}
|
|
...> )
|
|
%{123 => 3}
|
|
|
|
"""
|
|
@spec get_used_packs_count_for_types([AmmoType.t()], User.t()) ::
|
|
%{optional(AmmoType.id()) => non_neg_integer()}
|
|
def get_used_packs_count_for_types(ammo_types, %User{id: user_id}) do
|
|
ammo_type_ids =
|
|
ammo_types
|
|
|> Enum.map(fn %AmmoType{id: ammo_type_id, user_id: ^user_id} -> ammo_type_id end)
|
|
|
|
Repo.all(
|
|
from ag in Pack,
|
|
where: ag.user_id == ^user_id,
|
|
where: ag.ammo_type_id in ^ammo_type_ids,
|
|
where: ag.count == 0,
|
|
group_by: ag.ammo_type_id,
|
|
select: {ag.ammo_type_id, count(ag.id)}
|
|
)
|
|
|> Map.new()
|
|
end
|
|
|
|
@doc """
|
|
Returns number of ammo packs in a container.
|
|
|
|
## Examples
|
|
|
|
iex> get_packs_count_for_container(
|
|
...> %Container{id: 123, user_id: 456},
|
|
...> %User{id: 456}
|
|
...> )
|
|
3
|
|
|
|
"""
|
|
@spec get_packs_count_for_container!(Container.t(), User.t()) :: non_neg_integer()
|
|
def get_packs_count_for_container!(
|
|
%Container{id: container_id} = container,
|
|
%User{} = user
|
|
) do
|
|
[container]
|
|
|> get_packs_count_for_containers(user)
|
|
|> Map.get(container_id, 0)
|
|
end
|
|
|
|
@doc """
|
|
Returns number of ammo packs in multiple containers.
|
|
|
|
## Examples
|
|
|
|
iex> get_packs_count_for_containers(
|
|
...> [%Container{id: 123, user_id: 456}],
|
|
...> %User{id: 456}
|
|
...> )
|
|
%{123 => 3}
|
|
|
|
"""
|
|
@spec get_packs_count_for_containers([Container.t()], User.t()) :: %{
|
|
Container.id() => non_neg_integer()
|
|
}
|
|
def get_packs_count_for_containers(containers, %User{id: user_id}) do
|
|
container_ids =
|
|
containers
|
|
|> Enum.map(fn %Container{id: container_id, user_id: ^user_id} -> container_id end)
|
|
|
|
Repo.all(
|
|
from ag in Pack,
|
|
where: ag.container_id in ^container_ids,
|
|
where: ag.count > 0,
|
|
group_by: ag.container_id,
|
|
select: {ag.container_id, count(ag.id)}
|
|
)
|
|
|> Map.new()
|
|
end
|
|
|
|
@doc """
|
|
Returns number of rounds in a container.
|
|
|
|
## Examples
|
|
|
|
iex> get_round_count_for_container(
|
|
...> %Container{id: 123, user_id: 456},
|
|
...> %User{id: 456}
|
|
...> )
|
|
5
|
|
|
|
"""
|
|
@spec get_round_count_for_container!(Container.t(), User.t()) :: non_neg_integer()
|
|
def get_round_count_for_container!(%Container{id: container_id} = container, user) do
|
|
[container]
|
|
|> get_round_count_for_containers(user)
|
|
|> Map.get(container_id, 0)
|
|
end
|
|
|
|
@doc """
|
|
Returns number of ammo packs in multiple containers.
|
|
|
|
## Examples
|
|
|
|
iex> get_round_count_for_containers(
|
|
...> [%Container{id: 123, user_id: 456}],
|
|
...> %User{id: 456}
|
|
...> )
|
|
%{123 => 5}
|
|
|
|
"""
|
|
@spec get_round_count_for_containers([Container.t()], User.t()) ::
|
|
%{Container.id() => non_neg_integer()}
|
|
def get_round_count_for_containers(containers, %User{id: user_id}) do
|
|
container_ids =
|
|
containers
|
|
|> Enum.map(fn %Container{id: container_id, user_id: ^user_id} -> container_id end)
|
|
|
|
Repo.all(
|
|
from ag in Pack,
|
|
where: ag.container_id in ^container_ids,
|
|
group_by: ag.container_id,
|
|
select: {ag.container_id, sum(ag.count)}
|
|
)
|
|
|> Map.new()
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of packs.
|
|
|
|
## Examples
|
|
|
|
iex> list_packs(%User{id: 123})
|
|
[%Pack{}, ...]
|
|
|
|
iex> list_packs("cool", %User{id: 123}, true)
|
|
[%Pack{notes: "My cool pack"}, ...]
|
|
|
|
"""
|
|
@spec list_packs(search :: String.t() | nil, AmmoType.class() | :all, User.t()) ::
|
|
[Pack.t()]
|
|
@spec list_packs(
|
|
search :: nil | String.t(),
|
|
AmmoType.class() | :all,
|
|
User.t(),
|
|
show_used :: boolean()
|
|
) :: [Pack.t()]
|
|
def list_packs(search, type, %{id: user_id}, show_used \\ false) do
|
|
from(ag in Pack,
|
|
as: :ag,
|
|
join: at in assoc(ag, :ammo_type),
|
|
as: :at,
|
|
join: c in Container,
|
|
on: ag.container_id == c.id,
|
|
on: ag.user_id == c.user_id,
|
|
as: :c,
|
|
left_join: ct in ContainerTag,
|
|
on: c.id == ct.container_id,
|
|
left_join: t in Tag,
|
|
on: ct.tag_id == t.id,
|
|
on: c.user_id == t.user_id,
|
|
as: :t,
|
|
where: ag.user_id == ^user_id,
|
|
distinct: ag.id,
|
|
preload: ^@pack_preloads
|
|
)
|
|
|> list_packs_filter_on_type(type)
|
|
|> list_packs_show_used(show_used)
|
|
|> list_packs_search(search)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec list_packs_filter_on_type(Queryable.t(), AmmoType.class() | :all) :: Queryable.t()
|
|
defp list_packs_filter_on_type(query, :rifle),
|
|
do: query |> where([at: at], at.class == :rifle)
|
|
|
|
defp list_packs_filter_on_type(query, :pistol),
|
|
do: query |> where([at: at], at.class == :pistol)
|
|
|
|
defp list_packs_filter_on_type(query, :shotgun),
|
|
do: query |> where([at: at], at.class == :shotgun)
|
|
|
|
defp list_packs_filter_on_type(query, _all), do: query
|
|
|
|
@spec list_packs_show_used(Queryable.t(), show_used :: boolean()) :: Queryable.t()
|
|
defp list_packs_show_used(query, true), do: query
|
|
|
|
defp list_packs_show_used(query, _false) do
|
|
query |> where([ag: ag], not (ag.count == 0))
|
|
end
|
|
|
|
@spec list_packs_show_used(Queryable.t(), search :: String.t() | nil) :: Queryable.t()
|
|
defp list_packs_search(query, nil), do: query
|
|
defp list_packs_search(query, ""), do: query
|
|
|
|
defp list_packs_search(query, search) do
|
|
trimmed_search = String.trim(search)
|
|
|
|
query
|
|
|> where(
|
|
[ag: ag, at: at, c: c, t: t],
|
|
fragment(
|
|
"? @@ websearch_to_tsquery('english', ?)",
|
|
ag.search,
|
|
^trimmed_search
|
|
) or
|
|
fragment(
|
|
"? @@ websearch_to_tsquery('english', ?)",
|
|
at.search,
|
|
^trimmed_search
|
|
) or
|
|
fragment(
|
|
"? @@ websearch_to_tsquery('english', ?)",
|
|
c.search,
|
|
^trimmed_search
|
|
) or
|
|
fragment(
|
|
"? @@ websearch_to_tsquery('english', ?)",
|
|
t.search,
|
|
^trimmed_search
|
|
)
|
|
)
|
|
|> order_by(
|
|
[ag: ag],
|
|
desc:
|
|
fragment(
|
|
"ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
|
|
ag.search,
|
|
^trimmed_search
|
|
)
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of staged packs for a user.
|
|
|
|
## Examples
|
|
|
|
iex> list_staged_packs(%User{id: 123})
|
|
[%Pack{}, ...]
|
|
|
|
"""
|
|
@spec list_staged_packs(User.t()) :: [Pack.t()]
|
|
def list_staged_packs(%User{id: user_id}) do
|
|
Repo.all(
|
|
from ag in Pack,
|
|
where: ag.user_id == ^user_id,
|
|
where: ag.staged == true,
|
|
preload: ^@pack_preloads
|
|
)
|
|
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 ag in Pack,
|
|
where: ag.id in ^ids,
|
|
where: ag.user_id == ^user_id,
|
|
preload: ^@pack_preloads,
|
|
select: {ag.id, ag}
|
|
)
|
|
|> 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_used_counts(packs, current_user)
|
|
|
|
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(
|
|
%{"ammo_type_id" => ammo_type_id, "container_id" => container_id} = attrs,
|
|
multiplier,
|
|
user
|
|
)
|
|
when multiplier >= 1 and
|
|
multiplier <= @pack_create_limit and
|
|
ammo_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_ammo_type!(ammo_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(
|
|
%{"ammo_type_id" => ammo_type_id, "container_id" => container_id} = attrs,
|
|
_multiplier,
|
|
user
|
|
)
|
|
when is_binary(ammo_type_id) and is_binary(container_id) do
|
|
changeset =
|
|
%Pack{}
|
|
|> Pack.create_changeset(
|
|
get_ammo_type!(ammo_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
|