Class: SpreeCmCommissioner::InventoryItem

Inherits:
Base
  • Object
show all
Includes:
ProductType
Defined in:
app/models/spree_cm_commissioner/inventory_item.rb

Constant Summary collapse

MAX_DISPLAY_STOCK =
20

Constants included from ProductType

ProductType::PERMANENT_STOCK_PRODUCT_TYPES, ProductType::PRE_INVENTORY_DAYS, ProductType::PRODUCT_TYPES

Instance Method Summary collapse

Methods included from ProductType

#permanent_stock?, #pre_inventory_days

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 104

def active?
  inventory_date.nil? || inventory_date >= Time.zone.today
end

#adjust_quantity!(quantity) ⇒ Object

This method is only used when admin update stock



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 50

def adjust_quantity!(quantity)
  with_lock do
    # IMPORTANT: Apply quantity changes directly without defensive clamping.
    # The model validation will catch any attempts to go negative, surfacing bugs
    # in upstream logic rather than silently losing data.
    #
    # ❌ DO NOT use defensive clamping like:
    #   self.max_capacity = [max_capacity + quantity, 0].max
    #
    # Why? Clamping masks bugs. Validation errors are better than silent data loss.
    # See: /docs/lessons-learned/inventory-consistency-issues.md#lesson-learned-async-job-validation-strategy
    self.max_capacity = max_capacity + quantity
    self.quantity_available = quantity_available + quantity
    save!

    # When user has been searched or booked a product, it has cached the quantity in redis,
    # So we need to update redis cache if inventory key has been created in redis
    adjust_quantity_in_redis(quantity)
  end
end

#adjust_quantity_in_redis(quantity) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 83

def adjust_quantity_in_redis(quantity)
  SpreeCmCommissioner.inventory_redis_pool.with do |redis|
    # Always update Redis cache, even if it doesn't exist yet.
    # This prevents admin adjustments from being lost when cache is later initialized.
    script = <<~LUA
      local key = KEYS[1]
      local increment = tonumber(ARGV[1])
      local expiry = tonumber(ARGV[2])
      local current = tonumber(redis.call('GET', key) or 0)
      local new_value = current + increment
      if new_value < 0 then
        new_value = 0
      end
      redis.call('SET', key, new_value, 'EX', expiry)
      return new_value
    LUA

    redis.eval(script, keys: [redis_key], argv: [quantity, redis_expired_in])
  end
end

#price_in(currency) ⇒ Object



45
46
47
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 45

def price_in(currency)
  prices.detect { |price| price.currency == currency } || prices.build(currency: currency)
end

#public_quantity_availableObject



41
42
43
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 41

def public_quantity_available
  [quantity_available, MAX_DISPLAY_STOCK].min
end

#quantity_in_redisObject



79
80
81
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 79

def quantity_in_redis
  SpreeCmCommissioner.inventory_redis_pool.with { |redis| redis.get(redis_key).to_i }
end

#redis_expired_inObject

1 year expiry, whether the inventory item is permanent stock or not.

Why even for permanent stock? Because if we expire it too soon (e.g. right after inventory_date), admin usually still need to look up past/old inventory items or adjust stock when needed.



112
113
114
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 112

def redis_expired_in
  31_536_000
end

#redis_hold_keyObject



75
76
77
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 75

def redis_hold_key
  "inventory:on_hold:#{id}"
end

#redis_keyObject



71
72
73
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 71

def redis_key
  "inventory:#{id}"
end

#schedule_product_cache_invalidationObject



28
29
30
31
32
33
34
35
36
37
38
39
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 28

def schedule_product_cache_invalidation
  return unless saved_change_to_quantity_available?

  # Only invalidate when transitioning between in-stock and out-of-stock:
  # - out-of-stock → in-stock (0 → N): invalidate
  # - in-stock → out-of-stock (N → 0): invalidate
  # - non-zero to non-zero (5 → 3): skip — cached in_stock value is unchanged
  old_val, new_val = saved_change_to_quantity_available
  return if old_val.to_i.positive? && new_val.to_i.positive?

  SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(maintainable: variant.product)
end