jumbotron 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/app/adapters/jumbotron/adapters/sandbox/nfl.rb +135 -0
  3. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/acquisition_scope.rb +74 -0
  4. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/competition_odds.rb +82 -0
  5. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/current_observation_state.rb +53 -0
  6. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/line_acquisition_scope.rb +30 -0
  7. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/monotonic_observation.rb +113 -0
  8. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/observation_values.rb +53 -0
  9. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/outcome_candidates.rb +38 -0
  10. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/projection.rb +87 -0
  11. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/scoreboard.rb +115 -0
  12. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/synthetic_observation.rb +32 -0
  13. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/timeline_observation.rb +119 -0
  14. data/app/deserializers/jumbotron/deserializers/public/register_sandbox_projection.rb +42 -0
  15. data/app/deserializers/jumbotron/deserializers/public/reset_sandbox_projection.rb +51 -0
  16. data/app/errors/jumbotron/error.rb +7 -0
  17. data/app/errors/jumbotron/errors/sandbox.rb +67 -0
  18. data/app/errors/jumbotron/sandbox_reset_rejected_error.rb +6 -0
  19. data/app/models/jumbotron/game.rb +11 -0
  20. data/app/models/jumbotron/public/translate_outcome.rb +13 -7
  21. data/app/models/jumbotron/public.rb +18 -1
  22. data/app/models/jumbotron/sandbox_game_mapping.rb +24 -0
  23. data/app/models/jumbotron/sandbox_game_plan.rb +23 -0
  24. data/app/models/jumbotron/sandbox_projection.rb +30 -0
  25. data/app/models/jumbotron/sandbox_projection_data.rb +32 -0
  26. data/app/models/jumbotron/season.rb +10 -0
  27. data/app/providers/jumbotron/providers/sandbox/resources/competition_odds.rb +24 -0
  28. data/app/providers/jumbotron/providers/sandbox/resources/projection.rb +23 -0
  29. data/app/providers/jumbotron/providers/sandbox/resources/scoreboard.rb +24 -0
  30. data/app/services/jumbotron/services/adapters/execute_line_acquisition_scope.rb +2 -1
  31. data/app/services/jumbotron/services/canonical/upsert_game_graph.rb +126 -0
  32. data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +23 -3
  33. data/app/services/jumbotron/services/public/assemble_sandbox_projection.rb +30 -0
  34. data/app/services/jumbotron/services/public/assemble_sandbox_projection_reset.rb +49 -0
  35. data/app/services/jumbotron/services/sandbox/build_source_snapshot.rb +224 -0
  36. data/app/services/jumbotron/services/sandbox/project_game.rb +73 -0
  37. data/app/services/jumbotron/services/sandbox/register_projection.rb +76 -0
  38. data/app/services/jumbotron/services/sandbox/remove_empty_projection_geometry.rb +46 -0
  39. data/app/services/jumbotron/services/sandbox/remove_mapped_game.rb +29 -0
  40. data/app/services/jumbotron/services/sandbox/resolve_game_observation.rb +106 -0
  41. data/app/services/jumbotron/services/sandbox/seed_projected_game.rb +95 -0
  42. data/app/services/jumbotron/services/sandbox/select_line_basis.rb +69 -0
  43. data/app/services/jumbotron/services/sandbox/validate_projected_season.rb +77 -0
  44. data/app/workflows/jumbotron/workflows/public_read.rb +2 -1
  45. data/app/workflows/jumbotron/workflows/sandbox/register_projection_workflow.rb +33 -0
  46. data/app/workflows/jumbotron/workflows/sandbox/reset_projection_workflow.rb +158 -0
  47. data/app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb +3 -51
  48. data/db/migrate/20260908000001_create_jumbotron_sandbox_game_plans.rb +16 -0
  49. data/db/migrate/20260908000002_create_jumbotron_sandbox_projections.rb +39 -0
  50. data/docs/upgrades/0.2.0.md +74 -0
  51. data/lib/jumbotron/client.rb +14 -0
  52. data/lib/jumbotron/version.rb +1 -1
  53. data/spec/factories/jumbotron/sandbox_game_mapping.rb +24 -0
  54. data/spec/factories/jumbotron/sandbox_game_plan.rb +10 -0
  55. data/spec/factories/jumbotron/sandbox_projection.rb +18 -0
  56. metadata +59 -2
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Services
5
+ module Sandbox
6
+ class RegisterProjection < CommandTower::Services::ApplicationService
7
+ validate :request, required: true
8
+
9
+ def call # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
10
+ source_season, source_phase = resolve_source
11
+ return if source_season.nil?
12
+
13
+ existing = Jumbotron::SandboxProjection.find_by(name: request.name)
14
+ return reuse_or_reject(existing, source_season, source_phase) if existing
15
+
16
+ sandbox_league = ensure_sandbox_league
17
+ return if sandbox_league.nil?
18
+
19
+ sandbox_season = Jumbotron::Season.find_or_create_by!(league: sandbox_league, name: request.name)
20
+ context.projection = Jumbotron::SandboxProjection.create!(
21
+ name: request.name,
22
+ source_season: source_season,
23
+ source_anchor_phase: source_phase,
24
+ sandbox_season: sandbox_season,
25
+ adapter_id: "sandbox_nfl",
26
+ calendar_time_zone: "America/New_York"
27
+ )
28
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
29
+ reject("sandbox_projection_conflict", e.message)
30
+ end
31
+
32
+ private
33
+
34
+ def resolve_source # rubocop:disable Metrics/AbcSize
35
+ selector = request.source
36
+ sport = Jumbotron::Sport.where("LOWER(name) = ?", selector.sport.downcase).first
37
+ league = sport.leagues.where("LOWER(name) = ?", selector.league.downcase).first if sport
38
+ season = league.seasons.where("LOWER(name) = ?", selector.season.downcase).first if league
39
+ phase = season.season_phases.where("LOWER(name) = ?", selector.season_phase.downcase).first if season
40
+ return [season, phase] if season && phase
41
+
42
+ reject("source_season_not_found", "registered source season or phase was not found")
43
+ [nil, nil]
44
+ end
45
+
46
+ def ensure_sandbox_league
47
+ result = Jumbotron::Services::Adapters::EnsureLeague.call(
48
+ adapter: Jumbotron::Adapters::Sandbox::Nfl
49
+ )
50
+ return result.data[:league] if result.success?
51
+
52
+ reject("sandbox_projection_conflict", result.errors.first.to_s)
53
+ nil
54
+ end
55
+
56
+ def reuse_or_reject(existing, source_season, source_phase)
57
+ if existing.source_season_id == source_season.id &&
58
+ existing.source_anchor_phase_id == source_phase.id
59
+ context.projection = existing
60
+ return
61
+ end
62
+
63
+ reject("sandbox_projection_conflict", "sandbox name is already registered to another source")
64
+ end
65
+
66
+ def reject(code, message)
67
+ context.fail!(
68
+ application_error: Jumbotron::Errors::Sandbox::ProjectionRejectedError.new(
69
+ details: { code: code, message: message }
70
+ )
71
+ )
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Services
5
+ module Sandbox
6
+ class RemoveEmptyProjectionGeometry < CommandTower::Services::ApplicationService
7
+ validate :projection, required: true
8
+
9
+ def call
10
+ remove_empty_groups
11
+ remove_empty_phases
12
+ rescue ActiveRecord::ActiveRecordError => e
13
+ reject(e)
14
+ end
15
+
16
+ private
17
+
18
+ def reject(error)
19
+ context.fail!(
20
+ application_error: Jumbotron::Errors::Sandbox::ProjectionRejectedError.new(
21
+ details: {
22
+ code: "sandbox_projection_incomplete",
23
+ message: "stale sandbox schedule geometry could not be removed",
24
+ failure: error.class.name
25
+ }
26
+ )
27
+ )
28
+ end
29
+
30
+ def remove_empty_groups
31
+ projection.sandbox_season.schedule_groups
32
+ .left_outer_joins(:games)
33
+ .where(jumbotron_games: { id: nil })
34
+ .destroy_all
35
+ end
36
+
37
+ def remove_empty_phases
38
+ projection.sandbox_season.season_phases
39
+ .left_outer_joins(:games, :schedule_groups)
40
+ .where(jumbotron_games: { id: nil }, jumbotron_schedule_groups: { id: nil })
41
+ .destroy_all
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Services
5
+ module Sandbox
6
+ class RemoveMappedGame < CommandTower::Services::ApplicationService
7
+ validate :mapping, required: true
8
+
9
+ def call # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
10
+ game = mapping.sandbox_game
11
+ mapping.destroy!
12
+ game.reload
13
+ Jumbotron::SandboxGamePlan.find_by(game_id: game.id)&.destroy!
14
+ game.line_observations.destroy_all
15
+ game.game_participants.destroy_all
16
+ game.provider_identities.destroy_all
17
+ game.destroy!
18
+ context.removed_game_id = game.id
19
+ rescue ActiveRecord::ActiveRecordError => e
20
+ context.fail!(
21
+ application_error: Jumbotron::Errors::Sandbox::ProjectionRejectedError.new(
22
+ details: { code: "sandbox_projection_incomplete", message: e.message }
23
+ )
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Jumbotron
6
+ module Services
7
+ module Sandbox
8
+ class ResolveGameObservation < CommandTower::Services::ApplicationService
9
+ validate :game_id, required: true
10
+ validate :observed_at, required: true
11
+
12
+ def call
13
+ game = resolve_game
14
+ return if game.nil?
15
+
16
+ plan = resolve_plan(game)
17
+ return if plan.nil?
18
+
19
+ assign_observation(game, plan)
20
+ rescue ArgumentError, TypeError => e
21
+ fail_invalid_progression(e)
22
+ end
23
+
24
+ private
25
+
26
+ def resolve_game
27
+ game = Game.find_by(id: game_id)
28
+ return game if sandbox_game?(game)
29
+
30
+ context.fail!(application_error: Errors::Sandbox::InvalidGameError.new)
31
+ nil
32
+ end
33
+
34
+ def assign_observation(game, plan)
35
+ game.reload
36
+ context.plan = plan
37
+ context.scheduled_at = game.scheduled_at
38
+ context.observation = observation_for(game, plan)
39
+ end
40
+
41
+ def fail_invalid_progression(error)
42
+ context.fail!(
43
+ application_error: Errors::Sandbox::InvalidProgressionStateError.new(
44
+ details: { message: error.message }
45
+ )
46
+ )
47
+ end
48
+
49
+ def sandbox_game?(game)
50
+ return false if game.nil? || game.league.name != "nfl-sandbox"
51
+
52
+ game.provider_identities.any? do |identity|
53
+ identity.provider == "sandbox" && identity.object_namespace == "event"
54
+ end
55
+ end
56
+
57
+ def resolve_plan(game)
58
+ game.with_lock do
59
+ game.reload
60
+ game.sandbox_game_plan || create_plan(game)
61
+ end
62
+ end
63
+
64
+ def create_plan(game)
65
+ lines = Canonical::DeriveConsensusLine.call(game: game)
66
+ return fail_missing_lines unless lines.success?
67
+
68
+ candidates = outcome_candidates(Array(lines.data[:consensus_lines]))
69
+ return fail_missing_lines if candidates.empty?
70
+
71
+ selected = candidates.fetch(SecureRandom.random_number(candidates.length))
72
+ game.create_sandbox_game_plan!(
73
+ home_score: selected.home_score,
74
+ away_score: selected.away_score,
75
+ selected_at: observed_at
76
+ )
77
+ end
78
+
79
+ def outcome_candidates(lines)
80
+ spread = lines.find { |line| line.market == "spread" && line.outcome == "home" }
81
+ total = lines.find { |line| line.market == "total" && line.outcome == "over" }
82
+ return [] if spread.nil? || total.nil?
83
+
84
+ Jumbotron::Adapters::Sandbox::NflHelpers::OutcomeCandidates.call(
85
+ home_spread: spread.line_value,
86
+ total: total.line_value
87
+ )
88
+ end
89
+
90
+ def fail_missing_lines
91
+ context.fail!(application_error: Errors::Sandbox::OutcomeLineUnavailableError.new)
92
+ nil
93
+ end
94
+
95
+ def observation_for(game, plan)
96
+ Jumbotron::Adapters::Sandbox::NflHelpers::SyntheticObservation.call(
97
+ now: observed_at,
98
+ kickoff: game.scheduled_at,
99
+ plan: plan,
100
+ current: Jumbotron::Adapters::Sandbox::NflHelpers::CurrentObservationState.call(game)
101
+ )
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Services
5
+ module Sandbox
6
+ class SeedProjectedGame < CommandTower::Services::ApplicationService
7
+ validate :game, required: true
8
+ validate :observed_at, required: true
9
+ validate :change_set, required: true
10
+
11
+ def call
12
+ clear_previous_generation
13
+ return unless persist_lines
14
+ return unless usable_consensus?
15
+ return unless apply_scoreboard
16
+
17
+ context.game = game.reload
18
+ end
19
+
20
+ private
21
+
22
+ def clear_previous_generation
23
+ game.sandbox_game_plan&.destroy!
24
+ game.line_observations.destroy_all
25
+ end
26
+
27
+ def persist_lines
28
+ scope = Jumbotron::Adapters::Sandbox::Nfl.line_acquisition_scope_for(game.reload)
29
+ return reject("sandbox line scope is unavailable") if scope.nil?
30
+
31
+ result = Jumbotron::Services::Adapters::ExecuteLineAcquisitionScope.call(
32
+ adapter: Jumbotron::Adapters::Sandbox::Nfl,
33
+ acquisition: scope.fetch(:acquisition),
34
+ observed_at: observed_at
35
+ )
36
+ return true if result.success? && result.data[:outcome] == :succeeded
37
+
38
+ reject("sandbox line persistence failed")
39
+ false
40
+ end
41
+
42
+ def usable_consensus? # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
43
+ result = Jumbotron::Services::Canonical::DeriveConsensusLine.call(game: game.reload)
44
+ return reject("sandbox consensus could not be derived") unless result.success?
45
+
46
+ lines = Array(result.data[:consensus_lines])
47
+ spread = lines.find { |line| line.market == "spread" && line.outcome == "home" }
48
+ total = lines.find { |line| line.market == "total" && line.outcome == "over" }
49
+ return true if spread && total && spread.line_value == game.sandbox_game_mapping.home_spread &&
50
+ total.line_value == game.sandbox_game_mapping.total
51
+
52
+ reject("sandbox consensus does not match selected line basis")
53
+ false
54
+ end
55
+
56
+ def apply_scoreboard # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
57
+ scope = Jumbotron::Adapters::Sandbox::Nfl.acquisition_scope_for(game.reload)
58
+ return reject("sandbox scoreboard scope is unavailable") if scope.nil?
59
+
60
+ executed = Jumbotron::Services::Adapters::ExecuteEndpoint.call(
61
+ adapter: Jumbotron::Adapters::Sandbox::Nfl,
62
+ endpoint: :scoreboard,
63
+ observed_at: observed_at,
64
+ acquisition: scope.fetch(:acquisition)
65
+ )
66
+ return fail_from(executed) unless executed.success?
67
+
68
+ input = executed.data[:sync_input].games.first
69
+ result = Jumbotron::Services::Canonical::UpsertGameGraph.call(
70
+ game_input: input,
71
+ league: game.league,
72
+ observed_at: observed_at,
73
+ change_set: change_set
74
+ )
75
+ return true if result.success?
76
+
77
+ fail_from(result)
78
+ end
79
+
80
+ def fail_from(result)
81
+ context.fail!(application_error: Array(result.errors).first)
82
+ nil
83
+ end
84
+
85
+ def reject(message)
86
+ context.fail!(
87
+ application_error: Jumbotron::Errors::Sandbox::ProjectionRejectedError.new(
88
+ details: { code: "sandbox_projection_incomplete", message: message }
89
+ )
90
+ )
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "digest"
5
+
6
+ module Jumbotron
7
+ module Services
8
+ module Sandbox
9
+ class SelectLineBasis < CommandTower::Services::ApplicationService
10
+ SPREADS = (-21..21).reject(&:zero?).map { |value| BigDecimal(value.to_s) / 2 }.freeze
11
+ TOTALS = (75..109).map { |value| BigDecimal(value.to_s) / 2 }.freeze
12
+
13
+ validate :source_game_id, required: true
14
+ validate :reset_fingerprint, required: true
15
+ validate :source_spread, required: false
16
+ validate :source_total, required: false
17
+
18
+ def call # rubocop:disable Metrics/AbcSize
19
+ source_pair = normalized_source_pair
20
+ pair = source_pair || fallback_pair
21
+ context.home_spread = pair.fetch(:home_spread)
22
+ context.total = pair.fetch(:total)
23
+ context.line_source = source_pair ? "source_consensus" : "synthetic_fallback"
24
+ context.line_fingerprint = Digest::SHA256.hexdigest(
25
+ "#{reset_fingerprint}:#{source_game_id}:#{context.home_spread}:#{context.total}"
26
+ )
27
+ end
28
+
29
+ private
30
+
31
+ def normalized_source_pair
32
+ return if source_spread.nil? || source_total.nil?
33
+
34
+ spread = round_half(source_spread)
35
+ total = round_half(source_total)
36
+ return unless total.positive? && implied_scores(spread, total).all?(&:positive?)
37
+
38
+ { home_spread: spread, total: total }
39
+ rescue ArgumentError, TypeError
40
+ nil
41
+ end
42
+
43
+ def fallback_pair
44
+ candidates = weighted_fallback_candidates
45
+ index = Digest::SHA256.hexdigest("#{reset_fingerprint}:#{source_game_id}").to_i(16) % candidates.length
46
+ candidates.fetch(index)
47
+ end
48
+
49
+ def weighted_fallback_candidates
50
+ SPREADS.product(TOTALS).filter_map do |spread, total|
51
+ next unless implied_scores(spread, total).all? { |score| score.between?(10, 38) }
52
+
53
+ pair = { home_spread: spread, total: total }
54
+ weight = (spread.abs <= 6.5 ? 2 : 1) * (total.between?(41.5, 48.5) ? 2 : 1)
55
+ Array.new(weight, pair)
56
+ end.flatten
57
+ end
58
+
59
+ def implied_scores(spread, total)
60
+ [(total - spread) / 2, (total + spread) / 2]
61
+ end
62
+
63
+ def round_half(value)
64
+ BigDecimal((BigDecimal(value.to_s) * 2).round.to_s) / 2
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Services
5
+ module Sandbox
6
+ class ValidateProjectedSeason < CommandTower::Services::ApplicationService
7
+ validate :projection, required: true
8
+ validate :snapshot, required: true
9
+ validate :now, required: true
10
+
11
+ def call # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
12
+ mappings = projection.sandbox_game_mappings.includes(
13
+ sandbox_game: %i[sandbox_game_plan provider_identities line_observations schedule_group]
14
+ ).to_a
15
+ return reject("mapping coverage is incomplete") unless mapping_coverage?(mappings)
16
+ return reject("sandbox game identity is invalid") unless mappings.all? { |mapping| valid_identity?(mapping) }
17
+
18
+ complete = mappings.all? { |mapping| complete_state?(mapping) }
19
+ return reject("sandbox lines or outcomes are incomplete") unless complete
20
+ return reject("sandbox week windows overlap") unless non_overlapping?(mappings)
21
+
22
+ start_games = mappings.filter_map do |mapping|
23
+ game = mapping.sandbox_game
24
+ game if game.schedule_group.number == snapshot.start_week
25
+ end
26
+ context.start_week_game_count = start_games.count
27
+ context.naturally_locked_game_count = start_games.count { |game| game.scheduled_at <= now }
28
+ end
29
+
30
+ private
31
+
32
+ def mapping_coverage?(mappings)
33
+ mappings.map(&:source_game_id).sort == snapshot.games.map(&:source_game_id).sort &&
34
+ mappings.map(&:sandbox_game_id).uniq.length == mappings.length
35
+ end
36
+
37
+ def valid_identity?(mapping)
38
+ game = mapping.sandbox_game
39
+ game.season_id == projection.sandbox_season_id &&
40
+ game.league.name == "nfl-sandbox" &&
41
+ game.provider_identities.any? do |identity|
42
+ identity.provider == "sandbox" && identity.object_namespace == "event"
43
+ end
44
+ end
45
+
46
+ def complete_state?(mapping) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
47
+ game = mapping.sandbox_game
48
+ return false if game.sandbox_game_plan.nil?
49
+
50
+ result = Jumbotron::Services::Canonical::DeriveConsensusLine.call(game: game)
51
+ return false unless result.success?
52
+
53
+ lines = Array(result.data[:consensus_lines])
54
+ spread = lines.find { |line| line.market == "spread" && line.outcome == "home" }
55
+ total = lines.find { |line| line.market == "total" && line.outcome == "over" }
56
+ spread&.line_value == mapping.home_spread && total&.line_value == mapping.total
57
+ end
58
+
59
+ def non_overlapping?(mappings)
60
+ windows = mappings.group_by { |mapping| mapping.sandbox_game.schedule_group_id }.values.map do |members|
61
+ times = members.map { |mapping| mapping.sandbox_game.scheduled_at }
62
+ times.minmax
63
+ end.sort_by(&:first)
64
+ windows.each_cons(2).all? { |left, right| left.last < right.first }
65
+ end
66
+
67
+ def reject(message)
68
+ context.fail!(
69
+ application_error: Jumbotron::Errors::Sandbox::ProjectionRejectedError.new(
70
+ details: { code: "sandbox_projection_incomplete", message: message }
71
+ )
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -21,8 +21,9 @@ module Jumbotron
21
21
  error = Array(result.errors).first
22
22
  code = error.respond_to?(:code) ? error.code.to_s : "invalid_request"
23
23
  message = error.respond_to?(:message) ? error.message : error.to_s
24
+ details = error.respond_to?(:details) && error.details.is_a?(Hash) ? error.details.except(:message) : {}
24
25
  status = Public::TranslateOutcome::NOT_FOUND_CODES.include?(code) ? :not_found : :unprocessable_entity
25
- failure(errors: [{ code: code, message: message }], http_status: status)
26
+ failure(errors: [{ code: code, message: message, details: details }], http_status: status)
26
27
  end
27
28
  end
28
29
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Workflows
5
+ module Sandbox
6
+ class RegisterProjectionWorkflow < CommandTower::Workflows::ApplicationWorkflow
7
+ include Jumbotron::Workflows::PublicRead
8
+
9
+ retry_strategy :none
10
+
11
+ def call(**kwargs)
12
+ deserialized = Jumbotron::Deserializers::Public::RegisterSandboxProjection.call(kwargs)
13
+ return map_deserializer_failure(deserialized) unless deserialized.success?
14
+
15
+ transaction do
16
+ registered = Jumbotron::Services::Sandbox::RegisterProjection.call(request: deserialized.input)
17
+ fail_transaction!(map_service_failure(registered)) unless registered.success?
18
+
19
+ assembled = Jumbotron::Services::Public::AssembleSandboxProjection.call(
20
+ projection: registered.data[:projection]
21
+ )
22
+ fail_transaction!(map_service_failure(assembled)) unless assembled.success?
23
+
24
+ success(
25
+ payload: { sandbox_projection: assembled.data[:public_projection] },
26
+ http_status: :ok
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end