Class: SpreeCmCommissioner::WaitingGuestsCaller

Inherits:
BaseInteractor show all
Defined in:
app/interactors/spree_cm_commissioner/waiting_guests_caller.rb

Constant Summary collapse

MIN_WAIT_TO_ENTER_SECONDS =

2 min floor — Waiting Room step (full journey, more uncertainty)

120
MIN_QUEUE_TO_ENTER_SECONDS =

1 min floor — Queue step (position assigned, one caller cycle minimum)

60

Instance Method Summary collapse

Instance Method Details

#callObject



9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 9

def call
  available_slots = fetch_available_slots
  return unless available_slots.positive?

  long_waiting_guests = fetch_long_waiting_guests(available_slots)
  calling_all(long_waiting_guests)

  mark_as(
    full: long_waiting_guests.size >= available_slots,
    available_slots: available_slots - long_waiting_guests.size,
    avg_wait_to_enter_seconds: compute_avg_wait_to_enter_seconds(long_waiting_guests),
    avg_queue_to_enter_seconds: compute_avg_queue_to_enter_seconds(long_waiting_guests)
  )
end

#calling_all(waiting_guests) ⇒ Object

For alert waiting guests to enter room, we just update :allow_to_enter_room_at. App will listen to firebase & start refresh session token to enter room.



73
74
75
76
77
78
79
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 73

def calling_all(waiting_guests)
  waiting_guests.each do |document|
    data = document.data.dup
    data[:allow_to_enter_room_at] = Time.zone.now
    document.ref.update(data)
  end
end

#compute_avg_queue_to_enter_seconds(guests) ⇒ Object

Average queue wait (position_set_at → allow_to_enter_room_at) for just-called guests. Used for the Queue step ETA: how long after getting a position until I actually enter. Returns nil when none of the called guests had position_set_at (first run, or no prior stamp).



110
111
112
113
114
115
116
117
118
119
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 110

def compute_avg_queue_to_enter_seconds(guests)
  return nil if guests.empty?

  now = Time.zone.now
  positioned = guests.select { |doc| doc.data[:position_set_at] }
  return nil if positioned.empty?

  total = positioned.sum { |doc| (now - doc.data[:position_set_at].to_time).to_f }
  [(total / positioned.size).round, MIN_QUEUE_TO_ENTER_SECONDS].max
end

#compute_avg_wait_to_enter_seconds(guests) ⇒ Object

Average total wait (queued_at → allow_to_enter_room_at) for just-called guests. Used for the Waiting Room step ETA: how long until I get in from when I joined.



99
100
101
102
103
104
105
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 99

def compute_avg_wait_to_enter_seconds(guests)
  return nil if guests.empty?

  now = Time.zone.now
  total = guests.sum { |doc| (now - doc.data[:queued_at].to_time).to_f }
  [(total / guests.size).round, MIN_WAIT_TO_ENTER_SECONDS].max
end

#current_dateObject



81
82
83
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 81

def current_date
  Time.zone.now.strftime('%Y-%m-%d')
end

#default_records_path(date) ⇒ Object



67
68
69
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 67

def default_records_path(date)
  "waiting_guests/#{date}/records"
end

#eligible_guests_in(records_path, limit) ⇒ Object



49
50
51
52
53
54
55
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 49

def eligible_guests_in(records_path, limit)
  firestore.col(records_path)
           .where('allow_to_enter_room_at', '==', nil)
           .order('queued_at')
           .limit(limit)
           .get.to_a
end

#fetch_available_slotsObject



24
25
26
27
28
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 24

def fetch_available_slots
  max_sessions = fetch_max_sessions
  active_sessions = SpreeCmCommissioner::WaitingRoomSession.active.count
  max_sessions - active_sessions
end

#fetch_long_waiting_guests(available_slots) ⇒ Object

This query requires an index; create it in Firebase beforehand. Client must create waiting_guests documents with :queued_at and :allow_to_enter_room_at set to nil to allow filter + order queries.

Yesterday’s guests are always older than today’s, so fill from yesterday first, then use any leftover slots for today. This way no one queued before the midnight rollover gets skipped.

e.g. 5 slots, 2 waiting in yesterday -> take both, then take 3 from today.


36
37
38
39
40
41
42
43
44
45
46
47
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 36

def fetch_long_waiting_guests(available_slots)
  previous_guests = eligible_guests_in(previous_records_path, available_slots)

  # Pre-flip window: the lobby pointer still points at yesterday, so both paths resolve to the
  # same partition — return now to avoid querying (and double-counting) it twice.
  return previous_guests if records_path == previous_records_path

  remaining_slots = available_slots - previous_guests.size
  return previous_guests if remaining_slots <= 0

  previous_guests + eligible_guests_in(records_path, remaining_slots)
end

#fetch_max_sessionsObject



129
130
131
132
133
134
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 129

def fetch_max_sessions
  fetcher = SpreeCmCommissioner::WaitingRoomSystemMetadataFetcher.new(firestore: firestore)
  fetcher.load_document_data

  fetcher.max_sessions_count_with_min
end

#firestoreObject



136
137
138
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 136

def firestore
  @firestore ||= Google::Cloud::Firestore.new(project_id: [:project_id], credentials: )
end

#lobby_dataObject



121
122
123
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 121

def lobby_data
  @lobby_data ||= lobby_document.get.data
end

#lobby_documentObject



125
126
127
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 125

def lobby_document
  @lobby_document ||= firestore.col('waiting_rooms').doc('lobby')
end

#mark_as(full:, available_slots:, avg_wait_to_enter_seconds: nil, avg_queue_to_enter_seconds: nil) ⇒ Object

merge: true so we preserve the published ‘waiting_guests_records_path` on the lobby doc.



90
91
92
93
94
95
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 90

def mark_as(full:, available_slots:, avg_wait_to_enter_seconds: nil, avg_queue_to_enter_seconds: nil)
  data = { full: full, available_slots: available_slots }
  data[:avg_wait_to_enter_seconds] = avg_wait_to_enter_seconds if avg_wait_to_enter_seconds
  data[:avg_queue_to_enter_seconds] = avg_queue_to_enter_seconds if avg_queue_to_enter_seconds
  lobby_document.set(data, merge: true)
end

#previous_dateObject



85
86
87
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 85

def previous_date
  1.day.ago.strftime('%Y-%m-%d')
end

#previous_records_pathObject

Drain target is derived from the server date, never the (possibly stale) lobby pointer.



63
64
65
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 63

def previous_records_path
  default_records_path(previous_date)
end

#records_pathObject

Published path is authoritative; fall back to the server’s own date if not yet published.



58
59
60
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 58

def records_path
  lobby_data&.dig(:waiting_guests_records_path).presence || default_records_path(current_date)
end

#service_accountObject



140
141
142
# File 'app/interactors/spree_cm_commissioner/waiting_guests_caller.rb', line 140

def 
  @service_account ||= Rails.application.credentials.
end