diff --git a/data/slots.csv b/data/slots.csv index 988a2aeb..8a8240b3 100644 --- a/data/slots.csv +++ b/data/slots.csv @@ -1,6 +1,7 @@ probability,multiplier -0.00005,10.0 -0.0001,7.0 -0.003,3.0 -0.009,2.0 -0.05,1.0 +0.3,1.0 +0.08,2.0 +0.04,3.0 +0.025,5.0 +0.00499,10.0 +0.00001,100.0 \ No newline at end of file diff --git a/lib/safira/slots/payout.ex b/lib/safira/slots/payout.ex index 9eb0ee39..7600cf95 100644 --- a/lib/safira/slots/payout.ex +++ b/lib/safira/slots/payout.ex @@ -18,5 +18,7 @@ defmodule Safira.Slots.Payout do payout |> cast(attrs, [:probability, :multiplier]) |> validate_required([:probability, :multiplier]) + |> validate_number(:multiplier, greater_than: 0) + |> validate_number(:probability, greater_than: 0, less_than_or_equal_to: 1) end end diff --git a/lib/safira/slots/slots.ex b/lib/safira/slots/slots.ex index 08d030b1..a3e50b10 100644 --- a/lib/safira/slots/slots.ex +++ b/lib/safira/slots/slots.ex @@ -1,15 +1,19 @@ defmodule Safira.Slots do + @moduledoc """ + The Slots context. + """ + import Ecto.Query, warn: false alias Ecto.Multi alias Safira.Repo + alias Safira.Accounts.Attendee alias Safira.Contest alias Safira.Contest.DailyToken - alias Safira.Slots.Payout - alias Safira.Accounts.Attendee alias Safira.Slots.AttendeePayout + alias Safira.Slots.Payout @doc """ Creates a payout. @@ -32,7 +36,7 @@ defmodule Safira.Slots do def spin(attendee, bet) do spin_transaction(attendee, bet) |> case do - {:error, :attendee, changeset, data} -> + {:error, :attendee_state, changeset, data} -> if Map.get(get_errors(changeset), :token_balance) != nil do {:error, :not_enough_tokens} else diff --git a/lib/safira_web/controllers/slots/slots_controller.ex b/lib/safira_web/controllers/slots/slots_controller.ex index 311f85e4..d02fe02d 100644 --- a/lib/safira_web/controllers/slots/slots_controller.ex +++ b/lib/safira_web/controllers/slots/slots_controller.ex @@ -12,18 +12,24 @@ defmodule SafiraWeb.SlotsController do if is_nil(attendee) do conn |> put_status(:unauthorized) - |> json(%{error: "Only attendees can spin the wheel"}) + |> json(%{error: "Only attendees can play the slots"}) else case Integer.parse(bet) do - {bet, _} -> - case Slots.spin(attendee, bet) do - {:ok, outcome} -> - render(conn, :spin_result, outcome) + {bet, ""} -> + if bet > 0 do + case Slots.spin(attendee, bet) do + {:ok, outcome} -> + render(conn, :spin_result, outcome) - {:error, :not_enough_tokens} -> - conn - |> put_status(:unauthorized) - |> json(%{error: "Insufficient token balance"}) + {:error, :not_enough_tokens} -> + conn + |> put_status(:unauthorized) + |> json(%{error: "Insufficient token balance"}) + end + else + conn + |> put_status(:bad_request) + |> json(%{error: "Bet should be a positive integer"}) end _ -> diff --git a/lib/safira_web/controllers/slots/slots_json.ex b/lib/safira_web/controllers/slots/slots_json.ex index 0c9ef96d..68708920 100644 --- a/lib/safira_web/controllers/slots/slots_json.ex +++ b/lib/safira_web/controllers/slots/slots_json.ex @@ -1,4 +1,6 @@ defmodule SafiraWeb.SlotsJSON do + @moduledoc false + def spin_result(data) do payout = Map.get(data, :payout) tokens = Map.get(data, :tokens) diff --git a/test/factories/slots_factory.ex b/test/factories/slots_factory.ex new file mode 100644 index 00000000..5aa494e7 --- /dev/null +++ b/test/factories/slots_factory.ex @@ -0,0 +1,28 @@ +defmodule Safira.SlotsFactory do + @moduledoc """ + A factory to build all slots related structs + """ + defmacro __using__(_opts) do + quote do + def attendee_payout_factory do + payout = build(:payout) + bet = Enum.random(1..100) + tokens = (payout.multiplier * bet) |> round() + + %Safira.Slots.AttendeePayout{ + attendee: build(:attendee), + payout: payout, + bet: bet, + tokens: tokens + } + end + + def payout_factory do + %Safira.Slots.Payout{ + probability: 0.5, + multiplier: Enum.random(1..10) / 1 + } + end + end + end +end diff --git a/test/safira/slots_test.exs b/test/safira/slots_test.exs new file mode 100644 index 00000000..878a5bc0 --- /dev/null +++ b/test/safira/slots_test.exs @@ -0,0 +1,40 @@ +defmodule Safira.SlotsTest do + use Safira.DataCase + alias Safira.Slots + + describe "payouts" do + alias Safira.Slots.Payout + + @valid_attrs %{ + multiplier: 10.0, + probability: 0.42 + } + + @invalid_attrs1 %{ + multiplier: -1.0, + probability: 0.42 + } + + @invalid_attrs2 %{ + multiplier: 10.0, + probability: 2.0 + } + + @invalid_attrs3 %{ + multiplier: 10.0, + probability: -2.0 + } + + test "create_payout/1 with valid data creates a payout" do + assert {:ok, %Payout{} = payout} = Slots.create_payout(@valid_attrs) + assert payout.multiplier == 10.0 + assert payout.probability == 0.42 + end + + test "create_payout/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Slots.create_payout(@invalid_attrs1) + assert {:error, %Ecto.Changeset{}} = Slots.create_payout(@invalid_attrs2) + assert {:error, %Ecto.Changeset{}} = Slots.create_payout(@invalid_attrs3) + end + end +end diff --git a/test/safira_web/controllers/slots_controller_test.exs b/test/safira_web/controllers/slots_controller_test.exs new file mode 100644 index 00000000..3260d1fe --- /dev/null +++ b/test/safira_web/controllers/slots_controller_test.exs @@ -0,0 +1,81 @@ +defmodule SafiraWeb.SlotsControllerTest do + use SafiraWeb.ConnCase + + describe "spin" do + test "with valid bet (attendee with enough tokens)" do + user = create_user_strategy(:user) + insert(:attendee, user: user, token_balance: 1000) + payout = create_payout_strategy(:payout, probability: 1.0) + bet = Enum.random(1..100) + %{conn: conn, user: _user} = api_authenticate(user) + + conn = + conn + |> post(Routes.slots_path(conn, :spin, bet: bet)) + |> doc() + + assert json_response(conn, 200) == %{ + "multiplier" => payout.multiplier, + "tokens" => (bet * payout.multiplier) |> round() + } + end + + test "with valid bet (attendee without enough tokens)" do + user = create_user_strategy(:user) + insert(:attendee, user: user, token_balance: 0) + create_payout_strategy(:payout, probability: 1.0) + %{conn: conn, user: _user} = api_authenticate(user) + + conn = + conn + |> post(Routes.slots_path(conn, :spin, bet: 100)) + + assert json_response(conn, 401) == %{ + "error" => "Insufficient token balance" + } + end + + test "with invalid bet (float value)" do + user = create_user_strategy(:user) + insert(:attendee, user: user) + create_payout_strategy(:payout, probability: 1.0) + %{conn: conn, user: _user} = api_authenticate(user) + + conn = + conn + |> post(Routes.slots_path(conn, :spin, bet: 1.2)) + + assert json_response(conn, 400) == %{ + "error" => "Bet should be an integer" + } + end + + test "with invalid bet (negative value)" do + user = create_user_strategy(:user) + insert(:attendee, user: user) + create_payout_strategy(:payout, probability: 1.0) + %{conn: conn, user: _user} = api_authenticate(user) + + conn = + conn + |> post(Routes.slots_path(conn, :spin, bet: -1)) + + assert json_response(conn, 400) == %{ + "error" => "Bet should be a positive integer" + } + end + + test "when user is not an attendee" do + user = create_user_strategy(:user) + insert(:staff, user: user) + create_payout_strategy(:payout, probability: 1.0) + %{conn: conn, user: _user} = api_authenticate(user) + + conn = + conn + |> post(Routes.slots_path(conn, :spin, bet: 100)) + + assert json_response(conn, 401)["error"] == "Only attendees can play the slots" + end + end +end diff --git a/test/strategies/payout_strategy.ex b/test/strategies/payout_strategy.ex new file mode 100644 index 00000000..b24cea75 --- /dev/null +++ b/test/strategies/payout_strategy.ex @@ -0,0 +1,16 @@ +defmodule Safira.PayoutStrategy do + @moduledoc """ + ExMachina strategy for creating payouts + """ + use ExMachina.Strategy, function_name: :create_payout_strategy + + def handle_create_payout_strategy(record, _opts) do + {:ok, payout} = + Safira.Slots.create_payout(%{ + probability: record.probability, + multiplier: record.multiplier + }) + + payout + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index 88bb4644..8f0d0a42 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -6,10 +6,12 @@ defmodule Safira.Factory do use Safira.UserStrategy use Safira.PrizeStrategy + use Safira.PayoutStrategy use Safira.AccountsFactory use Safira.ContestFactory use Safira.StoreFactory + use Safira.SlotsFactory use Safira.RouletteFactory use Safira.InteractionFactory end