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,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Workflows
|
|
5
|
+
class ExecuteDiscoveryWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
6
|
+
retry_strategy :delayed_continuation, max_attempts: 24
|
|
7
|
+
|
|
8
|
+
CONTENTION_WAIT_SECONDS = 5
|
|
9
|
+
|
|
10
|
+
def call(adapter_id:, discovery_id:)
|
|
11
|
+
resolved = Services::Adapters::ResolveDiscovery.call(
|
|
12
|
+
adapter_id: adapter_id,
|
|
13
|
+
discovery_id: discovery_id
|
|
14
|
+
)
|
|
15
|
+
return map_resolution_failure(resolved) unless resolved.success?
|
|
16
|
+
|
|
17
|
+
adapter = resolved.data[:adapter]
|
|
18
|
+
definition = resolved.data[:definition]
|
|
19
|
+
ensured = Services::Adapters::EnsureLeague.call(adapter: adapter)
|
|
20
|
+
return map_resolution_failure(ensured) unless ensured.success?
|
|
21
|
+
|
|
22
|
+
league = ensured.data[:league]
|
|
23
|
+
return cooldown_deferred(adapter) if adapter.provider_cooling_down?
|
|
24
|
+
|
|
25
|
+
expanded = adapter.scopes_for_discovery(definition)
|
|
26
|
+
return map_expand_failure(adapter, expanded) unless expanded.success?
|
|
27
|
+
|
|
28
|
+
scopes = Array(expanded.output).map do |acquisition|
|
|
29
|
+
{ endpoint: definition.endpoint, acquisition: acquisition }
|
|
30
|
+
end
|
|
31
|
+
return empty_scopes_failure(adapter, definition) if scopes.empty?
|
|
32
|
+
|
|
33
|
+
run_scopes(adapter, league, scopes, discovery_id)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def run_scopes(adapter, league, scopes, discovery_id)
|
|
39
|
+
scopes.each do |scope|
|
|
40
|
+
return cooldown_deferred(adapter) if adapter.provider_cooling_down?
|
|
41
|
+
|
|
42
|
+
executed = Services::Adapters::ExecuteSynchronizationScope.call(
|
|
43
|
+
adapter: adapter,
|
|
44
|
+
league: league,
|
|
45
|
+
endpoint: scope.fetch(:endpoint),
|
|
46
|
+
acquisition: scope.fetch(:acquisition)
|
|
47
|
+
)
|
|
48
|
+
classified = classify_scope(adapter, executed)
|
|
49
|
+
return classified unless classified.nil?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
success(
|
|
53
|
+
payload: { adapter_id: adapter.adapter_id, discovery_id: discovery_id.to_s },
|
|
54
|
+
http_status: :ok
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def classify_scope(adapter, executed)
|
|
59
|
+
unless executed.success?
|
|
60
|
+
return failure(
|
|
61
|
+
errors: [{ code: "discovery_execution_failed", message: "synchronization scope failed" }],
|
|
62
|
+
http_status: :unprocessable_entity,
|
|
63
|
+
meta: { failure_class: "mutation" }
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
data = executed.data
|
|
68
|
+
case data[:outcome]
|
|
69
|
+
when :succeeded
|
|
70
|
+
nil
|
|
71
|
+
when :contention
|
|
72
|
+
deferred(reason: :lease_contention, retry_after: CONTENTION_WAIT_SECONDS)
|
|
73
|
+
when :cooldown_active
|
|
74
|
+
cooldown_deferred(adapter)
|
|
75
|
+
else
|
|
76
|
+
classify_failed_scope(adapter, data)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def classify_failed_scope(adapter, data)
|
|
81
|
+
return cooldown_deferred(adapter) if data[:failure_class] == "acquisition"
|
|
82
|
+
|
|
83
|
+
failure(
|
|
84
|
+
errors: [{ code: "discovery_execution_failed", message: "canonical or transform failure" }],
|
|
85
|
+
http_status: :unprocessable_entity,
|
|
86
|
+
meta: { failure_class: data[:failure_class] || "mutation" }
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def map_expand_failure(adapter, expanded)
|
|
91
|
+
return cooldown_deferred(adapter) if adapter.provider_cooling_down?
|
|
92
|
+
|
|
93
|
+
error = expanded.respond_to?(:error) ? expanded.error : nil
|
|
94
|
+
failure(
|
|
95
|
+
errors: [{
|
|
96
|
+
code: "discovery_expansion_failed",
|
|
97
|
+
message: error.respond_to?(:message) ? error.message : "discovery expansion failed"
|
|
98
|
+
}],
|
|
99
|
+
http_status: :unprocessable_entity,
|
|
100
|
+
meta: { failure_class: "validation" }
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def empty_scopes_failure(adapter, definition)
|
|
105
|
+
failure(
|
|
106
|
+
errors: [{
|
|
107
|
+
code: "discovery_expansion_failed",
|
|
108
|
+
message: "discovery #{definition.id} produced no acquisition scopes"
|
|
109
|
+
}],
|
|
110
|
+
http_status: :unprocessable_entity,
|
|
111
|
+
meta: { failure_class: "validation", adapter_id: adapter.adapter_id }
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def cooldown_deferred(adapter)
|
|
116
|
+
wait = [Integer(adapter.provider_retry_after), 1].max
|
|
117
|
+
deferred(reason: :provider_cooldown, retry_after: wait)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def map_resolution_failure(result)
|
|
121
|
+
error = Array(result.errors).first
|
|
122
|
+
code = error.respond_to?(:code) ? error.code : "unknown_adapter"
|
|
123
|
+
failure(
|
|
124
|
+
errors: [{ code: code, message: error.respond_to?(:message) ? error.message : error.to_s }],
|
|
125
|
+
http_status: :unprocessable_entity,
|
|
126
|
+
meta: { failure_class: "validation" }
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Workflows
|
|
5
|
+
class ExecuteGameUpdatePolicyWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
6
|
+
retry_strategy :scheduled_cadence
|
|
7
|
+
|
|
8
|
+
def call(adapter_id:, policy_id:)
|
|
9
|
+
now = Time.current
|
|
10
|
+
resolved = Services::Adapters::ResolveGameUpdatePolicy.call(
|
|
11
|
+
adapter_id: adapter_id,
|
|
12
|
+
policy_id: policy_id
|
|
13
|
+
)
|
|
14
|
+
return map_resolution_failure(resolved) unless resolved.success?
|
|
15
|
+
|
|
16
|
+
adapter = resolved.data[:adapter]
|
|
17
|
+
policy = resolved.data[:policy]
|
|
18
|
+
selected = Services::Adapters::SelectEligibleGames.call(
|
|
19
|
+
adapter: adapter,
|
|
20
|
+
policy: policy,
|
|
21
|
+
now: now
|
|
22
|
+
)
|
|
23
|
+
return map_selection_failure(selected) unless selected.success?
|
|
24
|
+
|
|
25
|
+
league = selected.data[:league]
|
|
26
|
+
games = selected.data[:games]
|
|
27
|
+
built = Services::Adapters::BuildAcquisitionScopes.call(adapter: adapter, games: games)
|
|
28
|
+
return map_selection_failure(built) unless built.success?
|
|
29
|
+
|
|
30
|
+
scopes = built.data[:scopes]
|
|
31
|
+
counts = blank_counts.merge(
|
|
32
|
+
eligible_games: games.size,
|
|
33
|
+
unique_scopes: scopes.size,
|
|
34
|
+
skipped_incomplete_scope: built.data[:skipped_incomplete_scope]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return success_payload(adapter, policy, counts) if scopes.empty?
|
|
38
|
+
|
|
39
|
+
run_scopes(adapter, league, scopes, counts)
|
|
40
|
+
finish(adapter, policy, counts)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def run_scopes(adapter, league, scopes, counts)
|
|
46
|
+
scopes.each_with_index do |scope, index|
|
|
47
|
+
if adapter.provider_cooling_down?
|
|
48
|
+
counts[:skipped_cooldown] += scopes.size - index
|
|
49
|
+
break
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
handle_scope(adapter, league, scope, counts)
|
|
53
|
+
next unless counts[:stop_remaining]
|
|
54
|
+
|
|
55
|
+
remaining = scopes.size - index - 1
|
|
56
|
+
counts[:skipped_cooldown] += remaining if remaining.positive?
|
|
57
|
+
break
|
|
58
|
+
end
|
|
59
|
+
counts.delete(:stop_remaining)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def handle_scope(adapter, league, scope, counts)
|
|
63
|
+
executed = Services::Adapters::ExecuteSynchronizationScope.call(
|
|
64
|
+
adapter: adapter,
|
|
65
|
+
league: league,
|
|
66
|
+
endpoint: scope.fetch(:endpoint),
|
|
67
|
+
acquisition: scope.fetch(:acquisition)
|
|
68
|
+
)
|
|
69
|
+
unless executed.success?
|
|
70
|
+
counts[:failed_scopes] += 1
|
|
71
|
+
counts[:failure_class] ||= "mutation"
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
record_scope_outcome(executed.data, counts)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def record_scope_outcome(data, counts)
|
|
79
|
+
case data[:outcome]
|
|
80
|
+
when :succeeded
|
|
81
|
+
counts[:succeeded_scopes] += 1
|
|
82
|
+
when :contention
|
|
83
|
+
counts[:skipped_contention] += 1
|
|
84
|
+
when :cooldown_active
|
|
85
|
+
counts[:skipped_cooldown] += 1
|
|
86
|
+
counts[:stop_remaining] = true
|
|
87
|
+
else
|
|
88
|
+
counts[:failed_scopes] += 1
|
|
89
|
+
counts[:failure_class] ||= data[:failure_class]
|
|
90
|
+
counts[:stop_remaining] = true if data[:failure_class] == "acquisition"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def finish(adapter, policy, counts)
|
|
95
|
+
payload = result_payload(adapter, policy, counts)
|
|
96
|
+
if counts[:failed_scopes].positive?
|
|
97
|
+
return failure(
|
|
98
|
+
errors: [{ code: "policy_execution_failed", message: "one or more scopes failed" }],
|
|
99
|
+
http_status: counts[:failure_class] == "acquisition" ? :bad_gateway : :unprocessable_entity,
|
|
100
|
+
meta: { failure_class: counts[:failure_class] || "mutation", execution: payload }
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
success(payload: payload, http_status: :ok)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def success_payload(adapter, policy, counts)
|
|
108
|
+
success(payload: result_payload(adapter, policy, counts), http_status: :ok)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def result_payload(adapter, policy, counts)
|
|
112
|
+
{
|
|
113
|
+
adapter_id: adapter.adapter_id,
|
|
114
|
+
policy_id: policy.id.to_s,
|
|
115
|
+
eligible_games: counts[:eligible_games],
|
|
116
|
+
unique_scopes: counts[:unique_scopes],
|
|
117
|
+
succeeded_scopes: counts[:succeeded_scopes],
|
|
118
|
+
skipped_contention: counts[:skipped_contention],
|
|
119
|
+
skipped_cooldown: counts[:skipped_cooldown],
|
|
120
|
+
skipped_incomplete_scope: counts[:skipped_incomplete_scope],
|
|
121
|
+
failed_scopes: counts[:failed_scopes]
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def blank_counts
|
|
126
|
+
{
|
|
127
|
+
succeeded_scopes: 0,
|
|
128
|
+
skipped_contention: 0,
|
|
129
|
+
skipped_cooldown: 0,
|
|
130
|
+
skipped_incomplete_scope: 0,
|
|
131
|
+
failed_scopes: 0
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def map_resolution_failure(result)
|
|
136
|
+
error = Array(result.errors).first
|
|
137
|
+
code = error.respond_to?(:code) ? error.code : "unknown_adapter"
|
|
138
|
+
failure(
|
|
139
|
+
errors: [{ code: code, message: error.respond_to?(:message) ? error.message : error.to_s }],
|
|
140
|
+
http_status: :unprocessable_entity,
|
|
141
|
+
meta: { failure_class: "validation" }
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def map_selection_failure(result)
|
|
146
|
+
error = Array(result.errors).first
|
|
147
|
+
if error.is_a?(CommandTower::Errors::ApplicationError)
|
|
148
|
+
return failure(
|
|
149
|
+
errors: [error],
|
|
150
|
+
http_status: :unprocessable_entity,
|
|
151
|
+
meta: { failure_class: "validation" }
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
map_resolution_failure(result)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Workflows
|
|
5
|
+
class ExecuteLineUpdatePolicyWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
6
|
+
retry_strategy :scheduled_cadence
|
|
7
|
+
|
|
8
|
+
def call(adapter_id:, policy_id:)
|
|
9
|
+
now = Time.current
|
|
10
|
+
resolved = Services::Adapters::ResolveLineUpdatePolicy.call(
|
|
11
|
+
adapter_id: adapter_id,
|
|
12
|
+
policy_id: policy_id
|
|
13
|
+
)
|
|
14
|
+
return map_resolution_failure(resolved) unless resolved.success?
|
|
15
|
+
|
|
16
|
+
adapter = resolved.data[:adapter]
|
|
17
|
+
policy = resolved.data[:policy]
|
|
18
|
+
selected = Services::Adapters::SelectEligibleGames.call(
|
|
19
|
+
adapter: adapter,
|
|
20
|
+
policy: policy,
|
|
21
|
+
now: now
|
|
22
|
+
)
|
|
23
|
+
return map_selection_failure(selected) unless selected.success?
|
|
24
|
+
|
|
25
|
+
games = selected.data[:games]
|
|
26
|
+
built = Services::Adapters::BuildLineAcquisitionScopes.call(adapter: adapter, games: games)
|
|
27
|
+
return map_selection_failure(built) unless built.success?
|
|
28
|
+
|
|
29
|
+
scopes = built.data[:scopes]
|
|
30
|
+
counts = blank_counts.merge(
|
|
31
|
+
eligible_games: games.size,
|
|
32
|
+
unique_scopes: scopes.size,
|
|
33
|
+
skipped_incomplete_scope: built.data[:skipped_incomplete_scope]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return success_payload(adapter, policy, counts) if scopes.empty?
|
|
37
|
+
|
|
38
|
+
run_scopes(adapter, scopes, counts)
|
|
39
|
+
finish(adapter, policy, counts)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def run_scopes(adapter, scopes, counts)
|
|
45
|
+
scopes.each_with_index do |scope, index|
|
|
46
|
+
if adapter.provider_cooling_down?
|
|
47
|
+
counts[:skipped_cooldown] += scopes.size - index
|
|
48
|
+
break
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
handle_scope(adapter, scope, counts)
|
|
52
|
+
next unless counts[:stop_remaining]
|
|
53
|
+
|
|
54
|
+
remaining = scopes.size - index - 1
|
|
55
|
+
counts[:skipped_cooldown] += remaining if remaining.positive?
|
|
56
|
+
break
|
|
57
|
+
end
|
|
58
|
+
counts.delete(:stop_remaining)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def handle_scope(adapter, scope, counts)
|
|
62
|
+
executed = Services::Adapters::ExecuteLineAcquisitionScope.call(
|
|
63
|
+
adapter: adapter,
|
|
64
|
+
acquisition: scope.fetch(:acquisition)
|
|
65
|
+
)
|
|
66
|
+
unless executed.success?
|
|
67
|
+
counts[:failed_scopes] += 1
|
|
68
|
+
counts[:failure_class] ||= "mutation"
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
record_scope_outcome(executed.data, counts)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def record_scope_outcome(data, counts)
|
|
76
|
+
case data[:outcome]
|
|
77
|
+
when :succeeded
|
|
78
|
+
counts[:succeeded_scopes] += 1
|
|
79
|
+
when :contention
|
|
80
|
+
counts[:skipped_contention] += 1
|
|
81
|
+
when :cooldown_active
|
|
82
|
+
counts[:skipped_cooldown] += 1
|
|
83
|
+
counts[:stop_remaining] = true
|
|
84
|
+
else
|
|
85
|
+
counts[:failed_scopes] += 1
|
|
86
|
+
counts[:failure_class] ||= data[:failure_class]
|
|
87
|
+
counts[:stop_remaining] = true if data[:failure_class] == "acquisition"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def finish(adapter, policy, counts)
|
|
92
|
+
payload = result_payload(adapter, policy, counts)
|
|
93
|
+
if counts[:failed_scopes].positive?
|
|
94
|
+
return failure(
|
|
95
|
+
errors: [{ code: "policy_execution_failed", message: "one or more scopes failed" }],
|
|
96
|
+
http_status: counts[:failure_class] == "acquisition" ? :bad_gateway : :unprocessable_entity,
|
|
97
|
+
meta: { failure_class: counts[:failure_class] || "mutation", execution: payload }
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
success(payload: payload, http_status: :ok)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def success_payload(adapter, policy, counts)
|
|
105
|
+
success(payload: result_payload(adapter, policy, counts), http_status: :ok)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def result_payload(adapter, policy, counts)
|
|
109
|
+
{
|
|
110
|
+
adapter_id: adapter.adapter_id,
|
|
111
|
+
policy_id: policy.id.to_s,
|
|
112
|
+
eligible_games: counts[:eligible_games],
|
|
113
|
+
unique_scopes: counts[:unique_scopes],
|
|
114
|
+
succeeded_scopes: counts[:succeeded_scopes],
|
|
115
|
+
skipped_contention: counts[:skipped_contention],
|
|
116
|
+
skipped_cooldown: counts[:skipped_cooldown],
|
|
117
|
+
skipped_incomplete_scope: counts[:skipped_incomplete_scope],
|
|
118
|
+
failed_scopes: counts[:failed_scopes]
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def blank_counts
|
|
123
|
+
{
|
|
124
|
+
succeeded_scopes: 0,
|
|
125
|
+
skipped_contention: 0,
|
|
126
|
+
skipped_cooldown: 0,
|
|
127
|
+
skipped_incomplete_scope: 0,
|
|
128
|
+
failed_scopes: 0
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def map_resolution_failure(result)
|
|
133
|
+
error = Array(result.errors).first
|
|
134
|
+
code = error.respond_to?(:code) ? error.code : "unknown_adapter"
|
|
135
|
+
failure(
|
|
136
|
+
errors: [{ code: code, message: error.respond_to?(:message) ? error.message : error.to_s }],
|
|
137
|
+
http_status: :unprocessable_entity,
|
|
138
|
+
meta: { failure_class: "validation" }
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def map_selection_failure(result)
|
|
143
|
+
error = Array(result.errors).first
|
|
144
|
+
if error.is_a?(CommandTower::Errors::ApplicationError)
|
|
145
|
+
return failure(
|
|
146
|
+
errors: [error],
|
|
147
|
+
http_status: :unprocessable_entity,
|
|
148
|
+
meta: { failure_class: "validation" }
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
map_resolution_failure(result)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Workflows
|
|
5
|
+
module PublicRead
|
|
6
|
+
def map_deserializer_failure(deserialized)
|
|
7
|
+
error = Array(deserialized.errors).first
|
|
8
|
+
message =
|
|
9
|
+
if error.is_a?(Hash)
|
|
10
|
+
error[:message].presence || error[:code].to_s
|
|
11
|
+
else
|
|
12
|
+
error.to_s
|
|
13
|
+
end
|
|
14
|
+
failure(
|
|
15
|
+
errors: [{ code: "invalid_request", message: message.presence || "invalid_request" }],
|
|
16
|
+
http_status: :unprocessable_entity
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def map_service_failure(result)
|
|
21
|
+
error = Array(result.errors).first
|
|
22
|
+
code = error.respond_to?(:code) ? error.code.to_s : "invalid_request"
|
|
23
|
+
message = error.respond_to?(:message) ? error.message : error.to_s
|
|
24
|
+
status = Public::TranslateOutcome::NOT_FOUND_CODES.include?(code) ? :not_found : :unprocessable_entity
|
|
25
|
+
failure(errors: [{ code: code, message: message }], http_status: status)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Workflows
|
|
5
|
+
class ReadConsensusWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
6
|
+
include PublicRead
|
|
7
|
+
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(**kwargs)
|
|
11
|
+
deserialized = Deserializers::Public::GameIds.call(kwargs)
|
|
12
|
+
return map_deserializer_failure(deserialized) unless deserialized.success?
|
|
13
|
+
|
|
14
|
+
loaded = Services::Public::LoadGamesByIds.call(game_ids: deserialized.input.game_ids)
|
|
15
|
+
return map_service_failure(loaded) unless loaded.success?
|
|
16
|
+
|
|
17
|
+
games = loaded.data[:games]
|
|
18
|
+
resolved = Services::Public::ResolveCurrentLinesForGames.call(games: games)
|
|
19
|
+
return map_service_failure(resolved) unless resolved.success?
|
|
20
|
+
|
|
21
|
+
derived = Services::Public::DeriveConsensusForGames.call(
|
|
22
|
+
current_lines: resolved.data[:current_lines]
|
|
23
|
+
)
|
|
24
|
+
return map_service_failure(derived) unless derived.success?
|
|
25
|
+
|
|
26
|
+
mapped = Services::Public::MapPublicConsensusLines.call(
|
|
27
|
+
consensus_lines: derived.data[:consensus_lines],
|
|
28
|
+
current_lines: resolved.data[:current_lines],
|
|
29
|
+
include_constituents: false
|
|
30
|
+
)
|
|
31
|
+
return map_service_failure(mapped) unless mapped.success?
|
|
32
|
+
|
|
33
|
+
success(payload: { consensus_lines: mapped.data[:public_consensus_lines] }, http_status: :ok)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Workflows
|
|
5
|
+
class ReadCurrentLinesWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
6
|
+
include PublicRead
|
|
7
|
+
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(**kwargs)
|
|
11
|
+
deserialized = Deserializers::Public::GameIds.call(kwargs)
|
|
12
|
+
return map_deserializer_failure(deserialized) unless deserialized.success?
|
|
13
|
+
|
|
14
|
+
loaded = Services::Public::LoadGamesByIds.call(game_ids: deserialized.input.game_ids)
|
|
15
|
+
return map_service_failure(loaded) unless loaded.success?
|
|
16
|
+
|
|
17
|
+
resolved = Services::Public::ResolveCurrentLinesForGames.call(games: loaded.data[:games])
|
|
18
|
+
return map_service_failure(resolved) unless resolved.success?
|
|
19
|
+
|
|
20
|
+
mapped = Services::Public::MapPublicCurrentLines.call(current_lines: resolved.data[:current_lines])
|
|
21
|
+
return map_service_failure(mapped) unless mapped.success?
|
|
22
|
+
|
|
23
|
+
success(payload: { current_lines: mapped.data[:public_current_lines] }, http_status: :ok)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Workflows
|
|
5
|
+
class ReadGameWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
6
|
+
include PublicRead
|
|
7
|
+
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(**kwargs)
|
|
11
|
+
deserialized = Deserializers::Public::Game.call(kwargs)
|
|
12
|
+
return map_deserializer_failure(deserialized) unless deserialized.success?
|
|
13
|
+
|
|
14
|
+
request = deserialized.input
|
|
15
|
+
read = Services::Public::ReadGame.call(game_id: request.id)
|
|
16
|
+
return map_service_failure(read) unless read.success?
|
|
17
|
+
|
|
18
|
+
game = read.data[:game]
|
|
19
|
+
included = Services::Public::LoadIncludedLines.call(games: [game], includes: request.includes)
|
|
20
|
+
return map_service_failure(included) unless included.success?
|
|
21
|
+
|
|
22
|
+
assembled = Services::Public::AssemblePublicGame.call(
|
|
23
|
+
game: game,
|
|
24
|
+
includes: request.includes,
|
|
25
|
+
current_lines: included.data[:current_lines],
|
|
26
|
+
consensus_lines: included.data[:consensus_lines]
|
|
27
|
+
)
|
|
28
|
+
return map_service_failure(assembled) unless assembled.success?
|
|
29
|
+
|
|
30
|
+
success(payload: { game: assembled.data[:public_game] }, http_status: :ok)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Workflows
|
|
5
|
+
class ReadScheduleGroupsWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
6
|
+
include PublicRead
|
|
7
|
+
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(**kwargs)
|
|
11
|
+
deserialized = Deserializers::Public::ScheduleGroups.call(kwargs)
|
|
12
|
+
return map_deserializer_failure(deserialized) unless deserialized.success?
|
|
13
|
+
|
|
14
|
+
read = Services::Public::ReadScheduleGroups.call(request: deserialized.input)
|
|
15
|
+
return map_service_failure(read) unless read.success?
|
|
16
|
+
|
|
17
|
+
success(payload: { schedule_groups: read.data[:schedule_groups] }, http_status: :ok)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Workflows
|
|
5
|
+
class ReadScheduleWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
6
|
+
include PublicRead
|
|
7
|
+
|
|
8
|
+
retry_strategy :none
|
|
9
|
+
|
|
10
|
+
def call(**kwargs)
|
|
11
|
+
deserialized = Deserializers::Public::Schedule.call(kwargs)
|
|
12
|
+
return map_deserializer_failure(deserialized) unless deserialized.success?
|
|
13
|
+
|
|
14
|
+
request = deserialized.input
|
|
15
|
+
read = Services::Public::ReadSchedule.call(request: request)
|
|
16
|
+
return map_service_failure(read) unless read.success?
|
|
17
|
+
|
|
18
|
+
games = Array(read.data[:games])
|
|
19
|
+
included = Services::Public::LoadIncludedLines.call(games: games, includes: request.includes)
|
|
20
|
+
return map_service_failure(included) unless included.success?
|
|
21
|
+
|
|
22
|
+
public_games = games.map do |game|
|
|
23
|
+
assembled = Services::Public::AssemblePublicGame.call(
|
|
24
|
+
game: game,
|
|
25
|
+
includes: request.includes,
|
|
26
|
+
current_lines: included.data[:current_lines],
|
|
27
|
+
consensus_lines: included.data[:consensus_lines]
|
|
28
|
+
)
|
|
29
|
+
return map_service_failure(assembled) unless assembled.success?
|
|
30
|
+
|
|
31
|
+
assembled.data[:public_game]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
schedule = Services::Public::AssemblePublicSchedule.call(
|
|
35
|
+
request: request,
|
|
36
|
+
league: read.data[:league],
|
|
37
|
+
season: read.data[:season],
|
|
38
|
+
season_phase: read.data[:season_phase],
|
|
39
|
+
schedule_group: read.data[:schedule_group],
|
|
40
|
+
public_games: public_games
|
|
41
|
+
)
|
|
42
|
+
return map_service_failure(schedule) unless schedule.success?
|
|
43
|
+
|
|
44
|
+
success(payload: { schedule: schedule.data[:schedule] }, http_status: :ok)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|