Module: ReservationExportHelpers

Overview

Shared helper methods for reservation export workers. This module extracts common functionality from NotificationWorkers::ReservationReport and Partner::BookingExportWorker to ensure consistency and eliminate code duplication.

Examples:

Usage in worker

class MyExportWorker < ApplicationWorker
  include ReservationExportHelpers

  def perform
    reservations.each do |reservation|
      commission = calculate_commission(reservation)
      revenue = calculate_restaurant_revenue(reservation)
      # ... use other helper methods
    end
  end
end

Constant Summary

Constants included from ExportConstants

ExportConstants::CHANNEL_IDS_TO_SKIP, ExportConstants::DEFAULT_QUEUE, ExportConstants::EMAIL_BATCH_SIZE, ExportConstants::ERROR_CATEGORIES, ExportConstants::EXCEL_FILE_EXTENSION, ExportConstants::LARGE_DATASET_WARNING_THRESHOLD, ExportConstants::LONG_PROCESS_DAYS_THRESHOLD, ExportConstants::LONG_PROCESS_QUEUE, ExportConstants::MAX_RETRIES, ExportConstants::PDF_FILE_EXTENSION, ExportConstants::PROGRESS_MESSAGES, ExportConstants::PROGRESS_PHASES, ExportConstants::PROGRESS_UPDATE_BATCH_SIZE, ExportConstants::RESERVATION_BATCH_SIZE, ExportConstants::RETRY_BASE_INTERVAL, ExportConstants::RETRY_MAX_ELAPSED_TIME, ExportConstants::ZERO_COMMISSION_CHANNEL_ID

Instance Method Summary collapse

Instance Method Details

#calc_prepayment(reservation) ⇒ String

Calculate prepayment amount for a reservation

Parameters:

  • reservation (Reservation)

    The reservation to calculate prepayment for

Returns:

  • (String)

    The prepayment amount as string



283
284
285
286
287
288
289
290
291
# File 'app/workers/concerns/reservation_export_helpers.rb', line 283

def calc_prepayment(reservation)
  prepayment = 0
  if reservation.property&.is_prepayment && reservation.property&.prepayment_cents
    prepayment += HhMoney.new(reservation.property.prepayment_cents, reservation.property.prepayment_currency).amount
  end

  prepayment += reservation.paid_amount.amount
  prepayment.to_s
end

#calculate_commission(reservation) ⇒ Numeric Also known as: revenue_amount

Calculate commission for a reservation (FIXED VERSION from main branch) This includes both package and add-on revenue as per the latest fix

Parameters:

  • reservation (Reservation)

    The reservation to calculate commission for

Returns:

  • (Numeric)

    The commission amount



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'app/workers/concerns/reservation_export_helpers.rb', line 27

def calculate_commission(reservation)
  return 0 unless reservation.package?
  return 0 if reservation.rejected?

  # requested by Ravi
  # he said "I need to have channel ID 1114 to be set as 0 commission"
  if reservation.channel != 1114
    package_revenue = reservation.package_obj&.revenue_amount.to_d || 0
    add_on_revenue = reservation.add_on_obj&.revenue_amount.to_d || 0

    total_revenue = package_revenue + add_on_revenue
    total_revenue = if total_revenue % 1 == 0
                      total_revenue.to_i
                    else
                      total_revenue
                    end
    return total_revenue
  end

  0
end

#calculate_restaurant_revenue(reservation) ⇒ Numeric Also known as: restaurant_revenue_amount

Calculate restaurant revenue amount (sum of package and add-on amounts)

Parameters:

  • reservation (Reservation)

    The reservation to calculate revenue for

Returns:

  • (Numeric)

    The restaurant revenue amount



53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'app/workers/concerns/reservation_export_helpers.rb', line 53

def calculate_restaurant_revenue(reservation)
  return 0 unless reservation.package?
  return 0 if reservation.rejected?

  package_revenue = reservation&.package_obj&.amount.to_d || 0
  add_on_revenue = reservation&.add_on_obj&.amount.to_d || 0

  total_restaurant_revenue = package_revenue + add_on_revenue
  if total_restaurant_revenue % 1 == 0
    total_restaurant_revenue.to_i
  else
    total_restaurant_revenue
  end
end

#calculate_total_package_price(reservation) ⇒ Numeric

Calculate total package price for a reservation

Parameters:

  • reservation (Reservation)

    The reservation to calculate total package price for

Returns:

  • (Numeric)

    Total package price



307
308
309
310
311
312
313
314
315
# File 'app/workers/concerns/reservation_export_helpers.rb', line 307

def calculate_total_package_price(reservation)
  return 0 unless reservation.package?

  total = reservation.package_obj.formatted_packages.sum do |item|
    Money.new(item['price_cents'], item['price_currency']).amount
  end

  decimal_format(total)
end

#date_format(date) ⇒ String

Format date in DD/MM/YYYY format

Parameters:

  • date (Date, Time)

    The date to format

Returns:

  • (String)

    Formatted date string or empty string if blank



297
298
299
300
301
# File 'app/workers/concerns/reservation_export_helpers.rb', line 297

def date_format(date)
  return '' if date.blank?

  date&.strftime('%d/%m/%Y')
end

#decimal_format(number) ⇒ Numeric

Format decimal numbers consistently

Parameters:

  • number (Numeric)

    The number to format

Returns:

  • (Numeric)

    Integer if whole number, float otherwise



244
245
246
# File 'app/workers/concerns/reservation_export_helpers.rb', line 244

def decimal_format(number)
  number % 1 == 0 ? number.to_i : number.to_f
end

#format_add_on_names_with_quantity(reservation) ⇒ Object

Enhanced formatter for add-ons adding explicit quantity suffix if missing. Similar logic to packages: localized strings may already contain x<quantity> (per_item template). If pricing_type per_person and localized string lacks quantity, append x<quantity> when available.



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
# File 'app/workers/concerns/reservation_export_helpers.rb', line 144

def format_add_on_names_with_quantity(reservation)
  return nil unless reservation.add_on?

  add_on_obj = reservation.add_on_obj
  return nil unless add_on_obj && add_on_obj.add_ons_bought.is_a?(Array)

  bought = add_on_obj.add_ons_bought
  formatted = add_on_obj.formatted_add_ons || []

  enriched = []
  bought.each_with_index do |raw, idx|
    next if raw.blank?

    base = sanitizer.strip_tags(raw.to_s).gsub(/\s+/, ' ')
    addon_hash = formatted[idx] || {}
    quantity = addon_hash[:quantity].to_i

    unless base =~ /x\s*\d+/ || quantity <= 0
      base = base + " x#{quantity}"
    end
    enriched << base
  end

  enriched.to_sentence
end

#format_addon_names_for_report(reservation) ⇒ String

Formats add-on names for report display with quantity information. Returns a final sanitized string ready for Excel export.

Examples:

format_addon_names_for_report(reservation)
# => "Extra Dessert ฿100 NET/person x2, Wine Bottle ฿500 NET/item x1"

Parameters:

  • reservation (Reservation)

    The reservation to format add-on names for

Returns:

  • (String)

    Sanitized add-on names with quantities, or empty string if no add-ons



394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'app/workers/concerns/reservation_export_helpers.rb', line 394

def format_addon_names_for_report(reservation)
  return '' unless reservation.add_on.present? && reservation.add_on_obj.present?

  prop_cache_key = reservation.property.id.nil? ? rand(1000) : reservation.property.cache_key
  res_cache_key = reservation.id.nil? ? rand(1000) : reservation.cache_key

  addon_data = Rails.cache.fetch("#{res_cache_key}:addon_names_for_report:#{prop_cache_key}") do
    add_ons = reservation.add_on[:add_on_data].map do |data|
      get_add_on = AddOns::AddOn.find_by(id: data[:id])
      name = get_add_on&.name || ''
      price = HhMoney.new(data[:price_cents], data[:price_currency])
      pricing_type = get_add_on&.pricing&.pricing_type

      {
        name: name,
        net_price: price.default_format,
        quantity: data[:quantity],
        pricing_type: pricing_type,
      }
    end

    add_ons.map do |aob|
      I18n.t("views.booking.add_on_data_for_report.#{aob[:pricing_type]}",
             name: aob[:name],
             net_price: aob[:net_price],
             quantity: aob[:quantity])
    end
  end

  # Return final sanitized string ready for export
  addon_data.present? ? sanitizer.strip_tags(addon_data.to_sentence).gsub(/\s+/, ' ') : ''
end

#format_money(amount) ⇒ String?

Format money amount using the same formatting as existing system

Parameters:

  • amount (Numeric)

    The amount to format

Returns:

  • (String, nil)

    Formatted money string or nil if amount is blank



174
175
176
177
178
179
180
181
182
183
# File 'app/workers/concerns/reservation_export_helpers.rb', line 174

def format_money(amount)
  return nil if amount.blank?

  # Use formatting with symbol for exports (booking export worker needs currency symbol)
  if respond_to?(:humanized_money_with_symbol)
    humanized_money_with_symbol(amount)
  else
    humanized_money(amount)
  end
end

#format_package_names(reservation) ⇒ String?

Format package names for display in exports (wrapper for compatibility)

Parameters:

  • reservation (Reservation)

    The reservation to format package names for

Returns:

  • (String, nil)

    Formatted package names or nil if no packages



86
87
88
89
90
91
92
93
94
# File 'app/workers/concerns/reservation_export_helpers.rb', line 86

def format_package_names(reservation)
  return nil unless reservation.package?

  packages = reservation.package_obj&.packages_bought
  return nil unless packages

  # Use same sanitization as existing system
  sanitize_package_name(packages)
end

#format_package_names_for_report(reservation) ⇒ String

Formats package names for report display with quantity information. Returns a final sanitized string ready for Excel export.

Examples:

format_package_names_for_report(reservation)
# => "Buffet Lunch ฿500 NET/adult x2, Premium Set ฿800 NET/set x1"

Parameters:

  • reservation (Reservation)

    The reservation to format package names for

Returns:

  • (String)

    Sanitized package names with quantities, or empty string if no packages



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
376
377
378
379
380
381
382
383
384
# File 'app/workers/concerns/reservation_export_helpers.rb', line 351

def format_package_names_for_report(reservation)
  return '' unless reservation.package? && reservation.package_obj.present?

  prop_cache_key = reservation.property.id.nil? ? rand(1000) : reservation.property.cache_key
  res_cache_key = reservation.id.nil? ? rand(1000) : reservation.cache_key

  package_data = Rails.cache.fetch("#{res_cache_key}:package_names_for_report:#{prop_cache_key}") do
    sym = reservation.package[:package_type].to_s.to_sym
    klass = "HhPackage::Package::#{HhPackage::PACKAGE_SHORT_TO_LIST[sym]}".constantize

    packages = reservation.package[:package_data].map do |data|
      get_package = klass.find_by(id: data[:id])
      name = get_package&.name || ''
      price = HhMoney.new(data[:price_cents], data[:price_currency])

      {
        name: name,
        net_price: price.default_format,
        quantity: data[:quantity],
        package_type: reservation.package[:package_type],
      }
    end

    packages.map do |p|
      I18n.t("views.booking.package_data_for_report.#{p[:package_type]}",
             name: p[:name],
             net_price: p[:net_price],
             quantity: p[:quantity])
    end
  end

  # Return final sanitized string ready for export
  package_data.present? ? sanitizer.strip_tags(package_data.to_sentence).gsub(/\s+/, ' ') : ''
end

#format_package_names_with_quantity(reservation) ⇒ Object

Enhanced formatter adding explicit quantity suffix (e.g. “DIY Choose All You Need STP ฿670 NET/Set x6”) Logic:

- Iterate packages_bought (already localized via i18n) aligned with formatted_packages
- If localized string does not already contain an "x<digits>" quantity and the formatted package hash
  contains :quantity > 0, append " x<quantity>"
- Special case AYCE (type "ayce") where the i18n template omits quantity: append party_size if quantity blank
- Preserve existing spacing & sanitization; collapse whitespace
- Return a to_sentence string like original implementation for multi-package reservations

Compatible with Ruby 2.7.8 & Rails 5.1.7 (no pattern matching / newer methods used)



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
# File 'app/workers/concerns/reservation_export_helpers.rb', line 105

def format_package_names_with_quantity(reservation)
  return nil unless reservation.package?

  pkg_obj = reservation.package_obj
  return nil unless pkg_obj && pkg_obj.packages_bought.is_a?(Array)

  bought = pkg_obj.packages_bought
  formatted = pkg_obj.formatted_packages || []

  enriched = []
  bought.each_with_index do |raw, idx|
    next if raw.blank?

    base = sanitizer.strip_tags(raw.to_s).gsub(/\s+/, ' ')
    pkg_hash = formatted[idx] || {}
    quantity = pkg_hash[:quantity].to_i

    # Append quantity if missing and available
    unless base =~ /x\s*\d+/ || quantity <= 0
      base = base + " x#{quantity}"
    end

    # AYCE fallback: use party_size if ayce and still no x<digits>
    if base !~ /x\s*\d+/ && quantity <= 0 && reservation.respond_to?(:party_size) && reservation.party_size.to_i > 0
      type_short = pkg_hash[:type_short] || pkg_hash[:dynamic_type_short] || pkg_obj.respond_to?(:type_short) && pkg_obj.type_short
      if type_short.to_s == 'ayce'
        base = base + " x#{reservation.party_size}"
      end
    end

    enriched << base
  end

  enriched.to_sentence
end

#format_voucher_amount(reservation) ⇒ String

Format voucher amounts for a reservation (combines HH and restaurant voucher amounts)

Parameters:

  • reservation (Reservation)

    The reservation to format voucher amounts for

Returns:

  • (String)

    Combined voucher amounts or empty string



200
201
202
203
204
205
206
207
208
209
210
# File 'app/workers/concerns/reservation_export_helpers.rb', line 200

def format_voucher_amount(reservation)
  return '' if reservation.vouchers.blank?

  # Use same approach as existing system - combine HH and restaurant voucher amounts
  hh_amount = voucher_amount(reservation.vouchers, reservation, by: :hungryhub)
  restaurant_amount = voucher_amount(reservation.vouchers, reservation, by: :restaurant)

  # Return combined amounts or empty if both are empty
  amounts = [hh_amount, restaurant_amount].reject(&:blank?)
  amounts.any? ? amounts.join(' + ') : ''
end

#format_voucher_code(reservation) ⇒ String

Format voucher codes for a reservation

Parameters:

  • reservation (Reservation)

    The reservation to format voucher codes for

Returns:

  • (String)

    Comma-separated voucher codes or empty string



189
190
191
192
193
194
# File 'app/workers/concerns/reservation_export_helpers.rb', line 189

def format_voucher_code(reservation)
  return '' if reservation.vouchers.blank?

  # Use same approach as existing system
  reservation.vouchers.map(&:voucher_code).join(', ')
end

#get_reservation_distance(distance) ⇒ Numeric?

Get reservation distance with proper formatting

Parameters:

  • distance (Numeric)

    The distance value

Returns:

  • (Numeric, nil)

    Formatted distance or nil if zero/negative



252
253
254
# File 'app/workers/concerns/reservation_export_helpers.rb', line 252

def get_reservation_distance(distance)
  distance.to_f.positive? ? distance.round(1) : nil
end

Get paid amount for a reservation

Parameters:

  • reservation (Reservation)

    The reservation to get paid amount for

Returns:

  • (Numeric)

    The paid amount



275
276
277
# File 'app/workers/concerns/reservation_export_helpers.rb', line 275

def paid_amount(reservation)
  reservation.paid_amount
end

#restaurant_delivery_fee(reservation) ⇒ Numeric

Calculate restaurant delivery fee

Parameters:

  • reservation (Reservation)

    The reservation to calculate delivery fee for

Returns:

  • (Numeric)

    The delivery fee amount



260
261
262
263
264
265
266
267
268
269
# File 'app/workers/concerns/reservation_export_helpers.rb', line 260

def restaurant_delivery_fee(reservation)
  return 0 if reservation.property&.covered_by_hh?
  return 0 unless reservation.service_type.delivery?

  return reservation.delivery_subsidize.amount if reservation.delivery_subsidize_cents.positive?

  return 0 if reservation.address.blank?

  reservation.delivery_channel&.restaurant_fee&.amount || 0
end

#sanitize_package_name(packages) ⇒ String

Sanitize package names for display in exports (exact same as original)

Parameters:

  • packages (Object)

    The packages_bought object to sanitize

Returns:

  • (String)

    Sanitized package names



78
79
80
# File 'app/workers/concerns/reservation_export_helpers.rb', line 78

def sanitize_package_name(packages)
  sanitizer.strip_tags(packages.to_sentence).gsub(/\s+/, ' ')
end

#sanitizerObject

Get sanitizer instance for HTML sanitization (same as existing system)

Returns:

  • (Object)

    Sanitizer instance with SanitizeHelper methods



328
329
330
331
332
333
334
335
# File 'app/workers/concerns/reservation_export_helpers.rb', line 328

def sanitizer
  @sanitizer ||= begin
    sanitizer_class = Class.new do
      include ActionView::Helpers::SanitizeHelper
    end
    sanitizer_class.new
  end
end

#valid_email?(email) ⇒ Boolean

Validate email format

Parameters:

  • email (String)

    Email to validate

Returns:

  • (Boolean)

    True if email is valid



321
322
323
# File 'app/workers/concerns/reservation_export_helpers.rb', line 321

def valid_email?(email)
  email.present? && email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end

#voucher_amount(vouchers, reservation, by:) ⇒ String

Calculate voucher amount by subsidizer (same logic as existing system)

Parameters:

  • vouchers (Array)

    Array of vouchers

  • reservation (Reservation)

    The reservation

  • by (Symbol)

    The subsidizer (:hungryhub or :restaurant)

Returns:

  • (String)

    Formatted voucher amount or empty string



218
219
220
221
222
223
224
# File 'app/workers/concerns/reservation_export_helpers.rb', line 218

def voucher_amount(vouchers, reservation, by:)
  return '' if vouchers.blank?

  return humanized_money(reservation.used_voucher_amount_by_hh.amount.to_i) if by == :hungryhub

  humanized_money(reservation.used_voucher_amount_by_restaurant.amount.to_i)
end

#voucher_code(vouchers, by:) ⇒ String

Get voucher codes by subsidizer

Parameters:

  • vouchers (Array)

    Array of vouchers

  • by (Symbol)

    The subsidizer (:hungryhub or :restaurant)

Returns:

  • (String)

    Voucher codes or empty string



231
232
233
234
235
236
237
238
# File 'app/workers/concerns/reservation_export_helpers.rb', line 231

def voucher_code(vouchers, by:)
  return '' if vouchers.blank?

  subsidized_vouchers = vouchers.select { |v| v.subsidized_by.to_s == by.to_s }
  return subsidized_vouchers.map(&:voucher_code).to_sentence if subsidized_vouchers.present?

  ''
end