Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-661] add buyables intermediate table #674

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fda7200
Add item costs to item templates config
Nico-Sanchez May 23, 2024
4274730
Add new ItemCost migration, schema and assocs
Nico-Sanchez May 23, 2024
89c0721
Add ItemCost to seeds
Nico-Sanchez May 23, 2024
9255d77
Add currencies behavior for current buy item endpoint
Nico-Sanchez May 23, 2024
cc2b31f
Remove unnecessary ItemCost table and link ItemTemplate directly with…
Nico-Sanchez May 28, 2024
a0288d5
Update items_templates json
Nico-Sanchez May 28, 2024
9d8aa02
Update buy_item in ItemController to use new associations
Nico-Sanchez May 28, 2024
0fb84be
Update @doc tags due to changes in the code
Nico-Sanchez May 28, 2024
91462b8
Move items templates config to new store config
Nico-Sanchez May 24, 2024
d103c50
Add new store config and move items insertion to CoM Config module
Nico-Sanchez May 24, 2024
eda95a6
Add new Store table and its associations, also remove unnecessary pur…
Nico-Sanchez May 24, 2024
e381f57
Add new StoreController to handle Store requests and Store module for…
Nico-Sanchez May 24, 2024
77e2897
Change Stores function is_available to is_active because makes more s…
Nico-Sanchez May 27, 2024
af11799
Fix list_items_in_store function return for json encoding
Nico-Sanchez May 27, 2024
9318e2b
Add item_in_store/3 function to check if item is in given store
Nico-Sanchez May 27, 2024
9ad570d
Update gateway router with store endpoints
Nico-Sanchez May 27, 2024
e7cb420
Update code due to changes in base-branch
Nico-Sanchez May 28, 2024
a56fa4d
Update list_items_with_prices/1 due to changes in base branch
Nico-Sanchez May 28, 2024
0b6eb54
Fix buy_item/2 handler in store controller
Nico-Sanchez May 28, 2024
c6e7bd0
Delete placeholder endpoint to buy items without a Store
Nico-Sanchez May 28, 2024
c8a277d
Add @doc tag for Stores functions
Nico-Sanchez May 28, 2024
c8505e8
Add TODO tag for buy_item endpoint
Nico-Sanchez May 28, 2024
c4960c2
Add :game_id field to Store table
Nico-Sanchez May 30, 2024
9e59c75
Upsert Store instead of inserting only
Nico-Sanchez May 30, 2024
9a22335
Add new Buyables table and its associations
Nico-Sanchez Jun 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 46 additions & 5 deletions apps/game_backend/lib/game_backend/curse_of_mirra/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ defmodule GameBackend.CurseOfMirra.Config do
"""

alias GameBackend.CurseOfMirra.Quests
alias GameBackend.Utils
alias GameBackend.Stores
alias GameBackend.Repo
alias GameBackend.Users.Currencies.Currency

def import_quest_descriptions_config() do
{:ok, skills_json} =
Expand All @@ -23,12 +27,49 @@ defmodule GameBackend.CurseOfMirra.Config do
|> Map.get(:characters)
end

def get_items_templates_config() do
{:ok, items_config_json} =
Application.app_dir(:game_backend, "priv/items_templates.json")
def import_stores_config() do
curse_of_mirra_id = Utils.get_game_id(:curse_of_mirra)

{:ok, stores_config_json} =
Application.app_dir(:game_backend, "priv/stores_config.json")
|> File.read()

Jason.decode!(items_config_json, [{:keys, :atoms}])
|> Map.get(:items)
Jason.decode!(stores_config_json, [{:keys, :atoms}])
|> Enum.map(fn {store_name, store_info} ->
Map.put(store_info, :name, Atom.to_string(store_name))
|> Map.put(:game_id, curse_of_mirra_id)
|> Map.put(:start_date, datetime_from_string(store_info.start_date))
|> Map.put(:end_date, datetime_from_string(store_info.end_date))
|> Map.put(
:items,
Enum.map(store_info.items, fn item_template ->
purchase_costs =
Enum.map(item_template.purchase_costs, fn purchase_cost ->
Map.put(
purchase_cost,
:currency_id,
Repo.get_by!(Currency, name: purchase_cost.currency, game_id: curse_of_mirra_id)
|> Map.get(:id)
)
end)

Map.put(item_template, :game_id, Utils.get_game_id(:curse_of_mirra))
|> Map.put(:rarity, 0)
|> Map.put(:config_id, item_template.name)
|> Map.put(:purchase_costs, purchase_costs)
end)
)
end)
|> Enum.each(fn store -> Stores.upsert_store(store) end)
end

################## Helpers ##################
defp datetime_from_string(nil), do: nil

defp datetime_from_string(string_date) do
{:ok, datetime, _offset} = DateTime.from_iso8601(string_date)
datetime
end

#############################################
end
39 changes: 35 additions & 4 deletions apps/game_backend/lib/game_backend/items.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule GameBackend.Items do
alias Ecto.Multi
alias GameBackend.Items.Item
alias GameBackend.Items.ItemTemplate
alias GameBackend.Users.Currencies
alias GameBackend.Repo
alias GameBackend.Units

Expand Down Expand Up @@ -202,15 +203,14 @@ defmodule GameBackend.Items do
Receives an item template name and a game id.
Returns {:ok, item_template} if found or {:error, :not_found} otherwise.
"""
def get_purchasable_template_id_by_name_and_game_id(name, game_id) do
def get_template_by_name_and_game_id(name, game_id) do
case Repo.one(
from(it in ItemTemplate,
where: it.name == ^name and it.game_id == ^game_id and it.purchasable?,
select: it.id
where: it.name == ^name and it.game_id == ^game_id
)
) do
nil -> {:error, :not_found}
item_template_id -> {:ok, item_template_id}
item_template -> {:ok, item_template}
end
end

Expand Down Expand Up @@ -244,4 +244,35 @@ defmodule GameBackend.Items do
{:error, :character_cannot_equip}
end
end

@doc """
Gets Item's purchase cost by given currency and item_template.
Returns {:ok, purchase_cost} if found one.
Returns {:error, :not_found} if there are none.
Fails if there are more than one.
"""
def get_purchase_cost_by_currency(currency_id, item_template) do
purchase_cost =
Map.get(item_template, :purchase_costs)
|> Enum.find(fn purchase_cost -> purchase_cost.currency_id == currency_id end)

case purchase_cost do
nil -> {:error, :not_found}
purchase_cost -> {:ok, purchase_cost}
end
end

@doc """
Receives a user_id, an item_template_id and a list of CurrencyCosts.
Inserts new Item from given ItemTemplate for given User.
Substract the amount of Currency to User by given params.
Returns {:ok, map_of_ran_operations} in case of success.
Returns {:error, failed_operation, failed_value, changes_so_far} if one of the operations fail.
"""
def buy_item(user_id, template_id, purchase_costs_list) do
Multi.new()
|> Multi.run(:item, fn _, _ -> insert_item(%{user_id: user_id, template_id: template_id}) end)
|> Multi.run(:currencies, fn _, _ -> Currencies.substract_currencies(user_id, purchase_costs_list) end)
|> Repo.transaction()
end
end
4 changes: 2 additions & 2 deletions apps/game_backend/lib/game_backend/items/item_template.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ defmodule GameBackend.Items.ItemTemplate do
"""
alias GameBackend.Items.Modifier
alias GameBackend.Users.Currencies.CurrencyCost
alias GameBackend.Stores.Buyable

use GameBackend.Schema
import Ecto.Changeset
Expand All @@ -30,9 +31,9 @@ defmodule GameBackend.Items.ItemTemplate do
field(:name, :string)
field(:rarity, :integer)
field(:type, :string)
field(:purchasable?, :boolean)
field(:characters, {:array, :string})
embeds_many(:modifiers, Modifier, on_replace: :delete)
belongs_to(:buyable, Buyable)

# Used to reference the ItemTemplate in the game's configuration
field(:config_id, :string)
Expand All @@ -56,7 +57,6 @@ defmodule GameBackend.Items.ItemTemplate do
:config_id,
:upgrades_from_config_id,
:upgrades_from_quantity,
:purchasable?,
:characters
])
|> validate_required([:game_id, :name, :rarity, :type, :config_id])
Expand Down
87 changes: 87 additions & 0 deletions apps/game_backend/lib/game_backend/stores.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
defmodule GameBackend.Stores do
@moduledoc """
Store operations.
"""
alias GameBackend.Stores.Store
alias GameBackend.Items.ItemTemplate
alias GameBackend.Repo
import Ecto.Query

@doc """
Upserts a Store.
"""
def upsert_store(attrs) do
case get_store_by_name_and_game(attrs.name, attrs.game_id) do
{:ok, store} -> Repo.preload(store, :items) |> Store.changeset(attrs) |> Repo.update()
{:error, :not_found} -> Store.changeset(%Store{}, attrs) |> Repo.insert()
end
end

@doc """
Get Store by name.
"""
def get_store_by_name_and_game(name, game_id) do
case Repo.get_by(Store, name: name, game_id: game_id) do
nil -> {:error, :not_found}
store -> {:ok, store}
end
end

@doc """
Returns {:ok, :available} if we are within Store dates.
Returns {:error, :not_available} otherwise.
"""
def is_active(store) do
now = DateTime.utc_now()

if (is_nil(store.start_date) or store.start_date <= now) and store.end_date >= now do
{:ok, :active}
else
{:error, :not_active}
end
end

@doc """
Returns a list of maps containing each combination of {item, purchase_cost} for given store.

## Examples

iex> list_items_with_prices(%Store{})
[%{"some_item" => %{"Gold" => 1000}}, %{"some_item" => %{"Gems" => 20}}, ...]

"""
def list_items_with_prices(store) do
store = Repo.preload(store, items: [purchase_costs: :currency])

Enum.flat_map(store.items, fn item ->
Enum.map(item.purchase_costs, fn purchase_cost ->
%{item.name => %{purchase_cost.currency.name => purchase_cost.amount}}
end)
end)
end

@doc """
Receives an item_name, a store_id and a game_id.
Returns {:ok, :item_in_store} if there's an item template with given name for given store and game.
Returns {:error, :not_found} otherwise.

## Examples

iex> item_in_store("muflus_gold", "some_store_id", "some_game_id")
{:ok, :item_in_store}

iex> item_in_store("sonic_silver", "some_store_id", "some_game_id")
{:error, :not_found}

"""
def item_in_store(item_name, store_id, game_id) do
case Repo.exists?(
from(it in ItemTemplate,
where: it.name == ^item_name and it.store_id == ^store_id and it.game_id == ^game_id
)
) do
false -> {:error, :not_found}
true -> {:ok, :item_in_store}
end
end
end
34 changes: 34 additions & 0 deletions apps/game_backend/lib/game_backend/stores/buyable.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule GameBackend.Stores.Buyable do
@moduledoc """
Buyable is anything that can be purchased in a Store.
"""

use GameBackend.Schema
import Ecto.Changeset

alias GameBackend.Stores.Store
alias GameBackend.Items.ItemTemplate
alias GameBackend.User.Currencies.Currency
alias GameBackend.User.Currencies.CurrencyCost

schema "buyables" do
field(:name, :string)
field(:stock, :integer)
field(:amount, :integer)
embeds_many(:purchase_costs, CurrencyCost, on_replace: :delete)
belongs_to(:store, Store, on_replace: :delete)
has_one(:currency, Currency)
has_one(:item_template, ItemTemplate)

timestamps()
end

@doc false
def changeset(store, attrs) do
store
|> cast(attrs, [:name, :stock, :amount, :end_date, :store_id])
|> cast_embed(:purchase_costs)
|> validate_required([:name, :stock, :amount])
|> cast_assoc(:items)
end
end
28 changes: 28 additions & 0 deletions apps/game_backend/lib/game_backend/stores/store.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule GameBackend.Stores.Store do
@moduledoc """
Store is the entity where a User can purchase Items.
"""

use GameBackend.Schema
import Ecto.Changeset

alias GameBackend.Items.ItemTemplate

schema "stores" do
field(:game_id, :integer)
field(:name, :string)
field(:start_date, :utc_datetime)
field(:end_date, :utc_datetime)
has_many(:items, ItemTemplate, on_replace: :delete)

timestamps()
end

@doc false
def changeset(store, attrs) do
store
|> cast(attrs, [:game_id, :name, :start_date, :end_date])
|> validate_required([:game_id, :name, :end_date])
|> cast_assoc(:items)
end
end
15 changes: 10 additions & 5 deletions apps/game_backend/lib/game_backend/users/currencies.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ defmodule GameBackend.Users.Currencies do

@doc """
Gets a single currency.

Returns nil if the Currency does not exist.
Returns {:ok, currency} if query succeeds.
Returns {:error, :not_found} if the Currency does not exist.

## Examples

Expand All @@ -60,7 +60,12 @@ defmodule GameBackend.Users.Currencies do
nil

"""
def get_currency_by_name_and_game(name, game_id), do: Repo.get_by(Currency, name: name, game_id: game_id)
def get_currency_by_name_and_game(name, game_id) do
case Repo.get_by(Currency, name: name, game_id: game_id) do
nil -> {:error, :not_found}
currency -> {:ok, currency}
end
end

@doc """
Gets how much a user has of a given currency.
Expand Down Expand Up @@ -205,8 +210,8 @@ defmodule GameBackend.Users.Currencies do
"""
def add_currency_by_name_and_game(user_id, currency_name, game_id, amount) do
case get_currency_by_name_and_game(currency_name, game_id) do
nil -> nil
currency -> add_currency(user_id, currency.id, amount)
{:error, :not_found} -> nil
{:ok, currency} -> add_currency(user_id, currency.id, amount)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ defmodule GameBackend.Users.Currencies.Currency do
@moduledoc """
Currencies.
"""
alias GameBackend.Stores.Buyable

use GameBackend.Schema
import Ecto.Changeset

schema "currencies" do
field(:game_id, :integer)
field(:name, :string)
belongs_to(:buyable, Buyable)
timestamps()
end

@doc false
def changeset(currency, attrs) do
currency
|> cast(attrs, [:game_id, :name])
|> cast(attrs, [:game_id, :name, :buyable_id])
|> validate_required([:game_id, :name])
end
end
19 changes: 15 additions & 4 deletions apps/game_backend/priv/items_templates.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
{
"items": [
{
"name": "Muflus Gold",
"name": "muflus_gold",
"characters": ["muflus"],
"type": "skin",
"purchasable?": true
"purchasable?": true,
"purchase_costs": [
{
"currency": "Gems",
"amount": 100
},
{
"currency": "Gold",
"amount": 1000
}
]
},
{
"name": "H4ck Silver",
"name": "h4ck_silver",
"characters": ["h4ck"],
"type": "skin",
"purchasable?": false
"purchasable?": false,
"purchase_costs": []
}
]
}
Loading