Class: HhPackage::ReservationPackages::PriceFinder

Inherits:
Object
  • Object
show all
Defined in:
app/my_lib/hh_package/reservation_packages/price_finder.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(use_dynamic_pricing: false, time_zone: nil) ⇒ PriceFinder

Returns a new instance of PriceFinder.



9
10
11
12
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 9

def initialize(use_dynamic_pricing: false, time_zone: nil)
  self.use_dynamic_pricing = use_dynamic_pricing
  @dynamic_pricing_flipper = DynamicPricingsFlipper.new(time_zone)
end

Instance Attribute Details

#dynamic_pricing_flipperObject (readonly)

Returns the value of attribute dynamic_pricing_flipper.



7
8
9
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 7

def dynamic_pricing_flipper
  @dynamic_pricing_flipper
end

#group_sectionsObject

Returns the value of attribute group_sections.



6
7
8
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 6

def group_sections
  @group_sections
end

Returns the value of attribute menu_sections.



6
7
8
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 6

def menu_sections
  @menu_sections
end

#use_dynamic_pricingObject

Returns the value of attribute use_dynamic_pricing.



6
7
8
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 6

def use_dynamic_pricing
  @use_dynamic_pricing
end

Instance Method Details

#calculate_come_more_pay_less_pricing(pricing, pricing_group, adult, date) ⇒ Integer

Calculate the price for a group of adults based on the provided pricing information

Parameters:

  • pricing (HhPackage::Pricing)

    the pricing information for the package

  • pricing_group (HhPackage::PricingGroup)

    the pricing group that the pricing information is associated with

  • adult (Integer)

    the total number of adults in the group

Returns:

  • (Integer)

    total price in cents



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 435

def calculate_come_more_pay_less_pricing(pricing, pricing_group, adult, date)
  if valid_come_more_pay_less_date?(date, pricing_group)
    group_size = pricing_group.group_size
    group_size_to_pay = pricing_group.group_size_to_pay
    adult = adult.to_i

    # If there are fewer adults than the group size, return the base price
    return (pricing.price_cents * adult.to_f).to_i if adult < group_size

    # Calculate the price for groups of size group_size_to_pay
    full_groups_price = (adult / group_size) * group_size_to_pay * pricing.price

    # Calculate the price for any remaining adults
    remaining_adults_price = (adult % group_size) * pricing.price

    # Add the prices together to get the total price
    (full_groups_price.cents + remaining_adults_price.cents).to_i
  else
    (pricing.price_cents * adult).to_i
  end
end

#come_more_pay_less_sample_price_for_preview(package, with_date: false, restaurant_id: nil) ⇒ Object

return any pricing_group just for preview to user when a package has come more pay less feature when adult price is 1000, group size is 5, group size to pay is 4

=> {:group_size=>5, :size_to_pay=>4, :price=>1000, :price_format=>"฿1,000", :currency=>"THB", :sample_price_per_person=>800, :sample_price_per_pesron_format=>"฿800"}

Parameters:



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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 175

def come_more_pay_less_sample_price_for_preview(package, with_date: false, restaurant_id: nil)
  if [:per_person_and_normal_price,
      :per_pack_and_normal_price].include?(package.pricing_model_and_dynamic_pricing_type) &&
      package.pricing_groups.present?
    pricing_group = package.pricing_groups.first
    return {} if pricing_group.blank?
    return {} if pricing_group.present? && !pricing_group.valid_to_show_as_preview?

    pricing = filter_dynamic_pricings(package).first
    return {} if pricing.blank?

    # Find restaurant package by restaurant_id if provided, otherwise fallback to first
    restaurant_package = if restaurant_id.present?
                           package.restaurant_packages.find_by(restaurant_id: restaurant_id)
                         else
                           backtrace = Rails.backtrace_cleaner.clean(caller)
                           APMErrorHandler.report('RestaurantPackage lookup without restaurant_id', {
                                                    package_id: package.id,
                                                    package_type: package.class.name,
                                                    caller_location: backtrace,
                                                  })
                           package.restaurant_packages.active_scope.first
                         end
    start_date = pricing_group.start_date.presence || restaurant_package&.start_date
    end_date = pricing_group.end_date.presence || restaurant_package&.end_date
    sample_price = calculate_come_more_pay_less_pricing(pricing, pricing_group, pricing_group.group_size,
                                                        start_date) / pricing_group.group_size
    price_format = HhMoney.new(pricing.price_cents, pricing.price_currency).default_format
    sample_price_format = HhMoney.new(sample_price, pricing.price_currency).default_format

    result = {
      group_size: pricing_group.group_size,
      size_to_pay: pricing_group.group_size_to_pay,
      price: pricing.price.amount.to_i,
      price_format: price_format,
      currency: pricing.price_currency,
      sample_price_per_person: sample_price,
      sample_price_per_person_format: sample_price_format,
      # TODO: remove below typo "peson" attributes after release,
      # for backward compatibility
      sample_price_per_peson: sample_price,
      sample_price_per_peson_format: sample_price_format,
    }

    if with_date
      return result.merge(
        start_date: start_date,
        end_date: end_date,
      )
    end

    result
  else
    {}
  end
end

#filter_dynamic_pricings(package) ⇒ Object



424
425
426
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 424

def filter_dynamic_pricings(package)
  dynamic_pricing_flipper.filter_by_package package.dynamic_pricings, package, use_dynamic_pricing?
end

#find_add_on_final_price(add_on, quantity, adult, kids) ⇒ Integer

Calculate the final price for an add-on based on its pricing model.

Parameters:

  • add_on (AddOns::AddOn)

    The add-on for which the price is to be calculated

  • quantity (Integer)

    The quantity of the add-on being purchased

  • adult (Integer)

    The number of adults in the reservation (used for per-person pricing)

  • kids (Integer)

    The number of kids in the reservation (used for per-person pricing)

Returns:

  • (Integer)

    The final price in cents for the add-on



303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 303

def find_add_on_final_price(add_on, quantity, adult, kids)
  pricing = add_on.pricing
  pricing_type = pricing.pricing_type

  case pricing_type
  when AddOns::Pricing::PRICING_TYPE_PER_PERSON
    add_on_price_calculation_per_person(pricing, adult, kids, add_on)
  when AddOns::Pricing::PRICING_TYPE_PER_ITEM
    add_on_price_calculation_per_item(pricing, quantity, kids, add_on)
  else
    APMErrorHandler.report("Unknown add-on pricing type: #{pricing_type}", add_on_id: add_on.id)
    0
  end
end

#find_add_on_kids_price(add_on) ⇒ Object



163
164
165
166
167
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 163

def find_add_on_kids_price(add_on)
  currency = fetch_add_on_currency(add_on)

  build_add_on_kids_price_hash(add_on, find_kids_price_by_add_on(add_on, currency), currency)
end

#find_add_on_refund_final_price(add_on, total_add_on_price_cents) ⇒ Integer

Calculate the final price for refund guarantee based on the add-on's refund policy.

Parameters:

  • add_on (AddOns::AddOn)

    The add-on for which the refund price is to be calculated

  • total_add_on_price_cents (Integer)

    The total price of the add-on in cents

Returns:

  • (Integer)

    The final refund price in cents



347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 347

def find_add_on_refund_final_price(add_on, total_add_on_price_cents)
  return 0 if add_on.blank? || !add_on.accept_refund_guarantee?

  case add_on.refund_type.to_sym
  when AddOns::AddOn::REFUND_TYPE_PERCENTAGE
    ((total_add_on_price_cents * add_on.refund_amount).to_d / 100).ceil
  when AddOns::AddOn::REFUND_TYPE_FIXED_AMOUNT
    # refund_amount is in cents
    add_on.refund_amount.to_i
  else
    APMErrorHandler.report("Unknown refund type: #{add_on.refund_type}", add_on_id: add_on.id)
    0
  end
end

#find_base_price(package, adult, date) ⇒ Object

set menus_sections= [] required for ala carte type

Parameters:

Returns:

  • HhPackage::Pricing or #<OpenStruct price_cents=111, price_currency='THB'>



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 23

def find_base_price(package, adult, date)
  case package.pricing_model_and_dynamic_pricing_type
  when :per_person_and_normal_price
    dynamic_pricings = filter_dynamic_pricings(package)
    dynamic_pricings.first
  when :per_person_and_by_party_size_price
    dynamic_pricings = filter_dynamic_pricings(package)
    pricing = dynamic_pricings.select do |p|
      adult.to_i.between?(p.min_seat.to_i, p.max_seat.to_i)
    end.first
    if pricing.blank?
      dynamic_pricing = dynamic_pricings.first

      if adult < dynamic_pricing.min_seat.to_i
        raise(InvalidPackageData,
              I18n.t('errors.package.ayce.min_party_size',
                     adults: dynamic_pricing.min_seat))
      end
      if adult > dynamic_pricing.max_seat.to_i
        raise(InvalidPackageData,
              I18n.t('errors.package.ayce.max_party_size',
                     adults: dynamic_pricing.max_seat))
      end
    else
      pricing
    end
  when :per_person_and_by_day_price, :per_pack_and_by_day_price
    dynamic_pricings = if date.present?
                         package.dynamic_pricings
                       else
                         filter_dynamic_pricings(package)
                       end

    if use_dynamic_pricing? || date.present?
      # prioritize pricing.by_day_category equal holiday first, then `custom_day, based on the `selected_days`
      holiday_pricings = dynamic_pricings.select do |p|
        p.by_day_category == 'holiday' && date.between?(p.start_date, p.end_date)
      end

      return holiday_pricings.first if holiday_pricings.present?

      wday = date.wday.to_s
      custom_day_pricings = dynamic_pricings.select do |p|
        # selected_days are integers representing the day of the week, 0 is Sunday, 6 is Saturday
        p.by_day_category == 'custom_day' && p.selected_days.include?(wday)
      end

      return custom_day_pricings.first if custom_day_pricings.present?

      # HH_LOGGER.debug('Price for selected day is not found',
      #                 package_id: package.id,
      #                 package_type: package.class.name,
      #                 adult: adult,
      #                 date: date,
      #                 dynamic_pricings: dynamic_pricings.map(&:attributes),
      #                 wday: wday)

      # send_invalid_dynamic_pricing_email(package, use_dynamic_pricing, adult, date, wday) if use_dynamic_pricing?

      # client app could send the date data, but they don't use dynamic pricing yet
      # so we need to return the most expensive price
      if use_dynamic_pricing?
        APMErrorHandler.set_default_context_and_labels(dynamic_pricings: dynamic_pricings.map(&:attributes),
                                                       use_dynamic_pricing: use_dynamic_pricing,
                                                       package: package.attributes,
                                                       adult: adult,
                                                       date: date)
        raise(InvalidPackageData, 'Price for selected day is not found')
      else
        filter_dynamic_pricings(package).first
      end
    else
      dynamic_pricings.first
    end
  when :per_pack_and_normal_price
    dynamic_pricings = filter_dynamic_pricings(package)
    dynamic_pricings.first.presence || raise(InvalidPackageData, 'Pricing is empty')
  when :per_set_and_normal_price
    dynamic_pricings = filter_dynamic_pricings(package)
    dynamic_pricings.first.presence || raise(InvalidPackageData, 'Pricing is empty')
  when :per_menu_ala_carte_and_normal_price
    total_price_cents = 0

    sections = menu_sections.map(&:with_indifferent_access)
    sections.each do |section|
      section['menus'].each do |menu|
        package_menu = HhPackage::PackageMenu.find_by(id: menu['id'])
        next if package_menu.blank?

        sub_price = package_menu.price_cents * menu['quantity'].to_i
        total_price_cents += sub_price
      end
    end

    currency = package.package_attr.price_currency.presence || HhPackage::Pricing::DEFAULT_CURRENCY

    OpenStruct.new(price_cents: total_price_cents,
                   kids_price_cents: 0,
                   price_currency: currency)
  when :per_item_and_normal_price
    # For DIY packages using per_item pricing model
    # Return a base pricing structure that will be calculated later in find_package_final_price
    currency = package.package_attr.price_currency.presence || HhPackage::Pricing::DEFAULT_CURRENCY

    # Create an OpenStruct that mimics HhPackage::Pricing with a price method
    OpenStruct.new(
      price_cents: 0,
      kids_price_cents: 0,
      price_currency: currency,
      price: Money.new(0, currency),
    )
  else
    raise InvalidPackageData, 'Unknown pricing model and dynamic pricing type'
  end
end

#find_kids_price(package, adult, date, packages: []) ⇒ Hash

Find the price for kids based on the selected packages. This method returns more than just a price, as it also identifies the package from which the kids' price is derived. This is necessary because the “mix and match” feature of AYCE (All-You-Can-Eat) packages allows two different kids' prices.

Parameters:

  • package (HhPackage::Package::Ayce, PartyPack, HungrySet)

    The selected package

  • adult (Integer)

    The number of adults for the reservation

  • date (Date)

    The date of the reservation

  • packages (Array) (defaults to: [])

    The list of selected packages for mix and match feature

Returns:

  • (Hash)

    { package: HhPackage::Package, restaurant_package_id: Integer, price: Money, currency: String }



148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 148

def find_kids_price(package, adult, date, packages: [])
  currency = fetch_package_currency(package)
  zero_price = HhMoney.new(0, currency)

  # Return a zero-price hash if the package is not of Ayce type
  return build_kids_price_hash(package, zero_price, currency) unless package.is_a?(HhPackage::Package::Ayce)

  # If mix and match is enabled, find the maximum kids price from the packages
  if package.use_mix_and_match?(packages.count)
    find_max_or_zero_kids_price(package, adult, date, packages, currency, zero_price)
  else
    build_kids_price_hash(package, find_kids_price_by_package(package, adult, date, currency, packages), currency)
  end
end

#find_package_final_price(package, quantity, adult, kids, date, custom_price_cents: nil, packages: []) ⇒ Object

Calculate the final price for a package based on its pricing model and dynamic pricing type.

Parameters:

  • package (HhPackage::Package::Ayce or other package types)

    the package for which the price is to be calculated

  • quantity (Integer)
  • adult (Integer)
  • kids (Integer)
  • date (Date)
  • custom_price_cents (Integer) (defaults to: nil)
  • packages (Array<HhPackage::Package::Ayce or other package types>) (defaults to: [])

Returns:

  • Integer final price in cents



243
244
245
246
247
248
249
250
251
252
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
284
285
286
287
288
289
290
291
292
293
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 243

def find_package_final_price(package, quantity, adult, kids, date, custom_price_cents: nil, packages: [])
  package_count = packages.count
  pricing = if custom_price_cents.blank?
              find_base_price(package, adult, date)
            else
              OpenStruct.new(price_cents: custom_price_cents,
                             kids_price_cents: 0,
                             price_currency: currency)
            end
  # set 1 as default charge_based_quantity for flat charge_amount_type
  # flat means charge amount is fixed per booking
  charge_based_quantity = package.charge_amount_type.to_sym == :flat ? 1 : quantity
  selected_qty = package.mix_n_match_qty(adult, charge_based_quantity, package_count)

  case package.pricing_model_and_dynamic_pricing_type(package_count: package_count)
  when :per_person_and_normal_price, :per_person_and_by_party_size_price, :per_person_and_by_day_price
    if valid_come_more_pay_less?(package, date)
      calculate_come_more_pay_less_total(pricing, package, selected_qty, kids, date, packages)
    else
      dynamic_price_calculation(pricing, selected_qty, kids, date, package, packages: packages)
    end
  # when :per_person_and_by_party_size_price, :per_person_and_by_day_price
  #   dynamic_price_calculation(pricing, selected_qty, kids, date, package, package_count: package_count)
  when :per_pack_and_by_day_price
    pricing.price_cents * quantity.to_f
  when :per_pack_and_normal_price, :per_set_and_normal_price
    # ayce package with mix and match feature will use per pack mode
    # possible case in this block is also ayce package with mix and match feature and come more pay less feature
    if valid_come_more_pay_less?(package, date)
      calculate_come_more_pay_less_total(pricing, package, selected_qty, kids, date, packages)
    elsif package.use_mix_and_match?(package_count)
      dynamic_price_calculation(pricing, selected_qty, kids, date, package, packages: packages)
    elsif package.ala_carte? || menu_sections.blank?
      pricing.price_cents * quantity.to_f
    else
      pricing.price_cents
    end
  when :per_menu_ala_carte_and_normal_price
    pricing.price_cents
  when :per_item_and_normal_price
    # Handle DIY packages with group_sections (pricing menus)
    if package.type_short == HhPackage::Package::Diy::TYPE_SHORT && group_sections.present?
      calculate_diy_group_sections_price(package)
    else
      pricing.price_cents * quantity.to_i
    end
  else
    raise NotImplementedError,
          "Unknown #{package.pricing_model_and_dynamic_pricing_type} pricing model and dynamic pricing type"
  end
end

#find_package_refund_final_price(package, total_package_price_cents) ⇒ Integer

Calculate the final price for refund guarantee based on the package's refund policy.

Parameters:

  • package (HhPackage::Package::Ayce or other package types)

    The package for which the refund price is to be calculated

  • total_package_price_cents (Integer)

    The total price of the package in cents

Returns:

  • (Integer)

    The final refund price in cents



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 324

def find_package_refund_final_price(package, total_package_price_cents)
  package_attr = package.package_attr

  return 0 if package_attr.blank? || !package_attr.accept_refund_guarantee?

  case package_attr.refund_type.to_sym
  when HhPackage::PackageAttr::REFUND_TYPE_PERCENTAGE
    ((total_package_price_cents * package_attr.refund_amount).to_d / 100).ceil
  when HhPackage::PackageAttr::REFUND_TYPE_FIXED_AMOUNT
    # refund_amount is in cents
    package_attr.refund_amount.to_i
  else
    APMErrorHandler.report("Unknown refund type: #{package_attr.refund_type}", package_id: package.id)
    0
  end
end

#kids_price_list(package) ⇒ Array<Hash>

This method is to transform the dynamic pricing's kids price to be `kids_price_v2` in the Serializer

Examples:

[
{ price_policy: 'Free', price_policy_short: 'Free', price_value: 'Free' },
{ price_policy: '50% off', price_policy_short: '50%', price_value: '฿500' }
]

Parameters:

Returns:

  • (Array<Hash>)

    list of kids price policy and value



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 379

def kids_price_list(package)
  if use_kids_price?(package)
    price_policy = package.package_attr.kids_policy.to_s
    price_policy_short = (package.package_attr&.kids_policy_short.presence || package.package_attr&.kids_policy_short_en).to_s
    # map dynamic_pricings but remove duplicate kids price cents
    filter_dynamic_pricings(package).map do |pricing|
      kids_price_cents = pricing.kids_price_cents
      price_value = if kids_price_cents.zero?
                      'Free'
                    else
                      HhMoney.format_rounded(kids_price_cents,
                                             pricing.price_currency, round: :down)
                    end
      { price_policy: price_policy,
        price_policy_short: price_policy_short,
        price_value: price_value,
        price_cents: kids_price_cents }
    end.uniq { |p| p[:price_value] }
  else
    []
  end
end

#use_dynamic_pricing?Boolean

Returns:

  • (Boolean)


14
15
16
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 14

def use_dynamic_pricing?
  @use_dynamic_pricing == true
end

#use_kids_price?(package) ⇒ Boolean

Returns:

  • (Boolean)


362
363
364
365
366
367
368
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 362

def use_kids_price?(package)
  return false unless package.is_a?(HhPackage::Package::Ayce)

  filter_dynamic_pricings(package).select do |pricing|
    pricing.kids_price_cents.positive?
  end.present?
end

#valid_come_more_pay_less?(package, date) ⇒ Boolean

Returns:

  • (Boolean)


415
416
417
418
419
420
421
422
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 415

def valid_come_more_pay_less?(package, date)
  return false unless package&.package_attr&.comemore_payless?

  pricing_group = package.pricing_groups.first
  return false if pricing_group.blank?

  valid_come_more_pay_less_date?(date, pricing_group)
end

#valid_come_more_pay_less_date?(date, pricing_group) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


404
405
406
407
408
409
410
411
412
413
# File 'app/my_lib/hh_package/reservation_packages/price_finder.rb', line 404

def valid_come_more_pay_less_date?(date, pricing_group)
  return false if date.blank?

  date = date.to_date

  return true if pricing_group.start_date.nil? || pricing_group.end_date.nil?
  return true if date.between?(pricing_group.start_date, pricing_group.end_date)

  false
end