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,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Errors
5
+ module Adapters
6
+ class InvalidAdapterError < CommandTower::Errors::ApplicationError
7
+ def code
8
+ "invalid_adapter"
9
+ end
10
+
11
+ def message
12
+ "adapter is invalid"
13
+ end
14
+ end
15
+
16
+ class NotRegisteredError < CommandTower::Errors::ApplicationError
17
+ def code
18
+ "adapter_not_registered"
19
+ end
20
+
21
+ def message
22
+ adapter = details.is_a?(Hash) ? details[:adapter] : nil
23
+ adapter ? "#{adapter} is not registered" : "adapter is not registered"
24
+ end
25
+ end
26
+
27
+ class UnknownEndpointError < CommandTower::Errors::ApplicationError
28
+ def code
29
+ "unknown_endpoint"
30
+ end
31
+
32
+ def message
33
+ endpoint = details.is_a?(Hash) ? details[:endpoint] : nil
34
+ endpoint ? "adapter has no endpoint :#{endpoint}" : "unknown endpoint"
35
+ end
36
+ end
37
+
38
+ class LeagueNotFoundError < CommandTower::Errors::ApplicationError
39
+ def code
40
+ "league_not_found"
41
+ end
42
+
43
+ def message
44
+ league_id = details.is_a?(Hash) ? details[:league_id] : nil
45
+ league_id ? "league #{league_id} not found" : "league not found"
46
+ end
47
+ end
48
+
49
+ class AcquisitionFailedError < CommandTower::Errors::ApplicationError
50
+ def code
51
+ "acquisition_failed"
52
+ end
53
+
54
+ def message
55
+ details.is_a?(Hash) && details[:message].present? ? details[:message].to_s : "provider acquisition failed"
56
+ end
57
+ end
58
+
59
+ class NormalizationFailedError < CommandTower::Errors::ApplicationError
60
+ def code
61
+ "normalization_failed"
62
+ end
63
+
64
+ def message
65
+ details.is_a?(Hash) && details[:message].present? ? details[:message].to_s : "normalization failed"
66
+ end
67
+ end
68
+
69
+ class UnknownAdapterError < CommandTower::Errors::ApplicationError
70
+ def code
71
+ "unknown_adapter"
72
+ end
73
+
74
+ def message
75
+ adapter_id = details.is_a?(Hash) ? details[:adapter_id] : nil
76
+ adapter_id ? "unknown adapter #{adapter_id}" : "unknown adapter"
77
+ end
78
+ end
79
+
80
+ class UnknownPolicyError < CommandTower::Errors::ApplicationError
81
+ def code
82
+ "unknown_policy"
83
+ end
84
+
85
+ def message
86
+ policy_id = details.is_a?(Hash) ? details[:policy_id] : nil
87
+ policy_id ? "unknown policy #{policy_id}" : "unknown policy"
88
+ end
89
+ end
90
+
91
+ class DiscoveryNotExecutableError < CommandTower::Errors::ApplicationError
92
+ def code
93
+ "discovery_not_executable"
94
+ end
95
+
96
+ def message
97
+ "discovery is not executable as a game-update policy"
98
+ end
99
+ end
100
+
101
+ class UnknownDiscoveryError < CommandTower::Errors::ApplicationError
102
+ def code
103
+ "unknown_discovery"
104
+ end
105
+
106
+ def message
107
+ discovery_id = details.is_a?(Hash) ? details[:discovery_id] : nil
108
+ discovery_id ? "unknown discovery #{discovery_id}" : "unknown discovery"
109
+ end
110
+ end
111
+
112
+ class InvalidPolicyTypeError < CommandTower::Errors::ApplicationError
113
+ def code
114
+ "invalid_policy_type"
115
+ end
116
+
117
+ def message
118
+ "policy is not an executable game-update policy"
119
+ end
120
+ end
121
+
122
+ class InvalidLinePolicyTypeError < CommandTower::Errors::ApplicationError
123
+ def code
124
+ "invalid_policy_type"
125
+ end
126
+
127
+ def message
128
+ "policy is not an executable line-update policy"
129
+ end
130
+ end
131
+
132
+ class ProviderCooldownActiveError < CommandTower::Errors::ApplicationError
133
+ def code
134
+ "provider_cooldown_active"
135
+ end
136
+
137
+ def message
138
+ "provider cooldown is active"
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Errors
5
+ module Canonical
6
+ class InvalidSyncInputError < CommandTower::Errors::ApplicationError
7
+ def code
8
+ "invalid_sync_input"
9
+ end
10
+
11
+ def message
12
+ "canonical input must be Canonical::SyncInput"
13
+ end
14
+ end
15
+
16
+ class ProviderIdentityConflictError < CommandTower::Errors::ApplicationError
17
+ def code
18
+ "provider_identity_conflict"
19
+ end
20
+
21
+ def message
22
+ "provider identity is already attached to a different target"
23
+ end
24
+ end
25
+
26
+ class MutationFailedError < CommandTower::Errors::ApplicationError
27
+ def code
28
+ "mutation_failed"
29
+ end
30
+
31
+ def message
32
+ details.is_a?(Hash) && details[:message].present? ? details[:message].to_s : "canonical mutation failed"
33
+ end
34
+ end
35
+
36
+ class InvalidLineIngestError < CommandTower::Errors::ApplicationError
37
+ def code
38
+ "invalid_line_ingest"
39
+ end
40
+
41
+ def message
42
+ details.is_a?(Hash) && details[:message].present? ? details[:message].to_s : "invalid line ingest"
43
+ end
44
+ end
45
+
46
+ class GameNotFoundError < CommandTower::Errors::ApplicationError
47
+ def code
48
+ "game_not_found"
49
+ end
50
+
51
+ def message
52
+ "canonical game was not found for the provided identities"
53
+ end
54
+ end
55
+
56
+ class ObservationEvidenceFailedError < CommandTower::Errors::ApplicationError
57
+ def code
58
+ "observation_evidence_failed"
59
+ end
60
+
61
+ def message
62
+ details.is_a?(Hash) && details[:message].present? ? details[:message].to_s : "observation evidence failed"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Errors
5
+ module Public
6
+ class NotFoundError < CommandTower::Errors::ApplicationError
7
+ def code
8
+ details.is_a?(Hash) && details[:code].present? ? details[:code].to_s : "not_found"
9
+ end
10
+
11
+ def message
12
+ details.is_a?(Hash) && details[:message].present? ? details[:message].to_s : "resource was not found"
13
+ end
14
+ end
15
+
16
+ class InvalidRequestError < CommandTower::Errors::ApplicationError
17
+ def code
18
+ "invalid_request"
19
+ end
20
+
21
+ def message
22
+ details.is_a?(Hash) && details[:message].present? ? details[:message].to_s : "invalid public request"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class InvalidRequestError < Error
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class NotFoundError < Error
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class UnsupportedCapabilityError < Error
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class ApplicationJob < CommandTower::ApplicationJob
5
+ end
6
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class DiscoveryJob < ApplicationJob
5
+ def perform(adapter_id:, discovery_id:, continuation_attempt: 1)
6
+ execute_workflow(
7
+ Workflows::ExecuteDiscoveryWorkflow,
8
+ adapter_id: adapter_id,
9
+ discovery_id: discovery_id,
10
+ continuation_attempt: continuation_attempt
11
+ )
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class GameUpdateJob < ApplicationJob
5
+ def perform(adapter_id:, policy_id:)
6
+ Workflows::ExecuteGameUpdatePolicyWorkflow.call_from_job(
7
+ adapter_id: adapter_id,
8
+ policy_id: policy_id
9
+ )
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class LineUpdateJob < ApplicationJob
5
+ def perform(adapter_id:, policy_id:)
6
+ Workflows::ExecuteLineUpdatePolicyWorkflow.call_from_job(
7
+ adapter_id: adapter_id,
8
+ policy_id: policy_id
9
+ )
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ self.table_name_prefix = "jumbotron_"
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class Bookmaker < ApplicationRecord
5
+ has_many :provider_identities, as: :target, inverse_of: :target, dependent: :restrict_with_exception
6
+ has_many :line_observations, inverse_of: :bookmaker, dependent: :restrict_with_exception
7
+
8
+ validates :name, presence: true
9
+ validates :observed_at, :changed_at, presence: true
10
+ end
11
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Canonical
5
+ ProviderIdentityRef = Data.define(:provider, :namespace, :id)
6
+
7
+ TeamInput = Data.define(:provider_identities, :name)
8
+
9
+ VenueInput = Data.define(:provider_identities, :name)
10
+
11
+ ParticipantInput = Data.define(
12
+ :provider_identities,
13
+ :team_name,
14
+ :role,
15
+ :score,
16
+ :result
17
+ )
18
+
19
+ ScheduleGroupInput = Data.define(:kind, :number, :name)
20
+
21
+ GameSegment = Data.define(:kind, :number)
22
+ # Clock label text. Name is canonical/public contract, not Kernel#display.
23
+ GameClock = Data.define(:mode, :seconds, :display) # rubocop:disable Lint/DataDefineOverride
24
+ GameProgress = Data.define(:state, :segment, :clock)
25
+
26
+ GameInput = Data.define(
27
+ :provider_identities,
28
+ :scheduled_at,
29
+ :lifecycle,
30
+ :neutral_site,
31
+ :season_year,
32
+ :season_phase_key,
33
+ :schedule_group,
34
+ :venue,
35
+ :participants,
36
+ :progress
37
+ ) do
38
+ def initialize(progress: nil, **members)
39
+ super
40
+ end
41
+ end
42
+
43
+ SyncInput = Data.define(:observed_at, :games, :teams)
44
+
45
+ LineObservationInput = Data.define(
46
+ :bookmaker_identity,
47
+ :bookmaker_name,
48
+ :market,
49
+ :outcome,
50
+ :source,
51
+ :line_value,
52
+ :price_american
53
+ )
54
+
55
+ LineIngestInput = Data.define(
56
+ :observed_at,
57
+ :provider,
58
+ :adapter_scope,
59
+ :game_identities,
60
+ :observations
61
+ )
62
+
63
+ CurrentLine = Data.define(
64
+ :line_observation_id,
65
+ :game_id,
66
+ :bookmaker_id,
67
+ :bookmaker_name,
68
+ :market,
69
+ :outcome,
70
+ :line_value,
71
+ :price_american,
72
+ :observed_at,
73
+ :changed_at
74
+ )
75
+
76
+ ConsensusConstituent = Data.define(:line_observation_id, :bookmaker_id)
77
+
78
+ ConsensusLine = Data.define(
79
+ :game_id,
80
+ :market,
81
+ :outcome,
82
+ :line_value,
83
+ :constituent_count,
84
+ :constituents
85
+ )
86
+
87
+ # Transaction-local accumulator for material canonical attribute changes.
88
+ class ChangeSet
89
+ include Enumerable
90
+
91
+ Entry = Data.define(:subject, :attribute, :previous, :new_value)
92
+
93
+ def initialize
94
+ @entries = []
95
+ end
96
+
97
+ def record(subject:, attribute:, previous:, new_value:)
98
+ return if serialize(previous) == serialize(new_value)
99
+
100
+ @entries << Entry.new(
101
+ subject: subject,
102
+ attribute: attribute.to_s,
103
+ previous: previous,
104
+ new_value: new_value
105
+ )
106
+ end
107
+
108
+ delegate :any?, :size, to: :entries
109
+
110
+ def each(&)
111
+ entries.each(&)
112
+ end
113
+
114
+ private
115
+
116
+ attr_reader :entries
117
+
118
+ def serialize(value)
119
+ case value
120
+ when Time, ActiveSupport::TimeWithZone
121
+ value.iso8601
122
+ else
123
+ value
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class Game < ApplicationRecord
5
+ LIFECYCLES = %w[
6
+ scheduled
7
+ in_progress
8
+ completed
9
+ postponed
10
+ cancelled
11
+ suspended
12
+ ].freeze
13
+
14
+ PROGRESS_STATES = %w[active intermission].freeze
15
+ PROGRESS_CLOCK_MODES = %w[remaining elapsed].freeze
16
+ PROGRESS_CORE_ATTRIBUTES = %i[
17
+ progress_state
18
+ progress_segment_kind
19
+ progress_segment_number
20
+ ].freeze
21
+ PROGRESS_CLOCK_ATTRIBUTES = %i[
22
+ progress_clock_mode
23
+ progress_clock_seconds
24
+ progress_clock_display
25
+ ].freeze
26
+ PROGRESS_ATTRIBUTES = (PROGRESS_CORE_ATTRIBUTES + PROGRESS_CLOCK_ATTRIBUTES).freeze
27
+
28
+ belongs_to :league, inverse_of: :games
29
+ belongs_to :season, inverse_of: :games
30
+ belongs_to :season_phase, optional: true, inverse_of: :games
31
+ belongs_to :schedule_group, optional: true, inverse_of: :games
32
+ belongs_to :venue, optional: true, inverse_of: :games
33
+ has_many :game_participants, inverse_of: :game, dependent: :restrict_with_exception
34
+ has_many :teams, through: :game_participants
35
+ has_many :provider_identities, as: :target, inverse_of: :target, dependent: :restrict_with_exception
36
+ has_many :line_observations, inverse_of: :game, dependent: :restrict_with_exception
37
+
38
+ attribute :lifecycle, :string, default: "scheduled"
39
+ attribute :neutral_site, :boolean, default: false
40
+
41
+ validates :lifecycle, inclusion: { in: LIFECYCLES }
42
+ validates :progress_state, inclusion: { in: PROGRESS_STATES, allow_nil: true }
43
+ validates :progress_clock_mode, inclusion: { in: PROGRESS_CLOCK_MODES, allow_nil: true }
44
+ validate :season_belongs_to_league
45
+ validate :season_phase_belongs_to_season
46
+ validate :schedule_group_belongs_to_season
47
+ validate :progress_is_all_nil_or_core_complete
48
+ validate :intermission_has_no_clock
49
+
50
+ private
51
+
52
+ def season_belongs_to_league
53
+ return if season.blank? || league.blank?
54
+ return if season.league_id == league_id
55
+
56
+ errors.add(:season, "must belong to the same league")
57
+ end
58
+
59
+ def season_phase_belongs_to_season
60
+ return if season_phase.blank? || season.blank?
61
+ return if season_phase.season_id == season_id
62
+
63
+ errors.add(:season_phase, "must belong to the same season")
64
+ end
65
+
66
+ def schedule_group_belongs_to_season
67
+ return if schedule_group.blank? || season.blank?
68
+ return if schedule_group.season_id == season_id
69
+
70
+ errors.add(:schedule_group, "must belong to the same season")
71
+ end
72
+
73
+ def progress_is_all_nil_or_core_complete
74
+ values = PROGRESS_ATTRIBUTES.map { |attribute| public_send(attribute) }
75
+ return if values.all?(&:nil?)
76
+ return if progress_core_complete?
77
+
78
+ errors.add(:progress_state, "must include state, segment kind, and segment number together")
79
+ end
80
+
81
+ def progress_core_complete?
82
+ progress_state.present? && progress_segment_kind.present? && !progress_segment_number.nil?
83
+ end
84
+
85
+ def intermission_has_no_clock
86
+ return unless progress_state == "intermission"
87
+ return if PROGRESS_CLOCK_ATTRIBUTES.all? { |attribute| public_send(attribute).nil? }
88
+
89
+ errors.add(:progress_clock_mode, "must be blank during intermission")
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class GameParticipant < ApplicationRecord
5
+ belongs_to :game, inverse_of: :game_participants
6
+ belongs_to :team, inverse_of: :game_participants
7
+
8
+ validates :team_id, uniqueness: { scope: :game_id }
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class HistoricalChange < ApplicationRecord
5
+ belongs_to :observation_batch, inverse_of: :historical_changes
6
+
7
+ validates :subject_type, :subject_id, :attribute_name, :observed_at, :provider, presence: true
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class League < ApplicationRecord
5
+ belongs_to :sport, inverse_of: :leagues
6
+ has_many :seasons, inverse_of: :league, dependent: :restrict_with_exception
7
+ has_many :games, inverse_of: :league, dependent: :restrict_with_exception
8
+ has_many :provider_identities, as: :target, inverse_of: :target, dependent: :restrict_with_exception
9
+
10
+ validates :name, presence: true
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class LineObservation < ApplicationRecord
5
+ MARKETS = %w[spread total moneyline].freeze
6
+ OUTCOMES = %w[home away over under draw].freeze
7
+ SOURCES = %w[observed provider_open provider_close].freeze
8
+
9
+ belongs_to :game, inverse_of: :line_observations
10
+ belongs_to :bookmaker, inverse_of: :line_observations
11
+ belongs_to :observation_batch, inverse_of: :line_observations
12
+
13
+ validates :market, inclusion: { in: MARKETS }
14
+ validates :outcome, inclusion: { in: OUTCOMES }
15
+ validates :source, inclusion: { in: SOURCES }
16
+ validates :observed_at, :changed_at, presence: true
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class ObservationBatch < ApplicationRecord
5
+ has_many :historical_changes,
6
+ inverse_of: :observation_batch,
7
+ dependent: :restrict_with_exception
8
+ has_many :line_observations,
9
+ inverse_of: :observation_batch,
10
+ dependent: :restrict_with_exception
11
+
12
+ validates :provider, :adapter_scope, :observed_at, presence: true
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class ProviderIdentity < ApplicationRecord
5
+ TARGET_TYPES = [
6
+ "Jumbotron::Sport",
7
+ "Jumbotron::League",
8
+ "Jumbotron::Season",
9
+ "Jumbotron::SeasonPhase",
10
+ "Jumbotron::Team",
11
+ "Jumbotron::Venue",
12
+ "Jumbotron::Game",
13
+ "Jumbotron::Bookmaker"
14
+ ].freeze
15
+
16
+ belongs_to :target, polymorphic: true, inverse_of: :provider_identities
17
+
18
+ before_validation :normalize_provider
19
+
20
+ validates :provider, presence: true
21
+ validates :object_namespace, presence: true
22
+ validates :provider_id, presence: true
23
+ validates :target_type, inclusion: { in: TARGET_TYPES }
24
+ validates :provider_id, uniqueness: { scope: %i[provider object_namespace] }
25
+
26
+ private
27
+
28
+ def normalize_provider
29
+ self.provider = provider&.strip&.downcase
30
+ end
31
+ end
32
+ end