jumbotron 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/app/adapters/jumbotron/adapters/espn/nfl.rb +15 -0
  3. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/scoreboard.rb +14 -2
  4. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/teams.rb +2 -1
  5. data/app/adapters/jumbotron/adapters/sandbox/nfl.rb +135 -0
  6. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/acquisition_scope.rb +74 -0
  7. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/competition_odds.rb +82 -0
  8. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/current_observation_state.rb +53 -0
  9. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/line_acquisition_scope.rb +30 -0
  10. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/monotonic_observation.rb +113 -0
  11. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/observation_values.rb +53 -0
  12. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/outcome_candidates.rb +38 -0
  13. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/projection.rb +87 -0
  14. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/scoreboard.rb +115 -0
  15. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/synthetic_observation.rb +32 -0
  16. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/timeline_observation.rb +119 -0
  17. data/app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb +14 -1
  18. data/app/deserializers/jumbotron/deserializers/clients/espn/competitor_record.rb +27 -0
  19. data/app/deserializers/jumbotron/deserializers/clients/espn/venue.rb +10 -1
  20. data/app/deserializers/jumbotron/deserializers/clients/espn/venue_address.rb +27 -0
  21. data/app/deserializers/jumbotron/deserializers/public/register_sandbox_projection.rb +42 -0
  22. data/app/deserializers/jumbotron/deserializers/public/reset_sandbox_projection.rb +51 -0
  23. data/app/errors/jumbotron/error.rb +7 -0
  24. data/app/errors/jumbotron/errors/sandbox.rb +67 -0
  25. data/app/errors/jumbotron/sandbox_reset_rejected_error.rb +6 -0
  26. data/app/models/jumbotron/canonical.rb +20 -5
  27. data/app/models/jumbotron/game.rb +11 -0
  28. data/app/models/jumbotron/public/translate_outcome.rb +13 -7
  29. data/app/models/jumbotron/public.rb +31 -6
  30. data/app/models/jumbotron/sandbox_game_mapping.rb +24 -0
  31. data/app/models/jumbotron/sandbox_game_plan.rb +23 -0
  32. data/app/models/jumbotron/sandbox_projection.rb +30 -0
  33. data/app/models/jumbotron/sandbox_projection_data.rb +32 -0
  34. data/app/models/jumbotron/season.rb +10 -0
  35. data/app/providers/jumbotron/providers/sandbox/resources/competition_odds.rb +24 -0
  36. data/app/providers/jumbotron/providers/sandbox/resources/projection.rb +23 -0
  37. data/app/providers/jumbotron/providers/sandbox/resources/scoreboard.rb +24 -0
  38. data/app/services/jumbotron/services/adapters/execute_line_acquisition_scope.rb +2 -1
  39. data/app/services/jumbotron/services/adapters/resolve_game_update_policy.rb +1 -0
  40. data/app/services/jumbotron/services/adapters/select_eligible_games.rb +1 -1
  41. data/app/services/jumbotron/services/canonical/upsert_game_graph.rb +126 -0
  42. data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +114 -6
  43. data/app/services/jumbotron/services/canonical/upsert_team.rb +24 -0
  44. data/app/services/jumbotron/services/canonical/upsert_venue.rb +33 -1
  45. data/app/services/jumbotron/services/public/assemble_public_game.rb +20 -2
  46. data/app/services/jumbotron/services/public/assemble_sandbox_projection.rb +30 -0
  47. data/app/services/jumbotron/services/public/assemble_sandbox_projection_reset.rb +49 -0
  48. data/app/services/jumbotron/services/sandbox/build_source_snapshot.rb +224 -0
  49. data/app/services/jumbotron/services/sandbox/project_game.rb +73 -0
  50. data/app/services/jumbotron/services/sandbox/register_projection.rb +76 -0
  51. data/app/services/jumbotron/services/sandbox/remove_empty_projection_geometry.rb +46 -0
  52. data/app/services/jumbotron/services/sandbox/remove_mapped_game.rb +29 -0
  53. data/app/services/jumbotron/services/sandbox/resolve_game_observation.rb +106 -0
  54. data/app/services/jumbotron/services/sandbox/seed_projected_game.rb +95 -0
  55. data/app/services/jumbotron/services/sandbox/select_line_basis.rb +69 -0
  56. data/app/services/jumbotron/services/sandbox/validate_projected_season.rb +77 -0
  57. data/app/workflows/jumbotron/workflows/public_read.rb +2 -1
  58. data/app/workflows/jumbotron/workflows/sandbox/register_projection_workflow.rb +33 -0
  59. data/app/workflows/jumbotron/workflows/sandbox/reset_projection_workflow.rb +158 -0
  60. data/app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb +3 -51
  61. data/db/migrate/20260828220001_add_game_presentation_contract_fields.rb +19 -0
  62. data/db/migrate/20260908000001_create_jumbotron_sandbox_game_plans.rb +16 -0
  63. data/db/migrate/20260908000002_create_jumbotron_sandbox_projections.rb +39 -0
  64. data/docs/upgrades/0.2.0.md +74 -0
  65. data/lib/jumbotron/client.rb +14 -0
  66. data/lib/jumbotron/version.rb +1 -1
  67. data/spec/factories/jumbotron/sandbox_game_mapping.rb +24 -0
  68. data/spec/factories/jumbotron/sandbox_game_plan.rb +10 -0
  69. data/spec/factories/jumbotron/sandbox_projection.rb +18 -0
  70. metadata +62 -2
@@ -4,13 +4,17 @@ module Jumbotron
4
4
  module Canonical
5
5
  ProviderIdentityRef = Data.define(:provider, :namespace, :id)
6
6
 
7
- TeamInput = Data.define(:provider_identities, :name, :nickname) do
8
- def initialize(provider_identities:, name:, nickname: nil)
7
+ TeamInput = Data.define(:provider_identities, :name, :nickname, :abbreviation) do
8
+ def initialize(provider_identities:, name:, nickname: nil, abbreviation: nil)
9
9
  super
10
10
  end
11
11
  end
12
12
 
13
- VenueInput = Data.define(:provider_identities, :name)
13
+ VenueInput = Data.define(:provider_identities, :name, :city, :region) do
14
+ def initialize(provider_identities:, name:, city: nil, region: nil)
15
+ super
16
+ end
17
+ end
14
18
 
15
19
  ParticipantInput = Data.define(
16
20
  :provider_identities,
@@ -18,9 +22,20 @@ module Jumbotron
18
22
  :role,
19
23
  :score,
20
24
  :result,
21
- :team_nickname
25
+ :team_nickname,
26
+ :team_abbreviation,
27
+ :record_summary
22
28
  ) do
23
- def initialize(provider_identities:, team_name:, role:, score:, result:, team_nickname: nil)
29
+ def initialize(
30
+ provider_identities:,
31
+ team_name:,
32
+ role:,
33
+ score:,
34
+ result:,
35
+ team_nickname: nil,
36
+ team_abbreviation: nil,
37
+ record_summary: nil
38
+ )
24
39
  super
25
40
  end
26
41
  end
@@ -34,6 +34,17 @@ module Jumbotron
34
34
  has_many :teams, through: :game_participants
35
35
  has_many :provider_identities, as: :target, inverse_of: :target, dependent: :restrict_with_exception
36
36
  has_many :line_observations, inverse_of: :game, dependent: :restrict_with_exception
37
+ has_one :sandbox_game_plan, inverse_of: :game, dependent: :restrict_with_exception
38
+ has_many :source_sandbox_game_mappings,
39
+ class_name: "Jumbotron::SandboxGameMapping",
40
+ foreign_key: :source_game_id,
41
+ inverse_of: :source_game,
42
+ dependent: :restrict_with_exception
43
+ has_one :sandbox_game_mapping,
44
+ class_name: "Jumbotron::SandboxGameMapping",
45
+ foreign_key: :sandbox_game_id,
46
+ inverse_of: :sandbox_game,
47
+ dependent: :restrict_with_exception
37
48
 
38
49
  attribute :lifecycle, :string, default: "scheduled"
39
50
  attribute :neutral_site, :boolean, default: false
@@ -10,6 +10,7 @@ module Jumbotron
10
10
  season_not_found
11
11
  season_phase_not_found
12
12
  schedule_group_not_found
13
+ sandbox_not_found
13
14
  ].freeze
14
15
 
15
16
  module_function
@@ -25,29 +26,34 @@ module Jumbotron
25
26
  end
26
27
 
27
28
  def public_error_for(result)
28
- code, message = error_code_and_message(result)
29
+ code, message, details = error_code_message_and_details(result)
29
30
  klass = exception_class_for(code)
30
- klass.new(message)
31
+ klass.new(message, code: code, details: details)
31
32
  end
32
33
  module_function :public_error_for
33
34
 
34
- def error_code_and_message(result)
35
+ def error_code_message_and_details(result) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
35
36
  error = Array(result.errors).first
36
37
  case error
37
38
  when CommandTower::Errors::ApplicationError
38
- [error.code.to_s, error.message]
39
+ [error.code.to_s, error.message, error.details || {}]
39
40
  when Hash
40
- [(error[:code] || error["code"]).to_s, (error[:message] || error["message"]).to_s]
41
+ [
42
+ (error[:code] || error["code"]).to_s,
43
+ (error[:message] || error["message"]).to_s,
44
+ error[:details] || error["details"] || {}
45
+ ]
41
46
  else
42
- ["internal_error", error.to_s]
47
+ ["internal_error", error.to_s, {}]
43
48
  end
44
49
  end
45
- module_function :error_code_and_message
50
+ module_function :error_code_message_and_details
46
51
 
47
52
  def exception_class_for(code)
48
53
  return Jumbotron::NotFoundError if NOT_FOUND_CODES.include?(code)
49
54
  return Jumbotron::InvalidRequestError if code == "invalid_request"
50
55
  return Jumbotron::UnsupportedCapabilityError if code == "unsupported"
56
+ return Jumbotron::SandboxResetRejectedError if code.start_with?("sandbox_", "source_", "anchor_")
51
57
 
52
58
  Jumbotron::Error
53
59
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jumbotron
4
- module Public
4
+ module Public # rubocop:disable Metrics/ModuleLength
5
5
  ALLOWED_INCLUDES = %i[consensus current_lines consensus_constituents].freeze
6
6
 
7
7
  Sport = Data.define(:id, :name)
@@ -10,6 +10,23 @@ module Jumbotron
10
10
  SeasonPhase = Data.define(:id, :name)
11
11
  ScheduleGroup = Data.define(:id, :kind, :number, :name)
12
12
  ScheduleGroupEnumeration = Data.define(:groups, :completeness)
13
+ SandboxScheduleSelector = Data.define(:sport, :league, :season, :season_phase)
14
+ RegisterSandboxProjectionRequest = Data.define(:name, :source)
15
+ ResetSandboxProjectionRequest = Data.define(:sandbox, :start_week, :anchor_day, :now)
16
+ SandboxProjection = Data.define(:name, :source, :sandbox)
17
+ SandboxProjectionReset = Data.define(
18
+ :projection,
19
+ :source_anchor_date,
20
+ :target_anchor_date,
21
+ :date_shift_days,
22
+ :projected_game_count,
23
+ :added_game_count,
24
+ :updated_game_count,
25
+ :removed_game_count,
26
+ :start_week_game_count,
27
+ :naturally_locked_game_count,
28
+ :reset_at
29
+ )
13
30
  ScheduleGroupsRequest = Data.define(
14
31
  :league_id,
15
32
  :sport,
@@ -19,13 +36,21 @@ module Jumbotron
19
36
  :season_phase,
20
37
  :kind
21
38
  )
22
- Team = Data.define(:id, :name, :nickname, :key) do
23
- def initialize(id:, name:, key:, nickname: nil)
24
- super(id:, name:, nickname:, key:)
39
+ Team = Data.define(:id, :name, :nickname, :key, :abbreviation) do
40
+ def initialize(id:, name:, key:, nickname: nil, abbreviation: nil)
41
+ super(id:, name:, nickname:, key:, abbreviation:)
42
+ end
43
+ end
44
+ Venue = Data.define(:id, :name, :city, :region) do
45
+ def initialize(id:, name:, city: nil, region: nil)
46
+ super
47
+ end
48
+ end
49
+ Participant = Data.define(:role, :score, :result, :team, :record) do
50
+ def initialize(role:, score:, result:, team:, record: nil)
51
+ super
25
52
  end
26
53
  end
27
- Venue = Data.define(:id, :name)
28
- Participant = Data.define(:role, :score, :result, :team)
29
54
  GameSegment = Data.define(:kind, :number)
30
55
  # Clock label text. Name is canonical/public contract, not Kernel#display.
31
56
  GameClock = Data.define(:mode, :seconds, :display) # rubocop:disable Lint/DataDefineOverride
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class SandboxGameMapping < ApplicationRecord
5
+ belongs_to :sandbox_projection, inverse_of: :sandbox_game_mappings
6
+ belongs_to :source_game, class_name: "Jumbotron::Game", inverse_of: :source_sandbox_game_mappings
7
+ belongs_to :sandbox_game, class_name: "Jumbotron::Game", inverse_of: :sandbox_game_mapping
8
+
9
+ validates :source_game_id, uniqueness: { scope: :sandbox_projection_id }
10
+ validates :sandbox_game_id, uniqueness: true
11
+ validates :home_spread, :total, numericality: true
12
+ validates :line_fingerprint, presence: true
13
+ validate :games_are_in_expected_worlds
14
+
15
+ private
16
+
17
+ def games_are_in_expected_worlds
18
+ return if source_game.nil? || sandbox_game.nil?
19
+ return if source_game.league.name != "nfl-sandbox" && sandbox_game.league.name == "nfl-sandbox"
20
+
21
+ errors.add(:base, "mapping requires live source and sandbox target games")
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class SandboxGamePlan < ApplicationRecord
5
+ IMMUTABLE_ATTRIBUTES = %w[game_id home_score away_score selected_at].freeze
6
+
7
+ belongs_to :game, inverse_of: :sandbox_game_plan
8
+
9
+ validates :game_id, uniqueness: true
10
+ validates :home_score, :away_score,
11
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
12
+ validates :selected_at, presence: true
13
+ validate :selected_outcome_is_immutable, on: :update
14
+
15
+ private
16
+
17
+ def selected_outcome_is_immutable
18
+ return unless IMMUTABLE_ATTRIBUTES.any? { |attribute| will_save_change_to_attribute?(attribute) }
19
+
20
+ errors.add(:base, "selected sandbox outcome is immutable")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class SandboxProjection < ApplicationRecord
5
+ belongs_to :source_season, class_name: "Jumbotron::Season", inverse_of: :source_sandbox_projections
6
+ belongs_to :source_anchor_phase, class_name: "Jumbotron::SeasonPhase"
7
+ belongs_to :sandbox_season, class_name: "Jumbotron::Season", inverse_of: :sandbox_projection
8
+ has_many :sandbox_game_mappings, inverse_of: :sandbox_projection, dependent: :restrict_with_exception
9
+
10
+ validates :name, presence: true, uniqueness: true, format: { with: /\A[a-z0-9][a-z0-9-]*\z/ }
11
+ validates :adapter_id, inclusion: { in: %w[sandbox_nfl] }
12
+ validates :calendar_time_zone, inclusion: { in: ActiveSupport::TimeZone.all.map(&:tzinfo).map(&:name) }
13
+ validate :anchor_phase_belongs_to_source
14
+ validate :source_and_sandbox_are_distinct
15
+
16
+ private
17
+
18
+ def anchor_phase_belongs_to_source
19
+ return if source_anchor_phase.nil? || source_anchor_phase.season_id == source_season_id
20
+
21
+ errors.add(:source_anchor_phase, "must belong to source season")
22
+ end
23
+
24
+ def source_and_sandbox_are_distinct
25
+ return unless source_season_id == sandbox_season_id
26
+
27
+ errors.add(:sandbox_season, "must differ from source season")
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module SandboxProjectionData
5
+ Team = Data.define(:provider_id, :name, :nickname, :abbreviation)
6
+ Game = Data.define(
7
+ :source_game_id,
8
+ :event_id,
9
+ :competition_id,
10
+ :scheduled_at,
11
+ :season,
12
+ :season_phase,
13
+ :week_number,
14
+ :week_name,
15
+ :neutral_site,
16
+ :home,
17
+ :away,
18
+ :home_spread,
19
+ :total,
20
+ :line_fingerprint,
21
+ :line_source
22
+ )
23
+ Snapshot = Data.define(
24
+ :games,
25
+ :source_anchor_date,
26
+ :target_anchor_date,
27
+ :date_shift_days,
28
+ :fingerprint,
29
+ :start_week
30
+ )
31
+ end
32
+ end
@@ -7,6 +7,16 @@ module Jumbotron
7
7
  has_many :schedule_groups, inverse_of: :season, dependent: :restrict_with_exception
8
8
  has_many :games, inverse_of: :season, dependent: :restrict_with_exception
9
9
  has_many :provider_identities, as: :target, inverse_of: :target, dependent: :restrict_with_exception
10
+ has_many :source_sandbox_projections,
11
+ class_name: "Jumbotron::SandboxProjection",
12
+ foreign_key: :source_season_id,
13
+ inverse_of: :source_season,
14
+ dependent: :restrict_with_exception
15
+ has_one :sandbox_projection,
16
+ class_name: "Jumbotron::SandboxProjection",
17
+ foreign_key: :sandbox_season_id,
18
+ inverse_of: :sandbox_season,
19
+ dependent: :restrict_with_exception
10
20
 
11
21
  validates :name, presence: true
12
22
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Providers
5
+ module Sandbox
6
+ module Resources
7
+ class CompetitionOdds
8
+ def self.acquire(adapter:, **query)
9
+ return invalid_adapter unless adapter.provider == "sandbox"
10
+
11
+ CommandTower::Clients::ClientResult.success(output: query.symbolize_keys)
12
+ end
13
+
14
+ def self.invalid_adapter
15
+ CommandTower::Clients::ClientResult.failure(
16
+ error: ArgumentError.new("sandbox provider requires a sandbox adapter")
17
+ )
18
+ end
19
+ private_class_method :invalid_adapter
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Providers
5
+ module Sandbox
6
+ module Resources
7
+ module Projection
8
+ module_function
9
+
10
+ def acquire(adapter:, **projection)
11
+ unless adapter.provider.to_s == "sandbox"
12
+ return CommandTower::Clients::ClientResult.failure(
13
+ error: ArgumentError.new("sandbox projection requires sandbox adapter")
14
+ )
15
+ end
16
+
17
+ CommandTower::Clients::ClientResult.success(output: projection)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Providers
5
+ module Sandbox
6
+ module Resources
7
+ class Scoreboard
8
+ def self.acquire(adapter:, **query)
9
+ return invalid_adapter unless adapter.provider == "sandbox"
10
+
11
+ CommandTower::Clients::ClientResult.success(output: query.symbolize_keys)
12
+ end
13
+
14
+ def self.invalid_adapter
15
+ CommandTower::Clients::ClientResult.failure(
16
+ error: ArgumentError.new("sandbox provider requires a sandbox adapter")
17
+ )
18
+ end
19
+ private_class_method :invalid_adapter
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -6,6 +6,7 @@ module Jumbotron
6
6
  class ExecuteLineAcquisitionScope < CommandTower::Services::ApplicationService
7
7
  validate :adapter, required: true
8
8
  validate :acquisition, is_a: Hash, required: true
9
+ validate :observed_at, required: false
9
10
 
10
11
  def call
11
12
  if adapter.provider_cooling_down?
@@ -43,7 +44,7 @@ module Jumbotron
43
44
 
44
45
  ingest = adapter.translate_line_odds(
45
46
  provider_result.output,
46
- observed_at: Time.current,
47
+ observed_at: observed_at || Time.current,
47
48
  acquisition: acquisition
48
49
  )
49
50
  persisted = Canonical::PersistLineObservations.call(ingest: ingest)
@@ -8,6 +8,7 @@ module Jumbotron
8
8
  future_game_update
9
9
  live_game_update
10
10
  interrupted_game_update
11
+ post_final_game_update
11
12
  ].freeze
12
13
 
13
14
  validate :adapter_id, required: true
@@ -20,7 +20,7 @@ module Jumbotron
20
20
  end
21
21
 
22
22
  games = Game.where(league_id: league.id)
23
- .includes(:season, :season_phase, :schedule_group)
23
+ .includes(:season, :season_phase, :schedule_group, :game_participants)
24
24
  .select { |game| policy.eligible?(game, now: now) }
25
25
 
26
26
  context.league = league
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Services
5
+ module Canonical
6
+ class UpsertGameGraph < CommandTower::Services::ApplicationService
7
+ validate :game_input, required: true
8
+ validate :league, required: true
9
+ validate :observed_at, required: true
10
+ validate :change_set, required: true
11
+
12
+ def call
13
+ season = resolve(EnsureSeason.call(league: league, year: game_input.season_year), :season)
14
+ return if season.nil?
15
+
16
+ phase = resolve(
17
+ EnsureSeasonPhase.call(season: season, phase_key: game_input.season_phase_key),
18
+ :season_phase
19
+ )
20
+ return if phase.nil?
21
+
22
+ group = resolve_group(season, phase)
23
+ return if context.failure?
24
+
25
+ venue = resolve(
26
+ UpsertVenue.call(
27
+ venue_input: game_input.venue,
28
+ observed_at: observed_at,
29
+ change_set: change_set
30
+ ),
31
+ :venue,
32
+ allow_nil: true
33
+ )
34
+ return if context.failure?
35
+
36
+ game = upsert_game(season, phase, group, venue)
37
+ return if game.nil?
38
+ return unless attach_identities?(game)
39
+ return unless upsert_participants?(game)
40
+
41
+ context.game = game
42
+ end
43
+
44
+ private
45
+
46
+ def resolve_group(season, phase)
47
+ resolve(
48
+ EnsureScheduleGroup.call(
49
+ season: season,
50
+ season_phase: phase,
51
+ schedule_group_input: game_input.schedule_group
52
+ ),
53
+ :schedule_group,
54
+ allow_nil: true
55
+ )
56
+ end
57
+
58
+ def upsert_game(season, phase, group, venue)
59
+ resolve(
60
+ UpsertGame.call(
61
+ game_input: game_input,
62
+ league: league,
63
+ season: season,
64
+ season_phase: phase,
65
+ schedule_group: group,
66
+ venue: venue,
67
+ observed_at: observed_at,
68
+ change_set: change_set
69
+ ),
70
+ :game
71
+ )
72
+ end
73
+
74
+ def attach_identities?(game)
75
+ result = AttachProviderIdentities.call(target: game, refs: game_input.provider_identities)
76
+ return true if result.success?
77
+
78
+ fail_from(result)
79
+ false
80
+ end
81
+
82
+ def upsert_participants?(game)
83
+ game_input.participants.each do |participant_input|
84
+ result = UpsertGameParticipant.call(
85
+ game: game,
86
+ participant_input: participant_input,
87
+ observed_at: observed_at,
88
+ change_set: change_set,
89
+ preserve_existing_team: sandbox_game_input?
90
+ )
91
+ unless result.success?
92
+ fail_from(result)
93
+ return false
94
+ end
95
+ end
96
+ true
97
+ end
98
+
99
+ def sandbox_game_input?
100
+ game_input.provider_identities.any? { |identity| identity.provider == "sandbox" }
101
+ end
102
+
103
+ def resolve(result, key, allow_nil: false)
104
+ unless result.success?
105
+ fail_from(result)
106
+ return
107
+ end
108
+
109
+ value = result.data[key]
110
+ return value if allow_nil || !value.nil?
111
+
112
+ context.fail!(
113
+ application_error: Jumbotron::Errors::Canonical::MutationFailedError.new(
114
+ details: { message: "#{key} was not resolved" }
115
+ )
116
+ )
117
+ nil
118
+ end
119
+
120
+ def fail_from(result)
121
+ context.fail!(application_error: Array(result.errors).first)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end