Module: ModelExt::Reservations::Callbacks

Extended by:
ActiveSupport::Concern
Includes:
ElasticAPM::SpanHelpers
Included in:
Reservation
Defined in:
lib/model_ext/reservations/callbacks.rb

Overview

callbacks stuff

Instance Method Summary collapse

Instance Method Details

#push_booking_notificationObject



206
207
208
209
210
211
212
213
# File 'lib/model_ext/reservations/callbacks.rb', line 206

def push_booking_notification
  is_pending_confirmation = ack == false && confirmed_by.nil? && active == true
  not_temporary = is_temporary == false && for_locking_system == false
  return unless is_pending_confirmation && not_temporary

  NotificationWorkers::Partner::BookingWorker.perform_async(id)
  true
end

#push_inventory_notificationObject



215
216
217
218
# File 'lib/model_ext/reservations/callbacks.rb', line 215

def push_inventory_notification
  NotificationWorkers::Partner::InventoryWorker.perform_async(id)
  true
end

#trigger_creation_sync(sync_type = 'full') ⇒ void

This method returns an undefined value.

Manual sync trigger for reservation creation (immediate for admin/staff, delayed for users)

Call this method explicitly after reservation creation to ensure proper timing and avoid missed relations during the creation process.

Parameters:

  • sync_type (String) (defaults to: 'full')

    the type of sync to perform (default: 'full')



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/model_ext/reservations/callbacks.rb', line 253

def trigger_creation_sync(sync_type = 'full')
  if created_by_admin_staff_or_owner?
    # Admin/Staff/Owner: Immediate sync for real-time partner portal updates
    begin
      Partner::ReservationSummarySyncWorker.new.perform(id, sync_type)
    rescue StandardError => e
      APMErrorHandler.report('Immediate creation sync failed', {
                               reservation_id: id,
                               sync_type: sync_type,
                               created_by: created_by,
                               error_message: e.message,
                               exception: e,
                             })
      # Fallback to async
      Partner::ReservationSummarySyncWorker.perform_async(id, sync_type)
    end
  else
    # User bookings: Use delayed sync with protection against for_locking_system
    unless for_locking_system?
      Partner::ReservationSummarySyncWorker.perform_in(1.second, id, sync_type)
    end
  end
rescue StandardError => e
  APMErrorHandler.report('Failed to trigger creation sync', {
                           reservation_id: id,
                           sync_type: sync_type,
                           created_by: created_by,
                           error_message: e.message,
                           exception: e,
                         })
end

#trigger_destroy_syncvoid

This method returns an undefined value.

Manual sync trigger for destruction (async)

Call this method when a reservation is being destroyed.



338
339
340
341
342
343
344
345
346
# File 'lib/model_ext/reservations/callbacks.rb', line 338

def trigger_destroy_sync
  Partner::ReservationSummarySyncWorker.perform_async(id, 'destroy')
rescue StandardError => e
  APMErrorHandler.report('Failed to trigger destroy sync', {
                           reservation_id: id,
                           error_message: e.message,
                           exception: e,
                         })
end

#trigger_immediate_sync(sync_type = 'full') ⇒ void

This method returns an undefined value.

Manual sync trigger for immediate updates

Parameters:

  • sync_type (String) (defaults to: 'full')

    the type of sync to perform (default: 'full')



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/model_ext/reservations/callbacks.rb', line 309

def trigger_immediate_sync(sync_type = 'full')
  begin
    Partner::ReservationSummarySyncWorker.new.perform(id, sync_type)
  rescue StandardError => e
    APMErrorHandler.report('Immediate sync failed', {
                             reservation_id: id,
                             sync_type: sync_type,
                             created_by: created_by,
                             error_message: e.message,
                             exception: e,
                           })
    # Fallback to async
    Partner::ReservationSummarySyncWorker.perform_async(id, sync_type)
  end
rescue StandardError => e
  APMErrorHandler.report('Failed to trigger immediate sync', {
                           reservation_id: id,
                           sync_type: sync_type,
                           created_by: created_by,
                           error_message: e.message,
                           exception: e,
                         })
end

#trigger_priority_sync(sync_type = 'full') ⇒ void

This method returns an undefined value.

Manual sync trigger for priority updates (non-blocking but fast)

Use this for critical updates that need fast processing but should not block the main application flow. Executes with minimal delay (0.1 seconds).

Parameters:

  • sync_type (String) (defaults to: 'full')

    the type of sync to perform (default: 'full')



292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/model_ext/reservations/callbacks.rb', line 292

def trigger_priority_sync(sync_type = 'full')
  # Non-blocking but high priority sync with minimal delay
  Partner::ReservationSummarySyncWorker.perform_in(1.second, id, sync_type)
rescue StandardError => e
  APMErrorHandler.report('Failed to trigger priority sync', {
                           reservation_id: id,
                           sync_type: sync_type,
                           created_by: created_by,
                           error_message: e.message,
                           exception: e,
                         })
end

#trigger_summary_sync(sync_type = 'full') ⇒ void

This method returns an undefined value.

Manual sync trigger for general updates (async)

Use this for non-critical updates like package/add-on changes, general status updates, or when immediate sync is not required.

Parameters:

  • sync_type (String) (defaults to: 'full')

    the type of sync to perform. Valid values:

    - 'full': Sync all reservation data (default).
    - 'package': Sync package-related data only.
    - 'status': Sync status updates only.
    - 'financial': Sync financial data only.
    - 'loyalty': Sync loyalty level changes only.
    - 'destroy': Sync reservation destruction.
    

    (Other values may be supported; see ReservationSummarySyncWorker for details.)



235
236
237
238
239
240
241
242
243
244
# File 'lib/model_ext/reservations/callbacks.rb', line 235

def trigger_summary_sync(sync_type = 'full')
  Partner::ReservationSummarySyncWorker.perform_async(id, sync_type)
rescue StandardError => e
  APMErrorHandler.report('Failed to trigger summary sync', {
                           reservation_id: id,
                           sync_type: sync_type,
                           error_message: e.message,
                           exception: e,
                         })
end

#update_inv_relationsObject



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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
195
196
197
198
199
200
201
202
203
204
# File 'lib/model_ext/reservations/callbacks.rb', line 52

def update_inv_relations
  # Guard against invalid states
  unless persisted? && !destroyed?
    APMErrorHandler.report("update_inv_relations called on invalid reservation state",
                          context: { reservation_id: id, persisted: persisted?, destroyed: destroyed? })
    return
  end

  # Ensure the reservation still exists in the database to avoid foreign key constraint violations
  unless self.class.exists?(id)
    APMErrorHandler.report("update_inv_relations called on non-existent reservation",
                          context: { reservation_id: id })
    return
  end

  changed_attribute_keys = transaction_changed_attributes.keys

  # Early return if no relevant attributes changed
  relevant_changed_attrs = %w[active ack adult date start_time end_time
               for_locking_system is_temporary restaurant_id service_type].select do |key|
              changed_attribute_keys.include?(key)
            end

  if relevant_changed_attrs.blank?
    return
  end

  affect_old_data = %w[service_type date start_time end_time].select do |key|
    changed_attribute_keys.include?(key)
  end.present?

  inv_reservation_class = nil
  inv_class = nil

  if affect_old_data
    old_service_type = service_type

    if changed_attribute_keys.include?('service_type')
      old_service_type = transaction_changed_attributes['service_type']
      inv_reservation_class = inventory_reservation_klass(old_service_type)
      inv_class = inventory_klass(old_service_type)
    else
      inv_reservation_class = inventory_reservation_klass
      # Use inventory_klass for consistency, but fall back to restaurant.selected_inventory_model
      # since the current service_type might determine a different inventory class
      inv_class = inventory_klass || restaurant.selected_inventory_model
    end

    # Query inventory reservations for the current reservation
    inventory_reservations = inv_reservation_class.where(restaurant_id: restaurant_id, reservation_id: id)
    # collect inventory IDs from the reservations
    inventory_ids = inventory_reservations.distinct.pluck(inv_reservation_class.inventory_id_column)
    # Delete all inventory reservations for this reservation as they will be recreated with updated data
    inventory_reservations.delete_all

    # Sort inventory IDs to ensure consistent lock ordering
    sorted_inventory_ids = inventory_ids.sort

    # No transaction needed here since this is an after_commit callback
    inv_class.where(id: sorted_inventory_ids).order(:id).each do |inv|
      inv.with_lock do
        # recalculate total booked seats for the inventory
        # it is needed to restore the seat left quota after this reservation has been updated
        # or created
        inv.calc_total_booked_seat
        inv.save!
      end
    end
  else
    inv_reservation_class = inventory_reservation_klass
    # Use inventory_klass for consistency, but fall back to restaurant.selected_inventory_model
    # since the current service_type might determine a different inventory class
    inv_class = inventory_klass || restaurant.selected_inventory_model
  end

  invs = inv_class.where(date: date, restaurant_id: restaurant_id).
    where('start_time >= ? AND start_time <= ? AND end_time >= ? AND end_time <= ?',
          start_time_format, end_time_format, start_time_format, end_time_format)

  inventory_reservations = []

  inventory_relation = if restaurant.use_third_party_inventory?
                          'inventory_id'
                        else
                          "#{inv_class.to_s.underscore}_id"
                        end

  booked_seat = if active?
                  if ack? || (is_temporary? || for_locking_system?)
                    adult
                  else
                    0
                  end
                else
                  0
                end

  ir_attributes_base = {
    reservation_id: id,
    booked_seat: booked_seat,
    restaurant_id: restaurant.id,
  }
  inv_class_as_string = inv_class.to_s

  invs.each do |inv|
    ir_attributes = ir_attributes_base.dup

    if service_type == 'dine_in'
      ir_attributes[inventory_relation] = inv.id[0]
      ir_attributes[:inventory_type] = inv_class_as_string
    else
      ir_attributes[inventory_relation] = inv.id
    end
    inventory_reservations.push ir_attributes
  end

  if inventory_reservations.present?
    Retriable.retriable(
      on: ActiveRecord::Deadlocked,
      tries: 3,
      base_interval: 0.1,
      multiplier: 2,
      rand_factor: 0.5,
      on_retry: APMErrorHandler.report_retriable_event("reservation.inventory.update")
    ) do
      inv_reservation_class.import!(inventory_reservations,
                                    on_duplicate_key_update: [:booked_seat],
                                    raise_error: true, validate: false, batch_size: 1000)
    end
  end

  # Sort inventories by ID to ensure consistent lock ordering across transactions
  sorted_invs = invs.sort_by(&:id)

  Retriable.retriable(
    on: ActiveRecord::Deadlocked,
    tries: 3,
    base_interval: 0.1,
    multiplier: 2,
    rand_factor: 0.5,
    on_retry: APMErrorHandler.report_retriable_event("reservation.inventory.update")
  ) do
    sorted_invs.each do |inv|
      inv.with_lock do
        # recalculate total booked seats for the inventory
        # it is needed to consume the seat left quota after this reservation has been updated
        # or created
        inv.calc_total_booked_seat
        inv.save!
      end
    end
  end
end