Class: HhPackage::RestaurantPackage
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- HhPackage::RestaurantPackage
- Includes:
- IdentityCache, SoftDelete
- Defined in:
- app/models/hh_package/restaurant_package.rb
Constant Summary collapse
- AGENDA_MODEL_LAST_MODIFIED_AT =
main logic for each package availability on the first level is in Agenda model so we need to refresh the cache if we update #available_times logic
'v2025-03-08'- SHOW_TO_ALL_USERS_MODE =
'Show to all users'.freeze
- PREVIEW_MODE =
'Preview Mode'.freeze
- HIDDEN_MODE =
'Hidden'.freeze
Instance Attribute Summary collapse
-
#is_visible_for_staff ⇒ Boolean
is_visible_for_staff column is in preview mode.
-
#skip_validation ⇒ Object
Returns the value of attribute skip_validation.
Instance Method Summary collapse
- #agenda_setting ⇒ Object
- #available_at?(date, time, time_zone) ⇒ Boolean
-
#available_times(date, time_zone) ⇒ Array
This method returns available times for the given date.
-
#available_times_by_dates(dates, time_zone) ⇒ Array
Available times for the given date.
-
#cleanup_stale_cache_keys(redis, current_key) ⇒ Integer
Cleans up stale cache keys for this restaurant package before writing new cache This prevents memory bloat from accumulating cache keys with different variable hashes.
- #comemore_payless_expiry ⇒ Object
- #determine_inventory_class(restaurant) ⇒ Object
- #fetch_package ⇒ Object
- #inventory_cache_key ⇒ Object
-
#inventory_cache_key_pattern ⇒ Object
Pattern for scanning all cache keys for this restaurant package Used for cleanup of stale cache keys when a new cache key is generated.
- #inventory_cache_ttl ⇒ Object
- #setup_slug ⇒ Object
- #status ⇒ Object
-
#write_available_times(redis_connection = nil) ⇒ Object
store available times in redis in UTC time zone.
Methods included from SoftDelete
Methods inherited from ApplicationRecord
Instance Attribute Details
#is_visible_for_staff ⇒ Boolean
is_visible_for_staff column is in preview mode. It's a symbol indicating that the admin is still reviewing a package. The package has not been published yet.
72 |
# File 'app/models/hh_package/restaurant_package.rb', line 72 before_save :setup_slug, if: :slug_is_blank? |
#skip_validation ⇒ Object
Returns the value of attribute skip_validation.
64 65 66 |
# File 'app/models/hh_package/restaurant_package.rb', line 64 def skip_validation @skip_validation end |
Instance Method Details
#agenda_setting ⇒ Object
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 |
# File 'app/models/hh_package/restaurant_package.rb', line 368 def agenda_setting package.agendas.flat_map do |agenda| attributes = agenda.attributes.slice('start_time', 'end_time', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'exception_occurrences', 'single_occurrences') attributes['all_day'] = agenda.all_day.presence || false attributes['exception_occurrences'].map! do |exception_occurrence| if exception_occurrence.respond_to?(:[]) && exception_occurrence['start_date'].present? exception_occurrence['start_date'] = exception_occurrence['start_date'].to_date end if exception_occurrence.respond_to?(:[]) && exception_occurrence['end_date'].present? exception_occurrence['end_date'] = exception_occurrence['end_date'].to_date end exception_occurrence['all_day'] = ActiveModel::Type::Boolean.new.cast(exception_occurrence['all_day']) exception_occurrence end attributes['single_occurrences'].map! do |single_occurrence| if single_occurrence['start_date'].present? single_occurrence['start_date'] = single_occurrence['start_date'].to_date end if single_occurrence['end_date'].present? single_occurrence['end_date'] = single_occurrence['end_date'].to_date end single_occurrence['all_day'] = ActiveModel::Type::Boolean.new.cast(single_occurrence['all_day']) single_occurrence end attributes end end |
#available_at?(date, time, time_zone) ⇒ Boolean
399 400 401 402 403 404 405 406 |
# File 'app/models/hh_package/restaurant_package.rb', line 399 def available_at?(date, time, time_zone) requested_time = Time.use_zone time_zone do Time.zone.parse("#{date} #{time}") end package.agendas.select do |agenda| agenda.available_at?(start_date, end_date, time_zone, requested_time) end.present? end |
#available_times(date, time_zone) ⇒ Array
This method returns available times for the given date. without checking restaurant level availability It uses the package's agendas to determine the available times.
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'app/models/hh_package/restaurant_package.rb', line 211 def available_times(date, time_zone) ElasticAPM.with_span('RestaurantPackage#available_times', 'app', subtype: 'method', action: 'query') do |_span| # Add context for better tracing ElasticAPM.set_label(:restaurant_package_id, id) ElasticAPM.set_label(:restaurant_id, restaurant_id) ElasticAPM.set_label(:date, date.to_s) ElasticAPM.set_label(:timezone, time_zone) # get start time and end time in UTC start_time = end_time = nil Time.use_zone(time_zone) do unless date.is_a?(Date) date = Time.zone.parse(date).to_date end start_time = Time.zone.parse(date.to_s).beginning_of_day.in_time_zone('UTC').to_i end_time = Time.zone.parse(date.to_s).end_of_day.in_time_zone('UTC').to_i iso8601_strings = [] ElasticAPM.with_span('Redis fetch available times', 'db', subtype: 'redis', action: 'query') do $inv_redis.with do |redis| write_available_times(redis) if redis.zcard(inventory_cache_key).zero? # Retrieve ISO8601 strings within the given date range iso8601_strings = redis.zrangebyscore(inventory_cache_key, start_time, end_time) end end # Parse ISO8601 strings to Time objects and format as HH:MM mapped_iso8601_strings = ElasticAPM.with_span('Format available times', 'app', subtype: 'method') do iso8601_strings.map do |iso8601| Time.zone.parse(iso8601).strftime('%H:%M') end end # Sort the times in ascending order and return mapped_iso8601_strings.sort.uniq end end end |
#available_times_by_dates(dates, time_zone) ⇒ Array
Returns available times for the given date.
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 |
# File 'app/models/hh_package/restaurant_package.rb', line 175 def available_times_by_dates(dates, time_zone) ElasticAPM.with_span('RestaurantPackage#available_times_by_dates', 'app', subtype: 'method', action: 'query') do |_span| # Add metadata to help with debugging ElasticAPM.set_label(:restaurant_package_id, id) ElasticAPM.set_label(:restaurant_id, restaurant_id) ElasticAPM.set_label(:dates_count, dates.size) # Initialize result hash result = {} # Process each date and collect available times dates.each do |date| # Normalize date to string format if it's already a Date object date_str = date.is_a?(Date) ? date.to_s : date # Get available times for this specific date using the existing method ElasticAPM.with_span("Get available times for #{date_str}", 'app', subtype: 'method') do times = available_times(date, time_zone) result[date_str] = times if times.present? end end result end end |
#cleanup_stale_cache_keys(redis, current_key) ⇒ Integer
Cleans up stale cache keys for this restaurant package before writing new cache This prevents memory bloat from accumulating cache keys with different variable hashes
337 338 339 340 341 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 |
# File 'app/models/hh_package/restaurant_package.rb', line 337 def cleanup_stale_cache_keys(redis, current_key) deleted_count = 0 cursor = '0' begin cursor, keys = redis.scan(cursor, match: inventory_cache_key_pattern, count: 100) keys_to_delete = keys.reject { |key| key == current_key } unless keys_to_delete.empty? # Use UNLINK for non-blocking delete; fallback to DEL if UNLINK unsupported begin deleted = redis.unlink(*keys_to_delete) rescue StandardError deleted = redis.del(*keys_to_delete) end deleted_count += deleted.to_i end end while cursor != '0' if deleted_count > 0 HH_LOGGER.info('Cleaned up stale inventory cache keys', { restaurant_package_id: id, restaurant_id: restaurant_id, deleted_count: deleted_count, current_key: current_key, }) end deleted_count end |
#comemore_payless_expiry ⇒ Object
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 |
# File 'app/models/hh_package/restaurant_package.rb', line 438 def comemore_payless_expiry return nil unless restaurant return nil unless package.dynamic_price_comemore_payless? return nil unless package.pricing_model_and_dynamic_pricing_type == :per_person_and_normal_price pricing_groups = package&.pricing_groups current_date = Time.now_in_tz(restaurant.time_zone).to_date pricing_group = pricing_groups&.find_by('end_date >= ?', current_date) || pricing_groups&.find_by(end_date: nil) # use the end date of the pricing group if it is not nil # else use the end date of the restaurant package pricing_group&.end_date.presence || end_date end |
#determine_inventory_class(restaurant) ⇒ Object
433 434 435 436 |
# File 'app/models/hh_package/restaurant_package.rb', line 433 def determine_inventory_class(restaurant) is_dine_in = restaurant.use_third_party_inventory? ? nil : package.for_dine_in? InventoryWrapper.new(restaurant_id: restaurant.id, is_dine_in: is_dine_in).inv_model end |
#fetch_package ⇒ Object
156 157 158 159 160 161 162 163 164 |
# File 'app/models/hh_package/restaurant_package.rb', line 156 def fetch_package @fetch_package ||= begin Rails.cache.fetch "#{cache_key}/fetch_package", expires_in: CACHEFLOW.generate_expiry do package end rescue TypeError package end end |
#inventory_cache_key ⇒ Object
325 326 327 328 329 330 |
# File 'app/models/hh_package/restaurant_package.rb', line 325 def inventory_cache_key @inventory_cache_key ||= "available_times_v2_version_#{AGENDA_MODEL_LAST_MODIFIED_AT}:{restaurant_package_#{id}}:variables_#{CityHash.hash32([ agenda_setting, start_date, end_date ])}" end |
#inventory_cache_key_pattern ⇒ Object
Pattern for scanning all cache keys for this restaurant package Used for cleanup of stale cache keys when a new cache key is generated
321 322 323 |
# File 'app/models/hh_package/restaurant_package.rb', line 321 def inventory_cache_key_pattern "available_times_v2_version_*:{restaurant_package_#{id}}:*" end |
#inventory_cache_ttl ⇒ Object
315 316 317 |
# File 'app/models/hh_package/restaurant_package.rb', line 315 def inventory_cache_ttl (end_date + 1.day).to_time end |
#setup_slug ⇒ Object
408 409 410 411 412 413 414 415 416 |
# File 'app/models/hh_package/restaurant_package.rb', line 408 def setup_slug new_slug = nil loop do new_slug = "#{restaurant_id}-#{package.slug_code}-#{package_order_number}" break if HhPackage::RestaurantPackage.find_by(slug: new_slug).blank? end self.slug = new_slug end |
#status ⇒ Object
422 423 424 425 426 427 428 429 430 431 |
# File 'app/models/hh_package/restaurant_package.rb', line 422 def status # Show to all user = Show on Client Side # Preview Mode = Show on client Side only if log-in by @hungryhub email account # Hidden = Hide on Client Side return PREVIEW_MODE if active? && is_visible_for_staff? return SHOW_TO_ALL_USERS_MODE if active? && !is_visible_for_staff? HIDDEN_MODE end |
#write_available_times(redis_connection = nil) ⇒ Object
store available times in redis in UTC time zone
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 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'app/models/hh_package/restaurant_package.rb', line 254 def write_available_times(redis_connection = nil) ElasticAPM.with_span('RestaurantPackage#write_available_times', 'app', subtype: 'method', action: 'write') do |_span| # Add context for better tracing ElasticAPM.set_label(:restaurant_package_id, id) ElasticAPM.set_label(:restaurant_id, restaurant_id) ElasticAPM.set_label(:date_range, "#{start_date} to #{end_date}") time_zone = ActiveSupport::TimeZone['UTC'] Time.use_zone(time_zone) do # need to use self.start_date because this method is called within a block # which causing `start_date` is nil # rubocop:disable Style/RedundantSelf if self.start_date.past? self.start_date = Time.now_in_tz(time_zone).to_date end data = ElasticAPM.with_span('Generate agenda occurrences', 'app', subtype: 'method') do package.agendas.flat_map do |agenda| # need to use self.start_date because this method is called within a block # which causing `start_date` is nil # we use `restaurant.time_zone` because admin input the data in restaurant's time zone agenda.schedule_obj(self.start_date, self.end_date, restaurant.time_zone)&.all_occurrences end.compact end # rubocop:enable Style/RedundantSelf logic = lambda do |redis| data.each do |available_time| # Convert available_time to UTC first available_time = available_time.in_time_zone(time_zone) # Store timestamp as score for sorting score = available_time.to_i # Store ISO8601 string as member (preserves timezone) member = available_time.iso8601 redis.zadd(inventory_cache_key, score, member) end redis.expire(inventory_cache_key, inventory_cache_ttl.to_i) end ElasticAPM.with_span('Write to Redis', 'db', subtype: 'redis', action: 'write') do if redis_connection.present? redis_connection.pipelined do logic.call(redis_connection) end else $inv_redis.with do |redis| redis.pipelined do logic.call(redis) end end end end end end end |