Merge pull request #58 from turniere/ticket/TURNIERE-231

Implement Match betting
This commit is contained in:
Daniel Schädler 2019-06-17 15:00:03 +02:00 committed by GitHub
commit 4384eb18e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 299 additions and 1 deletions

View File

@ -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

View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
class UserServiceError < StandardError
end

7
app/models/bet.rb Normal file
View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class Bet < ApplicationRecord
belongs_to :user
belongs_to :match
belongs_to :team, optional: true
end

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
class BetSerializer < ApplicationSerializer
belongs_to :match
belongs_to :team
end

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

9
spec/factories/bets.rb Normal file
View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :bet do
user
team
match
end
end

11
spec/models/bet_spec.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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