spree_cm_commissioner 2.12.6 → 2.12.7.pre.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/blazer/queries_controller_decorator.rb +1 -1
  4. data/app/controllers/spree/admin/blazer_queries_controller.rb +43 -0
  5. data/app/controllers/spree/admin/vendor_authorized_users_controller.rb +15 -0
  6. data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +19 -4
  7. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +3 -2
  8. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert_job.rb +3 -2
  9. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +3 -2
  10. data/app/models/blazer/query_decorator.rb +9 -0
  11. data/app/models/concerns/spree_cm_commissioner/order_channel_bitwise.rb +70 -0
  12. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +21 -6
  13. data/app/models/concerns/spree_cm_commissioner/ticket_transferable.rb +2 -2
  14. data/app/models/concerns/spree_cm_commissioner/vendor_preference.rb +19 -0
  15. data/app/models/spree_cm_commissioner/inventory_item.rb +22 -2
  16. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -13
  17. data/app/overrides/spree/admin/shared/sub_menu/_configuration/blazer_queries_tab.html.erb.deface +3 -0
  18. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +6 -1
  19. data/app/services/spree_cm_commissioner/intercity_taxi_order/create.rb +4 -2
  20. data/app/services/spree_cm_commissioner/inventory_holds/acquire.rb +106 -40
  21. data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +76 -9
  22. data/app/services/spree_cm_commissioner/inventory_holds/release.rb +48 -6
  23. data/app/services/spree_cm_commissioner/inventory_items/adjust_available_quantity.rb +4 -3
  24. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities.rb +6 -4
  25. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_by_variant.rb +11 -1
  26. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert.rb +6 -4
  27. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold.rb +6 -3
  28. data/app/services/spree_cm_commissioner/inventory_items/move_to_locked.rb +57 -12
  29. data/app/services/spree_cm_commissioner/redis_stock/base.rb +3 -2
  30. data/app/services/spree_cm_commissioner/redis_stock/restock.rb +35 -3
  31. data/app/services/spree_cm_commissioner/redis_stock/unstock.rb +35 -3
  32. data/app/services/spree_cm_commissioner/show/clone.rb +145 -0
  33. data/app/services/spree_cm_commissioner/ticket_transfers/accept.rb +1 -1
  34. data/app/services/spree_cm_commissioner/transit_order/create.rb +10 -4
  35. data/app/services/spree_cm_commissioner/waiting_room/stamp_queue_positions.rb +10 -25
  36. data/app/views/blazer/queries/_content.html.erb +0 -2
  37. data/app/views/spree/admin/blazer_queries/_form.html.erb +37 -0
  38. data/app/views/spree/admin/blazer_queries/edit.html.erb +11 -0
  39. data/app/views/spree/admin/blazer_queries/index.html.erb +72 -0
  40. data/app/views/spree/admin/blazer_queries/new.html.erb +11 -0
  41. data/app/views/spree/admin/vendor_authorized_users/index.html.erb +15 -0
  42. data/config/locales/en.yml +7 -0
  43. data/config/locales/km.yml +2 -0
  44. data/config/routes.rb +1 -0
  45. data/lib/spree_cm_commissioner/version.rb +1 -1
  46. metadata +13 -5
  47. data/app/views/blazer/queries/_preset.html.erb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed84dcf2382e8d3720366acd62551189adfa4edba9533d8ca5e94b0704942836
4
- data.tar.gz: aa37bf6b43593f7f7ff91d71d1839b1ceeeb8c9271d5db371451f94958399366
3
+ metadata.gz: 4f4e6fad42b4e85ffc7259488c4594acea3dd6949dfbbd5dca113cff65740f7f
4
+ data.tar.gz: 81a670727506c0b79c5b18789c0b88bd042cc7a5b94318d49616251389b88129
5
5
  SHA512:
6
- metadata.gz: a5b89052a82aedeed1144cf9a5892e3d8ec2428c3a4cd26cc5806010c1bbcc3ad353a768e6854c3fdaf69a2ae2fdb5f6f049616b7cac1b2a9d3cd532344708d3
7
- data.tar.gz: 7842ce22211f5e072e308efda5f4f5a0b95625729d18388ec6c3c22406fd76b4e5577daca6d5a9f717fd28990639d3897b3a0d58c1d18bebd2c1959f152b7b80
6
+ metadata.gz: 16e72d5ccf30ad000ba4377824ce84a8245104f55835a8e298ca5d93117fdaf06f0a1f6adeef544dae996e5cf527980669129f6230068f7eb6a445d021dcd2b2
7
+ data.tar.gz: 1688cbabe227af883abc06175772c009ecf138433ff180a197f668010db60298c49ac113eb33027c9c63bd14a2d7b80e403ab246961b8c3af94381d40763b668
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.12.6)
37
+ spree_cm_commissioner (2.12.7.pre.pre1)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -1,7 +1,7 @@
1
1
  module Blazer
2
2
  module QueriesControllerDecorator
3
3
  def query_params
4
- params.require(:query).permit(:name, :description, :statement, :data_source, :preset)
4
+ params.require(:query).permit(:name, :description, :statement, :data_source)
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,43 @@
1
+ module Spree
2
+ module Admin
3
+ class BlazerQueriesController < Spree::Admin::ResourceController
4
+ STATEMENT_PLACEHOLDER = '-- write your SQL for this query in Blazer'.freeze
5
+
6
+ def index
7
+ @search = model_class.ransack(params[:q])
8
+ @collection = @search.result
9
+ .order(created_at: :desc)
10
+ .page(params[:page])
11
+ .per(25)
12
+ end
13
+
14
+ private
15
+
16
+ def model_class
17
+ Blazer::Query
18
+ end
19
+
20
+ def object_name
21
+ 'blazer_query'
22
+ end
23
+
24
+ def collection_url
25
+ admin_blazer_queries_url
26
+ end
27
+
28
+ def location_after_save
29
+ return blazer.edit_query_path(@object) if action_name == 'create'
30
+
31
+ edit_admin_blazer_query_path(@object)
32
+ end
33
+
34
+ def build_resource
35
+ model_class.new(statement: STATEMENT_PLACEHOLDER, data_source: Blazer.data_sources.keys.first)
36
+ end
37
+
38
+ def permitted_resource_params
39
+ params.require(:blazer_query).permit(:name, :description, :data_source, :preset)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -14,6 +14,8 @@ module Spree
14
14
  end
15
15
 
16
16
  def update_telegram_chat
17
+ @vendor.update(preferred_telegram_alert_channels: submitted_alert_channels_mask)
18
+
17
19
  context = ::SpreeCmCommissioner::TelegramChatsFinder.call(
18
20
  telegram_chat_name: params['vendor']['preferred_telegram_chat_name'],
19
21
  telegram_chat_type: params['vendor']['preferred_telegram_chat_type']
@@ -34,6 +36,19 @@ module Spree
34
36
  redirect_to collection_url(@vendor)
35
37
  end
36
38
 
39
+ # OR the bits, never sum them. Sum is not idempotent: if the same bit arrives twice
40
+ # (duplicated checkbox, double-submit, hand-crafted post) 1 + 1 = 2, which silently means
41
+ # google_form instead of spree. ["0","1","1","16"].sum => 18 => [google_form, offline_sell];
42
+ # the same input OR'd => 17 => [spree, offline_sell].
43
+ #
44
+ # Masking against the known bits also drops junk from a crafted post (999 => 7).
45
+ def submitted_alert_channels_mask
46
+ known = SpreeCmCommissioner::OrderChannelBitwise::CHANNEL_ALERT_BITS.values.reduce(:|)
47
+
48
+ Array(params.dig(:vendor, :telegram_alert_channels))
49
+ .reduce(0) { |acc, value| acc | value.to_i } & known
50
+ end
51
+
37
52
  # @overrided
38
53
  def model_class
39
54
  Spree::User
@@ -22,7 +22,7 @@
22
22
  # Delivery Address: #123, St. 456, Phnom Penh
23
23
 
24
24
  module SpreeCmCommissioner
25
- class OrderTelegramMessageFactory < TelegramMessageFactory
25
+ class OrderTelegramMessageFactory < TelegramMessageFactory # rubocop:disable Metrics/ClassLength
26
26
  attr_reader :order, :vendor, :show_details_link
27
27
 
28
28
  def initialize(title:, order:, subtitle: nil, show_details_link: nil, vendor: nil)
@@ -57,10 +57,20 @@ module SpreeCmCommissioner
57
57
  text << italic("👉 #{line_item.options_text}") if line_item.options_text.present?
58
58
  text << italic(pretty_date_for(line_item)) if pretty_date_for(line_item).present?
59
59
  text << italic("🏪 #{line_item.vendor.name}") if line_item.vendor&.name.present? && vendor.blank?
60
+ text << seat_labels_text(line_item) if seat_labels_text(line_item).present?
60
61
 
61
62
  text.compact.join("\n")
62
63
  end
63
64
 
65
+ def seat_labels_text(line_item)
66
+ return nil unless line_item.required_seat_selection?
67
+
68
+ labels = line_item.guests_with_blocks.includes(:block).map { |guest| guest.block&.label }.compact
69
+ return nil if labels.empty?
70
+
71
+ "Seat: #{labels.join(', ')}"
72
+ end
73
+
64
74
  def pretty_date_for(line_item)
65
75
  return nil unless line_item.date_present?
66
76
 
@@ -150,11 +160,14 @@ module SpreeCmCommissioner
150
160
  def transit_line_item_content(line_item)
151
161
  text = []
152
162
  route_text = transit_route_text(line_item)
163
+ vehicle_text = transit_vehicle_text(line_item)
153
164
  departure_text = transit_departure_text(line_item)
154
165
  text << bold('🚗 Trip Details')
155
166
  text << "Route: #{route_text}" if route_text.present?
167
+ text << "Vehicle: #{vehicle_text}" if vehicle_text.present?
156
168
  text << "Passengers: #{line_item.passenger_count || line_item.quantity}"
157
169
  text << "Departure: #{departure_text}" if departure_text.present?
170
+ text << seat_labels_text(line_item) if seat_labels_text(line_item).present?
158
171
 
159
172
  text.compact.join("\n")
160
173
  end
@@ -165,10 +178,12 @@ module SpreeCmCommissioner
165
178
 
166
179
  origin = trip.origin_place&.name
167
180
  destination = trip.destination_place&.name
168
- vehicle = trip.vehicle_type&.name
169
181
 
170
- parts = [origin, destination].compact_blank.join(' → ')
171
- vehicle.present? ? "#{parts} (#{vehicle})" : parts
182
+ [origin, destination].compact_blank.join(' → ')
183
+ end
184
+
185
+ def transit_vehicle_text(line_item)
186
+ trip_for(line_item)&.vehicle_type&.name
172
187
  end
173
188
 
174
189
  def transit_departure_text(line_item)
@@ -23,13 +23,14 @@ module SpreeCmCommissioner
23
23
  "line_item:#{options[:line_item_ids].compact.uniq.sort.join(':')}"
24
24
  end
25
25
 
26
- # :line_item_ids, :inventory_id_and_quantities, :caller_source
26
+ # :line_item_ids, :inventory_id_and_quantities, :caller_source, :correlation_id
27
27
  #
28
28
  # :caller_source is a string like "ClassName#method" identifying who enqueued the job.
29
29
  def bulk_adjust_quantities!(options = {})
30
30
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantities.call!(
31
31
  inventory_id_and_quantities: options[:inventory_id_and_quantities],
32
- caller_source: options[:caller_source]
32
+ caller_source: options[:caller_source],
33
+ correlation_id: options[:correlation_id]
33
34
  )
34
35
  end
35
36
  end
@@ -32,14 +32,15 @@ module SpreeCmCommissioner
32
32
  .join(',')
33
33
  end
34
34
 
35
- # :order_id, :inventory_id_and_quantities, :caller_source
35
+ # :order_id, :inventory_id_and_quantities, :caller_source, :correlation_id
36
36
  def bulk_adjust_quantities_on_convert(options = {})
37
37
  raise ArgumentError, 'order_id is required' if options[:order_id].blank?
38
38
  raise ArgumentError, 'inventory_id_and_quantities is required' if options[:inventory_id_and_quantities].blank?
39
39
 
40
40
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnConvert.call(
41
41
  inventory_id_and_quantities: options[:inventory_id_and_quantities],
42
- caller_source: options[:caller_source]
42
+ caller_source: options[:caller_source],
43
+ correlation_id: options[:correlation_id]
43
44
  )
44
45
  end
45
46
  end
@@ -38,7 +38,7 @@ module SpreeCmCommissioner
38
38
  .join(',')
39
39
  end
40
40
 
41
- # :order_id, :inventory_id_and_quantities, :caller_source
41
+ # :order_id, :inventory_id_and_quantities, :caller_source, :correlation_id
42
42
  #
43
43
  # :order_id is used for unique idempotency key generation to prevent duplicate
44
44
  # processing, though it's not used in the adjustment itself.
@@ -49,7 +49,8 @@ module SpreeCmCommissioner
49
49
 
50
50
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHold.call(
51
51
  inventory_id_and_quantities: options[:inventory_id_and_quantities],
52
- caller_source: options[:caller_source]
52
+ caller_source: options[:caller_source],
53
+ correlation_id: options[:correlation_id]
53
54
  )
54
55
  end
55
56
  end
@@ -0,0 +1,9 @@
1
+ module Blazer
2
+ module QueryDecorator
3
+ def ransackable_attributes(auth_object = nil)
4
+ super & %w[name description data_source preset created_at updated_at]
5
+ end
6
+ end
7
+ end
8
+
9
+ Blazer::Query.singleton_class.prepend(Blazer::QueryDecorator) unless Blazer::Query.singleton_class.ancestors.include?(Blazer::QueryDecorator)
@@ -0,0 +1,70 @@
1
+ # Owns everything about an order's `channel`: the allowed values, how a stored value
2
+ # resolves back to one of them, and the bitmask vendors use to choose which channels
3
+ # may alert their Telegram group.
4
+ #
5
+ # `channel` is validated by PREFIX, not equality, because the CSV importer stores
6
+ # "google_form-<import_order_id>-<slug>". Always compare via #channel_key, never `==`.
7
+ module SpreeCmCommissioner
8
+ module OrderChannelBitwise
9
+ extend ActiveSupport::Concern
10
+
11
+ SPREE_CHANNEL = 'spree'.freeze
12
+ GOOGLE_FORM_CHANNEL = 'google_form'.freeze
13
+ TELEGRAM_CHANNEL = 'telegram'.freeze
14
+ TICKET_TRANSFER_CHANNEL = 'ticket_transfer'.freeze
15
+ OFFLINE_SELL_CHANNEL = 'offline_sell'.freeze
16
+
17
+ ALLOWED_CHANNEL_PREFIXES = [
18
+ SPREE_CHANNEL, GOOGLE_FORM_CHANNEL, TELEGRAM_CHANNEL, TICKET_TRANSFER_CHANNEL, OFFLINE_SELL_CHANNEL
19
+ ].freeze
20
+
21
+ # Matched longest-first so a future 'spree_x' can never be swallowed by 'spree'.
22
+ SORTED_CHANNEL_PREFIXES = ALLOWED_CHANNEL_PREFIXES.sort_by { |prefix| -prefix.length }.freeze
23
+
24
+ # Bit values are PERSISTED in vendor preferences and are a permanent contract.
25
+ # Never renumber, reorder, or reuse a bit. To retire a channel, leave its bit
26
+ # burned and take the next free power of two.
27
+ #
28
+ # Deliberately literal, never derived from ALLOWED_CHANNEL_PREFIXES.index —
29
+ # reordering that array would silently remap every vendor's saved setting.
30
+ CHANNEL_ALERT_BITS = {
31
+ SPREE_CHANNEL => 1, # 1 << 0
32
+ GOOGLE_FORM_CHANNEL => 2, # 1 << 1
33
+ TELEGRAM_CHANNEL => 4, # 1 << 2
34
+ TICKET_TRANSFER_CHANNEL => 8, # 1 << 3
35
+ OFFLINE_SELL_CHANNEL => 16 # 1 << 4
36
+ }.freeze
37
+
38
+ # Every bit currently in use, for masking untrusted input down to known channels.
39
+ KNOWN_CHANNEL_ALERT_BITS = CHANNEL_ALERT_BITS.values.reduce(:|).freeze
40
+
41
+ # Only online orders alert by default. google_form & ticket_transfer set the state column
42
+ # directly instead of firing the state machine, so they have never sent an alert; this
43
+ # default therefore encodes today's real behaviour rather than changing it.
44
+ DEFAULT_TELEGRAM_ALERT_CHANNELS = CHANNEL_ALERT_BITS[SPREE_CHANNEL]
45
+
46
+ included do
47
+ validate :validate_channel_prefix, if: :channel_changed?
48
+ end
49
+
50
+ # The stored channel may carry a suffix — the importer writes
51
+ # "google_form-<import_order_id>-<slug>" — so the prefix, not the whole
52
+ # string, is the channel's identity. Returns nil for an unrecognized channel.
53
+ def channel_key
54
+ SORTED_CHANNEL_PREFIXES.find { |prefix| channel&.start_with?(prefix) }
55
+ end
56
+
57
+ # Order was created by an operator through the back-office Sell Ticket console.
58
+ def offline_sell?
59
+ channel_key == OFFLINE_SELL_CHANNEL
60
+ end
61
+
62
+ private
63
+
64
+ def validate_channel_prefix
65
+ return if channel_key.present?
66
+
67
+ errors.add(:channel, "must start with one of the following: #{ALLOWED_CHANNEL_PREFIXES.join(', ')}")
68
+ end
69
+ end
70
+ end
@@ -1,7 +1,7 @@
1
1
  # Make sure to put this concern below other concern or methods that generating additional order info like guests, seat number, etc.
2
2
  # This will ensure that when each notification is sent with neccessary data.
3
3
  module SpreeCmCommissioner
4
- module OrderStateMachine
4
+ module OrderStateMachine # rubocop:disable Metrics/ModuleLength
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do # rubocop:disable Metrics/BlockLength
@@ -271,15 +271,30 @@ module SpreeCmCommissioner
271
271
  return unless telegram_alert_enabled?
272
272
 
273
273
  vendor_list.each do |vendor|
274
- title = '🎫 --- [NEW ORDER FROM BOOKME+] ---'
274
+ # `next`, not `return`: the mask is per-vendor, so one opted-out vendor on a
275
+ # multi-vendor order must not suppress the others.
276
+ next unless vendor.alerts_telegram_for_channel?(channel_key)
277
+
275
278
  chat_id = vendor.preferred_telegram_chat_id
276
- if chat_id.present?
277
- factory = OrderTelegramMessageFactory.new(title: title, order: self, vendor: vendor)
278
- TelegramNotificationSenderJob.perform_later(chat_id: chat_id, message: factory.message, parse_mode: factory.parse_mode)
279
- end
279
+ next if chat_id.blank?
280
+
281
+ factory = OrderTelegramMessageFactory.new(title: vendor_alert_title(vendor), order: self, vendor: vendor)
282
+ TelegramNotificationSenderJob.perform_later(chat_id: chat_id, message: factory.message, parse_mode: factory.parse_mode)
280
283
  end
281
284
  end
282
285
 
286
+ # White-label vendors announce their own brand; platform vendors stay BookMe+.
287
+ #
288
+ # vendor.name is operator-supplied data interpolated into an HTML parse_mode message
289
+ # (TelegramMessageFactory#header wraps the title in <b>), so it MUST be escaped —
290
+ # Telegram rejects unescaped &, < and > with "400 can't parse entities", and
291
+ # TelegramNotificationSender swallows that error, so the alert would vanish silently.
292
+ def vendor_alert_title(vendor)
293
+ return '🎫 --- [NEW ORDER FROM BOOKME+] ---' unless vendor.tenant_present?
294
+
295
+ "🎫 --- [NEW ORDER FROM #{CGI.escapeHTML(vendor.name.to_s)}] ---"
296
+ end
297
+
283
298
  def notify_order_complete_app_notification_to_user
284
299
  return if products.all?(&:vote_package?) # disable app push notification for vote package orders
285
300
 
@@ -5,11 +5,11 @@ module SpreeCmCommissioner
5
5
  included do
6
6
  has_one :ticket_transfer, class_name: 'SpreeCmCommissioner::TicketTransfer'
7
7
 
8
- scope :not_ticket_transfer, -> { where.not(channel: SpreeCmCommissioner::OrderDecorator::TICKET_TRANSFER_CHANNEL) }
8
+ scope :not_ticket_transfer, -> { where.not(channel: SpreeCmCommissioner::OrderChannelBitwise::TICKET_TRANSFER_CHANNEL) }
9
9
  end
10
10
 
11
11
  def ticket_transfer?
12
- channel == SpreeCmCommissioner::OrderDecorator::TICKET_TRANSFER_CHANNEL
12
+ channel == SpreeCmCommissioner::OrderChannelBitwise::TICKET_TRANSFER_CHANNEL
13
13
  end
14
14
 
15
15
  # Completes an accepted ticket transfer for this order. Called from
@@ -8,6 +8,25 @@ module SpreeCmCommissioner
8
8
  preference :telegram_chat_id, :string
9
9
  preference :telegram_chat_name, :string
10
10
  preference :telegram_chat_type, :string
11
+ preference :telegram_alert_channels, :integer, default: 1
12
+ end
13
+
14
+ # Should this vendor's Telegram group be alerted for an order on this channel?
15
+ # An unmapped channel alerts — a stray message is a better failure than silence.
16
+ def alerts_telegram_for_channel?(channel_key)
17
+ bit = SpreeCmCommissioner::OrderChannelBitwise::CHANNEL_ALERT_BITS[channel_key]
18
+ return true if bit.nil?
19
+
20
+ telegram_alert_channels_mask.anybits?(bit)
21
+ end
22
+
23
+ # Spree's getter is `preferences.fetch(name) { default }`, which falls back only when the
24
+ # key is ABSENT. A key present with a nil value returns nil, and nil.to_i == 0 would mute
25
+ # EVERY channel silently. Fall back to the default instead; a deliberate 0 survives
26
+ # because 0 is truthy in Ruby.
27
+ def telegram_alert_channels_mask
28
+ (preferred_telegram_alert_channels ||
29
+ SpreeCmCommissioner::OrderChannelBitwise::DEFAULT_TELEGRAM_ALERT_CHANNELS).to_i
11
30
  end
12
31
  end
13
32
  end
@@ -107,7 +107,10 @@ module SpreeCmCommissioner
107
107
  end
108
108
 
109
109
  # This method is only used when admin update stock
110
- def adjust_quantity!(quantity)
110
+ def adjust_quantity!(quantity, correlation_id: nil)
111
+ correlation_id ||= SecureRandom.uuid
112
+ quantity_available_before = redis_quantity_after = nil
113
+
111
114
  with_lock do
112
115
  # IMPORTANT: Apply quantity changes directly without defensive clamping.
113
116
  # The model validation will catch any attempts to go negative, surfacing bugs
@@ -118,6 +121,8 @@ module SpreeCmCommissioner
118
121
  #
119
122
  # Why? Clamping masks bugs. Validation errors are better than silent data loss.
120
123
  # See: /docs/lessons-learned/inventory-consistency-issues.md#lesson-learned-async-job-validation-strategy
124
+ quantity_available_before = quantity_available
125
+
121
126
  self.max_capacity = max_capacity + quantity
122
127
  self.quantity_available = quantity_available + quantity
123
128
  save!
@@ -127,8 +132,23 @@ module SpreeCmCommissioner
127
132
  # high-contention hot path, so keeping it in the same transaction as `save!` means a
128
133
  # Redis failure rolls the DB change back too, instead of leaving the DB and Redis counts
129
134
  # out of sync with nothing to reconcile them.
130
- adjust_quantity_in_redis(quantity)
135
+ redis_quantity_after = adjust_quantity_in_redis(quantity)
131
136
  end
137
+
138
+ # Logged after with_lock releases: I/O here would otherwise extend how long the row's
139
+ # SELECT ... FOR UPDATE is held under high-traffic contention — same reasoning as the other
140
+ # Bulk* adjusters in this gem.
141
+ CmAppLogger.log(
142
+ label: "#{self.class.name}#adjust_quantity!",
143
+ data: {
144
+ correlation_id: correlation_id,
145
+ inventory_item_id: id,
146
+ delta: quantity,
147
+ quantity_available_before: quantity_available_before,
148
+ quantity_available_after: quantity_available,
149
+ redis_quantity_after: redis_quantity_after
150
+ }
151
+ )
132
152
  end
133
153
 
134
154
  # Stock and hold keys. `locked: true` addresses the withheld pool — see LineItem#locked_stock?,
@@ -1,12 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  module OrderDecorator
3
- SPREE_CHANNEL = 'spree'.freeze
4
- GOOGLE_FORM_CHANNEL = 'google_form'.freeze
5
- TELEGRAM_CHANNEL = 'telegram'.freeze
6
- TICKET_TRANSFER_CHANNEL = 'ticket_transfer'.freeze
7
- ALLOWED_CHANNEL_PREFIXES = [SPREE_CHANNEL, GOOGLE_FORM_CHANNEL, TELEGRAM_CHANNEL, TICKET_TRANSFER_CHANNEL].freeze
8
-
9
3
  def self.prepended(base) # rubocop:disable Metrics/MethodLength
4
+ base.include SpreeCmCommissioner::OrderChannelBitwise
10
5
  base.include SpreeCmCommissioner::StoreMetadata
11
6
  base.include SpreeCmCommissioner::PhoneNumberSanitizer
12
7
  base.include SpreeCmCommissioner::OrderIntegration
@@ -27,7 +22,6 @@ module SpreeCmCommissioner
27
22
  base.store_public_metadata :order_options, :array, default: []
28
23
 
29
24
  base.validates :promo_total, base::MONEY_VALIDATION
30
- base.validate :validate_channel_prefix, if: :channel_changed?
31
25
  base.validates :phone_number, presence: true, if: :require_phone_number
32
26
 
33
27
  base.has_one :invoice, dependent: :destroy, class_name: 'SpreeCmCommissioner::Invoice'
@@ -363,12 +357,6 @@ module SpreeCmCommissioner
363
357
  self.ship_address ||= customer.ship_address.try(:clone)
364
358
  end
365
359
 
366
- def validate_channel_prefix
367
- return if ALLOWED_CHANNEL_PREFIXES.any? { |prefix| channel&.start_with?(prefix) }
368
-
369
- errors.add(:channel, "must start with one of the following: #{ALLOWED_CHANNEL_PREFIXES.join(', ')}")
370
- end
371
-
372
360
  def set_tenant_id
373
361
  self.tenant_id = MultiTenant.current_tenant_id
374
362
  end
@@ -0,0 +1,3 @@
1
+ <!-- insert_bottom "[data-hook='admin_configurations_sidebar_menu']" -->
2
+
3
+ <%= tab :blazer_queries, match_path: '/blazer_queries', label: 'Blazer Queries' if can? :manage, Blazer::Query %>
@@ -70,7 +70,12 @@ module SpreeCmCommissioner
70
70
  '/api/v2/storefront/menus/:id*',
71
71
  '/api/v2/storefront/homepage_data*'
72
72
  ],
73
- 'Spree::Taxon' => ['/api/v2/storefront/taxons/:id*'],
73
+ 'Spree::Taxon' => [
74
+ '/api/v2/tenant/taxons/:id*',
75
+ '/api/v2/tenant/taxons/:permalink*',
76
+ '/api/v2/storefront/taxons/:id*',
77
+ '/api/v2/storefront/taxons/:permalink*'
78
+ ],
74
79
  'Spree::Product' => [
75
80
  '/api/v2/tenant/products/:id*',
76
81
  '/api/v2/tenant/products/:slug*',
@@ -3,7 +3,9 @@ module SpreeCmCommissioner
3
3
  class Create
4
4
  prepend ::Spree::ServiceModule::Base
5
5
 
6
- def call(trip_id:, from_date:, to_date:, user: nil, quantity: 1)
6
+ def call(trip_id:, from_date:, to_date:, user: nil, quantity: 1, created_by: nil, # rubocop:disable Metrics/ParameterLists
7
+ channel: SpreeCmCommissioner::OrderChannelBitwise::SPREE_CHANNEL
8
+ )
7
9
  validate!(trip_id: trip_id, from_date: from_date, to_date: to_date, quantity: quantity)
8
10
 
9
11
  quantity = quantity.to_i
@@ -13,7 +15,7 @@ module SpreeCmCommissioner
13
15
  variant = trip.product.variants.first
14
16
  raise ArgumentError, 'No variant found for trip' if variant.blank?
15
17
 
16
- order = Spree::Order.new(state: 'cart', use_billing: true, user: user)
18
+ order = Spree::Order.new(state: 'cart', use_billing: true, user: user, created_by: created_by, channel: channel)
17
19
 
18
20
  metadata = {
19
21
  trip_id: trip.id,