Class: Item::InventoryCommitter
- Inherits:
-
Object
- Object
- Item::InventoryCommitter
- Defined in:
- app/services/item/inventory_committer.rb
Constant Summary collapse
- UNCOMMIT_BATCH_SIZE =
100
Instance Attribute Summary collapse
-
#item ⇒ Object
readonly
Returns the value of attribute item.
-
#line_item ⇒ Object
readonly
Returns the value of attribute line_item.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#options ⇒ Object
readonly
Returns the value of attribute options.
-
#store_item ⇒ Object
readonly
Returns the value of attribute store_item.
Class Method Summary collapse
-
.commitable?(line_item) ⇒ Boolean
Checks if a line item is commitable or not.
- .consolidate_commit_counts(store_item_ids: nil, logger: Rails.logger) ⇒ Object
-
.crm_commit(line_items, creator_id: CurrentScope.user_id, expires_on: nil) ⇒ Object
Commit multiple lines and reconcile in one go Wrapped in retry logic to handle occasional deadlocks when multiple shipments process concurrently and update the same store_items.
-
.crm_uncommit(line_items, creator_id: CurrentScope.user_id) ⇒ Object
Uncommit multiple lines in one go with consolidation.
- .kit_commit(line_item:, expires_on:, creator_id:, deficit: false) ⇒ Object
- .line_item_commit(line_item:, store_item:, quantity:, expires_on: nil, creator_id: nil, kit_line_item: nil, deficit: false) ⇒ Object
- .line_item_uncommit(line_item:, store_item:) ⇒ Object
- .uncommit_expired ⇒ Object
- .uncommit_invoiced ⇒ Object
Instance Method Summary collapse
- #crm_commit ⇒ Object
- #crm_uncommit ⇒ Object
-
#initialize(line_item, options = {}) ⇒ InventoryCommitter
constructor
A new instance of InventoryCommitter.
Constructor Details
#initialize(line_item, options = {}) ⇒ InventoryCommitter
Returns a new instance of InventoryCommitter.
118 119 120 121 122 123 124 |
# File 'app/services/item/inventory_committer.rb', line 118 def initialize(line_item, = {}) @line_item = line_item @store_item = line_item.store_item @item = store_item.item @logger = [:logger] || Rails.logger @options = end |
Instance Attribute Details
#item ⇒ Object (readonly)
Returns the value of attribute item.
2 3 4 |
# File 'app/services/item/inventory_committer.rb', line 2 def item @item end |
#line_item ⇒ Object (readonly)
Returns the value of attribute line_item.
2 3 4 |
# File 'app/services/item/inventory_committer.rb', line 2 def line_item @line_item end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
2 3 4 |
# File 'app/services/item/inventory_committer.rb', line 2 def logger @logger end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
2 3 4 |
# File 'app/services/item/inventory_committer.rb', line 2 def @options end |
#store_item ⇒ Object (readonly)
Returns the value of attribute store_item.
2 3 4 |
# File 'app/services/item/inventory_committer.rb', line 2 def store_item @store_item end |
Class Method Details
.commitable?(line_item) ⇒ Boolean
Checks if a line item is commitable or not
5 6 7 8 9 10 11 12 |
# File 'app/services/item/inventory_committer.rb', line 5 def self.commitable?(line_item) line_item.is_goods? && # Only solids can be committed line_item.catalog_item && # Only those with catalog item line_item.quantity.positive? && # You don't commit unless quantities are positive ( !line_item.dropship? || # Drop ship items are not committed, unless... (line_item.dropship? && line_item.resource.single_origin?)) # ..It's a dropship but you fufill from our warehouse end |
.consolidate_commit_counts(store_item_ids: nil, logger: Rails.logger) ⇒ Object
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'app/services/item/inventory_committer.rb', line 55 def self.consolidate_commit_counts(store_item_ids: nil, logger: Rails.logger) logger.info('Beginning item reconcilliation process') store_item_ids ||= StoreItem.where(is_discontinued: false).pluck(:id) store_item_ids.each_slice(50) do |store_item_ids_batch| # Sometimes active record lock Retryable.retryable(tries: 3, sleep: lambda { |n| 2**n }, on: [ActiveRecord::Deadlocked]) do |attempt_number, exception| msg = "consolidate_commit_counts: attempt #{attempt_number}, exception was #{exception}" logger.warn msg ErrorReporting.warning(msg) if attempt_number > 1 store_items = StoreItem.where(id: store_item_ids_batch) store_items.lock('FOR UPDATE') store_items.each do |si| # Optionally if you don't want to log qty committed you can wrap inside PaperTrail.request(enabled: false) do qty_committed = InventoryCommit.where(store_item_id: si.id).sum(:quantity) si.update_columns(qty_committed:) end # update_all('qty_committed = coalesce((select sum(quantity) from inventory_commits where store_item_id = store_items.id),0)') store_items.includes(:item).where(items: { is_kit: true }).order(:item_id, :store_id).each do |si| Item::KitConsolidator.new(si.item, store_id: si.store_id).consolidate_committed_qty.commit end end end end |
.crm_commit(line_items, creator_id: CurrentScope.user_id, expires_on: nil) ⇒ Object
Commit multiple lines and reconcile in one go
Wrapped in retry logic to handle occasional deadlocks when multiple
shipments process concurrently and update the same store_items
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'app/services/item/inventory_committer.rb', line 17 def self.crm_commit(line_items, creator_id: CurrentScope.user_id, expires_on: nil) Retryable.retryable(tries: 3, sleep: ->(n) { 2**n }, on: [ActiveRecord::Deadlocked]) do |attempt_number, exception| Rails.logger.warn("crm_commit: attempt #{attempt_number}, exception: #{exception}") if attempt_number > 1 InventoryCommit.transaction(requires_new: true) do store_item_ids = [] # Order by store_item_id to ensure consistent lock ordering and reduce deadlocks commitable_items = line_items.select { |li| commitable?(li) } commitable_items.sort_by { |li| li.catalog_item.store_item_id }.each do |li| store_item_ids << li.catalog_item.store_item_id if new(li, creator_id:, expires_on:).crm_commit end # Now consolidate at once store_item_ids.uniq! consolidate_commit_counts(store_item_ids:) if store_item_ids.present? end end end |
.crm_uncommit(line_items, creator_id: CurrentScope.user_id) ⇒ Object
Uncommit multiple lines in one go with consolidation.
Unlike crm_commit, this does NOT filter by commitable? — any existing
commit must be releasable regardless of the line item's current state
(catalog_item removed, quantity zeroed, etc.).
Wrapped in retry logic to handle occasional deadlocks.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'app/services/item/inventory_committer.rb', line 39 def self.crm_uncommit(line_items, creator_id: CurrentScope.user_id) Retryable.retryable(tries: 3, sleep: ->(n) { 2**n }, on: [ActiveRecord::Deadlocked]) do |attempt_number, exception| Rails.logger.warn("crm_uncommit: attempt #{attempt_number}, exception: #{exception}") if attempt_number > 1 InventoryCommit.transaction(requires_new: true) do store_item_ids = [] line_item_ids = line_items.map(&:id) commits = InventoryCommit.where( 'line_item_id IN (:ids) OR kit_line_item_id IN (:ids)', ids: line_item_ids ) store_item_ids = commits.pluck(:store_item_id).uniq commits.destroy_all consolidate_commit_counts(store_item_ids:) if store_item_ids.present? end end end |
.kit_commit(line_item:, expires_on:, creator_id:, deficit: false) ⇒ Object
158 159 160 161 162 163 164 165 |
# File 'app/services/item/inventory_committer.rb', line 158 def self.kit_commit(line_item:, expires_on:, creator_id:, deficit: false) line_item.children.each do |child| raise "store item is missing for component #{child.item.sku}" unless child.store_item line_item_commit(line_item: child, store_item: child.store_item, quantity: child.quantity, expires_on:, creator_id:, kit_line_item: line_item, deficit:) end line_item_commit(line_item:, store_item: line_item.store_item, quantity: line_item.quantity, expires_on:, creator_id:, deficit:) end |
.line_item_commit(line_item:, store_item:, quantity:, expires_on: nil, creator_id: nil, kit_line_item: nil, deficit: false) ⇒ Object
145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'app/services/item/inventory_committer.rb', line 145 def self.line_item_commit(line_item:, store_item:, quantity:, expires_on: nil, creator_id: nil, kit_line_item: nil, deficit: false) ic = InventoryCommit.find_or_initialize_by( store_item:, line_item: ) ic.quantity = quantity ic.expires_on = expires_on ic.kit_line_item = kit_line_item ic.creator_id = creator_id ic.deficit = deficit ic.save! end |
.line_item_uncommit(line_item:, store_item:) ⇒ Object
167 168 169 170 171 |
# File 'app/services/item/inventory_committer.rb', line 167 def self.line_item_uncommit(line_item:, store_item:) InventoryCommit.where('line_item_id = :line_item_id or kit_line_item_id = :line_item_id', { line_item_id: line_item.id }).each do |ic| ic.destroy end end |
.uncommit_expired ⇒ Object
79 80 81 82 83 84 85 86 87 88 89 |
# File 'app/services/item/inventory_committer.rb', line 79 def self.uncommit_expired resources = [] InventoryCommit.where(InventoryCommit[:expires_on].lt(Date.current)).each do |ic| resources << ic.line_item.resource Item::InventoryCommitter.new(ic.line_item).crm_uncommit end resources.uniq.each do |r| # send a notification email to the rep that line item commits have expired InternalMailer.inventory_commit_expiration_notification(r).deliver_later end end |
.uncommit_invoiced ⇒ Object
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'app/services/item/inventory_committer.rb', line 93 def self.uncommit_invoiced # Pass 1: line items assigned to an invoiced delivery InventoryCommit .joins(line_item: :delivery) .where(deliveries: { state: 'invoiced' }) .distinct .pluck(:line_item_id) .each_slice(UNCOMMIT_BATCH_SIZE) do |ids| crm_uncommit(LineItem.where(id: ids)) end # Pass 2: line items never assigned to a delivery on orders in terminal states. # These are invisible to Pass 1 because the INNER JOIN excludes NULL delivery_id. InventoryCommit .joins(:line_item) .joins("INNER JOIN orders ON orders.id = line_items.resource_id AND line_items.resource_type = 'Order'") .where(line_items: { delivery_id: nil }) .where(orders: { state: %w[invoiced shipped cancelled fraudulent] }) .distinct .pluck(:line_item_id) .each_slice(UNCOMMIT_BATCH_SIZE) do |ids| crm_uncommit(LineItem.where(id: ids)) end end |
Instance Method Details
#crm_commit ⇒ Object
126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'app/services/item/inventory_committer.rb', line 126 def crm_commit return false unless self.class.commitable?(@line_item) return false if (@store_item.qty_available + @line_item.inventory_commits.sum(:quantity)) < @line_item.quantity InventoryCommit.transaction(requires_new: true) do if line_item.has_children? self.class.kit_commit(line_item:, expires_on: [:expires_on], creator_id: [:creator_id]) else self.class.line_item_commit(line_item:, store_item:, quantity: line_item.quantity, expires_on: [:expires_on], creator_id: [:creator_id]) end end end |
#crm_uncommit ⇒ Object
139 140 141 142 143 |
# File 'app/services/item/inventory_committer.rb', line 139 def crm_uncommit InventoryCommit.transaction(requires_new: true) do self.class.line_item_uncommit(line_item:, store_item:) end end |