Class: CloverSandboxSimulator::Services::Clover::DiscountService

Inherits:
BaseService
  • Object
show all
Defined in:
lib/clover_sandbox_simulator/services/clover/discount_service.rb

Overview

Manages Clover discounts with enhanced functionality for line-items, promo codes, combos, time-based, and loyalty discounts

Constant Summary collapse

TIME_PERIODS =

Time-based discount periods

{
  happy_hour: { start_hour: 15, end_hour: 18, name: "Happy Hour" },
  lunch: { start_hour: 11, end_hour: 14, name: "Lunch" },
  early_bird: { start_hour: 0, end_hour: 18, name: "Early Bird" }
}.freeze
LOYALTY_TIERS =

Loyalty tier thresholds

{
  platinum: { min_visits: 50, percentage: 20 },
  gold: { min_visits: 25, percentage: 15 },
  silver: { min_visits: 10, percentage: 10 },
  bronze: { min_visits: 5, percentage: 5 }
}.freeze
CACHE_TTL_SECONDS =

Cache configuration

300

Instance Attribute Summary

Attributes inherited from BaseService

#config, #logger

Instance Method Summary collapse

Methods inherited from BaseService

#initialize

Constructor Details

This class inherits a constructor from CloverSandboxSimulator::Services::BaseService

Instance Method Details

#apply_category_line_item_discounts(order_id, line_items:, eligible_categories:, discount_config:) ⇒ Object

Apply discounts to multiple line items based on category eligibility



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 127

def apply_category_line_item_discounts(order_id, line_items:, eligible_categories:, discount_config:)
  logger.info "Applying category-based line item discounts to order #{order_id}"

  applied_discounts = []

  line_items.each do |line_item|
    category = line_item.dig("item", "categories", "elements", 0, "name")
    next unless category && eligible_categories.include?(category)

    result = apply_line_item_discount(
      order_id,
      line_item_id: line_item["id"],
      name: discount_config[:name],
      percentage: discount_config[:percentage],
      amount: discount_config[:amount]
    )

    applied_discounts << result if result
  end

  applied_discounts
end

#apply_combo_discount(order_id, combo:, line_items:) ⇒ Hash?

Apply a combo discount to an order

Parameters:

  • order_id (String)

    The order ID

  • combo (Hash)

    The combo configuration

  • line_items (Array)

    Order line items

Returns:

  • (Hash, nil)

    Applied discount



322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 322

def apply_combo_discount(order_id, combo:, line_items:)
  logger.info "Applying combo '#{combo['name']}' to order #{order_id}"

  discount_info = calculate_combo_discount(combo, line_items)

  if combo["applies_to"] == "matching_items" || combo["applies_to"] == "cheapest_items"
    # Apply to specific line items
    matching = find_combo_items(combo, line_items)
    apply_discount_to_items(order_id, matching, combo, discount_info)
  else
    # Apply to order total
    apply_order_discount(order_id, combo["name"], discount_info)
  end
end

#apply_line_item_discount(order_id, line_item_id:, discount_id: nil, name: nil, percentage: nil, amount: nil, item_price: nil) ⇒ Object

Apply discount to a specific line item Uses Clover’s line item discount API

Parameters:

  • item_price (Integer) (defaults to: nil)

    Item price in cents (required for percentage discounts)



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 106

def apply_line_item_discount(order_id, line_item_id:, discount_id: nil, name: nil, percentage: nil, amount: nil, item_price: nil)
  logger.info "Applying line item discount to order #{order_id}, line item #{line_item_id}"

  payload = build_discount_payload(
    discount_id: discount_id,
    name: name,
    percentage: percentage,
    amount: amount,
    item_price: item_price
  )

  return nil if payload.empty?

  request(
    :post,
    endpoint("orders/#{order_id}/line_items/#{line_item_id}/discounts"),
    payload: payload
  )
end

#apply_loyalty_discount(order_id, customer:) ⇒ Object

Apply loyalty discount to an order



439
440
441
442
443
444
445
446
447
448
449
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 439

def apply_loyalty_discount(order_id, customer:)
  discount = get_loyalty_discount(customer)
  return nil unless discount

  logger.info "Applying loyalty discount '#{discount['name']}' to order #{order_id}"

  request(:post, endpoint("orders/#{order_id}/discounts"), payload: {
    "name" => discount["name"],
    "percentage" => discount["percentage"].to_s
  })
end

#apply_promo_code(order_id, code:, order_total: 0, line_items: nil, customer: nil, current_time: Time.now) ⇒ Hash?

Apply a validated promo code to an order

Parameters:

  • order_id (String)

    The order ID

  • code (String)

    The validated promo code

  • order_total (Integer) (defaults to: 0)

    Order total in cents

  • line_items (Array, nil) (defaults to: nil)

    Line items for category-specific discounts

Returns:

  • (Hash, nil)

    Applied discount or nil if failed



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 246

def apply_promo_code(order_id, code:, order_total: 0, line_items: nil, customer: nil, current_time: Time.now)
  validation = validate_promo_code(
    code,
    order_total: order_total,
    customer: customer,
    line_items: line_items,
    current_time: current_time
  )

  unless validation[:valid]
    logger.warn "Promo code validation failed: #{validation[:error]}"
    return nil
  end

  coupon = validation[:coupon]
  logger.info "Applying promo code #{code} to order #{order_id}"

  # Apply to specific line items if category-restricted
  if coupon["applicable_categories"] && line_items
    apply_promo_to_line_items(order_id, coupon, line_items)
  else
    apply_promo_to_order(order_id, coupon, order_total)
  end
end

#cache_expired?Boolean

Check if cache is expired

Returns:

  • (Boolean)


45
46
47
48
49
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 45

def cache_expired?
  return true if @cache_loaded_at.nil?

  (Time.now - @cache_loaded_at) > CACHE_TTL_SECONDS
end

#clear_cacheObject

Clear all cached data



28
29
30
31
32
33
34
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 28

def clear_cache
  @discount_definitions = nil
  @coupon_codes = nil
  @combos = nil
  @cache_loaded_at = nil
  logger.debug "Discount service cache cleared"
end

#create_fixed_discount(name:, amount:) ⇒ Object

Create a fixed amount discount



66
67
68
69
70
71
72
73
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 66

def create_fixed_discount(name:, amount:)
  logger.info "Creating fixed discount: #{name} ($#{amount / 100.0})"

  request(:post, endpoint("discounts"), payload: {
    "name" => name,
    "amount" => -amount.abs # Clover expects negative for discounts
  })
end

#create_percentage_discount(name:, percentage:) ⇒ Object

Create a percentage discount



76
77
78
79
80
81
82
83
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 76

def create_percentage_discount(name:, percentage:)
  logger.info "Creating percentage discount: #{name} (#{percentage}%)"

  request(:post, endpoint("discounts"), payload: {
    "name" => name,
    "percentage" => percentage
  })
end

#current_meal_period(current_time = Time.now) ⇒ Object

Determine current meal period



371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 371

def current_meal_period(current_time = Time.now)
  hour = current_time.hour

  case hour
  when 7..10 then :breakfast
  when 11..14 then :lunch
  when 15..17 then :happy_hour
  when 17..21 then :dinner
  when 21..23 then :late_night
  else :closed
  end
end

#delete_discount(discount_id) ⇒ Object

Delete a discount



86
87
88
89
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 86

def delete_discount(discount_id)
  logger.info "Deleting discount: #{discount_id}"
  request(:delete, endpoint("discounts/#{discount_id}"))
end

#delete_line_item_discount(order_id, line_item_id, discount_id) ⇒ Object

Delete a line item discount



157
158
159
160
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 157

def delete_line_item_discount(order_id, line_item_id, discount_id)
  logger.info "Removing discount #{discount_id} from line item #{line_item_id}"
  request(:delete, endpoint("orders/#{order_id}/line_items/#{line_item_id}/discounts/#{discount_id}"))
end

#detect_combos(line_items, current_time: Time.now) ⇒ Array<Hash>

Detect applicable combos for order items

Parameters:

  • line_items (Array)

    Line items with category information

  • current_time (Time) (defaults to: Time.now)

    Time for time-restricted combos

Returns:

  • (Array<Hash>)

    List of applicable combos with discount info



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
314
315
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 284

def detect_combos(line_items, current_time: Time.now)
  logger.info "Detecting combo deals for #{line_items.size} items"

  combos = load_combos
  applicable_combos = []

  combos.each do |combo|
    next unless combo["active"]

    # Check time restrictions
    if combo["time_restricted"]
      next unless within_time_period?(combo["time_rules"], current_time)
    end

    # Check day restrictions
    if combo["day_restricted"]
      next unless combo["valid_days"].include?(current_time.wday)
    end

    # Check if items satisfy combo requirements
    if combo_requirements_met?(combo, line_items)
      applicable_combos << {
        combo: combo,
        matching_items: find_combo_items(combo, line_items),
        discount: calculate_combo_discount(combo, line_items)
      }
    end
  end

  # Sort by discount value (best deals first)
  applicable_combos.sort_by { |c| -c[:discount][:amount] }
end

#first_order_discount?(customer) ⇒ Boolean

Check if customer qualifies for first-order discount

Returns:

  • (Boolean)


452
453
454
455
456
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 452

def first_order_discount?(customer)
  return true unless customer

  customer_visit_count(customer) <= 1
end

#get_combosObject

Get all available combos



338
339
340
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 338

def get_combos
  load_combos
end

#get_coupon_codesObject

Get all available coupon codes



272
273
274
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 272

def get_coupon_codes
  load_coupon_codes
end

#get_discount(discount_id) ⇒ Object

Get a specific discount



61
62
63
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 61

def get_discount(discount_id)
  request(:get, endpoint("discounts/#{discount_id}"))
end

#get_discountsObject

Fetch all discounts



52
53
54
55
56
57
58
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 52

def get_discounts
  logger.info "Fetching discounts..."
  response = request(:get, endpoint("discounts"))
  elements = response&.dig("elements") || []
  logger.info "Found #{elements.size} discounts"
  elements
end

#get_line_item_discounts(order_id, line_item_id) ⇒ Object

Get all line item discounts for an order



151
152
153
154
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 151

def get_line_item_discounts(order_id, line_item_id)
  response = request(:get, endpoint("orders/#{order_id}/line_items/#{line_item_id}/discounts"))
  response&.dig("elements") || []
end

#get_loyalty_discount(customer) ⇒ Hash?

Get applicable loyalty discount for a customer

Parameters:

  • customer (Hash)

    Customer data

Returns:

  • (Hash, nil)

    Loyalty discount config or nil



425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 425

def get_loyalty_discount(customer)
  tier_info = loyalty_tier(customer)
  return nil unless tier_info

  discounts = load_discount_definitions
  loyalty_discounts = discounts.select { |d| d["type"] == "loyalty" }

  # Find the matching loyalty discount
  loyalty_discounts.find do |d|
    d["min_visits"] == tier_info[:min_visits]
  end
end

#get_time_based_discounts(current_time: Time.now) ⇒ Array<Hash>

Get applicable time-based discounts for current time

Parameters:

  • current_time (Time) (defaults to: Time.now)

    Time to check against

Returns:

  • (Array<Hash>)

    Applicable time-based discounts



349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 349

def get_time_based_discounts(current_time: Time.now)
  discounts = load_discount_definitions
  hour = current_time.hour

  discounts.select do |discount|
    next false unless discount["type"] == "time_based" || discount["type"] == "line_item_time_based"
    next false unless discount["auto_apply"]

    rules = discount["time_rules"]
    hour >= rules["start_hour"] && hour < rules["end_hour"]
  end
end

#happy_hour_discounts(current_time: Time.now) ⇒ Object

Get happy hour discounts if applicable



385
386
387
388
389
390
391
392
393
394
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 385

def happy_hour_discounts(current_time: Time.now)
  hour = current_time.hour
  happy_hour_config = TIME_PERIODS[:happy_hour]
  return [] unless hour >= happy_hour_config[:start_hour] && hour < happy_hour_config[:end_hour]

  discounts = load_discount_definitions
  discounts.select do |d|
    d["id"]&.include?("happy") || d["name"]&.downcase&.include?("happy")
  end
end

#load_discount_definitionsObject

Load discount definitions from JSON (with TTL)



531
532
533
534
535
536
537
538
539
540
541
542
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 531

def load_discount_definitions
  refresh_cache_if_needed

  @discount_definitions ||= begin
    @cache_loaded_at = Time.now
    path = File.join(data_path, "discounts.json")
    return [] unless File.exist?(path)

    data = JSON.parse(File.read(path))
    data["discounts"] || []
  end
end

#loyalty_tier(customer) ⇒ Hash?

Determine loyalty tier for a customer

Parameters:

  • customer (Hash)

    Customer with visit_count or metadata

Returns:

  • (Hash, nil)

    Loyalty tier info or nil



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 403

def loyalty_tier(customer)
  return nil unless customer

  visit_count = customer_visit_count(customer)

  LOYALTY_TIERS.each do |tier, config|
    if visit_count >= config[:min_visits]
      return {
        tier: tier,
        percentage: config[:percentage],
        min_visits: config[:min_visits],
        visit_count: visit_count
      }
    end
  end

  nil # No tier reached
end

#random_discountObject

Select a random discount (30% chance of returning nil)



92
93
94
95
96
97
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 92

def random_discount
  return nil if rand < 0.7 # 70% chance of no discount

  discounts = get_discounts
  discounts.sample
end

#refresh_cache_if_neededObject

Reload cache if TTL expired



37
38
39
40
41
42
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 37

def refresh_cache_if_needed
  return unless cache_expired?

  clear_cache
  logger.debug "Discount service cache refreshed (TTL expired)"
end

#select_best_discount(order_total:, line_items: [], customer: nil, current_time: Time.now) ⇒ Hash

Select the best applicable discount for an order Considers time-based, loyalty, combos, and thresholds

Parameters:

  • order_total (Integer)

    Order total in cents

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

    Line items with categories

  • customer (Hash, nil) (defaults to: nil)

    Customer data

  • current_time (Time) (defaults to: Time.now)

    Current time

Returns:

  • (Hash)

    Best discount recommendation



469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 469

def select_best_discount(order_total:, line_items: [], customer: nil, current_time: Time.now)
  candidates = []

  # Check time-based discounts
  time_discounts = get_time_based_discounts(current_time: current_time)
  time_discounts.each do |d|
    candidates << {
      type: :time_based,
      discount: d,
      value: calculate_discount_value(d, order_total),
      priority: 2
    }
  end

  # Check loyalty discounts
  if customer
    loyalty = get_loyalty_discount(customer)
    if loyalty
      candidates << {
        type: :loyalty,
        discount: loyalty,
        value: (order_total * loyalty["percentage"] / 100.0).round,
        priority: 3
      }
    end
  end

  # Check combos
  unless line_items.empty?
    combos = detect_combos(line_items, current_time: current_time)
    combos.each do |combo_match|
      candidates << {
        type: :combo,
        discount: combo_match[:combo],
        value: combo_match[:discount][:amount],
        matching_items: combo_match[:matching_items],
        priority: 1
      }
    end
  end

  # Check threshold discounts
  threshold_discounts = load_discount_definitions.select do |d|
    d["type"] == "threshold" && d["min_order_amount"] && order_total >= d["min_order_amount"]
  end

  threshold_discounts.each do |d|
    candidates << {
      type: :threshold,
      discount: d,
      value: d["amount"] || (order_total * d["percentage"] / 100.0).round,
      priority: 4
    }
  end

  return nil if candidates.empty?

  # Sort by value (highest discount first), then by priority
  candidates.sort_by { |c| [-c[:value], c[:priority]] }.first
end

#validate_promo_code(code, order_total: 0, customer: nil, line_items: nil, current_time: Time.now) ⇒ Hash

Validate a promo code

Parameters:

  • code (String)

    The promo code to validate

  • order_total (Integer) (defaults to: 0)

    Order total in cents

  • customer (Hash, nil) (defaults to: nil)

    Customer data with visit_count, is_vip, etc.

  • line_items (Array, nil) (defaults to: nil)

    Line items for category validation

  • current_time (Time) (defaults to: Time.now)

    Time to validate against (for testing)

Returns:

  • (Hash)

    Validation result with :valid, :error, :coupon keys



173
174
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
231
232
233
234
235
236
237
238
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 173

def validate_promo_code(code, order_total: 0, customer: nil, line_items: nil, current_time: Time.now)
  logger.info "Validating promo code: #{code}"

  coupon = find_coupon_by_code(code)

  return validation_error("Invalid promo code") unless coupon
  return validation_error("Promo code is inactive") unless coupon["active"]

  # Check expiration
  valid_from = Time.parse(coupon["valid_from"])
  valid_until = Time.parse(coupon["valid_until"])
  return validation_error("Promo code has expired") if current_time > valid_until
  return validation_error("Promo code is not yet valid") if current_time < valid_from

  # Check usage limits
  if coupon["usage_limit"] && coupon["usage_count"] >= coupon["usage_limit"]
    return validation_error("Promo code has reached its usage limit")
  end

  # Check minimum order amount
  if coupon["min_order_amount"] && order_total < coupon["min_order_amount"]
    min_amount = format_currency(coupon["min_order_amount"])
    return validation_error("Minimum order amount of #{min_amount} required")
  end

  # Check customer restrictions
  if coupon["new_customers_only"] && customer && customer_visit_count(customer) > 0
    return validation_error("Promo code is for new customers only")
  end

  if coupon["vip_only"] && (!customer || !customer["is_vip"])
    return validation_error("Promo code is for VIP members only")
  end

  if coupon["birthday_required"] && (!customer || !customer_has_birthday_today?(customer))
    return validation_error("Promo code requires birthday verification")
  end

  # Check time restrictions
  if coupon["time_restricted"]
    unless within_time_period?(coupon["time_rules"], current_time)
      return validation_error("Promo code is only valid during specific hours")
    end
  end

  # Check day restrictions
  if coupon["day_restricted"]
    unless coupon["valid_days"].include?(current_time.wday)
      return validation_error("Promo code is not valid today")
    end
  end

  # Check category restrictions if line items provided
  if line_items && coupon["applicable_categories"]
    applicable = find_applicable_items(line_items, coupon["applicable_categories"])
    if applicable.empty?
      return validation_error("No eligible items for this promo code")
    end
  end

  {
    valid: true,
    coupon: coupon,
    discount_preview: calculate_coupon_discount(coupon, order_total, line_items)
  }
end

#within_time_period?(time_rules, current_time = Time.now) ⇒ Boolean

Check if current time is within a specific period

Returns:

  • (Boolean)


363
364
365
366
367
368
# File 'lib/clover_sandbox_simulator/services/clover/discount_service.rb', line 363

def within_time_period?(time_rules, current_time = Time.now)
  return true unless time_rules

  hour = current_time.hour
  hour >= time_rules["start_hour"] && hour < time_rules["end_hour"]
end