Class: SpreeCmCommissioner::InventoryItem
- 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
- #active? ⇒ Boolean
-
#adjust_quantity!(quantity) ⇒ Object
This method is only used when admin update stock.
- #adjust_quantity_in_redis(quantity) ⇒ Object
- #price_in(currency) ⇒ Object
- #public_quantity_available ⇒ Object
- #quantity_in_redis ⇒ Object
-
#redis_expired_in ⇒ Object
1 year expiry, whether the inventory item is permanent stock or not.
- #redis_hold_key ⇒ Object
- #redis_key ⇒ Object
-
#schedule_product_cache_invalidation ⇒ Object
Only for ecommerce: the app uses product.in_stock? (derived from quantity_available) for product cards.
Methods included from ProductType
#permanent_stock?, #pre_inventory_days
Instance Method Details
#active? ⇒ 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_available ⇒ Object
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_redis ⇒ Object
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_in ⇒ Object
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_key ⇒ Object
78 79 80 |
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 78 def redis_hold_key "inventory:on_hold:#{id}" end |
#redis_key ⇒ Object
74 75 76 |
# File 'app/models/spree_cm_commissioner/inventory_item.rb', line 74 def redis_key "inventory:#{id}" end |
#schedule_product_cache_invalidation ⇒ Object
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 |