Class: Reservation

Direct Known Subclasses

Booking

Constant Summary collapse

PERIODS =
%w[00:00 00:15 00:30 00:45 01:00 01:15 01:30 01:45 02:00 02:15 02:30 02:45 03:00 03:15 03:30 03:45 04:00
04:15 04:30 04:45 05:00 05:15 05:30 05:45 06:00 06:15 06:30 06:45 07:00 07:15 07:30 07:45 08:00 08:15 08:30 08:45 09:00 09:15 09:30 09:45 10:00 10:15 10:30 10:45 11:00 11:15 11:30 11:45 12:00 12:15 12:30 12:45 13:00 13:15 13:30 13:45 14:00 14:15 14:30 14:45 15:00 15:15 15:30 15:45 16:00 16:15 16:30 16:45 17:00 17:15 17:30 17:45 18:00 18:15 18:30 18:45 19:00 19:15 19:30 19:45 20:00 20:15 20:30 20:45 21:00 21:15 21:30 21:45 22:00 22:15 22:30 22:45 23:00 23:15 23:30 23:45 24:00].freeze
PERIODS_WITHOUT_00_AM =
%w[00:00 00:15 00:30 00:45 01:00 01:15 01:30 01:45 02:00 02:15 02:30 02:45 03:00 03:15 03:30
03:45 04:00 04:15 04:30 04:45 05:00 05:15 05:30 05:45 06:00 06:15 06:30 06:45 07:00 07:15 07:30 07:45 08:00 08:15 08:30 08:45 09:00 09:15 09:30 09:45 10:00 10:15 10:30 10:45 11:00 11:15 11:30 11:45 12:00 12:15 12:30 12:45 13:00 13:15 13:30 13:45 14:00 14:15 14:30 14:45 15:00 15:15 15:30 15:45 16:00 16:15 16:30 16:45 17:00 17:15 17:30 17:45 18:00 18:15 18:30 18:45 19:00 19:15 19:30 19:45 20:00 20:15 20:30 20:45 21:00 21:15 21:30 21:45 22:00 22:15 22:30 22:45 23:00 23:15 23:30 23:45].freeze
LUNCH_PERIODS =
%w[12:00 12:15 12:30 12:45 13:00 13:15 13:30 13:45 14:00 14:15 14:30 14:45 15:00 15:15 15:30 15:45
16:00].freeze
DINNER_PERIODS =
%w[16:00 16:15 16:30 16:45 17:00 17:15 17:30 17:45 18:00 18:15 18:30 18:45 19:00 19:15 19:30 19:45
20:00 20:15 20:30 20:45 21:00 21:15 21:30 21:45 22:00 22:15 22:30 22:45 23:00 23:15 23:30 23:45].freeze
RATING_DELAY_TIME =
3.hours
AMOUNT_REFERRER_REWARD_POINT_TH =
100
AMOUNT_REFERRER_REWARD_POINT_SG =
10
AMOUNT_REFERRER_REWARD_POINT_MY =
15
THANK_YOU_MSG_DELAY =
(2.hours + 30.minutes).freeze
DEFAULT_END_TIME =
1.hour.freeze
CREATED_BY =
%I[user admin owner].freeze

Constants included from ModelExt::Reservations::InstanceMethods

ModelExt::Reservations::InstanceMethods::RAddOn, ModelExt::Reservations::InstanceMethods::Rpackage

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ModelExt::CreatedFromWebsite

#created_from_website?

Methods included from ModelExt::Reservations::PartnerInstanceMethods

#customer_icon, #delivery_detail, #delivery_status_partner, #dine_in_status_partner, #driver_details, #format_percentage, #note_to_driver, #package_type, #paid_by_admin_status, #paid_status, #partner_status_property, #payment_method, #payment_status, #payment_summary, #self_pickup_status_partner

Methods included from ProgressStatus

#available_delivery_status, #convert_from_grab_as_symbol, #convert_from_lalamove, #convert_from_lalamove_as_symbol, #convert_from_reservation, #owner_delivery_progress

Methods included from ModelExt::Reservations::Callbacks

#push_booking_notification, #push_inventory_notification, #trigger_creation_sync, #trigger_destroy_sync, #trigger_immediate_sync, #trigger_priority_sync, #trigger_summary_sync, #update_inv_relations

Methods included from ModelExt::Reservations::CreditCard

#change_payment_type_provider!, #change_promptpay_provider, #charge_amount, #charged_amount, #charged_amount_in_baht, #charged_by_gb_primepay?, #charged_by_omise?, #could_charge_again?, #paid_amount, #paid_at, #paid_cc?, #paid_charges, #paid_non_cc?, #payment_gateway, #payment_gateway_account, #payment_provider, #payment_type, #payment_type_as_symbol, #payment_type_provider, #payment_type_record, #pending_payment_non_cc?, #prepayment_percent, #require_prepayment?, #using_charge_channel?, #using_on_hold_channel?

Methods included from ModelExt::Reservations::InstanceMethods

#add_on_bought, #add_on_data_for_report, #add_on_obj, #allowed_to_rectify?, #aoa_channel?, #assign_charged_data, #assign_refund_guarantee_data, #by_bistrochat?, #by_corporate_employee?, #by_marketplace?, #by_mymenu?, #by_seven_rooms?, #by_tablecheck?, #by_weeloy?, #call_order_later_driver_at, #call_order_now_driver_at, #can_request_more_bike?, #cancel_preparing!, #cancelled?, #channel_group_name, #channel_name, #channel_uri_name, #corporate_order?, #courier_tracking_link, #created_at_format, #created_by_admin_staff_or_owner?, #date_format, #date_format_ext, #delivery?, #delivery_address_humanize, #delivery_address_link, #delivery_fee_money, #delivery_status, #delivery_type?, #dine_in?, #dine_in_type?, #dont_mark_as_arrived!, #dont_mark_as_no_show!, #eligible_to_get_instant_reward?, #eligible_to_get_reward?, #email, #end_time=, #end_time_format, #firebase_tracking_key, #food_ready_by, #formatted_menus, #full_datetime, #generate_tag_list, #get_driving_duration, #google_reserve_channel?, #has_cancelled_with_refund?, #inventory_class, #inventory_klass, #inventory_reservation_klass, #inventory_times, #is_past?, #mark_as_arrived!, #mark_as_canceled!, #mark_as_for_locking_system!, #mark_as_invalid_temporary_booking!, #mark_as_no_show!, #mark_as_prepared!, #mark_as_valid_reservation!, #mark_voucher_as_active!, #mark_voucher_as_inactive!, #name, #need_request_choice?, #new_formatted_menus, #no_show?, #omise_payment_gateway?, #openrice_channel?, #original_delivery_fee_money, #owner_phone, #package_bought, #package_data_for_report, #package_obj, #package_price_currency, #party_size_changed?, #payment_failed_url, #payment_success_url, #pending?, #phone, #phone=, #phone_intl, #private_channel?, #qr_code_for_payment, #qr_code_for_payment_expired_at, #reached_goal?, #ready_to_cook?, #redeemed_points, #refund_fee_amount_float, #refundable_amount_float, #rejected?, #reservation_time, #reservation_time_24h_passed?, #revenue_amount, #review, #self_pickup?, #service_type_humanize, #shopee_pay_url, #skip_sending_netcore_event?, #special_request_without_package_menus, #start_time_and_date_changed?, #start_time_format, #status, #status_as_symbol, #status_changed?, #status_for_owner, #tagthai_channel?, #temporary_lock?, #true_wallet_for_payment_expired_at, #true_wallet_url, #upcoming?, #use_third_party_reservation?, #username, #valid_to_cancel_temporary_booking?, #voucher_codes_humanize, #voucher_names_humanize, #vouchers_amount, #web_url, #web_v2_host_vendor

Methods included from RefundGuaranteeHelper

#calculate_min_refund_hours, #calculate_refundable_until_time

Methods inherited from ApplicationRecord

sync_carrierwave_url

Class Method Details

.remove_phone_prefix(phone) ⇒ Object

to make phone searchable we need to remove the prefix of the phone



530
531
532
533
534
535
536
537
538
539
# File 'app/models/reservation.rb', line 530

def self.remove_phone_prefix(phone)
  case phone
  when /^668/ # If the phone starts with '668'
    phone.gsub(/^66/, '') # Remove '66'
  when /^6608/ # If the phone starts with '6608'
    phone.gsub(/^660/, '') # Remove '660'
  else
    phone # Return the phone unchanged if no prefix match
  end
end

Instance Method Details

#additional_special_requestObject



418
419
420
# File 'app/models/reservation.rb', line 418

def additional_special_request
  special_request
end

#additional_special_request=(sr = nil) ⇒ Object



414
415
416
# File 'app/models/reservation.rb', line 414

def additional_special_request=(sr = nil)
  self.special_request = "#{special_request} (#{sr})" if sr.present?
end

#adultObject



404
405
406
407
408
409
410
411
412
# File 'app/models/reservation.rb', line 404

def adult
  if self[:adult].to_i.positive?
    self[:adult]
  elsif !self[:adult].to_i.positive? && self[:kids].to_i.positive?
    0
  else
    party_size
  end
end

#adult=(amount) ⇒ Object



399
400
401
402
# File 'app/models/reservation.rb', line 399

def adult=(amount)
  self.party_size = amount.to_i + kids.to_i
  self[:adult] = amount.to_i
end

#business_bookingObject



271
272
273
274
275
# File 'app/models/reservation.rb', line 271

def business_booking
  return property.business_booking if property.present?

  nil
end

#business_booking=(val) ⇒ Object



277
278
279
280
# File 'app/models/reservation.rb', line 277

def business_booking=(val)
  build_property if property.nil?
  property.business_booking = val
end

#cache_keyObject



282
283
284
# File 'app/models/reservation.rb', line 282

def cache_key
  "#{super}|#{I18n.locale}|"
end

#channel=(c) ⇒ Object



386
387
388
389
390
391
392
# File 'app/models/reservation.rb', line 386

def channel=(c)
  if c.is_a?(Channel)
    self[:channel] = c.channel_id
  else
    super
  end
end

#charge_priceObject

charge_price is overridden by the charge_price_v2 that implements decimal pricing



239
240
241
242
243
# File 'app/models/reservation.rb', line 239

def charge_price
  return 0 if charge_price_v2.blank?

  charge_price_v2.amount
end

#charge_price_floatObject

charge_price_float is used for serializer because we need to return Float, BigDecimal will be render as String in JSON



266
267
268
269
# File 'app/models/reservation.rb', line 266

def charge_price_float
  price = charge_price
  price % 1 == 0 ? price.to_i : price.to_f.round(2)
end

#charge_price_v1Object

charge_price_v1 is used in the old system that uses integer pricing the charge_price_v1 will be rounded-up to the nearest integer



253
254
255
# File 'app/models/reservation.rb', line 253

def charge_price_v1
  charge_price.ceil
end

#dummy?Boolean

Returns:

  • (Boolean)


286
287
288
# File 'app/models/reservation.rb', line 286

def dummy?
  email == 'dummy@hungryhub.com'
end

#email_menu_templateObject



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'app/models/reservation.rb', line 427

def email_menu_template
  reservation = self
  reservation.package_obj&.formatted_packages&.map do |p|
    menu_list = []
    menu_sections = reservation.menu_sections.select do |section, _menus|
      package = section.package_menu_section&.package&.presence || HhPackage::Package::Ayce.new
      restaurant_package = HhPackage::RestaurantPackage.fetch(p['restaurant_package_id'])
      package == restaurant_package.package
    end
    menu_sections.each do |section, _menus|
      grouped_menu = section.menus.group(:package_menu_id)
      grouped_menu.each do |menu|
        menu_list << {
          name: menu.package_menu.name,
          quantity: menu.subsections.present? ? box_quantity : menu.quantity,
        }
      end
    end
    p.slice(:name, :amount, :kids_amount, :net_price, :quantity, :total_kids_price,
            :total_adult_price).merge(menus: menu_list)
  end
end

#find_package_by_name(name) ⇒ Hash?

Finds a package by name from the selected packages

Parameters:

  • name (String)

    The name of the package to find

Returns:

  • (Hash, nil)

    The package hash if found, nil otherwise



507
508
509
510
511
512
513
514
# File 'app/models/reservation.rb', line 507

def find_package_by_name(name)
  return nil if property.blank? || property.selected_packages.blank?

  selected_packages = parse_selected_packages
  return nil if selected_packages.blank?

  selected_packages.detect { |package| package['name'] == name }
end

#kids=(amount) ⇒ Object



394
395
396
397
# File 'app/models/reservation.rb', line 394

def kids=(amount)
  self.party_size = amount.to_i + adult.to_i if adult.to_i.positive?
  self[:kids] = amount.to_i
end

#last_minute?Boolean

Returns:

  • (Boolean)


322
323
324
# File 'app/models/reservation.rb', line 322

def last_minute?
  self&.property&.last_minute == true
end

#modified?Boolean

Returns:

  • (Boolean)


326
327
328
329
330
331
332
333
334
# File 'app/models/reservation.rb', line 326

def modified?
  if changed?
    return false if changes.include?('special_request') && special_request.empty?

    return true
  end

  false
end

#modified_reservationsObject

Returns array of all reservations in the modification chain Starting from the current reservation, follows new_reservation_id links Returns reservations ordered from oldest to newest Example: if reservation was modified 10 times, returns 10 reservations

  • First 9 will have active=false (adjusted=true)

  • Last one will have active=true (unless it was cancelled)



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'app/models/reservation.rb', line 342

def modified_reservations
  reservations = []
  current = self
  max_iterations = 50
  iterations = 0
  visited_ids = Set.new([id])

  # Traverse forward through the modification chain
  while current.present? && iterations < max_iterations
    reservations << current

    break if current.new_reservation_id.blank?
    break if visited_ids.include?(current.new_reservation_id)

    next_reservation = Reservation.find_by(id: current.new_reservation_id)
    break if next_reservation.nil?

    visited_ids.add(next_reservation.id)
    current = next_reservation
    iterations += 1
  end

  # Log warning if we hit the iteration limit
  if iterations >= max_iterations
    BUSINESS_LOGGER.set_business_context(
      original_reservation_id: id,
      last_checked_reservation_id: current&.id,
      warning: 'max_iterations_reached_in_modified_reservations',
    )
    BUSINESS_LOGGER.warn('Reservation modification chain traversal reached maximum iterations limit')
  end

  reservations
end

#origin_saveObject



208
# File 'app/models/reservation.rb', line 208

alias origin_save save

#origin_save!Object



209
# File 'app/models/reservation.rb', line 209

alias origin_save! save!

#reduced_with_promo?(restaurant_package_id) ⇒ Boolean

Returns:

  • (Boolean)


488
489
490
491
492
493
494
# File 'app/models/reservation.rb', line 488

def reduced_with_promo?(restaurant_package_id)
  return false if selected_packages.blank?

  selected_packages.detect do |p|
    p['type'] == 'promo' && p['restaurant_package_id'] == restaurant_package_id
  end.present?
end

#reservation_packages_with_menusObject

this method and reservation_single_package_with_menus method are used to generate the package & menu list for the email template



452
453
454
455
456
# File 'app/models/reservation.rb', line 452

def reservation_packages_with_menus
  package_obj&.formatted_packages&.map do |formatted_package|
    reservation_single_package_with_menus(formatted_package, packages: package['package_data'])
  end
end

#reservation_single_package_with_menus(formatted_package, packages: []) ⇒ Object

Parameters:

  • formatted_package (Hash)

    We can get this from Reservation#package_obj#formatted_packages



459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'app/models/reservation.rb', line 459

def reservation_single_package_with_menus(formatted_package, packages: [])
  restaurant_package_id = formatted_package['restaurant_package_id']
  package = HhPackage::RestaurantPackage.fetch(restaurant_package_id).package
  result = formatted_package.slice(
    :name, :amount, :kids_amount, :net_price, :quantity, :total_kids_price, :total_adult_price, :restaurant_package_id
  )

  price_finder = HhPackage::ReservationPackages::PriceFinder.new(use_dynamic_pricing: booking_channel.support_dynamic_pricing,
                                                                 time_zone: restaurant.time_zone)
  # we need to set default packages to prevent crash in Mix N Match code
  packages = packages || self.package['package_data']
  kids_price_v2 = price_finder.find_kids_price(package, adult, date, packages: packages)
  use_kids_price = price_finder.use_kids_price?(package)

  result.merge!(
    kids_price_rate: 0, # TODO: remove this
    kids_price_v2: HhMoney.new(kids_price_v2[:price].cents, package_obj.currency).default_format,
    use_kids_price: use_kids_price,
  )

  if reduced_with_promo?(restaurant_package_id)
    promo_package = selected_promo_package(restaurant_package_id)
    original_total_adult_price = result['total_adult_price'] + promo_package['price'].abs
    result['total_adult_price'] = original_total_adult_price
  end

  result
end

#save(*args) ⇒ Object



211
212
213
214
215
216
217
218
219
# File 'app/models/reservation.rb', line 211

def save(*args)
  if args.first == :sneaky
    perform_sneaky_operation
  else
    origin_save(*args)
  end
rescue StandardError => e
  handle_error(e)
end

#save!(*args) ⇒ Object



221
222
223
224
225
226
227
228
229
# File 'app/models/reservation.rb', line 221

def save!(*args)
  if args.first == :sneaky
    perform_sneaky_operation(bang: true)
  else
    origin_save!(*args)
  end
rescue StandardError, SystemStackError => e
  handle_error(e)
end

#selected_packagesArray

Returns the selected packages array from reservation property

Returns:

  • (Array)

    The selected packages array, or empty array if none found



525
526
527
# File 'app/models/reservation.rb', line 525

def selected_packages
  parse_selected_packages
end

#selected_promo_package(restaurant_package_id) ⇒ Object



496
497
498
499
500
501
502
# File 'app/models/reservation.rb', line 496

def selected_promo_package(restaurant_package_id)
  return {} if selected_packages.blank?

  (selected_packages.detect do |p|
    p['type'] == 'promo' && p['restaurant_package_id'] == restaurant_package_id
  end.presence || {}).with_indifferent_access
end

#show_delivery_fee?(is_admin:, original_delivery:) ⇒ Boolean

Determines if delivery fee should be displayed based on admin status and package type

Parameters:

  • is_admin (Boolean)

    Whether the current user is an admin

  • original_delivery (Money, nil)

    The original delivery fee amount

Returns:

  • (Boolean)

    True if delivery fee should be displayed



303
304
305
306
307
308
309
310
# File 'app/models/reservation.rb', line 303

def show_delivery_fee?(is_admin:, original_delivery:)
  return false unless original_delivery.present? && original_delivery > 0
  return true if is_admin

  package.present? &&
    package[:package_type].present? &&
    package[:package_type] == 'hah'
end

#show_hah_delivery_details?Boolean

Determines if delivery details should be shown for HAH packages (excluding pickup)

Returns:

  • (Boolean)

    True if delivery details should be displayed



314
315
316
317
318
319
320
# File 'app/models/reservation.rb', line 314

def show_hah_delivery_details?
  package.present? &&
    package[:package_type].present? &&
    package[:package_type] == 'hah' &&
    service_type.present? &&
    service_type.to_sym != :pick_up
end

#special_request_for_staffObject Also known as: special_request_for_restaurant



290
291
292
293
294
295
# File 'app/models/reservation.rb', line 290

def special_request_for_staff
  return special_request if charges.blank?
  return special_request if charges.success_scope.blank?

  "#{payment_type} #{charged_amount_in_baht} - #{special_request}"
end

#spending_tier_discountOpenStruct?

Finds the spending tier discount package as an OpenStruct for easier access

Returns:

  • (OpenStruct, nil)

    The spending tier discount package as OpenStruct if found, nil otherwise



518
519
520
521
# File 'app/models/reservation.rb', line 518

def spending_tier_discount
  discount = find_package_by_name('Spending Tier Discount')
  discount ? OpenStruct.new(discount) : nil
end

#start_time=(time) ⇒ Object



422
423
424
425
# File 'app/models/reservation.rb', line 422

def start_time=(time)
  self[:start_time] = time
  set_end_time
end

#to_json(options = {}) ⇒ Object



382
383
384
# File 'app/models/reservation.rb', line 382

def to_json(options = {})
  super(options.merge(methods: [options[:methods], :username].flatten.compact))
end

#to_url_hashObject



378
379
380
# File 'app/models/reservation.rb', line 378

def to_url_hash
  HungryHub::Reservation::Codec.encode(id)
end

#total_priceObject

total_price is overridden by the total_price_v2 that implements decimal pricing



232
233
234
235
236
# File 'app/models/reservation.rb', line 232

def total_price
  return 0 if total_price_v2.blank?

  total_price_v2.amount
end

#total_price_floatObject

total_price_float is used for serializer because we need to return Float, BigDecimal will be render as String in JSON



259
260
261
262
# File 'app/models/reservation.rb', line 259

def total_price_float
  price = total_price
  price % 1 == 0 ? price.to_i : price.to_f.round(2)
end

#total_price_v1Object

total_price_v1 and charge_price are used in the old system that uses integer pricing the total_price_v1 will be rounded-up to the nearest integer



247
248
249
# File 'app/models/reservation.rb', line 247

def total_price_v1
  total_price.ceil
end

#validate_date?Boolean

Custom method for conditional validation

Returns:

  • (Boolean)


542
543
544
# File 'app/models/reservation.rb', line 542

def validate_date?
  new_record? || date_changed?
end