jumbotron 0.1.1

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 (161) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +5 -0
  4. data/app/adapters/jumbotron/adapters/base.rb +136 -0
  5. data/app/adapters/jumbotron/adapters/cadence.rb +42 -0
  6. data/app/adapters/jumbotron/adapters/discovery_definition.rb +7 -0
  7. data/app/adapters/jumbotron/adapters/espn/nfl.rb +169 -0
  8. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/acquisition_scope.rb +29 -0
  9. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/competition_odds.rb +185 -0
  10. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/lifecycle.rb +30 -0
  11. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/line_acquisition_scope.rb +26 -0
  12. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/progress.rb +54 -0
  13. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/scoreboard.rb +146 -0
  14. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/season_phase.rb +33 -0
  15. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/teams.rb +35 -0
  16. data/app/adapters/jumbotron/adapters/line_acquisition.rb +19 -0
  17. data/app/adapters/jumbotron/adapters/operation.rb +23 -0
  18. data/app/adapters/jumbotron/adapters/policy_definition.rb +14 -0
  19. data/app/adapters/jumbotron/adapters/registry.rb +78 -0
  20. data/app/adapters/jumbotron/adapters.rb +11 -0
  21. data/app/boot/jumbotron/boot/enqueue_discoveries.rb +41 -0
  22. data/app/clients/jumbotron/clients/espn/competition_odds/get.rb +26 -0
  23. data/app/clients/jumbotron/clients/espn/competition_odds/get_input.rb +16 -0
  24. data/app/clients/jumbotron/clients/espn/scoreboard/get.rb +18 -0
  25. data/app/clients/jumbotron/clients/espn/scoreboard/get_input.rb +17 -0
  26. data/app/clients/jumbotron/clients/espn/teams/get.rb +18 -0
  27. data/app/clients/jumbotron/clients/espn/teams/get_input.rb +15 -0
  28. data/app/clients/jumbotron/clients/espn.rb +44 -0
  29. data/app/clients/jumbotron/clients.rb +8 -0
  30. data/app/deserializers/jumbotron/deserializers/clients/espn/competition.rb +79 -0
  31. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/draw_odds.rb +31 -0
  32. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/get.rb +66 -0
  33. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/item.rb +60 -0
  34. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/listed_provider.rb +31 -0
  35. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/market_window.rb +31 -0
  36. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/point_spread.rb +27 -0
  37. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/price.rb +35 -0
  38. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/side_quote.rb +35 -0
  39. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/team_odds.rb +40 -0
  40. data/app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb +49 -0
  41. data/app/deserializers/jumbotron/deserializers/clients/espn/event.rb +81 -0
  42. data/app/deserializers/jumbotron/deserializers/clients/espn/scoreboard/get.rb +88 -0
  43. data/app/deserializers/jumbotron/deserializers/clients/espn/season.rb +27 -0
  44. data/app/deserializers/jumbotron/deserializers/clients/espn/status.rb +69 -0
  45. data/app/deserializers/jumbotron/deserializers/clients/espn/support.rb +61 -0
  46. data/app/deserializers/jumbotron/deserializers/clients/espn/team.rb +42 -0
  47. data/app/deserializers/jumbotron/deserializers/clients/espn/teams/get.rb +76 -0
  48. data/app/deserializers/jumbotron/deserializers/clients/espn/venue.rb +34 -0
  49. data/app/deserializers/jumbotron/deserializers/clients/espn/week.rb +23 -0
  50. data/app/deserializers/jumbotron/deserializers/public/game.rb +26 -0
  51. data/app/deserializers/jumbotron/deserializers/public/game_ids.rb +28 -0
  52. data/app/deserializers/jumbotron/deserializers/public/includes.rb +32 -0
  53. data/app/deserializers/jumbotron/deserializers/public/schedule.rb +151 -0
  54. data/app/deserializers/jumbotron/deserializers/public/schedule_groups.rb +91 -0
  55. data/app/errors/jumbotron/error.rb +6 -0
  56. data/app/errors/jumbotron/errors/adapters.rb +143 -0
  57. data/app/errors/jumbotron/errors/canonical.rb +67 -0
  58. data/app/errors/jumbotron/errors/public.rb +27 -0
  59. data/app/errors/jumbotron/invalid_request_error.rb +6 -0
  60. data/app/errors/jumbotron/not_found_error.rb +6 -0
  61. data/app/errors/jumbotron/unsupported_capability_error.rb +6 -0
  62. data/app/jobs/jumbotron/application_job.rb +6 -0
  63. data/app/jobs/jumbotron/discovery_job.rb +14 -0
  64. data/app/jobs/jumbotron/game_update_job.rb +12 -0
  65. data/app/jobs/jumbotron/line_update_job.rb +12 -0
  66. data/app/models/jumbotron/application_record.rb +8 -0
  67. data/app/models/jumbotron/bookmaker.rb +11 -0
  68. data/app/models/jumbotron/canonical.rb +128 -0
  69. data/app/models/jumbotron/game.rb +92 -0
  70. data/app/models/jumbotron/game_participant.rb +10 -0
  71. data/app/models/jumbotron/historical_change.rb +9 -0
  72. data/app/models/jumbotron/league.rb +12 -0
  73. data/app/models/jumbotron/line_observation.rb +18 -0
  74. data/app/models/jumbotron/observation_batch.rb +14 -0
  75. data/app/models/jumbotron/provider_identity.rb +32 -0
  76. data/app/models/jumbotron/public/translate_outcome.rb +57 -0
  77. data/app/models/jumbotron/public.rb +88 -0
  78. data/app/models/jumbotron/schedule_group.rb +22 -0
  79. data/app/models/jumbotron/season.rb +13 -0
  80. data/app/models/jumbotron/season_phase.rb +12 -0
  81. data/app/models/jumbotron/sport.rb +10 -0
  82. data/app/models/jumbotron/team.rb +11 -0
  83. data/app/models/jumbotron/venue.rb +10 -0
  84. data/app/providers/jumbotron/providers/espn/cooldown.rb +88 -0
  85. data/app/providers/jumbotron/providers/espn/cooldown_active_error.rb +13 -0
  86. data/app/providers/jumbotron/providers/espn/provider_failure.rb +40 -0
  87. data/app/providers/jumbotron/providers/espn/resources/competition_odds.rb +29 -0
  88. data/app/providers/jumbotron/providers/espn/resources/scoreboard.rb +90 -0
  89. data/app/providers/jumbotron/providers/espn/resources/teams.rb +19 -0
  90. data/app/scheduling/jumbotron/scheduling/backends/sidekiq_cron.rb +70 -0
  91. data/app/scheduling/jumbotron/scheduling/backends/solid_queue.rb +92 -0
  92. data/app/scheduling/jumbotron/scheduling/clock.rb +61 -0
  93. data/app/scheduling/jumbotron/scheduling/desired_schedules.rb +51 -0
  94. data/app/scheduling/jumbotron/scheduling/detect_integration.rb +45 -0
  95. data/app/scheduling/jumbotron/scheduling/materializer.rb +13 -0
  96. data/app/scheduling/jumbotron/scheduling/owned.rb +19 -0
  97. data/app/scheduling/jumbotron/scheduling/schedule_definition.rb +7 -0
  98. data/app/serializers/jumbotron/serializers/clients/espn/competition_odds/get.rb +17 -0
  99. data/app/serializers/jumbotron/serializers/clients/espn/scoreboard/get.rb +22 -0
  100. data/app/serializers/jumbotron/serializers/clients/espn/teams/get.rb +20 -0
  101. data/app/services/jumbotron/services/adapters/build_acquisition_scopes.rb +37 -0
  102. data/app/services/jumbotron/services/adapters/build_line_acquisition_scopes.rb +39 -0
  103. data/app/services/jumbotron/services/adapters/ensure_league.rb +29 -0
  104. data/app/services/jumbotron/services/adapters/execute_endpoint.rb +49 -0
  105. data/app/services/jumbotron/services/adapters/execute_line_acquisition_scope.rb +75 -0
  106. data/app/services/jumbotron/services/adapters/execute_synchronization_scope.rb +64 -0
  107. data/app/services/jumbotron/services/adapters/resolve_discovery.rb +53 -0
  108. data/app/services/jumbotron/services/adapters/resolve_game_update_policy.rb +74 -0
  109. data/app/services/jumbotron/services/adapters/resolve_line_update_policy.rb +74 -0
  110. data/app/services/jumbotron/services/adapters/select_eligible_games.rb +41 -0
  111. data/app/services/jumbotron/services/adapters/validate_sync_request.rb +51 -0
  112. data/app/services/jumbotron/services/canonical/attach_provider_identities.rb +59 -0
  113. data/app/services/jumbotron/services/canonical/derive_consensus_for_games.rb +18 -0
  114. data/app/services/jumbotron/services/canonical/derive_consensus_line.rb +70 -0
  115. data/app/services/jumbotron/services/canonical/ensure_bookmaker.rb +70 -0
  116. data/app/services/jumbotron/services/canonical/ensure_schedule_group.rb +40 -0
  117. data/app/services/jumbotron/services/canonical/ensure_season.rb +22 -0
  118. data/app/services/jumbotron/services/canonical/ensure_season_phase.rb +22 -0
  119. data/app/services/jumbotron/services/canonical/persist_line_observations.rb +145 -0
  120. data/app/services/jumbotron/services/canonical/record_observation_evidence.rb +51 -0
  121. data/app/services/jumbotron/services/canonical/resolve_current_lines.rb +21 -0
  122. data/app/services/jumbotron/services/canonical/resolve_current_lines_for_games.rb +69 -0
  123. data/app/services/jumbotron/services/canonical/upsert_game.rb +186 -0
  124. data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +88 -0
  125. data/app/services/jumbotron/services/canonical/upsert_team.rb +90 -0
  126. data/app/services/jumbotron/services/canonical/upsert_venue.rb +92 -0
  127. data/app/services/jumbotron/services/public/assemble_public_game.rb +131 -0
  128. data/app/services/jumbotron/services/public/assemble_public_schedule.rb +63 -0
  129. data/app/services/jumbotron/services/public/derive_consensus_for_games.rb +21 -0
  130. data/app/services/jumbotron/services/public/load_games_by_ids.rb +18 -0
  131. data/app/services/jumbotron/services/public/load_included_lines.rb +47 -0
  132. data/app/services/jumbotron/services/public/map_public_consensus_lines.rb +44 -0
  133. data/app/services/jumbotron/services/public/map_public_current_lines.rb +27 -0
  134. data/app/services/jumbotron/services/public/read_game.rb +32 -0
  135. data/app/services/jumbotron/services/public/read_schedule.rb +116 -0
  136. data/app/services/jumbotron/services/public/read_schedule_groups.rb +90 -0
  137. data/app/services/jumbotron/services/public/resolve_current_lines_for_games.rb +21 -0
  138. data/app/synchronization/jumbotron/synchronization/lease.rb +127 -0
  139. data/app/workflows/jumbotron/workflows/execute_discovery_workflow.rb +131 -0
  140. data/app/workflows/jumbotron/workflows/execute_game_update_policy_workflow.rb +159 -0
  141. data/app/workflows/jumbotron/workflows/execute_line_update_policy_workflow.rb +156 -0
  142. data/app/workflows/jumbotron/workflows/public_read.rb +29 -0
  143. data/app/workflows/jumbotron/workflows/read_consensus_workflow.rb +37 -0
  144. data/app/workflows/jumbotron/workflows/read_current_lines_workflow.rb +27 -0
  145. data/app/workflows/jumbotron/workflows/read_game_workflow.rb +34 -0
  146. data/app/workflows/jumbotron/workflows/read_schedule_groups_workflow.rb +21 -0
  147. data/app/workflows/jumbotron/workflows/read_schedule_workflow.rb +48 -0
  148. data/app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb +183 -0
  149. data/db/migrate/20260812000001_create_jumbotron_foundational_catalog.rb +68 -0
  150. data/db/migrate/20260812000002_create_jumbotron_game_graph.rb +70 -0
  151. data/db/migrate/20260812000003_add_freshness_to_jumbotron_provider_mutated_entities.rb +31 -0
  152. data/db/migrate/20260812000004_create_jumbotron_observation_evidence.rb +34 -0
  153. data/db/migrate/20260813000001_create_jumbotron_schedule_groups.rb +24 -0
  154. data/db/migrate/20260814000001_create_jumbotron_line_observations.rb +38 -0
  155. data/db/migrate/20260820000001_add_progress_to_jumbotron_games.rb +14 -0
  156. data/lib/jumbotron/client.rb +40 -0
  157. data/lib/jumbotron/engine.rb +32 -0
  158. data/lib/jumbotron/version.rb +5 -0
  159. data/lib/jumbotron.rb +13 -0
  160. data/lib/tasks/jumbotron_schedules.rake +12 -0
  161. metadata +239 -0
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Workflows
5
+ class SynchronizeAdapterWorkflow < CommandTower::Workflows::ApplicationWorkflow
6
+ retry_strategy :none
7
+
8
+ def call(adapter:, endpoint:, league_id:, observed_at: nil, acquisition: {})
9
+ observed_at ||= Time.current
10
+
11
+ validated = Jumbotron::Services::Adapters::ValidateSyncRequest.call(
12
+ adapter: adapter,
13
+ endpoint: endpoint,
14
+ league_id: league_id
15
+ )
16
+ return map_service_failure(validated, failure_class: "validation") unless validated.success?
17
+
18
+ executed = Jumbotron::Services::Adapters::ExecuteEndpoint.call(
19
+ adapter: adapter,
20
+ endpoint: endpoint,
21
+ observed_at: observed_at,
22
+ acquisition: acquisition || {}
23
+ )
24
+ return map_execute_failure(executed) unless executed.success?
25
+
26
+ transaction do
27
+ apply_canonical_sync!(
28
+ input: executed.data[:sync_input],
29
+ league: validated.data[:league],
30
+ provider: adapter.provider,
31
+ adapter_scope: "#{adapter.provider}:#{adapter.sport}:#{adapter.league}:#{endpoint}"
32
+ )
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def apply_canonical_sync!(input:, league:, provider:, adapter_scope:)
39
+ unless input.is_a?(Jumbotron::Canonical::SyncInput)
40
+ fail_transaction!(
41
+ map_service_failure(
42
+ CommandTower::Services::ServiceResult.failure(
43
+ errors: [Jumbotron::Errors::Canonical::InvalidSyncInputError.new]
44
+ ),
45
+ failure_class: "mutation"
46
+ )
47
+ )
48
+ end
49
+
50
+ change_set = Jumbotron::Canonical::ChangeSet.new
51
+ teams = []
52
+
53
+ games = input.games.map do |game_input|
54
+ sync_one_game!(game_input, league, input.observed_at, change_set)
55
+ end
56
+
57
+ input.teams.each do |team_input|
58
+ team_result = Services::Canonical::UpsertTeam.call(
59
+ team_input: team_input,
60
+ observed_at: input.observed_at,
61
+ change_set: change_set
62
+ )
63
+ fail_on_service_failure!(team_result)
64
+ teams << team_result.data[:team]
65
+ end
66
+
67
+ evidence = Services::Canonical::RecordObservationEvidence.call(
68
+ change_set: change_set,
69
+ provider: provider,
70
+ adapter_scope: adapter_scope,
71
+ observed_at: input.observed_at
72
+ )
73
+ fail_on_service_failure!(evidence)
74
+
75
+ success(
76
+ payload: {
77
+ games: games,
78
+ teams: teams,
79
+ observation_batch_id: evidence.data[:observation_batch_id],
80
+ historical_change_count: evidence.data[:historical_change_count]
81
+ },
82
+ http_status: :ok
83
+ )
84
+ rescue ActiveRecord::RecordInvalid => e
85
+ fail_transaction!(
86
+ failure(
87
+ errors: [
88
+ Jumbotron::Errors::Canonical::MutationFailedError.new(details: { message: e.message })
89
+ ],
90
+ http_status: :unprocessable_entity,
91
+ meta: { failure_class: "mutation" }
92
+ )
93
+ )
94
+ end
95
+
96
+ def sync_one_game!(game_input, league, observed_at, change_set)
97
+ season_result = Services::Canonical::EnsureSeason.call(
98
+ league: league,
99
+ year: game_input.season_year
100
+ )
101
+ fail_on_service_failure!(season_result)
102
+
103
+ phase_result = Services::Canonical::EnsureSeasonPhase.call(
104
+ season: season_result.data[:season],
105
+ phase_key: game_input.season_phase_key
106
+ )
107
+ fail_on_service_failure!(phase_result)
108
+
109
+ group_result = Services::Canonical::EnsureScheduleGroup.call(
110
+ season: season_result.data[:season],
111
+ season_phase: phase_result.data[:season_phase],
112
+ schedule_group_input: game_input.schedule_group
113
+ )
114
+ fail_on_service_failure!(group_result)
115
+
116
+ venue_result = Services::Canonical::UpsertVenue.call(
117
+ venue_input: game_input.venue,
118
+ observed_at: observed_at,
119
+ change_set: change_set
120
+ )
121
+ fail_on_service_failure!(venue_result)
122
+
123
+ game_result = Services::Canonical::UpsertGame.call(
124
+ game_input: game_input,
125
+ league: league,
126
+ season: season_result.data[:season],
127
+ season_phase: phase_result.data[:season_phase],
128
+ schedule_group: group_result.data[:schedule_group],
129
+ venue: venue_result.data[:venue],
130
+ observed_at: observed_at,
131
+ change_set: change_set
132
+ )
133
+ fail_on_service_failure!(game_result)
134
+
135
+ game = game_result.data[:game]
136
+
137
+ attach = Services::Canonical::AttachProviderIdentities.call(
138
+ target: game,
139
+ refs: game_input.provider_identities
140
+ )
141
+ fail_on_service_failure!(attach)
142
+
143
+ game_input.participants.each do |participant_input|
144
+ gp_result = Services::Canonical::UpsertGameParticipant.call(
145
+ game: game,
146
+ participant_input: participant_input,
147
+ observed_at: observed_at,
148
+ change_set: change_set
149
+ )
150
+ fail_on_service_failure!(gp_result)
151
+ end
152
+
153
+ game
154
+ end
155
+
156
+ def fail_on_service_failure!(service_result)
157
+ return if service_result.success?
158
+
159
+ fail_transaction!(map_service_failure(service_result, failure_class: "mutation"))
160
+ end
161
+
162
+ def map_execute_failure(result)
163
+ error = Array(result.errors).first
164
+ code = error.respond_to?(:code) ? error.code : "normalization_failed"
165
+ failure(
166
+ errors: [{ code: code, message: error.respond_to?(:message) ? error.message : error.to_s }],
167
+ http_status: code == "acquisition_failed" ? :bad_gateway : :unprocessable_entity,
168
+ meta: { failure_class: code == "acquisition_failed" ? "acquisition" : "normalization" }
169
+ )
170
+ end
171
+
172
+ def map_service_failure(result, failure_class:)
173
+ error = Array(result.errors).first
174
+ code = error.respond_to?(:code) ? error.code : "#{failure_class}_failed"
175
+ failure(
176
+ errors: [{ code: code, message: error.respond_to?(:message) ? error.message : error.to_s }],
177
+ http_status: :unprocessable_entity,
178
+ meta: { failure_class: failure_class }
179
+ )
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateJumbotronFoundationalCatalog < ActiveRecord::Migration[8.1]
4
+ def change
5
+ create_sports
6
+ create_leagues
7
+ create_seasons
8
+ create_season_phases
9
+ create_provider_identities
10
+ index_provider_identities
11
+ end
12
+
13
+ private
14
+
15
+ def create_sports
16
+ create_table :jumbotron_sports do |t|
17
+ t.string :name, null: false
18
+ t.timestamps
19
+ end
20
+ end
21
+
22
+ def create_leagues
23
+ create_table :jumbotron_leagues do |t|
24
+ t.references :sport, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_sports }
25
+ t.string :name, null: false
26
+ t.timestamps
27
+ end
28
+ end
29
+
30
+ def create_seasons
31
+ create_table :jumbotron_seasons do |t|
32
+ t.references :league, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_leagues }
33
+ t.string :name, null: false
34
+ t.date :starts_on
35
+ t.date :ends_on
36
+ t.timestamps
37
+ end
38
+ end
39
+
40
+ def create_season_phases
41
+ create_table :jumbotron_season_phases do |t|
42
+ t.references :season, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_seasons }
43
+ t.string :name, null: false
44
+ t.timestamps
45
+ end
46
+ end
47
+
48
+ def create_provider_identities
49
+ create_table :jumbotron_provider_identities do |t|
50
+ t.string :provider, null: false, limit: 64
51
+ t.string :object_namespace, null: false, limit: 64
52
+ t.string :provider_id, null: false, limit: 191
53
+ t.string :target_type, null: false
54
+ t.bigint :target_id, null: false
55
+ t.timestamps
56
+ end
57
+ end
58
+
59
+ def index_provider_identities
60
+ add_index :jumbotron_provider_identities,
61
+ %i[provider object_namespace provider_id],
62
+ unique: true,
63
+ name: "idx_jumbotron_pi_identity"
64
+ add_index :jumbotron_provider_identities,
65
+ %i[target_type target_id],
66
+ name: "idx_jumbotron_pi_target"
67
+ end
68
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateJumbotronGameGraph < ActiveRecord::Migration[8.1]
4
+ def change
5
+ create_teams
6
+ create_venues
7
+ create_bookmakers
8
+ create_games
9
+ create_game_participants
10
+ index_game_participants
11
+ end
12
+
13
+ private
14
+
15
+ def create_teams
16
+ create_table :jumbotron_teams do |t|
17
+ t.string :name, null: false
18
+ t.timestamps
19
+ end
20
+ end
21
+
22
+ def create_venues
23
+ create_table :jumbotron_venues do |t|
24
+ t.string :name, null: false
25
+ t.timestamps
26
+ end
27
+ end
28
+
29
+ def create_bookmakers
30
+ create_table :jumbotron_bookmakers do |t|
31
+ t.string :name, null: false
32
+ t.timestamps
33
+ end
34
+ end
35
+
36
+ def create_games
37
+ create_table :jumbotron_games do |t|
38
+ add_game_references(t)
39
+ t.datetime :scheduled_at
40
+ t.string :lifecycle, null: false, default: "scheduled"
41
+ t.boolean :neutral_site, null: false, default: false
42
+ t.timestamps
43
+ end
44
+ end
45
+
46
+ def add_game_references(table)
47
+ table.references :league, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_leagues }
48
+ table.references :season, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_seasons }
49
+ table.references :season_phase, type: :bigint, foreign_key: { to_table: :jumbotron_season_phases }
50
+ table.references :venue, type: :bigint, foreign_key: { to_table: :jumbotron_venues }
51
+ end
52
+
53
+ def create_game_participants
54
+ create_table :jumbotron_game_participants do |t|
55
+ t.references :game, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_games }
56
+ t.references :team, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_teams }
57
+ t.string :role
58
+ t.integer :score
59
+ t.string :result
60
+ t.timestamps
61
+ end
62
+ end
63
+
64
+ def index_game_participants
65
+ add_index :jumbotron_game_participants,
66
+ %i[game_id team_id],
67
+ unique: true,
68
+ name: "idx_jumbotron_gp_game_team"
69
+ end
70
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddFreshnessToJumbotronProviderMutatedEntities < ActiveRecord::Migration[7.2]
4
+ TABLES = %w[
5
+ jumbotron_games
6
+ jumbotron_teams
7
+ jumbotron_venues
8
+ jumbotron_game_participants
9
+ ].freeze
10
+
11
+ def up
12
+ TABLES.each do |table|
13
+ add_column table, :observed_at, :datetime
14
+ add_column table, :changed_at, :datetime
15
+ execute <<~SQL.squish
16
+ UPDATE #{table}
17
+ SET observed_at = created_at, changed_at = created_at
18
+ WHERE observed_at IS NULL OR changed_at IS NULL
19
+ SQL
20
+ change_column_null table, :observed_at, false
21
+ change_column_null table, :changed_at, false
22
+ end
23
+ end
24
+
25
+ def down
26
+ TABLES.each do |table|
27
+ remove_column table, :observed_at
28
+ remove_column table, :changed_at
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateJumbotronObservationEvidence < ActiveRecord::Migration[7.2]
4
+ def change
5
+ create_table :jumbotron_observation_batches do |t|
6
+ t.string :provider, null: false
7
+ t.string :adapter_scope, null: false
8
+ t.datetime :observed_at, null: false
9
+ t.json :metadata
10
+ t.timestamps
11
+ end
12
+ add_index :jumbotron_observation_batches,
13
+ %i[provider adapter_scope observed_at],
14
+ name: "idx_jumbotron_obs_batches_scope_time"
15
+
16
+ create_table :jumbotron_historical_changes do |t|
17
+ t.references :observation_batch,
18
+ null: false,
19
+ foreign_key: { to_table: :jumbotron_observation_batches },
20
+ index: { name: "idx_jumbotron_hist_changes_batch" }
21
+ t.string :subject_type, null: false
22
+ t.bigint :subject_id, null: false
23
+ t.string :attribute_name, null: false
24
+ t.json :previous_value
25
+ t.json :new_value
26
+ t.datetime :observed_at, null: false
27
+ t.string :provider, null: false
28
+ t.timestamps
29
+ end
30
+ add_index :jumbotron_historical_changes,
31
+ %i[subject_type subject_id],
32
+ name: "idx_jumbotron_hist_changes_subject"
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateJumbotronScheduleGroups < ActiveRecord::Migration[8.1]
4
+ def change
5
+ create_table :jumbotron_schedule_groups do |t|
6
+ t.references :season, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_seasons }
7
+ t.references :season_phase, type: :bigint, foreign_key: { to_table: :jumbotron_season_phases }
8
+ t.string :kind, null: false
9
+ t.integer :number
10
+ t.string :name, null: false
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :jumbotron_schedule_groups,
15
+ %i[season_id season_phase_id kind name],
16
+ unique: true,
17
+ name: "idx_jumbotron_sg_identity"
18
+
19
+ add_reference :jumbotron_games,
20
+ :schedule_group,
21
+ type: :bigint,
22
+ foreign_key: { to_table: :jumbotron_schedule_groups }
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateJumbotronLineObservations < ActiveRecord::Migration[8.1]
4
+ def change
5
+ add_column :jumbotron_bookmakers, :observed_at, :datetime
6
+ add_column :jumbotron_bookmakers, :changed_at, :datetime
7
+ reversible do |dir|
8
+ dir.up do
9
+ execute <<~SQL.squish
10
+ UPDATE jumbotron_bookmakers
11
+ SET observed_at = created_at, changed_at = created_at
12
+ WHERE observed_at IS NULL OR changed_at IS NULL
13
+ SQL
14
+ end
15
+ end
16
+ change_column_null :jumbotron_bookmakers, :observed_at, false
17
+ change_column_null :jumbotron_bookmakers, :changed_at, false
18
+
19
+ create_table :jumbotron_line_observations do |t|
20
+ t.references :game, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_games }
21
+ t.references :bookmaker, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_bookmakers }
22
+ t.references :observation_batch, null: false, type: :bigint,
23
+ foreign_key: { to_table: :jumbotron_observation_batches }
24
+ t.string :market, null: false, limit: 32
25
+ t.string :outcome, null: false, limit: 32
26
+ t.string :source, null: false, limit: 32
27
+ t.decimal :line_value, precision: 8, scale: 3
28
+ t.integer :price_american
29
+ t.datetime :observed_at, null: false
30
+ t.datetime :changed_at, null: false
31
+ t.timestamps
32
+ end
33
+
34
+ add_index :jumbotron_line_observations,
35
+ %i[game_id bookmaker_id market outcome source observed_at],
36
+ name: "idx_jumbotron_line_obs_latest"
37
+ end
38
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddProgressToJumbotronGames < ActiveRecord::Migration[8.1]
4
+ def change
5
+ change_table :jumbotron_games do |t|
6
+ t.string :progress_state
7
+ t.string :progress_segment_kind
8
+ t.integer :progress_segment_number
9
+ t.string :progress_clock_mode
10
+ t.integer :progress_clock_seconds
11
+ t.string :progress_clock_display
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class Client
5
+ def schedule(**)
6
+ Public::TranslateOutcome.call(
7
+ Workflows::ReadScheduleWorkflow.call(**),
8
+ payload_key: :schedule
9
+ )
10
+ end
11
+
12
+ def schedule_groups(**)
13
+ Public::TranslateOutcome.call(
14
+ Workflows::ReadScheduleGroupsWorkflow.call(**),
15
+ payload_key: :schedule_groups
16
+ )
17
+ end
18
+
19
+ def game(**)
20
+ Public::TranslateOutcome.call(
21
+ Workflows::ReadGameWorkflow.call(**),
22
+ payload_key: :game
23
+ )
24
+ end
25
+
26
+ def consensus(**)
27
+ Public::TranslateOutcome.call(
28
+ Workflows::ReadConsensusWorkflow.call(**),
29
+ payload_key: :consensus_lines
30
+ )
31
+ end
32
+
33
+ def current_lines(**)
34
+ Public::TranslateOutcome.call(
35
+ Workflows::ReadCurrentLinesWorkflow.call(**),
36
+ payload_key: :current_lines
37
+ )
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Jumbotron
6
+
7
+ # Ensure app/synchronization is on Zeitwerk paths (alongside adapters/clients/providers).
8
+ config.autoload_paths << root.join("app/synchronization")
9
+ config.eager_load_paths << root.join("app/synchronization")
10
+ config.autoload_paths << root.join("app/scheduling")
11
+ config.eager_load_paths << root.join("app/scheduling")
12
+
13
+ config.autoload_paths << root.join("app/boot")
14
+ config.eager_load_paths << root.join("app/boot")
15
+
16
+ config.boot_discovery = true
17
+
18
+ # Reconcile jumbotron:* Sidekiq-Cron jobs on Sidekiq server start only.
19
+ # Do not require Sidekiq; skip when the host has not loaded Sidekiq-Cron.
20
+ config.after_initialize do
21
+ Jumbotron::Boot::EnqueueDiscoveries.call
22
+
23
+ next unless defined?(::Sidekiq) && defined?(::Sidekiq::Cron::Job)
24
+
25
+ ::Sidekiq.configure_server do |sidekiq|
26
+ sidekiq.on(:startup) do
27
+ Jumbotron::Scheduling::Materializer.call
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ VERSION = "0.1.1"
5
+ end
data/lib/jumbotron.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "command_tower"
5
+ require "jumbotron/version"
6
+ require "jumbotron/engine"
7
+ require "jumbotron/client"
8
+
9
+ module Jumbotron
10
+ def self.client
11
+ Client.new
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :jumbotron do
4
+ namespace :schedules do
5
+ desc "Materialize Jumbotron recurring schedules into the host scheduler"
6
+ task materialize: :environment do
7
+ Jumbotron::Scheduling::Materializer.call(
8
+ solid_queue_path: ENV.fetch("JUMBOTRON_SOLID_QUEUE_RECURRING", nil)
9
+ )
10
+ end
11
+ end
12
+ end