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 :group, optional: true
|
||||
|
||||
has_many :match_scores, dependent: :destroy
|
||||
has_many :bets, dependent: :destroy
|
||||
|
||||
validates :match_scores, length: { maximum: 2 }
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ class Team < ApplicationRecord
|
|||
belongs_to :tournament, optional: true
|
||||
has_many :group_scores, dependent: :destroy
|
||||
has_many :match_scores, dependent: :destroy
|
||||
has_many :bets, dependent: :destroy
|
||||
|
||||
validates :name, presence: true
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ class User < ApplicationRecord
|
|||
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
||||
|
||||
has_many :tournaments, dependent: :destroy
|
||||
has_many :bets, dependent: :destroy
|
||||
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'
|
||||
}
|
||||
|
||||
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 :tournaments do
|
||||
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
|
||||
context 'association' do
|
||||
it { should have_many :match_scores }
|
||||
it { should have_many :bets }
|
||||
it { should belong_to(:stage).optional }
|
||||
it { should belong_to(:group).optional }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@ RSpec.describe Team, type: :model do
|
|||
it { should belong_to(:tournament).optional }
|
||||
it { should have_many :group_scores }
|
||||
it { should have_many :match_scores }
|
||||
it { should have_many :bets }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ require 'rails_helper'
|
|||
RSpec.describe User, type: :model do
|
||||
describe 'association' do
|
||||
it { should have_many :tournaments }
|
||||
it { should have_many :bets }
|
||||
end
|
||||
|
||||
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