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)


107
108
109
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 107

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

#adjust_quantity!(quantity) ⇒ Object

This method is only used when admin update stock



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

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



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

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



48
49
50
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 48

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

#public_quantity_availableObject



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

def public_quantity_available
  [quantity_available, MAX_DISPLAY_STOCK].min
end

#quantity_in_redisObject



82
83
84
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 82

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.



115
116
117
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 115

def redis_expired_in
  31_536_000
end

#redis_hold_keyObject



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

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

#redis_keyObject



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

def redis_key
  "inventory:#{id}"
end

#schedule_product_cache_invalidationObject

Only for ecommerce: the app uses product.in_stock? (derived from quantity_available) for product cards. Other types (e.g. accommodation, bus) have many inventory items (including daily-generated ones), so invalidating per item would flood the task queue.

This is temporary until we have a more robust cache invalidation strategy in place.



33
34
35
36
37
38
39
40
41
42
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 33

def schedule_product_cache_invalidation
  return unless ecommerce?
  return unless saved_change_to_quantity_available?

  # Only fires on in_stock status transition (0 ↔ N); non-zero to non-zero changes are skipped.
  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