Merge pull request #58 from turniere/ticket/TURNIERE-231
Implement Match betting
This commit is contained in:
commit
4384eb18e7
|
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BetsController < ApplicationController
|
||||||
|
before_action :set_match, only: %i[index create]
|
||||||
|
before_action :authenticate_user!, only: %i[create]
|
||||||
|
rescue_from UserServiceError, with: :handle_user_service_error
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @match.bets.group_by(&:team).map { |team, bets|
|
||||||
|
{
|
||||||
|
team: ActiveModelSerializers::SerializableResource.new(team).as_json,
|
||||||
|
bets: bets.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
render json: user_service.bet!(@match, Team.find_by(id: params[:team]))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def user_service
|
||||||
|
@user_service ||= UserService.new current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_match
|
||||||
|
@match = Match.find params[:match_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_user_service_error(exception)
|
||||||
|
render json: { error: exception.message }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UserServiceError < StandardError
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Bet < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :match
|
||||||
|
belongs_to :team, optional: true
|
||||||
|
end
|
||||||
|
|
@ -5,7 +5,9 @@ class Match < ApplicationRecord
|
||||||
|
|
||||||
belongs_to :stage, optional: true
|
belongs_to :stage, optional: true
|
||||||
belongs_to :group, optional: true
|
belongs_to :group, optional: true
|
||||||
|
|
||||||
has_many :match_scores, dependent: :destroy
|
has_many :match_scores, dependent: :destroy
|
||||||
|
has_many :bets, dependent: :destroy
|
||||||
|
|
||||||
validates :match_scores, length: { maximum: 2 }
|
validates :match_scores, length: { maximum: 2 }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ class Team < ApplicationRecord
|
||||||
belongs_to :tournament, optional: true
|
belongs_to :tournament, optional: true
|
||||||
has_many :group_scores, dependent: :destroy
|
has_many :group_scores, dependent: :destroy
|
||||||
has_many :match_scores, dependent: :destroy
|
has_many :match_scores, dependent: :destroy
|
||||||
|
has_many :bets, dependent: :destroy
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@ class User < ApplicationRecord
|
||||||
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
||||||
|
|
||||||
has_many :tournaments, dependent: :destroy
|
has_many :tournaments, dependent: :destroy
|
||||||
|
has_many :bets, dependent: :destroy
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BetSerializer < ApplicationSerializer
|
||||||
|
belongs_to :match
|
||||||
|
belongs_to :team
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UserService
|
||||||
|
def initialize(user)
|
||||||
|
@user = user
|
||||||
|
end
|
||||||
|
|
||||||
|
def bet!(match, team)
|
||||||
|
validate_bet! match, team
|
||||||
|
@user.bets.create! match: match, team: team
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_bet!(match, team)
|
||||||
|
if team.nil?
|
||||||
|
raise UserServiceError, 'Betting on no team in a playoff match is not supported' unless match.group_match?
|
||||||
|
else
|
||||||
|
raise UserServiceError, 'The given team is not involved in the given match' unless match.teams.include? team
|
||||||
|
end
|
||||||
|
raise UserServiceError, 'This user already created a bet on this match' if match.bets.map(&:user).include? @user
|
||||||
|
raise UserServiceError, "Betting is not allowed while match is #{match.state}" \
|
||||||
|
unless %w[not_ready not_started].include? match.state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -6,7 +6,9 @@ Rails.application.routes.draw do
|
||||||
sessions: 'overrides/sessions'
|
sessions: 'overrides/sessions'
|
||||||
}
|
}
|
||||||
|
|
||||||
resources :matches, only: %i[show update]
|
resources :matches, only: %i[show update] do
|
||||||
|
resources :bets, only: %i[index create]
|
||||||
|
end
|
||||||
resources :teams, only: %i[show update]
|
resources :teams, only: %i[show update]
|
||||||
resources :tournaments do
|
resources :tournaments do
|
||||||
resources :statistics, only: %i[index]
|
resources :statistics, only: %i[index]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateBets < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :bets do |t|
|
||||||
|
t.references :user, index: true, null: false, foreign_key: { on_delete: :cascade }
|
||||||
|
t.references :match, index: true, null: false, foreign_key: { on_delete: :cascade }
|
||||||
|
t.references :team, index: true, null: true, foreign_key: { on_delete: :cascade }
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe BetsController, type: :controller do
|
||||||
|
let(:team) do
|
||||||
|
create(:team)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:match) do
|
||||||
|
match = create(:playoff_match)
|
||||||
|
match.bets << create(:bet, team: team)
|
||||||
|
match
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
match_id: match.to_param
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
it 'returns a list of bet counts' do
|
||||||
|
get :index, params: params
|
||||||
|
body = deserialize_response response
|
||||||
|
expect(body.size).to eq(1)
|
||||||
|
expect(body.first[:team][:id]).to eq(team.id)
|
||||||
|
expect(body.first[:bets]).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
let(:create_params) do
|
||||||
|
params.merge(team: team.to_param)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:user_service) do
|
||||||
|
instance_double('UserService')
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:user_service).and_return(user_service)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without authentication headers' do
|
||||||
|
it 'renders an unauthorized error response' do
|
||||||
|
post :create, params: params
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with authentication headers' do
|
||||||
|
before(:each) do
|
||||||
|
apply_authentication_headers_for create(:user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the created bet' do
|
||||||
|
bet = create(:bet)
|
||||||
|
expect(user_service).to receive(:bet!).and_return(bet)
|
||||||
|
post :create, params: create_params
|
||||||
|
expect(response).to be_successful
|
||||||
|
body = deserialize_response(response)
|
||||||
|
expect(body[:id]).to eq(bet.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'given a team' do
|
||||||
|
it 'calls the service' do
|
||||||
|
expect(user_service).to receive(:bet!).with(match, team)
|
||||||
|
post :create, params: create_params
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'given no team' do
|
||||||
|
it 'calls the service' do
|
||||||
|
expect(user_service).to receive(:bet!).with(match, nil)
|
||||||
|
post :create, params: params.merge(team: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on service exception' do
|
||||||
|
it 'returns an error response' do
|
||||||
|
msg = 'an error'
|
||||||
|
expect(user_service).to receive(:bet!).and_raise(UserServiceError, msg)
|
||||||
|
post :create, params: create_params
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(deserialize_response(response)[:error]).to eq(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :bet do
|
||||||
|
user
|
||||||
|
team
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Bet, type: :model do
|
||||||
|
describe 'association' do
|
||||||
|
it { should belong_to :user }
|
||||||
|
it { should belong_to :match }
|
||||||
|
it { should belong_to(:team).optional }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -5,6 +5,7 @@ require 'rails_helper'
|
||||||
RSpec.describe Match, type: :model do
|
RSpec.describe Match, type: :model do
|
||||||
context 'association' do
|
context 'association' do
|
||||||
it { should have_many :match_scores }
|
it { should have_many :match_scores }
|
||||||
|
it { should have_many :bets }
|
||||||
it { should belong_to(:stage).optional }
|
it { should belong_to(:stage).optional }
|
||||||
it { should belong_to(:group).optional }
|
it { should belong_to(:group).optional }
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@ RSpec.describe Team, type: :model do
|
||||||
it { should belong_to(:tournament).optional }
|
it { should belong_to(:tournament).optional }
|
||||||
it { should have_many :group_scores }
|
it { should have_many :group_scores }
|
||||||
it { should have_many :match_scores }
|
it { should have_many :match_scores }
|
||||||
|
it { should have_many :bets }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ require 'rails_helper'
|
||||||
RSpec.describe User, type: :model do
|
RSpec.describe User, type: :model do
|
||||||
describe 'association' do
|
describe 'association' do
|
||||||
it { should have_many :tournaments }
|
it { should have_many :tournaments }
|
||||||
|
it { should have_many :bets }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'validation' do
|
describe 'validation' do
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe UserService do
|
||||||
|
let(:user) do
|
||||||
|
create(:user)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:user_service) do
|
||||||
|
UserService.new(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:team) do
|
||||||
|
create(:team)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_match(involved_team = team, factory = :playoff_match)
|
||||||
|
create(factory, state: :not_started, match_scores: [create(:match_score, team: involved_team)])
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#bet!' do
|
||||||
|
context 'with an unrelated team' do
|
||||||
|
it 'throws an exception' do
|
||||||
|
expect do
|
||||||
|
user_service.bet! build_match(create(:team)), team
|
||||||
|
end.to raise_error(UserServiceError, 'The given team is not involved in the given match')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on a running match' do
|
||||||
|
it 'throws an exception' do
|
||||||
|
match = build_match
|
||||||
|
match.state = :in_progress
|
||||||
|
expect do
|
||||||
|
user_service.bet! match, team
|
||||||
|
end.to raise_error(UserServiceError, 'Betting is not allowed while match is in_progress')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an existing team' do
|
||||||
|
let(:match) do
|
||||||
|
build_match
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:bet) do
|
||||||
|
user_service.bet! match, team
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'associates the bet with the given team' do
|
||||||
|
expect(team.bets.reload).to include(bet)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'associates the bet with the given match' do
|
||||||
|
expect(match.bets.reload).to include(bet)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'associates the bet with the creating user' do
|
||||||
|
expect(user.bets.reload).to include(bet)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an already existing bet' do
|
||||||
|
it 'throws an exception' do
|
||||||
|
match = build_match
|
||||||
|
user_service.bet! match, team
|
||||||
|
user.reload
|
||||||
|
match.reload
|
||||||
|
expect do
|
||||||
|
user_service.bet! match, team
|
||||||
|
end.to raise_error(UserServiceError, 'This user already created a bet on this match')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without a team' do
|
||||||
|
context 'on a playoff stage' do
|
||||||
|
it 'throws an exception' do
|
||||||
|
expect do
|
||||||
|
user_service.bet! build_match, nil
|
||||||
|
end.to raise_error(UserServiceError, 'Betting on no team in a playoff match is not supported')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on a group stage' do
|
||||||
|
it 'succeeds' do
|
||||||
|
user_service.bet! build_match(team, :group_match), nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue