Class: SchedulerGoogleCalendarService
- Inherits:
-
Object
- Object
- SchedulerGoogleCalendarService
- Defined in:
- app/services/scheduler_google_calendar_service.rb
Overview
Service object: scheduler google calendar service.
Constant Summary collapse
- CALENDAR_ID =
Calendar id.
'primary'- TIMEZONE =
Timezone.
'America/Chicago'- MAX_REFRESH_ATTEMPTS =
One token refresh per service instance, mirroring
TimeOffRequests::GoogleCalendar::MAX_RETRIES. A successful refresh is good
for the better part of an hour, so a second attempt would only ever be
retrying a grant that is genuinely broken. 1- FREEBUSY_BATCH_SIZE =
Google's FreeBusy endpoint takes up to this many calendars per request.
50
Class Method Summary collapse
-
.freebusy_for(employees, date, asker: nil) ⇒ Hash{Integer => Array<Hash>, nil}
Busy windows for a whole roster in ONE API call, keyed by employee id.
Instance Method Summary collapse
-
#busy_times(date) ⇒ Object
Returns an array of busy time ranges
[{start: Time, end: Time}, ...]for the given date. -
#busy_times_range(start_date, end_date) ⇒ Object
Single API call to fetch busy times for an entire date range.
- #connected? ⇒ Boolean
- #create_event(booking) ⇒ Object
- #delete_event(google_event_id) ⇒ Object
-
#initialize(employee) ⇒ SchedulerGoogleCalendarService
constructor
A new instance of SchedulerGoogleCalendarService.
- #update_event(booking) ⇒ Object
Constructor Details
#initialize(employee) ⇒ SchedulerGoogleCalendarService
Returns a new instance of SchedulerGoogleCalendarService.
55 56 57 58 59 |
# File 'app/services/scheduler_google_calendar_service.rb', line 55 def initialize(employee) @employee = employee @client = TimeOffRequests::GoogleAuthService.(employee) setup_service if @client end |
Class Method Details
.freebusy_for(employees, date, asker: nil) ⇒ Hash{Integer => Array<Hash>, nil}
Busy windows for a whole roster in ONE API call, keyed by employee id.
Reading a colleague's free/busy needs no service account and no
domain-wide delegation — the Workspace already permits it between users —
so any one healthy grant answers for everybody. That matters as much as the
call count: a per-rep loop fails per-rep, and a revoked grant then reads as
an empty calendar, which looks like "no meetings" rather than "no data".
FreeBusy returns intervals only: no titles (which we never use) and no
all-day flag, so all-day is inferred from a full-day span. That can only
ever over-state coverage, and CRM time off is the authority on whole-day
absence anyway.
Every employee gets a key. A missing key reads downstream as "no meetings",
which shows the rep free all day — so anyone we could not look up (no email
address, no working grant, a calendar Google omitted) is recorded as nil,
meaning "unreadable", not "free".
41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'app/services/scheduler_google_calendar_service.rb', line 41 def self.freebusy_for(employees, date, asker: nil) employees = employees.to_a return {} if employees.empty? unreadable = employees.to_h { |employee| [employee.id, nil] } addressable = employees.select { |employee| employee.email.present? } return unreadable if addressable.empty? client = (asker ? [asker, *addressable] : addressable) return unreadable unless client unreadable.merge(new_freebusy_query(client, addressable, date)) end |
Instance Method Details
#busy_times(date) ⇒ Object
Returns an array of busy time ranges [{start: Time, end: Time}, ...] for the given date
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'app/services/scheduler_google_calendar_service.rb', line 115 def busy_times(date) return [] unless connected? tz = ActiveSupport::TimeZone[TIMEZONE] time_min = tz.local(date.year, date.month, date.day).iso8601 time_max = tz.local(date.year, date.month, date.day, 23, 59, 59).iso8601 events = fetch_events(time_min, time_max) events.filter_map do |event| next if event.transparency == 'transparent' start_time = parse_event_time(event.start) end_time = parse_event_time(event.end) next unless start_time && end_time # all_day comes from the source rather than being inferred downstream: # Google sends date-only endpoints for all-day events, and a timed event # that happens to run midnight-to-midnight is indistinguishable once the # times are compared. Callers that only want real meetings need the flag. { start: start_time, end: end_time, all_day: event.start&.date_time.nil? } end rescue Google::Apis::AuthorizationError refresh_and_retry(:busy_times, date) rescue Google::Apis::ClientError => e Rails.logger.error("SchedulerGoogleCalendarService#busy_times failed for #{@employee.email}: #{e.}") [] end |
#busy_times_range(start_date, end_date) ⇒ Object
Single API call to fetch busy times for an entire date range.
Returns { Date => [{start: Time, end: Time}, ...], ... }
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'app/services/scheduler_google_calendar_service.rb', line 157 def busy_times_range(start_date, end_date) return {} unless connected? tz = ActiveSupport::TimeZone[TIMEZONE] time_min = tz.local(start_date.year, start_date.month, start_date.day).iso8601 time_max = tz.local(end_date.year, end_date.month, end_date.day, 23, 59, 59).iso8601 events = fetch_events(time_min, time_max) result = Hash.new { |h, k| h[k] = [] } events.each do |event| next if event.transparency == 'transparent' if event.start&.date event_start_date = event.start.date.is_a?(Date) ? event.start.date : Date.parse(event.start.date) event_end_date = event.end.date.is_a?(Date) ? event.end.date : Date.parse(event.end.date) (event_start_date...event_end_date).each do |d| next if d < start_date || d > end_date day_start = tz.local(d.year, d.month, d.day) result[d] << { start: day_start, end: day_start + 1.day } end else start_time = parse_event_time(event.start) end_time = parse_event_time(event.end) next unless start_time && end_time result[start_time.to_date] << { start: start_time, end: end_time } end end result rescue Google::Apis::AuthorizationError refresh_and_retry(:busy_times_range, start_date, end_date) rescue Google::Apis::ClientError => e Rails.logger.error("SchedulerGoogleCalendarService#busy_times_range failed for #{@employee.email}: #{e.}") {} end |
#connected? ⇒ Boolean
110 111 112 |
# File 'app/services/scheduler_google_calendar_service.rb', line 110 def connected? @client.present? end |
#create_event(booking) ⇒ Object
143 144 145 146 147 148 149 150 151 152 153 |
# File 'app/services/scheduler_google_calendar_service.rb', line 143 def create_event(booking) return unless connected? event = build_event(booking) created = @service.insert_event(CALENDAR_ID, event, send_updates: 'all') booking.update_column(:google_event_id, created.id) rescue Google::Apis::AuthorizationError refresh_and_retry(:create_event, booking) rescue Google::Apis::ClientError => e Rails.logger.error("SchedulerGoogleCalendarService#create_event failed: #{e.}") end |
#delete_event(google_event_id) ⇒ Object
207 208 209 210 211 212 213 214 215 |
# File 'app/services/scheduler_google_calendar_service.rb', line 207 def delete_event(google_event_id) return unless connected? && google_event_id.present? @service.delete_event(CALENDAR_ID, google_event_id) rescue Google::Apis::AuthorizationError refresh_and_retry(:delete_event, google_event_id) rescue Google::Apis::ClientError => e Rails.logger.error("SchedulerGoogleCalendarService#delete_event failed: #{e.}") end |
#update_event(booking) ⇒ Object
196 197 198 199 200 201 202 203 204 205 |
# File 'app/services/scheduler_google_calendar_service.rb', line 196 def update_event(booking) return create_event(booking) unless connected? && booking.google_event_id.present? event = build_event(booking) @service.patch_event(CALENDAR_ID, booking.google_event_id, event, send_updates: 'all') rescue Google::Apis::AuthorizationError refresh_and_retry(:update_event, booking) rescue Google::Apis::ClientError => e Rails.logger.error("SchedulerGoogleCalendarService#update_event failed: #{e.}") end |