spree_cm_commissioner 2.8.7 → 2.8.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f8d11251736536036263c451dde5cb82e34a6d54b12fe116866a338346d8097
4
- data.tar.gz: f1cec56cfe9db369b4bf70a40c8545e09fd2b89a9d0fd1e2d0547cf4818a04eb
3
+ metadata.gz: ca304c9bd308649d477548d5a5930737a651dc8fa9f1dc55eaff6d60c5b91ba0
4
+ data.tar.gz: e3fc4442166c3a178f759ec32ddc1feb39d7240505623be3d8437a55bffc963d
5
5
  SHA512:
6
- metadata.gz: b0d1ef1dca4921e6493b8455b31c31b9289444aa64b3d5378dc6d87c0676c081ee2c0148dfd3a46fb8572d30fb8fb86d6c53a24249768b3b9001fbb3f2f6a4fa
7
- data.tar.gz: a3dfc479c74f38e3889e84ab72246fc01e31134a630563f4f3bf2b3713e3e13d2df1389a0bdf1aae4337e56d891cb6a6dc29e21570d28b84d9d75a5b1d1cd152
6
+ metadata.gz: f469ec325a78e226309906f0ccf6a1b9d66d3354505919cca451c16dd70c8dec2229c9c44fbe6cf17a8d0109d1a4805c1cd38fb1787ccf0f8d3d0ed174164f64
7
+ data.tar.gz: 78d7c13c8c6107ff36203f79e290f44b3543a2db9fa994772ed08a049da97dac9368102855b0726e1b3417f17a22ec7feb4be0778627dd7dd7057d81f2f54d98
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.8.7)
37
+ spree_cm_commissioner (2.8.8)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -9,6 +9,7 @@ module Spree
9
9
  q['s'] ||= 'created_at desc'
10
10
  q['created_at_gt'] = parse_time_or_nil(q['created_at_gt'], :beginning_of_day) if q['created_at_gt'].present?
11
11
  q['created_at_lt'] = parse_time_or_nil(q['created_at_lt'], :end_of_day) if q['created_at_lt'].present?
12
+ normalize_enum_filters!(q)
12
13
 
13
14
  @search = scope.ransack(q)
14
15
  result_scope = @search.result(distinct: true)
@@ -18,7 +19,11 @@ module Spree
18
19
  .page(params[:page])
19
20
  .per(params[:per_page] || Spree::Backend::Config[:admin_orders_per_page])
20
21
 
21
- @status_counts = result_scope.reorder(nil).group(:status).count
22
+ # Status tab counts must reflect every other filter but NOT the status filter, so
23
+ # each tab shows how many holds it would return (otherwise selecting one status
24
+ # zeroes out all the other tabs).
25
+ @status_counts = scope.ransack(q.except('status_eq')).result(distinct: true)
26
+ .reorder(nil).group(:status).count
22
27
  end
23
28
 
24
29
  def release
@@ -44,6 +49,20 @@ module Spree
44
49
 
45
50
  private
46
51
 
52
+ # `status` and `release_reason` are integer-backed enums. Ransack casts a string
53
+ # value for an integer column with String#to_i, so "payment_locked" becomes 0
54
+ # (== :pending) and every label silently filters to the wrong status. Translate
55
+ # enum labels to their stored integer before searching.
56
+ def normalize_enum_filters!(query)
57
+ {
58
+ 'status_eq' => SpreeCmCommissioner::InventoryHold.statuses,
59
+ 'release_reason_eq' => SpreeCmCommissioner::InventoryHold.release_reasons
60
+ }.each do |key, mapping|
61
+ value = query[key]
62
+ query[key] = mapping[value.to_s] if value.present? && mapping.key?(value.to_s)
63
+ end
64
+ end
65
+
47
66
  def back_params
48
67
  raw_params = params[:back_params].presence || params
49
68
  ActionController::Parameters.new(raw_params).permit(:page, :per_page, q: {})
@@ -30,13 +30,18 @@ module Spree
30
30
  private
31
31
 
32
32
  def collection
33
+ # WARNING: Do NOT remove .order(id: :desc).
34
+ # Mobile relies on this ordering for paginated order_histories.
35
+ # Removing it will cause mobile to paginate by oldest records first (last 10 oldest),
36
+ # breaking the "most recent orders" display on the order history screen.
37
+ # It related to user cancel or complete order flow, which relies on the most recent orders being returned first.
33
38
  if spree_current_user.present?
34
- spree_current_user.orders.not_archived
39
+ spree_current_user.orders.not_archived.order(id: :desc)
35
40
  else
36
41
  order_tokens = Array(params[:order_tokens])
37
42
  return Spree::Order.none if order_tokens.empty?
38
43
 
39
- Spree::Order.not_archived.without_user.where(token: order_tokens)
44
+ Spree::Order.not_archived.without_user.where(token: order_tokens).order(id: :desc)
40
45
  end
41
46
  end
42
47
 
@@ -37,13 +37,18 @@ module Spree
37
37
  end
38
38
 
39
39
  def collection
40
+ # WARNING: Do NOT remove .order(id: :desc).
41
+ # Mobile relies on this ordering for paginated order_histories.
42
+ # Removing it will cause mobile to paginate by oldest records first (last 10 oldest),
43
+ # breaking the "most recent orders" display on the order history screen.
44
+ # It related to user cancel or complete order flow, which relies on the most recent orders being returned first.
40
45
  if spree_current_user.present?
41
- spree_current_user.orders.not_archived
46
+ spree_current_user.orders.not_archived.order(id: :desc)
42
47
  else
43
48
  order_tokens = Array(params[:order_tokens])
44
49
  return Spree::Order.none if order_tokens.empty?
45
50
 
46
- Spree::Order.not_archived.without_user.where(token: order_tokens)
51
+ Spree::Order.not_archived.without_user.where(token: order_tokens).order(id: :desc)
47
52
  end
48
53
  end
49
54
 
@@ -13,25 +13,31 @@ module SpreeCmCommissioner
13
13
  FIRESTORE_BATCH_SIZE = (ENV['WAITING_ROOM_FIRESTORE_BATCH_SIZE'] || 500).to_i
14
14
 
15
15
  def call
16
+ started_at = Time.zone.now
16
17
  available_slots = fetch_available_slots
17
- return unless available_slots.positive?
18
18
 
19
- long_waiting_guests = fetch_long_waiting_guests(available_slots)
20
- calling_all(long_waiting_guests)
19
+ # Always run through to mark_as — even with no slots — so the lobby keeps a fresh heartbeat
20
+ # (logs.waiting_guests_caller_job) during peak/full periods instead of going stale
21
+ # and looking like a dead cron.
22
+ long_waiting_guests = available_slots.positive? ? fetch_long_waiting_guests(available_slots) : []
23
+ calling_all(long_waiting_guests) if long_waiting_guests.any?
21
24
 
22
25
  mark_as(
23
26
  full: long_waiting_guests.size >= available_slots,
24
27
  available_slots: available_slots - long_waiting_guests.size,
25
28
  avg_wait_to_enter_seconds: compute_avg_wait_to_enter_seconds(long_waiting_guests),
26
29
  avg_queue_to_enter_seconds: compute_avg_queue_to_enter_seconds(long_waiting_guests),
27
- slots_per_call: available_slots
30
+ recent_slots_per_call: available_slots.positive? ? available_slots : nil,
31
+ active_sessions: @active_sessions,
32
+ max_sessions: @max_sessions,
33
+ logs: call_logs(started_at: started_at, called_count: long_waiting_guests.size)
28
34
  )
29
35
  end
30
36
 
31
37
  def fetch_available_slots
32
- max_sessions = fetch_max_sessions
33
- active_sessions = SpreeCmCommissioner::WaitingRoomSession.active.count
34
- max_sessions - active_sessions
38
+ @max_sessions = fetch_max_sessions
39
+ @active_sessions = SpreeCmCommissioner::WaitingRoomSession.active.count
40
+ @max_sessions - @active_sessions
35
41
  end
36
42
 
37
43
  # This query requires an index; create it in Firebase beforehand.
@@ -100,21 +106,44 @@ module SpreeCmCommissioner
100
106
  end
101
107
 
102
108
  # merge: true so we preserve the published `waiting_guests_records_path` on the lobby doc.
103
- def mark_as(
109
+ # App-facing fields plus live capacity state (full / available_slots / active_sessions /
110
+ # max_sessions / ETAs) stay top-level; this run's observability snapshot is namespaced under
111
+ # `logs.waiting_guests_caller_job` (see #call_logs). merge: true deep-merges nested maps, so
112
+ # each job owns its own key under `logs` without clobbering the others.
113
+ def mark_as( # rubocop:disable Metrics/ParameterLists
104
114
  full:,
105
115
  available_slots:,
106
116
  avg_wait_to_enter_seconds: nil,
107
117
  avg_queue_to_enter_seconds: nil,
108
- slots_per_call: nil
118
+ recent_slots_per_call: nil,
119
+ active_sessions: nil,
120
+ max_sessions: nil,
121
+ logs: nil
109
122
  )
110
123
  data = { full: full, available_slots: available_slots }
111
124
  data[:avg_wait_to_enter_seconds] = avg_wait_to_enter_seconds if avg_wait_to_enter_seconds
112
125
  data[:avg_queue_to_enter_seconds] = avg_queue_to_enter_seconds if avg_queue_to_enter_seconds
113
- data[:slots_per_call] = slots_per_call if slots_per_call
126
+ data[:recent_slots_per_call] = recent_slots_per_call if recent_slots_per_call
127
+ data[:active_sessions] = active_sessions unless active_sessions.nil?
128
+ data[:max_sessions] = max_sessions unless max_sessions.nil?
129
+ data[:logs] = { waiting_guests_caller_job: logs } if logs
114
130
 
115
131
  lobby_document.set(data, merge: true)
116
132
  end
117
133
 
134
+ # Point-in-time snapshot (overwritten each run, never cumulative) of this caller run: last-run
135
+ # timing (heartbeat) and how many guests were called. Capacity state (active/max sessions,
136
+ # available_slots) lives top-level, not here.
137
+ def call_logs(started_at:, called_count:)
138
+ finished_at = Time.zone.now
139
+ {
140
+ called_count: called_count,
141
+ last_started_at: started_at,
142
+ last_finished_at: finished_at,
143
+ last_duration_ms: ((finished_at - started_at) * 1000).round
144
+ }
145
+ end
146
+
118
147
  # Average total wait (queued_at → allow_to_enter_room_at) for just-called guests.
119
148
  # Used for the Waiting Room step ETA: how long until I get in from when I joined.
120
149
  def compute_avg_wait_to_enter_seconds(guests)
@@ -33,6 +33,7 @@ module Spree
33
33
  @brand_color = @order.tenant.preferred_brand_primary_color
34
34
  end
35
35
  @product_type = @order.products.first&.product_type || 'accommodation'
36
+ @is_email = true
36
37
 
37
38
  subject = build_subject(resend)
38
39
 
@@ -86,6 +87,7 @@ module Spree
86
87
 
87
88
  @current_store = @order.store
88
89
  @product_type = @line_item.product_type
90
+ @is_email = true
89
91
 
90
92
  subject = "#{@current_store&.name} Booking Confirmation ##{@order.number}"
91
93
 
@@ -271,7 +271,18 @@ module SpreeCmCommissioner
271
271
 
272
272
  def send_order_request_telegram_confirmation_alert_to_vendor; end
273
273
 
274
+ # Telegram order alerts are sent unless EVERY purchased product opts out.
275
+ # Default is true (see product_decorator), so existing orders are unaffected.
276
+ # Checked at enqueue time so disabled products create no telegram jobs at all.
277
+ def telegram_alert_enabled?
278
+ return @telegram_alert_enabled if defined?(@telegram_alert_enabled)
279
+
280
+ @telegram_alert_enabled = line_items.any?(&:enable_telegram_alert?)
281
+ end
282
+
274
283
  def send_order_complete_telegram_alert_to_vendors
284
+ return unless telegram_alert_enabled?
285
+
275
286
  vendor_list.each do |vendor|
276
287
  title = '🎫 --- [NEW ORDER FROM BOOKME+] ---'
277
288
  chat_id = vendor.preferred_telegram_chat_id
@@ -287,6 +298,8 @@ module SpreeCmCommissioner
287
298
  end
288
299
 
289
300
  def notify_order_complete_telegram_notification_to_user
301
+ return unless telegram_alert_enabled?
302
+
290
303
  SpreeCmCommissioner::OrderCompleteTelegramSenderJob.perform_later(order_id: id) if user_id.present?
291
304
  end
292
305
 
@@ -303,6 +316,8 @@ module SpreeCmCommissioner
303
316
  end
304
317
 
305
318
  def send_order_requested_telegram_alert_to_store
319
+ return unless telegram_alert_enabled?
320
+
306
321
  title = '🔔 --- [NEW REQUESTED BY USER] ---'
307
322
  chat_id = store.preferred_telegram_order_request_alert_chat_id
308
323
  factory = OrderTelegramMessageFactory.new(title: title, order: self)
@@ -310,6 +325,8 @@ module SpreeCmCommissioner
310
325
  end
311
326
 
312
327
  def send_order_accepted_telegram_alert_to_store
328
+ return unless telegram_alert_enabled?
329
+
313
330
  title = '✅ --- [ORDER ACCEPTED BY VENDOR] ---'
314
331
  chat_id = store.preferred_telegram_order_request_alert_chat_id
315
332
  factory = OrderTelegramMessageFactory.new(title: title, order: self)
@@ -317,6 +334,8 @@ module SpreeCmCommissioner
317
334
  end
318
335
 
319
336
  def send_order_rejected_telegram_alert_to_store
337
+ return unless telegram_alert_enabled?
338
+
320
339
  title = '❌ --- [ORDER REJECTED BY VENDOR] ---'
321
340
  chat_id = store.preferred_telegram_order_request_alert_chat_id
322
341
  factory = OrderTelegramMessageFactory.new(title: title, order: self)
@@ -325,6 +344,7 @@ module SpreeCmCommissioner
325
344
 
326
345
  def run_order_complete_alerts
327
346
  return unless ENV['ORDER_INTEGRITY_TELEGRAM_ALERT_ENABLED'] == 'yes'
347
+ return unless telegram_alert_enabled?
328
348
 
329
349
  # Allow async payment processors time to settle before checking for alerts
330
350
  SpreeCmCommissioner::TelegramAlerts::OrderIntegrityCheckJob
@@ -333,6 +353,8 @@ module SpreeCmCommissioner
333
353
  end
334
354
 
335
355
  def send_order_complete_telegram_alert_to_store
356
+ return unless telegram_alert_enabled?
357
+
336
358
  title = '🎫 --- [NEW ORDER] ---'
337
359
  chat_id = store.preferred_telegram_order_alert_chat_id
338
360
  factory = OrderTelegramMessageFactory.new(title: title, order: self)
@@ -11,6 +11,8 @@ module SpreeCmCommissioner
11
11
  :allow_self_check_in?,
12
12
  :required_self_check_in_location,
13
13
  :required_self_check_in_location?,
14
+ :enable_telegram_alert,
15
+ :enable_telegram_alert?,
14
16
  to: :product
15
17
  end
16
18
  end
@@ -85,6 +85,7 @@ module SpreeCmCommissioner
85
85
 
86
86
  base.store_public_metadata :open_dated_validity_days, :integer, default: 90
87
87
  base.store_public_metadata :enable_inventory_hold, :boolean, default: false
88
+ base.store_public_metadata :enable_telegram_alert, :boolean, default: true
88
89
 
89
90
  base.after_update :update_variants_vendor_id, if: :saved_change_to_vendor_id?
90
91
  base.after_update :sync_event_id_to_children, if: :saved_change_to_event_id?
@@ -0,0 +1,8 @@
1
+ <!-- insert_after ".date-range-filter" -->
2
+
3
+ <div class="col-12 col-lg-4">
4
+ <div class="form-group">
5
+ <%= label_tag :q_payments_number_cont, 'Payment Number' %>
6
+ <%= f.text_field :payments_number_cont, class: 'form-control' %>
7
+ </div>
8
+ </div>
@@ -0,0 +1,14 @@
1
+ <!-- insert_after "[data-hook='admin_product_form_promotionable']" -->
2
+
3
+ <div>
4
+ <%= f.field_container :enable_telegram_alert do %>
5
+ <%= f.label :enable_telegram_alert do %>
6
+ <%= f.check_box :enable_telegram_alert %>
7
+ <%= Spree.t(:enable_telegram_alert) %>
8
+ <% end %>
9
+ <small class="form-text text-muted">
10
+ When enabled, Telegram order alerts are sent for this product. Turn off for
11
+ high-volume products to avoid flooding Telegram with order notifications.
12
+ </small>
13
+ <% end %>
14
+ </div>
@@ -13,9 +13,23 @@ module SpreeCmCommissioner
13
13
  create_blank_guest(line_item)
14
14
  increase_quantity(line_item) if should_increase_quantity?(line_item)
15
15
  recalculate_cart(order, line_item)
16
-
17
- success(order: order, line_item: line_item)
18
16
  end
17
+
18
+ success(order: order, line_item: line_item)
19
+ rescue ActiveRecord::RecordInvalid => e
20
+ # Convert ONLY the out-of-stock failure into a failure result. The stock validator
21
+ # tags its error with the type :selected_quantity_not_available (see
22
+ # Spree::Stock::AvailabilityValidator), so we match on that type rather than on any
23
+ # RecordInvalid. The raise rolls back the lock transaction (so the blank guest is
24
+ # not persisted); returning it as a failure lets the caller render the overridden
25
+ # availability message instead of a 500.
26
+ #
27
+ # Anything else a different line item validation, or an invalid order/shipment
28
+ # raised by recalculate_cart is re-raised so genuine backend errors are not
29
+ # masked behind an empty quantity error.
30
+ raise unless e.record == line_item && e.record.errors.of_kind?(:quantity, :selected_quantity_not_available)
31
+
32
+ failure(e.record)
19
33
  end
20
34
 
21
35
  private
@@ -17,8 +17,17 @@ module SpreeCmCommissioner
17
17
  extend SpreeCmCommissioner::ServiceModuleThrowable
18
18
 
19
19
  def call
20
- # merge: true so we never clobber the lobby's `full` / `available_slots` fields.
21
- lobby_document.set({ waiting_guests_records_path: records_path }, merge: true)
20
+ # merge: true so we never clobber the lobby's `full` / `available_slots` fields, and it
21
+ # deep-merges the nested `logs` map so each job keeps its own namespaced key.
22
+ # logs.publish_lobby_path_job.last_published_at is a point-in-time heartbeat: it confirms
23
+ # the 5-min publisher cron is alive and (with the path above) shows the date partition.
24
+ lobby_document.set(
25
+ {
26
+ waiting_guests_records_path: records_path,
27
+ logs: { publish_lobby_path_job: { last_published_at: Time.zone.now } }
28
+ },
29
+ merge: true
30
+ )
22
31
 
23
32
  success(records_path: records_path)
24
33
  rescue StandardError => e
@@ -23,7 +23,7 @@ module SpreeCmCommissioner
23
23
  CALLER_INTERVAL_SECONDS = (ENV['WAITING_ROOM_CALLER_INTERVAL_SECONDS'] || 60).to_i
24
24
 
25
25
  # Caps the batch size assumed when estimating the ETA (not a release limit). The lobby's
26
- # slots_per_call is a single run's available_slots, which spikes on cold starts / lulls
26
+ # recent_slots_per_call is a single run's available_slots, which spikes on cold starts / lulls
27
27
  # (active_sessions low → available_slots ≈ max_sessions). An uncapped spike collapses hundreds
28
28
  # of positions into batch 1 and under-promises the wait, so we bound it to keep ETAs
29
29
  # conservative. Tune to roughly the real per-minute release capacity.
@@ -39,11 +39,11 @@ module SpreeCmCommissioner
39
39
  private
40
40
 
41
41
  def stamp_positions
42
+ started_at = Time.zone.now
42
43
  paths = stamp_paths
43
44
  total = paths.sum { |path| waiting_count(path) }
44
- return if total.zero?
45
45
 
46
- @slots_per_call = lobby_data[:slots_per_call].to_i
46
+ @recent_slots_per_call = lobby_data[:recent_slots_per_call].to_i
47
47
  @flat_wait = lobby_data[:avg_queue_to_enter_seconds].to_i
48
48
  @stamped_at = Time.zone.now
49
49
  @stamped = 0
@@ -60,6 +60,31 @@ module SpreeCmCommissioner
60
60
  firestore.batch { |batch| slice.each { |doc| stamp_doc(batch, doc) } }
61
61
  end
62
62
  end
63
+
64
+ publish_logs(started_at: started_at, waiting_total: total)
65
+ end
66
+
67
+ # Point-in-time heartbeat for the position-stamper cron, mirroring the caller/publisher writers.
68
+ # Written every run (even when nobody is waiting) so a stale timestamp signals a dead cron rather
69
+ # than an empty queue. merge: true deep-merges into the shared lobby `logs` map, so this job's
70
+ # key sits alongside the caller's and publisher's without clobbering them.
71
+ def publish_logs(started_at:, waiting_total:)
72
+ finished_at = Time.zone.now
73
+ lobby_document.set(
74
+ {
75
+ logs: {
76
+ stamp_queue_positions_job: {
77
+ stamped_count: @stamped,
78
+ waiting_total: waiting_total,
79
+ stamp_limit_reached: waiting_total > STAMP_LIMIT,
80
+ last_started_at: started_at,
81
+ last_finished_at: finished_at,
82
+ last_duration_ms: ((finished_at - started_at) * 1000).round
83
+ }
84
+ }
85
+ },
86
+ merge: true
87
+ )
63
88
  end
64
89
 
65
90
  def stamp_doc(batch, doc)
@@ -98,16 +123,16 @@ module SpreeCmCommissioner
98
123
  end
99
124
 
100
125
  # Groups positions into batches that enter together, so everyone in a batch gets the same ETA.
101
- # The batch size is slots_per_call, capped by MAX_ETA_BATCH_SIZE to keep a spiking release rate
102
- # from under-promising. nil when slots_per_call is unknown (cold start) → falls back to the
126
+ # The batch size is recent_slots_per_call, capped by MAX_ETA_BATCH_SIZE to keep a spiking release rate
127
+ # from under-promising. nil when recent_slots_per_call is unknown (cold start) → falls back to the
103
128
  # flat lobby average. e.g. batch size 50, 1 call/min:
104
129
  # flat estimate: position 1 and position 50 both see ~20 min ❌
105
130
  # batch estimate: positions 1–50 → batch 1 → ~10 min
106
131
  # positions 51–100 → batch 2 → ~20 min ✅
107
132
  def batch_wait(position)
108
- return nil unless @slots_per_call.positive?
133
+ return nil unless @recent_slots_per_call.positive?
109
134
 
110
- batch_size = [@slots_per_call, MAX_ETA_BATCH_SIZE].min
135
+ batch_size = [@recent_slots_per_call, MAX_ETA_BATCH_SIZE].min
111
136
  batch_number = ((position - 1) / batch_size) + 1
112
137
  [batch_number * CALLER_INTERVAL_SECONDS, min_queue_floor].max
113
138
  end
@@ -141,7 +166,11 @@ module SpreeCmCommissioner
141
166
  end
142
167
 
143
168
  def lobby_data
144
- @lobby_data ||= firestore.col('waiting_rooms').doc('lobby').get.data || {}
169
+ @lobby_data ||= lobby_document.get.data || {}
170
+ end
171
+
172
+ def lobby_document
173
+ @lobby_document ||= firestore.col('waiting_rooms').doc('lobby')
145
174
  end
146
175
 
147
176
  def firestore
@@ -61,15 +61,18 @@
61
61
  <% end %>
62
62
  </td>
63
63
  </tr>
64
- <tr>
65
- <td></td>
66
- <td align="left" style="padding-top:6px;">
67
- <%= link_to(
68
- raw('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle; margin-right: 8px;"><rect width="5" height="5" x="3" y="3" rx="1"/><rect width="5" height="5" x="16" y="3" rx="1"/><rect width="5" height="5" x="3" y="16" rx="1"/><path d="M21 16h-3a2 2 0 0 0-2 2v3"/><path d="M21 21v.01"/><path d="M12 7v3a2 2 0 0 1-2 2H7"/><path d="M3 12h.01"/><path d="M12 3h.01"/><path d="M12 16v.01"/><path d="M16 12h1"/><path d="M21 12v.01"/><path d="M12 21v-1"/></svg> View Your Digital Ticket'),
69
- custom_product_line_item_url(line_item), class: "btn-primary", target: :_blank
70
- ) %>
71
- </td>
72
- </tr>
64
+ <%# In the email only, hide the ticket link for app-only products. %>
65
+ <% unless @is_email && line_item&.product&.purchasable_on_app? %>
66
+ <tr>
67
+ <td></td>
68
+ <td align="left" style="padding-top:6px;">
69
+ <%= link_to(
70
+ raw('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle; margin-right: 8px;"><rect width="5" height="5" x="3" y="3" rx="1"/><rect width="5" height="5" x="16" y="3" rx="1"/><rect width="5" height="5" x="3" y="16" rx="1"/><path d="M21 16h-3a2 2 0 0 0-2 2v3"/><path d="M21 21v.01"/><path d="M12 7v3a2 2 0 0 1-2 2H7"/><path d="M3 12h.01"/><path d="M12 3h.01"/><path d="M12 16v.01"/><path d="M16 12h1"/><path d="M21 12v.01"/><path d="M12 21v-1"/></svg> View Your Digital Ticket'),
71
+ custom_product_line_item_url(line_item), class: "btn-primary", target: :_blank
72
+ ) %>
73
+ </td>
74
+ </tr>
75
+ <% end %>
73
76
  </table>
74
77
  </td>
75
78
  </tr>
@@ -6,6 +6,9 @@ module Spree
6
6
  @@vendor_attributes << :logo
7
7
  @@vendor_attributes << :service_origin_id
8
8
 
9
+ # store_accessor key (public_metadata), not a DB column, so it must be permitted explicitly
10
+ @@product_attributes << :enable_telegram_alert unless @@product_attributes.include?(:enable_telegram_alert)
11
+
9
12
  # Permitted all guest attributes for now as permitting only some guest attributes is not working by design
10
13
  # of spree add_item service, if we want to only permit some guest attributes,
11
14
  # we need to override most part of add_item service.
@@ -217,6 +217,7 @@ en:
217
217
 
218
218
  spree:
219
219
  description: Description
220
+ enable_telegram_alert: Enable Telegram Alert
220
221
  registered_by:
221
222
  system_registered: "System Registered"
222
223
  self_registered: "Self Registered"
@@ -414,6 +415,9 @@ en:
414
415
  zero: "Rooms are not available on %{date}"
415
416
  one: "Only 1 room available on %{date}"
416
417
  other: "Only %{count} rooms available on %{date}"
418
+ # Overrides Spree core's "selected of %{item} is not available." Must stay under
419
+ # `spree:` -- Spree::Stock::AvailabilityValidator looks it up as Spree.t(:selected_quantity_not_available).
420
+ selected_quantity_not_available: "Not enough %{item} are available for the quantity you selected. Please try again."
417
421
  auto_apply: "Auto apply"
418
422
  auto_apply_info: "Path & code will be removed"
419
423
  transit:
@@ -715,7 +719,7 @@ en:
715
719
  inventory_hold:
716
720
  acquire:
717
721
  success: "Item reserved for %{minutes} minutes"
718
- insufficient_stock: "Sorry, this item is no longer available."
722
+ insufficient_stock: "Some items in your cart are currently held by another customer or no longer available. Please update your cart or try again shortly."
719
723
  limit_exceeded: "You have too many pending reservations. Please complete or cancel a reservation before trying another."
720
724
  system_error: "We encountered an issue reserving this item. Please try again."
721
725
  lock_timeout: "Another reservation is in progress for this order. Please wait and try again."
@@ -357,6 +357,7 @@ km:
357
357
  unknown: "មិនស្គាល់"
358
358
  upsupported_payment: "មិនស្គាល់ការបង់ប្រាក់"
359
359
  selected_item_not_available_on_date: "Selected item not available on %{date}"
360
+ selected_quantity_not_available: "%{item} នៅសល់តិចជាងការអ្នកស្នើសុំរបស់អ្នក ឬអស់ពីស្តុក សូមព្យាយាមម្ដងទៀត។"
360
361
  auto_apply: "ដាក់ប្រើដោយស្វ័យប្រវត្តិ"
361
362
  auto_apply_info: "Path & code will be removed"
362
363
  device_tokens: "Device Tokens"
@@ -518,7 +519,7 @@ km:
518
519
  inventory_hold:
519
520
  acquire:
520
521
  success: "ទំនិញត្រូវបានកក់រយៈពេល %{minutes} នាទី"
521
- insufficient_stock: "សុំទោស! ទំនិញមិនមានស្តុកគ្រប់គ្រាន់សម្រាប់ការកក់របស់អ្នកទេ។"
522
+ insufficient_stock: "ទំនិញមួយចំនួននៅក្នុងកន្ត្រករបស់អ្នកកំពុងត្រូវបានកក់ដោយអតិថិជនផ្សេងទៀត ឬលែងមានស្តុក។ សូមកែប្រែកន្ត្រករបស់អ្នក ឬព្យាយាមម្តងទៀតក្នុងពេលបន្តិចទៀត។"
522
523
  limit_exceeded: "អ្នកមានការកក់ដែលរង់ចាំច្រើនពេក។ សូមបញ្ចប់ ឬលុបការកក់មួយមុនពេលព្យាយាមវិញ។"
523
524
  system_error: "មានបញ្ហាក្នុងការកក់ទំនិញនេះ។ សូមព្យាយាមម្តងទៀត។"
524
525
  lock_timeout: "កំពុងកក់សម្រាប់ការបញ្ជាទិញនេះ។ សូមរង់ចាំ ហើយព្យាយាមម្តងទៀត។"
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.8.7'.freeze
2
+ VERSION = '2.8.8'.freeze
3
3
 
4
4
  module_function
5
5
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_cm_commissioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.7
4
+ version: 2.8.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
@@ -1734,6 +1734,7 @@ files:
1734
1734
  - app/overrides/spree/admin/orders/_search/intel_phone_number_search_field.html.erb.deface
1735
1735
  - app/overrides/spree/admin/orders/_search/line_item_number_search_field.html.erb.deface
1736
1736
  - app/overrides/spree/admin/orders/_search/payment_method_search_field.html.erb.deface
1737
+ - app/overrides/spree/admin/orders/_search/payment_number_search_field.html.erb.deface
1737
1738
  - app/overrides/spree/admin/orders/_search/vendor_search_field.html.erb.deface
1738
1739
  - app/overrides/spree/admin/orders/customer_details/_form/phone_number_field.html.erb.deface
1739
1740
  - app/overrides/spree/admin/payment_methods/_form/display_on_field.html.erb.deface
@@ -1746,6 +1747,7 @@ files:
1746
1747
  - app/overrides/spree/admin/products/_form/available_on.html.erb.deface
1747
1748
  - app/overrides/spree/admin/products/_form/discontinue_on.html.erb.deface
1748
1749
  - app/overrides/spree/admin/products/_form/enable_inventory_hold.html.erb.deface
1750
+ - app/overrides/spree/admin/products/_form/enable_telegram_alert.html.erb.deface
1749
1751
  - app/overrides/spree/admin/products/_form/industry_taxons.html.erb.deface
1750
1752
  - app/overrides/spree/admin/products/_form/need_confirmation_checkbox.html.erb.deface
1751
1753
  - app/overrides/spree/admin/products/_form/option_types.html.erb.deface