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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +5 -0
- data/app/adapters/jumbotron/adapters/base.rb +136 -0
- data/app/adapters/jumbotron/adapters/cadence.rb +42 -0
- data/app/adapters/jumbotron/adapters/discovery_definition.rb +7 -0
- data/app/adapters/jumbotron/adapters/espn/nfl.rb +169 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/acquisition_scope.rb +29 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/competition_odds.rb +185 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/lifecycle.rb +30 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/line_acquisition_scope.rb +26 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/progress.rb +54 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/scoreboard.rb +146 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/season_phase.rb +33 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/teams.rb +35 -0
- data/app/adapters/jumbotron/adapters/line_acquisition.rb +19 -0
- data/app/adapters/jumbotron/adapters/operation.rb +23 -0
- data/app/adapters/jumbotron/adapters/policy_definition.rb +14 -0
- data/app/adapters/jumbotron/adapters/registry.rb +78 -0
- data/app/adapters/jumbotron/adapters.rb +11 -0
- data/app/boot/jumbotron/boot/enqueue_discoveries.rb +41 -0
- data/app/clients/jumbotron/clients/espn/competition_odds/get.rb +26 -0
- data/app/clients/jumbotron/clients/espn/competition_odds/get_input.rb +16 -0
- data/app/clients/jumbotron/clients/espn/scoreboard/get.rb +18 -0
- data/app/clients/jumbotron/clients/espn/scoreboard/get_input.rb +17 -0
- data/app/clients/jumbotron/clients/espn/teams/get.rb +18 -0
- data/app/clients/jumbotron/clients/espn/teams/get_input.rb +15 -0
- data/app/clients/jumbotron/clients/espn.rb +44 -0
- data/app/clients/jumbotron/clients.rb +8 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition.rb +79 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/draw_odds.rb +31 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/get.rb +66 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/item.rb +60 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/listed_provider.rb +31 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/market_window.rb +31 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/point_spread.rb +27 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/price.rb +35 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/side_quote.rb +35 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/team_odds.rb +40 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb +49 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/event.rb +81 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/scoreboard/get.rb +88 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/season.rb +27 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/status.rb +69 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/support.rb +61 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/team.rb +42 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/teams/get.rb +76 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/venue.rb +34 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/week.rb +23 -0
- data/app/deserializers/jumbotron/deserializers/public/game.rb +26 -0
- data/app/deserializers/jumbotron/deserializers/public/game_ids.rb +28 -0
- data/app/deserializers/jumbotron/deserializers/public/includes.rb +32 -0
- data/app/deserializers/jumbotron/deserializers/public/schedule.rb +151 -0
- data/app/deserializers/jumbotron/deserializers/public/schedule_groups.rb +91 -0
- data/app/errors/jumbotron/error.rb +6 -0
- data/app/errors/jumbotron/errors/adapters.rb +143 -0
- data/app/errors/jumbotron/errors/canonical.rb +67 -0
- data/app/errors/jumbotron/errors/public.rb +27 -0
- data/app/errors/jumbotron/invalid_request_error.rb +6 -0
- data/app/errors/jumbotron/not_found_error.rb +6 -0
- data/app/errors/jumbotron/unsupported_capability_error.rb +6 -0
- data/app/jobs/jumbotron/application_job.rb +6 -0
- data/app/jobs/jumbotron/discovery_job.rb +14 -0
- data/app/jobs/jumbotron/game_update_job.rb +12 -0
- data/app/jobs/jumbotron/line_update_job.rb +12 -0
- data/app/models/jumbotron/application_record.rb +8 -0
- data/app/models/jumbotron/bookmaker.rb +11 -0
- data/app/models/jumbotron/canonical.rb +128 -0
- data/app/models/jumbotron/game.rb +92 -0
- data/app/models/jumbotron/game_participant.rb +10 -0
- data/app/models/jumbotron/historical_change.rb +9 -0
- data/app/models/jumbotron/league.rb +12 -0
- data/app/models/jumbotron/line_observation.rb +18 -0
- data/app/models/jumbotron/observation_batch.rb +14 -0
- data/app/models/jumbotron/provider_identity.rb +32 -0
- data/app/models/jumbotron/public/translate_outcome.rb +57 -0
- data/app/models/jumbotron/public.rb +88 -0
- data/app/models/jumbotron/schedule_group.rb +22 -0
- data/app/models/jumbotron/season.rb +13 -0
- data/app/models/jumbotron/season_phase.rb +12 -0
- data/app/models/jumbotron/sport.rb +10 -0
- data/app/models/jumbotron/team.rb +11 -0
- data/app/models/jumbotron/venue.rb +10 -0
- data/app/providers/jumbotron/providers/espn/cooldown.rb +88 -0
- data/app/providers/jumbotron/providers/espn/cooldown_active_error.rb +13 -0
- data/app/providers/jumbotron/providers/espn/provider_failure.rb +40 -0
- data/app/providers/jumbotron/providers/espn/resources/competition_odds.rb +29 -0
- data/app/providers/jumbotron/providers/espn/resources/scoreboard.rb +90 -0
- data/app/providers/jumbotron/providers/espn/resources/teams.rb +19 -0
- data/app/scheduling/jumbotron/scheduling/backends/sidekiq_cron.rb +70 -0
- data/app/scheduling/jumbotron/scheduling/backends/solid_queue.rb +92 -0
- data/app/scheduling/jumbotron/scheduling/clock.rb +61 -0
- data/app/scheduling/jumbotron/scheduling/desired_schedules.rb +51 -0
- data/app/scheduling/jumbotron/scheduling/detect_integration.rb +45 -0
- data/app/scheduling/jumbotron/scheduling/materializer.rb +13 -0
- data/app/scheduling/jumbotron/scheduling/owned.rb +19 -0
- data/app/scheduling/jumbotron/scheduling/schedule_definition.rb +7 -0
- data/app/serializers/jumbotron/serializers/clients/espn/competition_odds/get.rb +17 -0
- data/app/serializers/jumbotron/serializers/clients/espn/scoreboard/get.rb +22 -0
- data/app/serializers/jumbotron/serializers/clients/espn/teams/get.rb +20 -0
- data/app/services/jumbotron/services/adapters/build_acquisition_scopes.rb +37 -0
- data/app/services/jumbotron/services/adapters/build_line_acquisition_scopes.rb +39 -0
- data/app/services/jumbotron/services/adapters/ensure_league.rb +29 -0
- data/app/services/jumbotron/services/adapters/execute_endpoint.rb +49 -0
- data/app/services/jumbotron/services/adapters/execute_line_acquisition_scope.rb +75 -0
- data/app/services/jumbotron/services/adapters/execute_synchronization_scope.rb +64 -0
- data/app/services/jumbotron/services/adapters/resolve_discovery.rb +53 -0
- data/app/services/jumbotron/services/adapters/resolve_game_update_policy.rb +74 -0
- data/app/services/jumbotron/services/adapters/resolve_line_update_policy.rb +74 -0
- data/app/services/jumbotron/services/adapters/select_eligible_games.rb +41 -0
- data/app/services/jumbotron/services/adapters/validate_sync_request.rb +51 -0
- data/app/services/jumbotron/services/canonical/attach_provider_identities.rb +59 -0
- data/app/services/jumbotron/services/canonical/derive_consensus_for_games.rb +18 -0
- data/app/services/jumbotron/services/canonical/derive_consensus_line.rb +70 -0
- data/app/services/jumbotron/services/canonical/ensure_bookmaker.rb +70 -0
- data/app/services/jumbotron/services/canonical/ensure_schedule_group.rb +40 -0
- data/app/services/jumbotron/services/canonical/ensure_season.rb +22 -0
- data/app/services/jumbotron/services/canonical/ensure_season_phase.rb +22 -0
- data/app/services/jumbotron/services/canonical/persist_line_observations.rb +145 -0
- data/app/services/jumbotron/services/canonical/record_observation_evidence.rb +51 -0
- data/app/services/jumbotron/services/canonical/resolve_current_lines.rb +21 -0
- data/app/services/jumbotron/services/canonical/resolve_current_lines_for_games.rb +69 -0
- data/app/services/jumbotron/services/canonical/upsert_game.rb +186 -0
- data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +88 -0
- data/app/services/jumbotron/services/canonical/upsert_team.rb +90 -0
- data/app/services/jumbotron/services/canonical/upsert_venue.rb +92 -0
- data/app/services/jumbotron/services/public/assemble_public_game.rb +131 -0
- data/app/services/jumbotron/services/public/assemble_public_schedule.rb +63 -0
- data/app/services/jumbotron/services/public/derive_consensus_for_games.rb +21 -0
- data/app/services/jumbotron/services/public/load_games_by_ids.rb +18 -0
- data/app/services/jumbotron/services/public/load_included_lines.rb +47 -0
- data/app/services/jumbotron/services/public/map_public_consensus_lines.rb +44 -0
- data/app/services/jumbotron/services/public/map_public_current_lines.rb +27 -0
- data/app/services/jumbotron/services/public/read_game.rb +32 -0
- data/app/services/jumbotron/services/public/read_schedule.rb +116 -0
- data/app/services/jumbotron/services/public/read_schedule_groups.rb +90 -0
- data/app/services/jumbotron/services/public/resolve_current_lines_for_games.rb +21 -0
- data/app/synchronization/jumbotron/synchronization/lease.rb +127 -0
- data/app/workflows/jumbotron/workflows/execute_discovery_workflow.rb +131 -0
- data/app/workflows/jumbotron/workflows/execute_game_update_policy_workflow.rb +159 -0
- data/app/workflows/jumbotron/workflows/execute_line_update_policy_workflow.rb +156 -0
- data/app/workflows/jumbotron/workflows/public_read.rb +29 -0
- data/app/workflows/jumbotron/workflows/read_consensus_workflow.rb +37 -0
- data/app/workflows/jumbotron/workflows/read_current_lines_workflow.rb +27 -0
- data/app/workflows/jumbotron/workflows/read_game_workflow.rb +34 -0
- data/app/workflows/jumbotron/workflows/read_schedule_groups_workflow.rb +21 -0
- data/app/workflows/jumbotron/workflows/read_schedule_workflow.rb +48 -0
- data/app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb +183 -0
- data/db/migrate/20260812000001_create_jumbotron_foundational_catalog.rb +68 -0
- data/db/migrate/20260812000002_create_jumbotron_game_graph.rb +70 -0
- data/db/migrate/20260812000003_add_freshness_to_jumbotron_provider_mutated_entities.rb +31 -0
- data/db/migrate/20260812000004_create_jumbotron_observation_evidence.rb +34 -0
- data/db/migrate/20260813000001_create_jumbotron_schedule_groups.rb +24 -0
- data/db/migrate/20260814000001_create_jumbotron_line_observations.rb +38 -0
- data/db/migrate/20260820000001_add_progress_to_jumbotron_games.rb +14 -0
- data/lib/jumbotron/client.rb +40 -0
- data/lib/jumbotron/engine.rb +32 -0
- data/lib/jumbotron/version.rb +5 -0
- data/lib/jumbotron.rb +13 -0
- data/lib/tasks/jumbotron_schedules.rake +12 -0
- metadata +239 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
class ResolveLineUpdatePolicy < CommandTower::Services::ApplicationService
|
|
7
|
+
LINE_POLICY_TYPES = %i[
|
|
8
|
+
future_line_update
|
|
9
|
+
live_line_update
|
|
10
|
+
interrupted_line_update
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
validate :adapter_id, required: true
|
|
14
|
+
validate :policy_id, required: true
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
Jumbotron::Adapters::Registry.ensure_loaded!
|
|
18
|
+
adapter = Jumbotron::Adapters::Registry.find(adapter_id)
|
|
19
|
+
if adapter.nil?
|
|
20
|
+
context.fail!(
|
|
21
|
+
application_error: Jumbotron::Errors::Adapters::UnknownAdapterError.new(
|
|
22
|
+
details: { adapter_id: adapter_id }
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
unless adapter.registered?
|
|
29
|
+
context.fail!(
|
|
30
|
+
application_error: Jumbotron::Errors::Adapters::NotRegisteredError.new(
|
|
31
|
+
details: { adapter: adapter.to_s }
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
key = policy_id.to_sym
|
|
38
|
+
if adapter.discovery_ids.include?(key)
|
|
39
|
+
context.fail!(
|
|
40
|
+
application_error: Jumbotron::Errors::Adapters::DiscoveryNotExecutableError.new(
|
|
41
|
+
details: { policy_id: policy_id }
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
policy = lookup_policy(adapter, key)
|
|
48
|
+
return if policy.nil?
|
|
49
|
+
|
|
50
|
+
unless LINE_POLICY_TYPES.include?(policy.type)
|
|
51
|
+
context.fail!(application_error: Jumbotron::Errors::Adapters::InvalidLinePolicyTypeError.new)
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context.adapter = adapter
|
|
56
|
+
context.policy = policy
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def lookup_policy(adapter, key)
|
|
62
|
+
adapter.policy(key)
|
|
63
|
+
rescue Jumbotron::Adapters::UnknownPolicyError
|
|
64
|
+
context.fail!(
|
|
65
|
+
application_error: Jumbotron::Errors::Adapters::UnknownPolicyError.new(
|
|
66
|
+
details: { policy_id: policy_id }
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
class SelectEligibleGames < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :adapter, required: true
|
|
8
|
+
validate :policy, required: true
|
|
9
|
+
validate :now, required: true
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
league = find_league
|
|
13
|
+
if league.nil?
|
|
14
|
+
context.fail!(
|
|
15
|
+
application_error: Jumbotron::Errors::Adapters::LeagueNotFoundError.new(
|
|
16
|
+
details: { league_id: "#{adapter.sport}/#{adapter.league}" }
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
games = Game.where(league_id: league.id)
|
|
23
|
+
.includes(:season, :season_phase, :schedule_group)
|
|
24
|
+
.select { |game| policy.eligible?(game, now: now) }
|
|
25
|
+
|
|
26
|
+
context.league = league
|
|
27
|
+
context.games = games
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def find_league
|
|
33
|
+
sport = Sport.where("LOWER(name) = ?", adapter.sport.downcase).first
|
|
34
|
+
return if sport.nil?
|
|
35
|
+
|
|
36
|
+
sport.leagues.where("LOWER(name) = ?", adapter.league.downcase).first
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
class ValidateSyncRequest < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :adapter, required: true
|
|
8
|
+
validate :endpoint, required: true
|
|
9
|
+
validate :league_id, required: true
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
unless adapter.respond_to?(:registered?) && adapter.respond_to?(:operation) &&
|
|
13
|
+
adapter.respond_to?(:endpoints)
|
|
14
|
+
context.fail!(application_error: Jumbotron::Errors::Adapters::InvalidAdapterError.new)
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
unless adapter.registered?
|
|
19
|
+
context.fail!(
|
|
20
|
+
application_error: Jumbotron::Errors::Adapters::NotRegisteredError.new(
|
|
21
|
+
details: { adapter: adapter.to_s }
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
unless adapter.endpoints.key?(endpoint.to_sym)
|
|
28
|
+
context.fail!(
|
|
29
|
+
application_error: Jumbotron::Errors::Adapters::UnknownEndpointError.new(
|
|
30
|
+
details: { endpoint: endpoint }
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
league = Jumbotron::League.find_by(id: league_id)
|
|
37
|
+
if league.nil?
|
|
38
|
+
context.fail!(
|
|
39
|
+
application_error: Jumbotron::Errors::Adapters::LeagueNotFoundError.new(
|
|
40
|
+
details: { league_id: league_id }
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
context.league = league
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Canonical
|
|
6
|
+
class AttachProviderIdentities < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :target, required: true
|
|
8
|
+
validate :refs, is_a: Array, required: true
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
conflict = refs.find { |ref| conflicting_identity?(ref) }
|
|
12
|
+
if conflict
|
|
13
|
+
context.fail!(
|
|
14
|
+
application_error: Jumbotron::Errors::Canonical::ProviderIdentityConflictError.new
|
|
15
|
+
)
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
refs.each { |ref| ensure_identity!(ref) }
|
|
20
|
+
context.target = target
|
|
21
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
22
|
+
context.fail!(
|
|
23
|
+
application_error: Jumbotron::Errors::Canonical::MutationFailedError.new(
|
|
24
|
+
details: { message: e.message }
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def conflicting_identity?(ref)
|
|
32
|
+
identity = find_identity(ref)
|
|
33
|
+
return false unless identity
|
|
34
|
+
|
|
35
|
+
identity.target_type != target.class.name || identity.target_id != target.id
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ensure_identity!(ref)
|
|
39
|
+
return if find_identity(ref)
|
|
40
|
+
|
|
41
|
+
ProviderIdentity.create!(
|
|
42
|
+
provider: ref.provider,
|
|
43
|
+
object_namespace: ref.namespace,
|
|
44
|
+
provider_id: ref.id,
|
|
45
|
+
target: target
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def find_identity(ref)
|
|
50
|
+
ProviderIdentity.find_by(
|
|
51
|
+
provider: ref.provider,
|
|
52
|
+
object_namespace: ref.namespace,
|
|
53
|
+
provider_id: ref.id
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Canonical
|
|
6
|
+
class DeriveConsensusForGames < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :current_lines, required: false
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
grouped = Array(current_lines).group_by(&:game_id)
|
|
11
|
+
context.consensus_lines = grouped.flat_map do |game_id, lines|
|
|
12
|
+
DeriveConsensusLine.from_current_lines(game_id: game_id, current_lines: lines)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Jumbotron
|
|
6
|
+
module Services
|
|
7
|
+
module Canonical
|
|
8
|
+
class DeriveConsensusLine < CommandTower::Services::ApplicationService
|
|
9
|
+
CONSENSUS_MARKETS = %w[spread total].freeze
|
|
10
|
+
LIVE_ODDS_NAME = /\blive odds\b/i
|
|
11
|
+
|
|
12
|
+
validate :game, required: true
|
|
13
|
+
validate :current_lines, required: false
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
currents = current_lines
|
|
17
|
+
if currents.nil?
|
|
18
|
+
resolved = ResolveCurrentLines.call(game: game)
|
|
19
|
+
unless resolved.success?
|
|
20
|
+
context.fail!(application_error: resolved.errors.first)
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
currents = Array(resolved.data[:current_lines])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context.consensus_lines = self.class.from_current_lines(
|
|
27
|
+
game_id: game.id,
|
|
28
|
+
current_lines: currents
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.from_current_lines(game_id:, current_lines:)
|
|
33
|
+
currents = Array(current_lines)
|
|
34
|
+
latest_at = currents.filter_map(&:observed_at).max
|
|
35
|
+
eligible = currents.select { |line| eligible?(line, latest_at) }
|
|
36
|
+
eligible.group_by { |line| [line.market, line.outcome] }.map do |(_key, members)|
|
|
37
|
+
to_consensus(game_id, members)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.eligible?(line, latest_at)
|
|
42
|
+
return false unless CONSENSUS_MARKETS.include?(line.market)
|
|
43
|
+
return false if line.line_value.nil?
|
|
44
|
+
return false if line.bookmaker_name.to_s.match?(LIVE_ODDS_NAME)
|
|
45
|
+
return false if latest_at.nil?
|
|
46
|
+
|
|
47
|
+
line.observed_at == latest_at
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.to_consensus(game_id, members)
|
|
51
|
+
values = members.map { |line| BigDecimal(line.line_value.to_s) }
|
|
52
|
+
mean = values.sum / values.size
|
|
53
|
+
Jumbotron::Canonical::ConsensusLine.new(
|
|
54
|
+
game_id: game_id,
|
|
55
|
+
market: members.first.market,
|
|
56
|
+
outcome: members.first.outcome,
|
|
57
|
+
line_value: mean,
|
|
58
|
+
constituent_count: members.size,
|
|
59
|
+
constituents: members.map do |line|
|
|
60
|
+
Jumbotron::Canonical::ConsensusConstituent.new(
|
|
61
|
+
line_observation_id: line.line_observation_id,
|
|
62
|
+
bookmaker_id: line.bookmaker_id
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Canonical
|
|
6
|
+
class EnsureBookmaker < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :identity, required: true
|
|
8
|
+
validate :name, is_a: String, required: true
|
|
9
|
+
validate :observed_at, required: true
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
existing = find_identity
|
|
13
|
+
bookmaker = if existing&.target.is_a?(Bookmaker)
|
|
14
|
+
update_existing!(existing.target)
|
|
15
|
+
else
|
|
16
|
+
create_new!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attach = AttachProviderIdentities.call(target: bookmaker, refs: [identity])
|
|
20
|
+
unless attach.success?
|
|
21
|
+
context.fail!(application_error: attach.errors.first)
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context.bookmaker = bookmaker
|
|
26
|
+
context.created = existing.nil?
|
|
27
|
+
rescue ActiveRecord::RecordNotUnique
|
|
28
|
+
recovered = find_identity
|
|
29
|
+
context.bookmaker = update_existing!(recovered.target)
|
|
30
|
+
context.created = false
|
|
31
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
32
|
+
context.fail!(
|
|
33
|
+
application_error: Jumbotron::Errors::Canonical::MutationFailedError.new(
|
|
34
|
+
details: { message: e.message }
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def find_identity
|
|
42
|
+
ProviderIdentity.find_by(
|
|
43
|
+
provider: identity.provider,
|
|
44
|
+
object_namespace: identity.namespace,
|
|
45
|
+
provider_id: identity.id
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def create_new!
|
|
50
|
+
Bookmaker.create!(
|
|
51
|
+
name: name,
|
|
52
|
+
observed_at: observed_at,
|
|
53
|
+
changed_at: observed_at
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def update_existing!(bookmaker)
|
|
58
|
+
locked = Bookmaker.lock.find(bookmaker.id)
|
|
59
|
+
if locked.name != name
|
|
60
|
+
locked.name = name
|
|
61
|
+
locked.changed_at = observed_at
|
|
62
|
+
end
|
|
63
|
+
locked.observed_at = observed_at
|
|
64
|
+
locked.save!
|
|
65
|
+
locked
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Canonical
|
|
6
|
+
class EnsureScheduleGroup < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :season, required: true
|
|
8
|
+
validate :season_phase, required: false
|
|
9
|
+
validate :schedule_group_input, required: false
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
context.schedule_group = resolve_group
|
|
13
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
14
|
+
context.fail!(
|
|
15
|
+
application_error: Jumbotron::Errors::Canonical::MutationFailedError.new(
|
|
16
|
+
details: { message: e.message }
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def resolve_group
|
|
24
|
+
input = schedule_group_input
|
|
25
|
+
return if input.nil?
|
|
26
|
+
|
|
27
|
+
group = ScheduleGroup.find_or_initialize_by(
|
|
28
|
+
season_id: season.id,
|
|
29
|
+
season_phase_id: season_phase&.id,
|
|
30
|
+
kind: input.kind.to_s,
|
|
31
|
+
name: input.name.to_s
|
|
32
|
+
)
|
|
33
|
+
group.number = input.number
|
|
34
|
+
group.save!
|
|
35
|
+
group
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Canonical
|
|
6
|
+
class EnsureSeason < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :league, required: true
|
|
8
|
+
validate :year, required: true
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
context.season = Season.find_or_create_by!(league_id: league.id, name: year.to_s)
|
|
12
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
13
|
+
context.fail!(
|
|
14
|
+
application_error: Jumbotron::Errors::Canonical::MutationFailedError.new(
|
|
15
|
+
details: { message: e.message }
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Canonical
|
|
6
|
+
class EnsureSeasonPhase < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :season, required: true
|
|
8
|
+
validate :phase_key, required: true
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
context.season_phase = SeasonPhase.find_or_create_by!(season_id: season.id, name: phase_key.to_s)
|
|
12
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
13
|
+
context.fail!(
|
|
14
|
+
application_error: Jumbotron::Errors::Canonical::MutationFailedError.new(
|
|
15
|
+
details: { message: e.message }
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Jumbotron
|
|
6
|
+
module Services
|
|
7
|
+
module Canonical
|
|
8
|
+
class PersistLineObservations < CommandTower::Services::ApplicationService
|
|
9
|
+
validate :ingest, required: true
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
unless ingest.is_a?(Jumbotron::Canonical::LineIngestInput)
|
|
13
|
+
context.fail!(
|
|
14
|
+
application_error: Jumbotron::Errors::Canonical::InvalidLineIngestError.new(
|
|
15
|
+
details: { message: "ingest must be Canonical::LineIngestInput" }
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if ingest.game_identities.blank? || ingest.provider.blank? || ingest.adapter_scope.blank? ||
|
|
22
|
+
ingest.observed_at.blank?
|
|
23
|
+
context.fail!(
|
|
24
|
+
application_error: Jumbotron::Errors::Canonical::InvalidLineIngestError.new(
|
|
25
|
+
details: { message: "provider, adapter_scope, observed_at, and game_identities are required" }
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
created = 0
|
|
32
|
+
updated = 0
|
|
33
|
+
bookmakers_created = 0
|
|
34
|
+
resolved_ids = {}
|
|
35
|
+
|
|
36
|
+
transaction do
|
|
37
|
+
game = resolve_game!
|
|
38
|
+
batch = ObservationBatch.create!(
|
|
39
|
+
provider: ingest.provider,
|
|
40
|
+
adapter_scope: ingest.adapter_scope,
|
|
41
|
+
observed_at: ingest.observed_at,
|
|
42
|
+
metadata: {}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
ingest.observations.each do |row|
|
|
46
|
+
bookmaker = resolved_ids[bookmaker_key(row.bookmaker_identity)]
|
|
47
|
+
unless bookmaker
|
|
48
|
+
ensured = EnsureBookmaker.call(
|
|
49
|
+
identity: row.bookmaker_identity,
|
|
50
|
+
name: row.bookmaker_name,
|
|
51
|
+
observed_at: ingest.observed_at
|
|
52
|
+
)
|
|
53
|
+
fail_transaction!(ensured) unless ensured.success?
|
|
54
|
+
|
|
55
|
+
bookmaker = ensured.data[:bookmaker]
|
|
56
|
+
resolved_ids[bookmaker_key(row.bookmaker_identity)] = bookmaker
|
|
57
|
+
bookmakers_created += 1 if ensured.data[:created]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
case persist_row!(game, bookmaker, batch, row)
|
|
61
|
+
when :created
|
|
62
|
+
created += 1
|
|
63
|
+
when :updated
|
|
64
|
+
updated += 1
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
context.observation_batch_id = batch.id
|
|
69
|
+
context.bookmakers_resolved = resolved_ids.size
|
|
70
|
+
context.bookmakers_created = bookmakers_created
|
|
71
|
+
context.observations_created = created
|
|
72
|
+
context.observations_updated = updated
|
|
73
|
+
end
|
|
74
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
|
|
75
|
+
context.fail!(
|
|
76
|
+
application_error: Jumbotron::Errors::Canonical::MutationFailedError.new(
|
|
77
|
+
details: { message: e.message }
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def resolve_game!
|
|
85
|
+
ingest.game_identities.each do |ref|
|
|
86
|
+
identity = ProviderIdentity.find_by(
|
|
87
|
+
provider: ref.provider,
|
|
88
|
+
object_namespace: ref.namespace,
|
|
89
|
+
provider_id: ref.id
|
|
90
|
+
)
|
|
91
|
+
return identity.target if identity&.target.is_a?(Game)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
fail_transaction!(Jumbotron::Errors::Canonical::GameNotFoundError.new)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def persist_row!(game, bookmaker, batch, row)
|
|
98
|
+
existing = LineObservation.lock.where(
|
|
99
|
+
game_id: game.id,
|
|
100
|
+
bookmaker_id: bookmaker.id,
|
|
101
|
+
market: row.market,
|
|
102
|
+
outcome: row.outcome,
|
|
103
|
+
source: row.source
|
|
104
|
+
).order(observed_at: :desc, id: :desc).first
|
|
105
|
+
|
|
106
|
+
if existing && same_values?(existing, row)
|
|
107
|
+
existing.observed_at = ingest.observed_at
|
|
108
|
+
existing.save!
|
|
109
|
+
return :updated
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
LineObservation.create!(
|
|
113
|
+
game: game,
|
|
114
|
+
bookmaker: bookmaker,
|
|
115
|
+
observation_batch: batch,
|
|
116
|
+
market: row.market,
|
|
117
|
+
outcome: row.outcome,
|
|
118
|
+
source: row.source,
|
|
119
|
+
line_value: row.line_value,
|
|
120
|
+
price_american: row.price_american,
|
|
121
|
+
observed_at: ingest.observed_at,
|
|
122
|
+
changed_at: ingest.observed_at
|
|
123
|
+
)
|
|
124
|
+
:created
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def same_values?(existing, row)
|
|
128
|
+
same_decimal?(existing.line_value, row.line_value) &&
|
|
129
|
+
existing.price_american == row.price_american
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def same_decimal?(left, right)
|
|
133
|
+
return true if left.nil? && right.nil?
|
|
134
|
+
return false if left.nil? || right.nil?
|
|
135
|
+
|
|
136
|
+
BigDecimal(left.to_s) == BigDecimal(right.to_s)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def bookmaker_key(identity)
|
|
140
|
+
[identity.provider, identity.namespace, identity.id]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Canonical
|
|
6
|
+
class RecordObservationEvidence < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :change_set, required: true
|
|
8
|
+
validate :provider, is_a: String, required: true
|
|
9
|
+
validate :adapter_scope, is_a: String, required: true
|
|
10
|
+
validate :observed_at, required: true
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
unless change_set.any?
|
|
14
|
+
context.observation_batch_id = nil
|
|
15
|
+
context.historical_change_count = 0
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
batch = ObservationBatch.create!(
|
|
20
|
+
provider: provider,
|
|
21
|
+
adapter_scope: adapter_scope,
|
|
22
|
+
observed_at: observed_at,
|
|
23
|
+
metadata: {}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
change_set.each do |entry|
|
|
27
|
+
HistoricalChange.create!(
|
|
28
|
+
observation_batch: batch,
|
|
29
|
+
subject_type: entry.subject.class.name,
|
|
30
|
+
subject_id: entry.subject.id,
|
|
31
|
+
attribute_name: entry.attribute,
|
|
32
|
+
previous_value: entry.previous,
|
|
33
|
+
new_value: entry.new_value,
|
|
34
|
+
observed_at: observed_at,
|
|
35
|
+
provider: provider
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context.observation_batch_id = batch.id
|
|
40
|
+
context.historical_change_count = change_set.size
|
|
41
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
42
|
+
context.fail!(
|
|
43
|
+
application_error: Jumbotron::Errors::Canonical::ObservationEvidenceFailedError.new(
|
|
44
|
+
details: { message: e.message }
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Canonical
|
|
6
|
+
class ResolveCurrentLines < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :game, required: true
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
resolved = ResolveCurrentLinesForGames.call(game_ids: [game.id])
|
|
11
|
+
unless resolved.success?
|
|
12
|
+
context.fail!(application_error: resolved.errors.first)
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context.current_lines = Array(resolved.data[:current_lines])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|