Class: UserLoyalty

Inherits:
ApplicationRecord show all
Defined in:
app/models/user_loyalty.rb

Overview

Schema Information

Table name: user_loyalties

id                               :bigint           not null, primary key
ab_testing_group                 :string(191)
progress_total_reservations      :integer          default(0)
progress_total_spending_cents    :integer          default(0), not null
progress_total_spending_currency :string(191)      default("THB"), not null
start_date_level                 :datetime
state                            :string(191)
total_reservations               :integer          default(0)
total_spending_cents             :bigint           default(0), not null
total_spending_currency          :string(191)      default("THB"), not null
created_at                       :datetime         not null
updated_at                       :datetime         not null
last_loyalty_level_id            :bigint
loyalty_level_id                 :bigint
session_id                       :string(191)
top_loyalty_level_id             :bigint
user_id                          :bigint

Indexes

index_user_loyalties_on_last_loyalty_level_id  (last_loyalty_level_id)
index_user_loyalties_on_loyalty_level_id       (loyalty_level_id)
index_user_loyalties_on_top_loyalty_level_id   (top_loyalty_level_id)
index_user_loyalties_on_updated_at             (updated_at)
index_user_loyalties_on_user_id                (user_id)

Constant Summary collapse

LOYALTY_SCORE =
{
  hunger: 1,
  silver: 2,
  gold: 3,
  platinum: 4,
}.freeze
TIER_IMAGES =
{
  hunger: 'red-tier.png',
  silver: 'silver-tier.png',
  gold: 'gold-tier.png',
  platinum: 'platinum-tier.png',
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

sync_carrierwave_url

Class Method Details

.update_data_user(user_ids, from_month) ⇒ Object

Parameters:

  • user_ids (Array)
  • from_month (Datetime)


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
# File 'app/models/user_loyalty.rb', line 179

def self.update_data_user(user_ids, from_month)
  return if user_ids.blank?

  User.includes(:reservations).where(id: user_ids).find_each do |user|
    total_reservation = user.reservations.reached_goal_scope.where('created_at >= ?', from_month.beginning_of_month)
    user_loyalty = user.user_loyalty

    user_loyalty.total_spending = 0
    user_loyalty.total_reservations = 0
    user_loyalty.progress_total_spending = total_reservation.sum(&:charge_price)
    user_loyalty.progress_total_reservations = total_reservation.count
    user_loyalty.state = :hunger
    user_loyalty.save

    current_level = :hunger
    (LoyaltyLevel::AVAILABLE_LEVELS.keys.reverse! - [:hunger]).each do |attr|
      qualification = LoyaltyProgram::Qualification.new(user_loyalty)

      next unless qualification.qualified_to_upgrade?(attr)

      user_loyalty.last_loyalty_level = user_loyalty.loyalty_level
      user_loyalty.top_loyalty_level  = LoyaltyLevel.send(attr)
      user_loyalty.loyalty_level      = LoyaltyLevel.send(attr)
      user_loyalty.state              = attr

      # update data and reset progress data
      user_loyalty.start_date_level = Time.now_in_tz(user_loyalty.user.time_zone)
      user_loyalty.total_spending = user_loyalty.total_data_spending
      user_loyalty.total_reservations = user_loyalty.total_data_reservations.to_i
      user_loyalty.progress_total_spending = 0
      user_loyalty.progress_total_reservations = 0
      user_loyalty.save!

      current_level = attr
      break
    end

    if current_level == :hunger
      user_loyalty.last_loyalty_level = LoyaltyLevel.hunger
      user_loyalty.top_loyalty_level  = LoyaltyLevel.hunger
      user_loyalty.loyalty_level      = LoyaltyLevel.hunger
      user_loyalty.save
    end
  end
end

Instance Method Details

#already_downgraded_today?Boolean

no need to downgrade again if user already downgraded when their points expired because when user points expired, points will be 0 and user level will be downgraded

Returns:

  • (Boolean)


121
122
123
124
125
126
127
128
129
# File 'app/models/user_loyalty.rb', line 121

def already_downgraded_today?
  last_reward = user.rewards.order(created_at: :desc).first
  return false unless last_reward

  same_day = last_reward.created_at.to_date == updated_at.to_date
  is_points_expiry_downgrade = last_reward.description.include?('Inactive account points expiry')

  same_day && is_points_expiry_downgrade
end

#benefit_value(key, cast: :to_f) ⇒ Numeric

Returns the value of a specific benefit for the user's loyalty level.

Examples:

Get the dine-in points benefit value

user_loyalty.benefit_value(:dine_in_point)

Parameters:

  • key (Symbol)

    The key of the benefit to retrieve.

  • cast (Symbol) (defaults to: :to_f)

    The method to cast the value (default is :to_f).

Returns:

  • (Numeric)

    The value of the benefit, or 0 if not found.



267
268
269
270
# File 'app/models/user_loyalty.rb', line 267

def benefit_value(key, cast: :to_f)
  value = loyalty_level&.benefits&.public_send(key)
  value.respond_to?(cast) ? value.public_send(cast) : 0.public_send(cast)
end

#dine_in_point_key_by_country(country) ⇒ String

Returns the dine-in point key name based on the given country.

Example:

dine_in_point_key_by_country(Country.find_by(alpha3: 'SGP')) # => "dine_in_point_sg"
dine_in_point_key_by_country(Country.find_by(alpha3: 'MYS')) # => "dine_in_point_my"
dine_in_point_key_by_country(nil)                            # => "dine_in_point_th"

Parameters:

  • country (Country, nil)

    the country object (can be nil)

Returns:

  • (String)

    the corresponding key for dine-in points, either 'dine_in_point_sg' for Singapore, 'dine_in_point_my' for Malaysia, or 'dine_in_point_th' by default



251
252
253
254
255
256
# File 'app/models/user_loyalty.rb', line 251

def dine_in_point_key_by_country(country)
  return 'dine_in_point_sg' if country&.alpha3 == ApiV5::Constants::COUNTRY_CODE_SG
  return 'dine_in_point_my' if country&.alpha3 == ApiV5::Constants::COUNTRY_CODE_MY

  'dine_in_point_th'
end

#expired_date_levelObject



143
144
145
146
147
# File 'app/models/user_loyalty.rb', line 143

def expired_date_level
  # same logic with points expiry date
  # to avoid duplicate logic, we use the same method
  Reward.points_expiry_date(user)
end

#init_loyalty_levelObject



92
93
94
95
96
97
98
99
100
# File 'app/models/user_loyalty.rb', line 92

def init_loyalty_level
  time_zone = user&.time_zone || Time::THAILAND_ZONE

  self.loyalty_level      = LoyaltyLevel.hunger
  self.last_loyalty_level = LoyaltyLevel.hunger
  self.top_loyalty_level  = LoyaltyLevel.hunger
  self.start_date_level   = Time.now_in_tz(time_zone).beginning_of_day
  self.ab_testing_group   = user&.id.to_i % 2 == 0 ? 'B' : 'A'
end

#next_tierObject



169
170
171
172
173
174
175
# File 'app/models/user_loyalty.rb', line 169

def next_tier
  current_tier = state.to_sym
  current_rank = UserLoyalty::LOYALTY_SCORE[current_tier]
  next_rank    = current_rank + 1

  UserLoyalty::LOYALTY_SCORE.invert[next_rank] || UserLoyalty::LOYALTY_SCORE.invert[UserLoyalty::LOYALTY_SCORE.invert.keys.max]
end

#no_booking_in_current_level?Boolean

Returns:

  • (Boolean)


131
132
133
# File 'app/models/user_loyalty.rb', line 131

def no_booking_in_current_level?
  user.reservations.reached_goal_scope.where(created_at: start_date_level..expired_date_level).blank?
end

#number_booking_to_next_tierObject



153
154
155
156
157
158
159
# File 'app/models/user_loyalty.rb', line 153

def number_booking_to_next_tier
  return 0 if state?(:platinum)

  next_qualification = LoyaltyProgram::Template::TIER_QUALIFICATIONS[next_tier]
  next_total_reservations = next_qualification[:total_reservations]
  next_total_reservations - total_data_reservations
end

#qualified_to_downgrade_now?Boolean

Returns:

  • (Boolean)


107
108
109
110
111
112
113
114
115
116
117
# File 'app/models/user_loyalty.rb', line 107

def qualified_to_downgrade_now?
  # return false because there is no lower level after hunger
  return false if state?(:hunger)

  return false if already_downgraded_today?

  today = Time.now_in_tz(user.time_zone)
  return false unless expired_date_level && expired_date_level.to_date == today.to_date

  no_booking_in_current_level?
end

#qualified_to_upgrade?(next_tier = nil) ⇒ Boolean

Returns:

  • (Boolean)


102
103
104
105
# File 'app/models/user_loyalty.rb', line 102

def qualified_to_upgrade?(next_tier = nil)
  qualification = LoyaltyProgram::Qualification.new(self, next_tier)
  qualification.qualified_to_upgrade?
end

#renew_session_idObject



149
150
151
# File 'app/models/user_loyalty.rb', line 149

def renew_session_id
  self.session_id = SecureRandom.hex(10)
end

#total_data_reservationsObject



135
136
137
# File 'app/models/user_loyalty.rb', line 135

def total_data_reservations
  total_reservations.to_i + progress_total_reservations.to_i
end

#total_data_spendingObject



139
140
141
# File 'app/models/user_loyalty.rb', line 139

def total_data_spending
  total_spending&.amount.to_i + progress_total_spending&.amount.to_i
end

#total_spend_to_next_tierObject



161
162
163
164
165
166
167
# File 'app/models/user_loyalty.rb', line 161

def total_spend_to_next_tier
  return 0 if state?(:platinum)

  next_qualification = LoyaltyProgram::Template::TIER_QUALIFICATIONS[next_tier]
  next_total_spending = next_qualification[:total_spending]
  next_total_spending - total_data_spending
end

#trigger_reservation_summary_sync_on_loyalty_changeObject



225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'app/models/user_loyalty.rb', line 225

def trigger_reservation_summary_sync_on_loyalty_change
  # Only trigger if loyalty_level_id has changed (indicating a level change)
  return unless saved_change_to_loyalty_level_id?
  return unless defined?(PartnerService::LoyaltyChangeDetectorService)

  begin
    # Trigger batch sync for reservation summaries
    PartnerService::LoyaltyChangeDetectorService.new(self).execute
  rescue StandardError => e
    Rails.logger.error "[UserLoyalty] Error triggering reservation summary sync for user #{user_id}: #{e.message}"
    Rails.logger.error e.backtrace.first(3).join("\n")
  end
end