Class: Voucher

Inherits:
ApplicationRecord show all
Extended by:
Enumerize
Includes:
IdentityCache
Defined in:
app/models/voucher.rb

Overview

Schema Information

Table name: vouchers

id                        :integer          not null, primary key
active                    :boolean
amount_cap_cents          :integer          default(0), not null
amount_cap_currency       :string(191)      default("THB"), not null
amount_cents              :integer          default(0), not null
amount_currency           :string(191)      default("USD"), not null
apply_for                 :string(191)      default("package"), not null
currency_code             :string(191)
delivery_fee_discount     :boolean          default(FALSE)
delivery_voucher          :boolean          default(FALSE)
description               :text(65535)
discount_type             :string(191)
end_date                  :date
expiry_date               :date
expiry_range_by           :string(191)      default("created_at")
expiry_type               :string(191)      default("single")
first_booking_only        :boolean          default(FALSE)
for_android               :boolean          default(TRUE)
for_ios                   :boolean          default(TRUE)
for_web                   :boolean          default(TRUE)
fri_active                :boolean          default(TRUE)
max_pax                   :integer
max_usage                 :integer          default(1)
max_usage_user            :integer          default(0)
min_total_price_cents     :integer          default(0)
min_total_price_currency  :string(191)      default("THB")
minimal_distance          :decimal(10, 6)
mix_with_points           :boolean          default(FALSE)
mon_active                :boolean          default(TRUE)
name                      :string(191)
non_refundable            :boolean          default(FALSE)
payment_type              :string(191)
percentage                :integer
prefix_number             :string(1000)
quota                     :integer
require_full_prepayment   :boolean          default(FALSE)
sat_active                :boolean          default(TRUE)
start_date                :date
subsidized_by             :string(191)      default("hungryhub"), not null
sun_active                :boolean          default(TRUE)
thu_active                :boolean          default(TRUE)
tue_active                :boolean          default(TRUE)
usage_type                :string(191)
voucher_category          :string(191)      default("marketing"), not null
voucher_code              :string(191)
voucher_type              :string(191)      not null
wed_active                :boolean          default(TRUE)
created_at                :datetime
updated_at                :datetime
adaptive_points_ratio_id  :bigint
branch_id                 :integer
city_id                   :bigint
country_id                :bigint
early_bird_reservation_id :integer
early_bird_restaurant_id  :integer
guest_id                  :bigint
restaurant_id             :integer
restaurant_tag_id         :bigint
user_id                   :integer
voucher_group_id          :integer

Indexes

index_vouchers_on_adaptive_points_ratio_id  (adaptive_points_ratio_id)
index_vouchers_on_branch_id                 (branch_id)
index_vouchers_on_city_id                   (city_id)
index_vouchers_on_country_id                (country_id)
index_vouchers_on_early_bird_restaurant_id  (early_bird_restaurant_id)
index_vouchers_on_guest_id                  (guest_id)
index_vouchers_on_restaurant_id             (restaurant_id)
index_vouchers_on_restaurant_tag_id         (restaurant_tag_id)
index_vouchers_on_user_id                   (user_id)
index_vouchers_on_voucher_code              (voucher_code)

Foreign Keys

fk_rails_...  (branch_id => branches.id)
fk_rails_...  (early_bird_restaurant_id => restaurants.id)
fk_rails_...  (restaurant_id => restaurants.id)
fk_rails_...  (user_id => users.id)

Constant Summary collapse

TYPES =

REMOVE VOUCHER TYPE (specific_guest)

%I[
  all_restaurants
  specific_branch
  specific_restaurant
  specific_restaurants
  specific_customer
  specific_restaurant_and_customer
  specific_restaurant_and_guest
  specific_branch_and_customer
  specific_restaurant_tag
  specific_restaurant_packages
  hungryhub_voucher
].freeze
PAYMENT_TYPES =

TODO: REMOVE VOUCHER PAYMENT TYPE

%i[qrcode creditcard qrcode_and_creditcard].freeze
CATEGORIES =
%i[gift marketing redemption first_app_voucher refund_guarantee].freeze
APPLY_FOR =
%i[package delivery_fee package_and_delivery_fee].freeze
USAGE_TYPES =
%i[one_time deductible].freeze
DISCOUNT_TYPES =
%i[amount percentage pack person].freeze
FIRST_APP_CODE =
'APPFIRST'.freeze
REGISTRATION_REWARD_AMOUNT_SGD =

Registration reward amounts

5
REGISTRATION_REWARD_AMOUNT_THB =
50
REGISTRATION_REWARD_AMOUNT_MYR =
10

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

sync_carrierwave_url

Instance Attribute Details

#custom_amountObject

Returns the value of attribute custom_amount.



100
101
102
# File 'app/models/voucher.rb', line 100

def custom_amount
  @custom_amount
end

#device_typeObject

Returns the value of attribute device_type.



100
101
102
# File 'app/models/voucher.rb', line 100

def device_type
  @device_type
end

Class Method Details

.default_countryObject



361
362
363
364
# File 'app/models/voucher.rb', line 361

def self.default_country
  # Thailand is default country
  Country.find_by alpha3: 'THA'
end

.generate_referral_code(referrer_code) ⇒ Object

Parameters:

  • referrer_code (String)


388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'app/models/voucher.rb', line 388

def self.generate_referral_code(referrer_code)
  if referrer_code.blank? || referrer_code.to_s.length != User::MIN_REFERRAL_CODE_LENGTH
    raise NotImplementedError, "Referral code must be #{User::MIN_REFERRAL_CODE_LENGTH} characters"
  end

  attempts = 0
  max_attempts = 1000
  codes = Set.new
  code = nil

  loop do
    code = SecureRandom.alphanumeric(3).upcase
    code = "#{User::REFERRAL_CODE_PREFIX}#{referrer_code}-#{code}".upcase
    codes.add(code)

    unless Voucher.exists?(voucher_code: code) || User.exists?(short_my_r_code: code)
      break
    end

    attempts += 1
    raise 'Too many attempts' if attempts > max_attempts
  end
  code
end

.generate_voucher_codeObject



366
367
368
369
370
371
372
373
374
375
376
377
# File 'app/models/voucher.rb', line 366

def self.generate_voucher_code
  @codes ||= []
  code = nil
  loop do
    code = SecureRandom.hex(5).upcase
    next if @codes.include? code

    @codes.push code
    break if Voucher.find_by(voucher_code: code).nil?
  end
  code
end

.static_template(template, locale = MyLocaleManager.normalize_locale, package = nil) ⇒ Object



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'app/models/voucher.rb', line 413

def self.static_template(template, locale = MyLocaleManager.normalize_locale, package = nil)
  return '' if template.blank?
  return '' unless MyLocaleManager.available_locales.include?(locale.to_sym)

  template_with_locale = "#{template}_#{locale}"

  render_template = ActionView::Base.new
  render_template.view_paths = Rails.root.join('app', 'views', 'vouchers').to_s

  # Try to render template with requested locale, fallback to English if template doesn't exist
  begin
    render_template.render(template: template_with_locale, layout: false, locals: { package: package }).gsub("\n", '')
  rescue ActionView::MissingTemplate => e
    # Fallback to English template if the requested locale template doesn't exist
    fallback_template = "#{template}_en"
    HH_LOGGER.warn("Voucher template not found for locale #{locale}, falling back to English",
                   { original_template: template_with_locale, fallback_template: fallback_template,
                     error: e.message })
    render_template.render(template: fallback_template, layout: false, locals: { package: package }).gsub("\n", '')
  end
end

.valid_referral_code?(voucher_code) ⇒ Boolean

Returns:

  • (Boolean)


379
380
381
382
383
384
385
# File 'app/models/voucher.rb', line 379

def self.valid_referral_code?(voucher_code)
  return false if voucher_code.blank?

  # in .generate_referral_code method, we add prefix to the code
  voucher_code.to_s.start_with?(User::REFERRAL_CODE_PREFIX) ||
    User.match_by_referral_code(voucher_code).present?
end

Instance Method Details

#country_codeObject

return country code, set default to TH if not set



314
315
316
317
# File 'app/models/voucher.rb', line 314

def country_code
  country = ISO3166::Country.find_country_by_name(fetch_country_name)
  country&.alpha2
end

#delivery_fee_and_package?Boolean

Returns:

  • (Boolean)


296
297
298
# File 'app/models/voucher.rb', line 296

def delivery_fee_and_package?
  apply_for == 'package_and_delivery_fee'
end

#fetch_country_nameObject



319
320
321
322
323
324
# File 'app/models/voucher.rb', line 319

def fetch_country_name
  return country.name if country&.name.present?
  return city.country.name if city&.country&.name.present?

  Country.find_thailand.name
end

#fetch_currency_codeObject

return default THB if not set



309
310
311
# File 'app/models/voucher.rb', line 309

def fetch_currency_code
  currency_code.presence || country&.currency_code || Country::THAI_CURRENCY_CODE
end

#for_all_device?Boolean

Returns:

  • (Boolean)


300
301
302
# File 'app/models/voucher.rb', line 300

def for_all_device?
  for_web? && for_ios? && for_android?
end

#for_delivery_fee?Boolean

Returns:

  • (Boolean)


288
289
290
# File 'app/models/voucher.rb', line 288

def for_delivery_fee?
  apply_for == 'delivery_fee'
end

#for_package?Boolean

Returns:

  • (Boolean)


292
293
294
# File 'app/models/voucher.rb', line 292

def for_package?
  apply_for == 'package'
end

#handle_device_typeObject



245
246
247
248
249
250
251
# File 'app/models/voucher.rb', line 245

def handle_device_type
  return unless device_type == 'all_device'

  self.for_ios = true
  self.for_web = true
  self.for_android = true
end

#invalid_prefix_number_messageObject



334
335
336
# File 'app/models/voucher.rb', line 334

def invalid_prefix_number_message
  prefix_number.to_s.split(',').to_sentence(words_connector: ' or ')
end

#require_ccObject



326
327
328
# File 'app/models/voucher.rb', line 326

def require_cc
  payment_types.map(&:name).include? 'creditcard'
end

#require_payment_provider?Boolean

Returns:

  • (Boolean)


330
331
332
# File 'app/models/voucher.rb', line 330

def require_payment_provider?
  payment_types.present?
end

#stakeholderObject



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'app/models/voucher.rb', line 338

def stakeholder
  case voucher_type.to_s.to_sym
  when :all_restaurants
    '-'
  when :specific_restaurants
    '-'
  when :specific_branch
    branch.name
  when :specific_restaurant
    "#{restaurant.name} (#{restaurant_id})"
  when :specific_customer
    "#{user.name} (#{user.id})"
  when :specific_restaurant_and_customer
    "restaurant: #{restaurant.name} (#{restaurant_id}) - user: #{user&.name} (#{user.id})"
  when :specific_branch_and_customer
    "branch: #{branch.name} - user: #{user&.name} (#{user.id})"
  end
end

#subsidized_by_hungryhub?Boolean

Checks if the voucher is subsidized by HungryHub.

Returns:

  • (Boolean)

    Returns true if the `subsidized_by` attribute is set to 'hungryhub', otherwise false. This method is useful for determining the source of subsidy for the voucher, which may affect business logic such as reporting or eligibility.



459
460
461
# File 'app/models/voucher.rb', line 459

def subsidized_by_hungryhub?
  subsidized_by == 'hungryhub'
end

#subsidized_by_restaurant?Boolean

Checks if the voucher is subsidized by the restaurant.

Returns:

  • (Boolean)

    Returns true if the `subsidized_by` attribute is set to 'restaurant', otherwise false. Use this method to distinguish vouchers subsidized by the restaurant from those by HungryHub.



467
468
469
# File 'app/models/voucher.rb', line 467

def subsidized_by_restaurant?
  subsidized_by == 'restaurant'
end

#tokenObject



357
358
359
# File 'app/models/voucher.rb', line 357

def token
  CityHash.hash32([id, created_at]).to_s
end

#total_used_amountObject



447
448
449
450
451
452
# File 'app/models/voucher.rb', line 447

def total_used_amount
  reservations.
    joins(:reservation_vouchers).
    where(reservation_vouchers: { active: true }).
    sum('reservation_vouchers.amount_cents')
end

#usageObject



304
305
306
# File 'app/models/voucher.rb', line 304

def usage
  @usage ||= reservations.where(reservation_vouchers: { active: true }).size
end

#valid_discount_typeObject



253
254
255
256
257
258
259
# File 'app/models/voucher.rb', line 253

def valid_discount_type
  return true if discount_type == 'amount'
  return true if one_time?

  errors.add(:base, 'Cannot set pack/person discount type with deductible')
  false
end

#valid_mix_with_pointsObject



279
280
281
282
283
284
285
286
# File 'app/models/voucher.rb', line 279

def valid_mix_with_points
  return true if marketing? || redemption?

  unless mix_with_points.to_s == 'false'
    errors.add(:base,
               'By default, non-marketing category vouchers can be combined with points, cannot disable this config')
  end
end

#valid_percentage_typeObject



269
270
271
272
273
274
275
276
277
# File 'app/models/voucher.rb', line 269

def valid_percentage_type
  return true unless percentage?

  if deductible?
    errors.add(:base, "Can't combine percentage type with deductible usage type")
  elsif voucher_category == 'gift'
    errors.add(:base, 'vouchers with a percentage discount type cannot be made as gift vouchers')
  end
end

#valid_specific_device_typesObject



261
262
263
264
265
266
267
# File 'app/models/voucher.rb', line 261

def valid_specific_device_types
  return true if device_type == 'all_device'
  return true unless !for_ios? && !for_android? && !for_web?

  errors.add(:base, 'Please select specific device type')
  false
end

#validate_amount_based_on_discount_typeObject



227
228
229
230
231
# File 'app/models/voucher.rb', line 227

def validate_amount_based_on_discount_type
  if discount_type == 'amount' && (amount.blank? || amount <= 0)
    errors.add(:amount, 'must be greater than 0 for amount discount type')
  end
end

#validate_amount_cap_based_on_discount_typeObject



233
234
235
236
237
# File 'app/models/voucher.rb', line 233

def validate_amount_cap_based_on_discount_type
  if discount_type.in?(%w[pack person]) && (amount_cap.blank? || amount_cap <= 0)
    errors.add(:amount_cap, 'must be greater than 0 for pack or person discount type')
  end
end

#validate_percentage_based_on_discount_typeObject



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

def validate_percentage_based_on_discount_type
  if discount_type == 'percentage' && (percentage.blank? || percentage <= 0)
    errors.add(:percentage, 'must be greater than 0 for percentage discount type')
  end
end