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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/blazer/queries_controller_decorator.rb +1 -1
- data/app/controllers/spree/admin/blazer_queries_controller.rb +43 -0
- data/app/controllers/spree/admin/vendor_authorized_users_controller.rb +15 -0
- data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +19 -4
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +3 -2
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert_job.rb +3 -2
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +3 -2
- data/app/models/blazer/query_decorator.rb +9 -0
- data/app/models/concerns/spree_cm_commissioner/order_channel_bitwise.rb +70 -0
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +21 -6
- data/app/models/concerns/spree_cm_commissioner/ticket_transferable.rb +2 -2
- data/app/models/concerns/spree_cm_commissioner/vendor_preference.rb +19 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +22 -2
- data/app/models/spree_cm_commissioner/order_decorator.rb +1 -13
- data/app/overrides/spree/admin/shared/sub_menu/_configuration/blazer_queries_tab.html.erb.deface +3 -0
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +6 -1
- data/app/services/spree_cm_commissioner/intercity_taxi_order/create.rb +4 -2
- data/app/services/spree_cm_commissioner/inventory_holds/acquire.rb +106 -40
- data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +76 -9
- data/app/services/spree_cm_commissioner/inventory_holds/release.rb +48 -6
- data/app/services/spree_cm_commissioner/inventory_items/adjust_available_quantity.rb +4 -3
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities.rb +6 -4
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_by_variant.rb +11 -1
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert.rb +6 -4
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold.rb +6 -3
- data/app/services/spree_cm_commissioner/inventory_items/move_to_locked.rb +57 -12
- data/app/services/spree_cm_commissioner/redis_stock/base.rb +3 -2
- data/app/services/spree_cm_commissioner/redis_stock/restock.rb +35 -3
- data/app/services/spree_cm_commissioner/redis_stock/unstock.rb +35 -3
- data/app/services/spree_cm_commissioner/show/clone.rb +145 -0
- data/app/services/spree_cm_commissioner/ticket_transfers/accept.rb +1 -1
- data/app/services/spree_cm_commissioner/transit_order/create.rb +10 -4
- data/app/services/spree_cm_commissioner/waiting_room/stamp_queue_positions.rb +10 -25
- data/app/views/blazer/queries/_content.html.erb +0 -2
- data/app/views/spree/admin/blazer_queries/_form.html.erb +37 -0
- data/app/views/spree/admin/blazer_queries/edit.html.erb +11 -0
- data/app/views/spree/admin/blazer_queries/index.html.erb +72 -0
- data/app/views/spree/admin/blazer_queries/new.html.erb +11 -0
- data/app/views/spree/admin/vendor_authorized_users/index.html.erb +15 -0
- data/config/locales/en.yml +7 -0
- data/config/locales/km.yml +2 -0
- data/config/routes.rb +1 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +13 -5
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f4e6fad42b4e85ffc7259488c4594acea3dd6949dfbbd5dca113cff65740f7f
|
|
4
|
+
data.tar.gz: 81a670727506c0b79c5b18789c0b88bd042cc7a5b94318d49616251389b88129
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16e72d5ccf30ad000ba4377824ce84a8245104f55835a8e298ca5d93117fdaf06f0a1f6adeef544dae996e5cf527980669129f6230068f7eb6a445d021dcd2b2
|
|
7
|
+
data.tar.gz: 1688cbabe227af883abc06175772c009ecf138433ff180a197f668010db60298c49ac113eb33027c9c63bd14a2d7b80e403ab246961b8c3af94381d40763b668
|
data/Gemfile.lock
CHANGED
|
@@ -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
|
-
|
|
171
|
-
|
|
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
|
data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert_job.rb
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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::
|
|
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::
|
|
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
|
|
@@ -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' => [
|
|
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,
|