Skip to content

Commit 6ba4fab

Browse files
committed
feat: implement scrim module
- Opponent teams carregando - Scrims funcionando - API com formato padronizado
1 parent 97d5f0b commit 6ba4fab

21 files changed

Lines changed: 1513 additions & 168 deletions

.env.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,12 @@ RACK_ATTACK_PERIOD=300
7171
# Set these in GitHub Secrets for CI/CD workflows
7272

7373
TEST_EMAIL=test@prostaff.gg
74-
TEST_PASSWORD=Test123!@#
74+
TEST_PASSWORD=Test123!@#
75+
76+
# ===========================================
77+
# PandaScore API Integration
78+
# ===========================================
79+
80+
PANDASCORE_API_KEY=your_pandascore_api_key_here
81+
PANDASCORE_BASE_URL=https://api.pandascore.co
82+
PANDASCORE_CACHE_TTL=3600
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
module Api
2+
module V1
3+
module Scrims
4+
# OpponentTeams Controller
5+
#
6+
# Manages opponent team records which are shared across organizations.
7+
# Security note: Update and delete operations are restricted to organizations
8+
# that have used this opponent team in scrims.
9+
#
10+
class OpponentTeamsController < Api::V1::BaseController
11+
include TierAuthorization
12+
13+
before_action :set_opponent_team, only: [:show, :update, :destroy, :scrim_history]
14+
before_action :verify_team_usage!, only: [:update, :destroy]
15+
16+
# GET /api/v1/scrims/opponent_teams
17+
def index
18+
teams = OpponentTeam.all.order(:name)
19+
20+
# Filters
21+
teams = teams.by_region(params[:region]) if params[:region].present?
22+
teams = teams.by_tier(params[:tier]) if params[:tier].present?
23+
teams = teams.by_league(params[:league]) if params[:league].present?
24+
teams = teams.with_scrims if params[:with_scrims] == 'true'
25+
26+
# Search
27+
if params[:search].present?
28+
teams = teams.where('name ILIKE ? OR tag ILIKE ?', "%#{params[:search]}%", "%#{params[:search]}%")
29+
end
30+
31+
# Pagination
32+
page = params[:page] || 1
33+
per_page = params[:per_page] || 20
34+
35+
teams = teams.page(page).per(per_page)
36+
37+
render json: {
38+
data: {
39+
opponent_teams: teams.map { |team| ScrimOpponentTeamSerializer.new(team).as_json },
40+
meta: pagination_meta(teams)
41+
}
42+
}
43+
end
44+
45+
# GET /api/v1/scrims/opponent_teams/:id
46+
def show
47+
render json: { data: ScrimOpponentTeamSerializer.new(@opponent_team, detailed: true).as_json }
48+
end
49+
50+
# GET /api/v1/scrims/opponent_teams/:id/scrim_history
51+
def scrim_history
52+
scrims = current_organization.scrims
53+
.where(opponent_team_id: @opponent_team.id)
54+
.includes(:match)
55+
.order(scheduled_at: :desc)
56+
57+
service = Scrims::ScrimAnalyticsService.new(current_organization)
58+
opponent_stats = service.opponent_performance(@opponent_team.id)
59+
60+
render json: {
61+
data: {
62+
opponent_team: ScrimOpponentTeamSerializer.new(@opponent_team).as_json,
63+
scrims: scrims.map { |scrim| ScrimSerializer.new(scrim).as_json },
64+
stats: opponent_stats
65+
}
66+
}
67+
end
68+
69+
# POST /api/v1/scrims/opponent_teams
70+
def create
71+
team = OpponentTeam.new(opponent_team_params)
72+
73+
if team.save
74+
render json: { data: ScrimOpponentTeamSerializer.new(team).as_json }, status: :created
75+
else
76+
render json: { errors: team.errors.full_messages }, status: :unprocessable_entity
77+
end
78+
end
79+
80+
# PATCH /api/v1/scrims/opponent_teams/:id
81+
def update
82+
if @opponent_team.update(opponent_team_params)
83+
render json: { data: ScrimOpponentTeamSerializer.new(@opponent_team).as_json }
84+
else
85+
render json: { errors: @opponent_team.errors.full_messages }, status: :unprocessable_entity
86+
end
87+
end
88+
89+
# DELETE /api/v1/scrims/opponent_teams/:id
90+
def destroy
91+
# Check if team has scrims from other organizations before deleting
92+
other_org_scrims = @opponent_team.scrims.where.not(organization_id: current_organization.id).exists?
93+
94+
if other_org_scrims
95+
return render json: {
96+
error: 'Cannot delete opponent team that is used by other organizations'
97+
}, status: :unprocessable_entity
98+
end
99+
100+
@opponent_team.destroy
101+
head :no_content
102+
end
103+
104+
private
105+
106+
# Finds opponent team by ID
107+
# Security Note: OpponentTeam is a shared resource across organizations.
108+
# Deletion is restricted to teams without cross-org usage (see destroy action).
109+
# Consider adding organization_id in future for proper multi-tenancy.
110+
def set_opponent_team
111+
@opponent_team = OpponentTeam.find(params[:id])
112+
rescue ActiveRecord::RecordNotFound
113+
render json: { error: 'Opponent team not found' }, status: :not_found
114+
end
115+
116+
# Verifies that current organization has used this opponent team
117+
# Prevents organizations from modifying/deleting teams they haven't interacted with
118+
def verify_team_usage!
119+
has_scrims = current_organization.scrims.exists?(opponent_team_id: @opponent_team.id)
120+
121+
unless has_scrims
122+
render json: {
123+
error: 'You cannot modify this opponent team. Your organization has not played against them.'
124+
}, status: :forbidden
125+
end
126+
end
127+
128+
def opponent_team_params
129+
params.require(:opponent_team).permit(
130+
:name,
131+
:tag,
132+
:region,
133+
:tier,
134+
:league,
135+
:logo_url,
136+
:playstyle_notes,
137+
:contact_email,
138+
:discord_server,
139+
known_players: [],
140+
strengths: [],
141+
weaknesses: [],
142+
recent_performance: {},
143+
preferred_champions: {}
144+
)
145+
end
146+
147+
def pagination_meta(collection)
148+
{
149+
current_page: collection.current_page,
150+
total_pages: collection.total_pages,
151+
total_count: collection.total_count,
152+
per_page: collection.limit_value
153+
}
154+
end
155+
end
156+
end
157+
end
158+
end
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
module Api
2+
module V1
3+
module Scrims
4+
class ScrimsController < Api::V1::BaseController
5+
include TierAuthorization
6+
7+
before_action :set_scrim, only: [:show, :update, :destroy, :add_game]
8+
9+
# GET /api/v1/scrims
10+
def index
11+
scrims = current_organization.scrims
12+
.includes(:opponent_team, :match)
13+
.order(scheduled_at: :desc)
14+
15+
# Filters
16+
scrims = scrims.by_type(params[:scrim_type]) if params[:scrim_type].present?
17+
scrims = scrims.by_focus_area(params[:focus_area]) if params[:focus_area].present?
18+
scrims = scrims.where(opponent_team_id: params[:opponent_team_id]) if params[:opponent_team_id].present?
19+
20+
# Status filter
21+
case params[:status]
22+
when 'upcoming'
23+
scrims = scrims.upcoming
24+
when 'past'
25+
scrims = scrims.past
26+
when 'completed'
27+
scrims = scrims.completed
28+
when 'in_progress'
29+
scrims = scrims.in_progress
30+
end
31+
32+
# Pagination
33+
page = params[:page] || 1
34+
per_page = params[:per_page] || 20
35+
36+
scrims = scrims.page(page).per(per_page)
37+
38+
render json: {
39+
data: {
40+
scrims: scrims.map { |scrim| ScrimSerializer.new(scrim).as_json },
41+
meta: pagination_meta(scrims)
42+
}
43+
}
44+
end
45+
46+
# GET /api/v1/scrims/calendar
47+
def calendar
48+
start_date = params[:start_date]&.to_date || Date.current.beginning_of_month
49+
end_date = params[:end_date]&.to_date || Date.current.end_of_month
50+
51+
scrims = current_organization.scrims
52+
.includes(:opponent_team)
53+
.where(scheduled_at: start_date..end_date)
54+
.order(scheduled_at: :asc)
55+
56+
render json: {
57+
data: {
58+
scrims: scrims.map { |scrim| ScrimSerializer.new(scrim, calendar_view: true).as_json },
59+
start_date: start_date,
60+
end_date: end_date
61+
}
62+
}
63+
end
64+
65+
# GET /api/v1/scrims/analytics
66+
def analytics
67+
service = ::Scrims::Services::ScrimAnalyticsService.new(current_organization)
68+
date_range = (params[:days]&.to_i || 30).days
69+
70+
render json: {
71+
overall_stats: service.overall_stats(date_range: date_range),
72+
by_opponent: service.stats_by_opponent,
73+
by_focus_area: service.stats_by_focus_area,
74+
success_patterns: service.success_patterns,
75+
improvement_trends: service.improvement_trends
76+
}
77+
end
78+
79+
# GET /api/v1/scrims/:id
80+
def show
81+
render json: { data: ScrimSerializer.new(@scrim, detailed: true).as_json }
82+
end
83+
84+
# POST /api/v1/scrims
85+
def create
86+
# Check scrim creation limit
87+
unless current_organization.can_create_scrim?
88+
return render json: {
89+
error: 'Scrim Limit Reached',
90+
message: 'You have reached your monthly scrim limit. Upgrade to create more scrims.'
91+
}, status: :forbidden
92+
end
93+
94+
scrim = current_organization.scrims.new(scrim_params)
95+
96+
if scrim.save
97+
render json: { data: ScrimSerializer.new(scrim).as_json }, status: :created
98+
else
99+
render json: { errors: scrim.errors.full_messages }, status: :unprocessable_entity
100+
end
101+
end
102+
103+
# PATCH /api/v1/scrims/:id
104+
def update
105+
if @scrim.update(scrim_params)
106+
render json: { data: ScrimSerializer.new(@scrim).as_json }
107+
else
108+
render json: { errors: @scrim.errors.full_messages }, status: :unprocessable_entity
109+
end
110+
end
111+
112+
# DELETE /api/v1/scrims/:id
113+
def destroy
114+
@scrim.destroy
115+
head :no_content
116+
end
117+
118+
# POST /api/v1/scrims/:id/add_game
119+
def add_game
120+
victory = params[:victory]
121+
duration = params[:duration]
122+
notes = params[:notes]
123+
124+
if @scrim.add_game_result(victory: victory, duration: duration, notes: notes)
125+
# Update opponent team stats if present
126+
if @scrim.opponent_team.present?
127+
@scrim.opponent_team.update_scrim_stats!(victory: victory)
128+
end
129+
130+
render json: { data: ScrimSerializer.new(@scrim.reload).as_json }
131+
else
132+
render json: { errors: @scrim.errors.full_messages }, status: :unprocessable_entity
133+
end
134+
end
135+
136+
private
137+
138+
def set_scrim
139+
@scrim = current_organization.scrims.find(params[:id])
140+
rescue ActiveRecord::RecordNotFound
141+
render json: { error: 'Scrim not found' }, status: :not_found
142+
end
143+
144+
def scrim_params
145+
params.require(:scrim).permit(
146+
:opponent_team_id,
147+
:match_id,
148+
:scheduled_at,
149+
:scrim_type,
150+
:focus_area,
151+
:pre_game_notes,
152+
:post_game_notes,
153+
:is_confidential,
154+
:visibility,
155+
:games_planned,
156+
:games_completed,
157+
game_results: [],
158+
objectives: {},
159+
outcomes: {}
160+
)
161+
end
162+
163+
def pagination_meta(collection)
164+
{
165+
current_page: collection.current_page,
166+
total_pages: collection.total_pages,
167+
total_count: collection.total_count,
168+
per_page: collection.limit_value
169+
}
170+
end
171+
end
172+
end
173+
end
174+
end

app/models/scrim.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Scrim < ApplicationRecord
3333
scope :completed, -> { where.not(games_completed: nil).where('games_completed >= games_planned') }
3434
scope :in_progress, -> { where.not(games_completed: nil).where('games_completed < games_planned') }
3535
scope :confidential, -> { where(is_confidential: true) }
36-
scope :public, -> { where(is_confidential: false) }
36+
scope :publicly_visible, -> { where(is_confidential: false) }
3737
scope :recent, ->(days = 30) { where('scheduled_at > ?', days.days.ago).order(scheduled_at: :desc) }
3838

3939
# Instance methods

app/modules/authentication/serializers/organization_serializer.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,27 @@ class OrganizationSerializer < Blueprinter::Base
5050
total_users: org.users.count
5151
}
5252
end
53+
54+
# Tier features and capabilities
55+
field :features do |org|
56+
{
57+
can_access_scrims: org.can_access_scrims?,
58+
can_access_competitive_data: org.can_access_competitive_data?,
59+
can_access_predictive_analytics: org.can_access_predictive_analytics?,
60+
available_features: org.available_features,
61+
available_data_sources: org.available_data_sources,
62+
available_analytics: org.available_analytics
63+
}
64+
end
65+
66+
field :limits do |org|
67+
{
68+
max_players: org.tier_limits[:max_players],
69+
max_matches_per_month: org.tier_limits[:max_matches_per_month],
70+
current_players: org.tier_limits[:current_players],
71+
current_monthly_matches: org.tier_limits[:current_monthly_matches],
72+
players_remaining: org.tier_limits[:players_remaining],
73+
matches_remaining: org.tier_limits[:matches_remaining]
74+
}
75+
end
5376
end

0 commit comments

Comments
 (0)