cannery/lib/cannery/ammo.ex

1124 lines
30 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.{AmmoGroup, AmmoType}
alias Ecto.Changeset
@ammo_group_create_limit 10_000
@ammo_group_preloads [:ammo_type]
@ammo_type_preloads [:ammo_groups]
@doc """
Returns the list of ammo_types.
## Examples
iex> list_ammo_types(%User{id: 123})
[%AmmoType{}, ...]
iex> list_ammo_types("cool", %User{id: 123})
[%AmmoType{name: "My cool ammo type"}, ...]
"""
@spec list_ammo_types(User.t()) :: [AmmoType.t()]
@spec list_ammo_types(search :: nil | String.t(), User.t()) :: [AmmoType.t()]
def list_ammo_types(search \\ nil, user)
def list_ammo_types(search, %{id: user_id}) when search |> is_nil() or search == "" do
Repo.all(
from at in AmmoType,
where: at.user_id == ^user_id,
order_by: at.name,
preload: ^@ammo_type_preloads
)
end
def list_ammo_types(search, %{id: user_id}) when search |> is_binary() do
trimmed_search = String.trim(search)
Repo.all(
from at in AmmoType,
where: at.user_id == ^user_id,
where:
fragment(
"? @@ websearch_to_tsquery('english', ?)",
at.search,
^trimmed_search
),
order_by: {
:desc,
fragment(
"ts_rank_cd(?, websearch_to_tsquery('english', ?), 4)",
at.search,
^trimmed_search
)
},
preload: ^@ammo_type_preloads
)
end
@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
)
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 ammo groups 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 ammo groups 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.ammo_group_id,
select: %{ammo_group_id: sg.ammo_group_id, total: sum(sg.count)}
Repo.all(
from ag in AmmoGroup,
as: :ammo_group,
left_join: sg_query in subquery(sg_total_query),
on: ag.id == sg_query.ammo_group_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 AmmoGroup,
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 ammo_groups for a user and type.
## Examples
iex> list_ammo_groups_for_type(
...> %AmmoType{id: 123, user_id: 456},
...> %User{id: 456}
...> )
[%AmmoGroup{}, ...]
iex> list_ammo_groups_for_type(
...> %AmmoType{id: 123, user_id: 456},
...> %User{id: 456},
...> true
...> )
[%AmmoGroup{}, %AmmoGroup{}, ...]
"""
@spec list_ammo_groups_for_type(AmmoType.t(), User.t()) :: [AmmoGroup.t()]
@spec list_ammo_groups_for_type(AmmoType.t(), User.t(), include_empty :: boolean()) ::
[AmmoGroup.t()]
def list_ammo_groups_for_type(ammo_type, user, include_empty \\ false)
def list_ammo_groups_for_type(
%AmmoType{id: ammo_type_id, user_id: user_id},
%User{id: user_id},
true = _include_empty
) do
Repo.all(
from ag in AmmoGroup,
where: ag.ammo_type_id == ^ammo_type_id,
where: ag.user_id == ^user_id,
preload: ^@ammo_group_preloads
)
end
def list_ammo_groups_for_type(
%AmmoType{id: ammo_type_id, user_id: user_id},
%User{id: user_id},
false = _include_empty
) do
Repo.all(
from ag in AmmoGroup,
where: ag.ammo_type_id == ^ammo_type_id,
where: ag.user_id == ^user_id,
where: not (ag.count == 0),
preload: ^@ammo_group_preloads
)
end
@doc """
Returns the list of ammo_groups for a user and container.
## Examples
iex> list_ammo_groups_for_container(
...> %Container{id: 123, user_id: 456},
...> %User{id: 456}
...> )
[%AmmoGroup{}, ...]
iex> list_ammo_groups_for_container(
...> %Container{id: 123, user_id: 456},
...> %User{id: 456},
...> true
...> )
[%AmmoGroup{}, %AmmoGroup{}, ...]
"""
@spec list_ammo_groups_for_container(Container.t(), User.t()) :: [AmmoGroup.t()]
@spec list_ammo_groups_for_container(Container.t(), User.t(), include_empty :: boolean()) ::
[AmmoGroup.t()]
def list_ammo_groups_for_container(container, user, include_empty \\ false)
def list_ammo_groups_for_container(
%Container{id: container_id, user_id: user_id},
%User{id: user_id},
true = _include_empty
) do
Repo.all(
from ag in AmmoGroup,
where: ag.container_id == ^container_id,
where: ag.user_id == ^user_id,
preload: ^@ammo_group_preloads
)
end
def list_ammo_groups_for_container(
%Container{id: container_id, user_id: user_id},
%User{id: user_id},
false = _include_empty
) do
Repo.all(
from ag in AmmoGroup,
where: ag.container_id == ^container_id,
where: ag.user_id == ^user_id,
where: not (ag.count == 0),
preload: ^@ammo_group_preloads
)
end
@doc """
Returns the count of ammo_groups for an ammo type.
## Examples
iex> get_ammo_groups_count_for_type(
...> %AmmoType{id: 123, user_id: 456},
...> %User{id: 456}
...> )
3
iex> get_ammo_groups_count_for_type(
...> %AmmoType{id: 123, user_id: 456},
...> %User{id: 456},
...> true
...> )
5
"""
@spec get_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: non_neg_integer()
@spec get_ammo_groups_count_for_type(AmmoType.t(), User.t(), include_empty :: boolean()) ::
non_neg_integer()
def get_ammo_groups_count_for_type(
%AmmoType{id: ammo_type_id} = ammo_type,
user,
include_empty \\ false
) do
[ammo_type]
|> get_ammo_groups_count_for_types(user, include_empty)
|> Map.get(ammo_type_id, 0)
end
@doc """
Returns the count of ammo_groups for multiple ammo types.
## Examples
iex> get_ammo_groups_count_for_types(
...> [%AmmoType{id: 123, user_id: 456}],
...> %User{id: 456}
...> )
3
iex> get_ammo_groups_count_for_types(
...> [%AmmoType{id: 123, user_id: 456}],
...> %User{id: 456},
...> true
...> )
5
"""
@spec get_ammo_groups_count_for_types([AmmoType.t()], User.t()) ::
%{optional(AmmoType.id()) => non_neg_integer()}
@spec get_ammo_groups_count_for_types([AmmoType.t()], User.t(), include_empty :: boolean()) ::
%{optional(AmmoType.id()) => non_neg_integer()}
def get_ammo_groups_count_for_types(ammo_types, %User{id: user_id}, include_empty \\ 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 AmmoGroup,
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)}
)
|> maybe_include_empty(include_empty)
|> Repo.all()
|> Map.new()
end
defp maybe_include_empty(query, true), do: query
defp maybe_include_empty(query, _false) do
query |> where([ag], not (ag.count == 0))
end
@doc """
Returns the count of used ammo_groups for an ammo type.
## Examples
iex> get_used_ammo_groups_count_for_type(
...> %AmmoType{id: 123, user_id: 456},
...> %User{id: 456}
...> )
3
"""
@spec get_used_ammo_groups_count_for_type(AmmoType.t(), User.t()) :: non_neg_integer()
def get_used_ammo_groups_count_for_type(%AmmoType{id: ammo_type_id} = ammo_type, user) do
[ammo_type]
|> get_used_ammo_groups_count_for_types(user)
|> Map.get(ammo_type_id, 0)
end
@doc """
Returns the count of used ammo_groups for multiple ammo types.
## Examples
iex> get_used_ammo_groups_count_for_types(
...> [%AmmoType{id: 123, user_id: 456}],
...> %User{id: 456}
...> )
%{123 => 3}
"""
@spec get_used_ammo_groups_count_for_types([AmmoType.t()], User.t()) ::
%{optional(AmmoType.id()) => non_neg_integer()}
def get_used_ammo_groups_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 AmmoGroup,
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_ammo_groups_count_for_container(
...> %Container{id: 123, user_id: 456},
...> %User{id: 456}
...> )
3
"""
@spec get_ammo_groups_count_for_container!(Container.t(), User.t()) :: non_neg_integer()
def get_ammo_groups_count_for_container!(
%Container{id: container_id} = container,
%User{} = user
) do
[container]
|> get_ammo_groups_count_for_containers(user)
|> Map.get(container_id, 0)
end
@doc """
Returns number of ammo packs in multiple containers.
## Examples
iex> get_ammo_groups_count_for_containers(
...> [%Container{id: 123, user_id: 456}],
...> %User{id: 456}
...> )
%{123 => 3}
"""
@spec get_ammo_groups_count_for_containers([Container.t()], User.t()) :: %{
Container.id() => non_neg_integer()
}
def get_ammo_groups_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 AmmoGroup,
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 AmmoGroup,
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 ammo_groups.
## Examples
iex> list_ammo_groups(%User{id: 123})
[%AmmoGroup{}, ...]
iex> list_ammo_groups("cool", true, %User{id: 123})
[%AmmoGroup{notes: "My cool ammo group"}, ...]
"""
@spec list_ammo_groups(User.t()) :: [AmmoGroup.t()]
@spec list_ammo_groups(search :: nil | String.t(), User.t()) :: [AmmoGroup.t()]
@spec list_ammo_groups(search :: nil | String.t(), include_empty :: boolean(), User.t()) ::
[AmmoGroup.t()]
def list_ammo_groups(search \\ nil, include_empty \\ false, %{id: user_id}) do
from(
ag in AmmoGroup,
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: ^@ammo_group_preloads
)
|> list_ammo_groups_include_empty(include_empty)
|> list_ammo_groups_search(search)
|> Repo.all()
end
defp list_ammo_groups_include_empty(query, true), do: query
defp list_ammo_groups_include_empty(query, false) do
query |> where([ag], not (ag.count == 0))
end
defp list_ammo_groups_search(query, nil), do: query
defp list_ammo_groups_search(query, ""), do: query
defp list_ammo_groups_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 ammo_groups for a user.
## Examples
iex> list_staged_ammo_groups(%User{id: 123})
[%AmmoGroup{}, ...]
"""
@spec list_staged_ammo_groups(User.t()) :: [AmmoGroup.t()]
def list_staged_ammo_groups(%User{id: user_id}) do
Repo.all(
from ag in AmmoGroup,
where: ag.user_id == ^user_id,
where: ag.staged == true,
preload: ^@ammo_group_preloads
)
end
@doc """
Gets a single ammo_group.
Raises `KeyError` if the Ammo group does not exist.
## Examples
iex> get_ammo_group!(123, %User{id: 123})
%AmmoGroup{}
iex> get_ammo_group!(456, %User{id: 123})
** (KeyError)
"""
@spec get_ammo_group!(AmmoGroup.id(), User.t()) :: AmmoGroup.t()
def get_ammo_group!(id, user) do
[id] |> get_ammo_groups(user) |> Map.fetch!(id)
end
@doc """
Gets a group of ammo_groups by their ID.
## Examples
iex> get_ammo_groups([123, 456], %User{id: 123})
%{123 => %AmmoGroup{}, 456 => %AmmoGroup{}}
"""
@spec get_ammo_groups([AmmoGroup.id()], User.t()) ::
%{optional(AmmoGroup.id()) => AmmoGroup.t()}
def get_ammo_groups(ids, %User{id: user_id}) do
Repo.all(
from ag in AmmoGroup,
where: ag.id in ^ids,
where: ag.user_id == ^user_id,
preload: ^@ammo_group_preloads,
select: {ag.id, ag}
)
|> Map.new()
end
@doc """
Calculates the percentage remaining of an ammo group out of 100
## Examples
iex> get_percentage_remaining(
...> %AmmoGroup{id: 123, count: 5, user_id: 456},
...> %User{id: 456}
...> )
100
"""
@spec get_percentage_remaining(AmmoGroup.t(), User.t()) :: non_neg_integer()
def get_percentage_remaining(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do
[ammo_group]
|> get_percentages_remaining(user)
|> Map.fetch!(ammo_group_id)
end
@doc """
Calculates the percentages remaining of multiple ammo groups out of 100
## Examples
iex> get_percentages_remaining(
...> [%AmmoGroup{id: 123, count: 5, user_id: 456}],
...> %User{id: 456}
...> )
%{123 => 100}
"""
@spec get_percentages_remaining([AmmoGroup.t()], User.t()) ::
%{optional(AmmoGroup.id()) => non_neg_integer()}
def get_percentages_remaining(ammo_groups, %User{id: user_id} = user) do
original_counts = get_original_counts(ammo_groups, user)
ammo_groups
|> Map.new(fn %AmmoGroup{id: ammo_group_id, count: count, user_id: ^user_id} ->
percentage =
case count do
0 -> 0
count -> round(count / Map.fetch!(original_counts, ammo_group_id) * 100)
end
{ammo_group_id, percentage}
end)
end
@doc """
Gets the original count for an ammo group
## Examples
iex> get_original_count(
...> %AmmoGroup{id: 123, count: 5, user_id: 456},
...> %User{id: 456}
...> )
5
"""
@spec get_original_count(AmmoGroup.t(), User.t()) :: non_neg_integer()
def get_original_count(%AmmoGroup{id: ammo_group_id} = ammo_group, current_user) do
[ammo_group]
|> get_original_counts(current_user)
|> Map.fetch!(ammo_group_id)
end
@doc """
Gets the original counts for multiple ammo groups
## Examples
iex> get_original_counts(
...> [%AmmoGroup{id: 123, count: 5, user_id: 456}],
...> %User{id: 456}
...> )
%{123 => 5}
"""
@spec get_original_counts([AmmoGroup.t()], User.t()) ::
%{optional(AmmoGroup.id()) => non_neg_integer()}
def get_original_counts(ammo_groups, %User{id: user_id} = current_user) do
used_counts = ActivityLog.get_used_counts(ammo_groups, current_user)
ammo_groups
|> Map.new(fn %AmmoGroup{id: ammo_group_id, count: count, user_id: ^user_id} ->
{ammo_group_id, count + Map.get(used_counts, ammo_group_id, 0)}
end)
end
@doc """
Calculates the CPR for a single ammo group
## Examples
iex> get_cpr(
...> %AmmoGroup{id: 123, price_paid: 5, count: 5, user_id: 456},
...> %User{id: 456}
...> )
1
"""
@spec get_cpr(AmmoGroup.t(), User.t()) :: float() | nil
def get_cpr(%AmmoGroup{id: ammo_group_id} = ammo_group, user) do
[ammo_group]
|> get_cprs(user)
|> Map.get(ammo_group_id)
end
@doc """
Calculates the CPR for multiple ammo groups
## Examples
iex> get_cprs(
...> [%AmmoGroup{id: 123, price_paid: 5, count: 5, user_id: 456}],
...> %User{id: 456}
...> )
%{123 => 1}
"""
@spec get_cprs([AmmoGroup.t()], User.t()) :: %{optional(AmmoGroup.id()) => float()}
def get_cprs(ammo_groups, %User{id: user_id} = current_user) do
original_counts = get_original_counts(ammo_groups, current_user)
ammo_groups
|> Enum.reject(fn %AmmoGroup{price_paid: price_paid, user_id: ^user_id} ->
price_paid |> is_nil()
end)
|> Map.new(fn %{id: ammo_group_id, price_paid: price_paid} ->
{ammo_group_id, calculate_cpr(price_paid, Map.fetch!(original_counts, ammo_group_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 ammo_groups at once.
## Examples
iex> create_ammo_groups(%{field: value}, 3, %User{id: 123})
{:ok, {3, [%AmmoGroup{}]}}
iex> create_ammo_groups(%{field: bad_value}, 3, %User{id: 123})
{:error, %Changeset{}}
"""
@spec create_ammo_groups(attrs :: map(), multiplier :: non_neg_integer(), User.t()) ::
{:ok, {count :: non_neg_integer(), [AmmoGroup.t()] | nil}}
| {:error, AmmoGroup.changeset()}
def create_ammo_groups(
%{"ammo_type_id" => ammo_type_id, "container_id" => container_id} = attrs,
multiplier,
%User{} = user
)
when multiplier >= 1 and multiplier <= @ammo_group_create_limit and
not (ammo_type_id |> is_nil()) and not (container_id |> is_nil()) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
changesets =
Enum.map(1..multiplier, fn _count ->
%AmmoGroup{}
|> AmmoGroup.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_ammo_groups} =
Repo.insert_all(
AmmoGroup,
changesets
|> Enum.map(fn changeset ->
changeset
|> Map.get(:changes)
|> Map.merge(%{inserted_at: now, updated_at: now})
end),
returning: true
)
{:ok, {count, inserted_ammo_groups |> preload_ammo_group()}}
else
changesets
|> Enum.reject(fn %{valid?: valid} -> valid end)
|> List.first()
|> Changeset.apply_action(:insert)
end
end
def create_ammo_groups(
%{"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 =
%AmmoGroup{}
|> AmmoGroup.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
def create_ammo_groups(invalid_attrs, _multiplier, user) do
{:error, %AmmoGroup{} |> AmmoGroup.create_changeset(nil, nil, user, invalid_attrs)}
end
@spec preload_ammo_group(AmmoGroup.t()) :: AmmoGroup.t()
@spec preload_ammo_group([AmmoGroup.t()]) :: [AmmoGroup.t()]
defp preload_ammo_group(ammo_group_or_ammo_groups) do
ammo_group_or_ammo_groups |> Repo.preload(@ammo_group_preloads)
end
@doc """
Updates a ammo_group.
## Examples
iex> update_ammo_group(ammo_group, %{field: new_value}, %User{id: 123})
{:ok, %AmmoGroup{}}
iex> update_ammo_group(ammo_group, %{field: bad_value}, %User{id: 123})
{:error, %Changeset{}}
"""
@spec update_ammo_group(AmmoGroup.t(), attrs :: map(), User.t()) ::
{:ok, AmmoGroup.t()} | {:error, AmmoGroup.changeset()}
def update_ammo_group(
%AmmoGroup{user_id: user_id} = ammo_group,
attrs,
%User{id: user_id} = user
) do
ammo_group
|> AmmoGroup.update_changeset(attrs, user)
|> Repo.update()
|> case do
{:ok, ammo_group} -> {:ok, ammo_group |> preload_ammo_group()}
{:error, changeset} -> {:error, changeset}
end
end
@doc """
Deletes a ammo_group.
## Examples
iex> delete_ammo_group(ammo_group, %User{id: 123})
{:ok, %AmmoGroup{}}
iex> delete_ammo_group(ammo_group, %User{id: 123})
{:error, %Changeset{}}
"""
@spec delete_ammo_group(AmmoGroup.t(), User.t()) ::
{:ok, AmmoGroup.t()} | {:error, AmmoGroup.changeset()}
def delete_ammo_group(%AmmoGroup{user_id: user_id} = ammo_group, %User{id: user_id}) do
ammo_group
|> Repo.delete()
|> case do
{:ok, ammo_group} -> {:ok, ammo_group |> preload_ammo_group()}
{:error, changeset} -> {:error, changeset}
end
end
@doc """
Deletes a ammo_group.
## Examples
iex> delete_ammo_group!(ammo_group, %User{id: 123})
%AmmoGroup{}
"""
@spec delete_ammo_group!(AmmoGroup.t(), User.t()) :: AmmoGroup.t()
def delete_ammo_group!(ammo_group, user) do
{:ok, ammo_group} = delete_ammo_group(ammo_group, user)
ammo_group
end
end