spree_cm_commissioner 2.8.6 → 2.8.7
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/.env.example +8 -0
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/import_new_orders_controller.rb +25 -0
- data/app/controllers/spree/api/v2/storefront/account/orders_controller_decorator.rb +5 -0
- data/app/controllers/spree/api/v2/storefront/order_histories_controller.rb +2 -2
- data/app/controllers/spree/api/v2/storefront/ticket_transfers_controller.rb +4 -3
- data/app/controllers/spree/api/v2/tenant/order_histories_controller.rb +2 -2
- data/app/finders/spree_cm_commissioner/orders/find.rb +33 -3
- data/app/finders/spree_cm_commissioner/orders/find_by_all_state.rb +11 -0
- data/app/interactors/spree_cm_commissioner/waiting_guests_caller.rb +16 -4
- data/app/jobs/concerns/spree_cm_commissioner/idempotent_job.rb +12 -0
- data/app/jobs/spree_cm_commissioner/cancel_import_order_job.rb +11 -0
- data/app/jobs/spree_cm_commissioner/idempotency_keys/prune_job.rb +20 -0
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +21 -3
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +34 -3
- data/app/jobs/spree_cm_commissioner/telegram_alerts/order_integrity_check_job.rb +17 -0
- data/app/jobs/spree_cm_commissioner/waiting_guests_caller_job.rb +5 -0
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +10 -0
- data/app/models/concerns/spree_cm_commissioner/store_preference.rb +1 -0
- data/app/models/spree_cm_commissioner/idempotency_key.rb +21 -0
- data/app/models/spree_cm_commissioner/import.rb +2 -1
- data/app/models/spree_cm_commissioner/imported_order.rb +6 -0
- data/app/models/spree_cm_commissioner/imports/import_order.rb +21 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +14 -0
- data/app/models/spree_cm_commissioner/payment_decorator.rb +1 -0
- data/app/overrides/spree/admin/products/_form/allow_gift_transfer.html.erb.deface +1 -1
- data/app/overrides/spree/admin/products/_form/allow_transfer.html.erb.deface +1 -1
- data/app/overrides/spree/admin/stores/_form/store_preferences.html.erb.deface +9 -0
- data/app/services/spree_cm_commissioner/imports/orders/cancel.rb +63 -0
- data/app/services/spree_cm_commissioner/imports/orders/create.rb +1 -0
- data/app/services/spree_cm_commissioner/telegram_alerts/checks/order_complete_payment_not_paid.rb +21 -0
- data/app/services/spree_cm_commissioner/telegram_alerts/checks/payment_paid_order_not_complete.rb +21 -0
- data/app/services/spree_cm_commissioner/telegram_alerts/order_integrity_alert.rb +112 -0
- data/app/services/spree_cm_commissioner/telegram_alerts/order_integrity_checks_runner.rb +32 -0
- data/app/services/spree_cm_commissioner/waiting_room/stamp_queue_positions.rb +66 -24
- data/app/views/spree/admin/import_new_orders/_cancel_modal.html.erb +35 -0
- data/app/views/spree/admin/import_new_orders/index.html.erb +4 -0
- data/app/views/spree/admin/import_new_orders/show.html.erb +30 -0
- data/app/views/spree/admin/imports/index.html.erb +2 -0
- data/app/views/spree/admin/inventory_holds/index.html.erb +8 -3
- data/config/initializers/spree_permitted_attributes.rb +1 -0
- data/config/locales/en.yml +4 -4
- data/config/locales/km.yml +4 -4
- data/config/routes.rb +1 -0
- data/db/migrate/20260616000001_add_canceled_by_id_to_cm_imports.rb +5 -0
- data/db/migrate/20260617000002_create_cm_imported_orders.rb +12 -0
- data/db/migrate/20260619000001_create_cm_idempotency_keys.rb +11 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +18 -2
data/app/services/spree_cm_commissioner/telegram_alerts/checks/order_complete_payment_not_paid.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module TelegramAlerts
|
|
3
|
+
module Checks
|
|
4
|
+
class OrderCompletePaymentNotPaid < OrderIntegrityAlert
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def anomaly?(order)
|
|
8
|
+
order.complete? && order.payment_state != 'paid'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def emoji
|
|
12
|
+
'🚨'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def title
|
|
16
|
+
'Order Complete With Unresolved Payment State'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/app/services/spree_cm_commissioner/telegram_alerts/checks/payment_paid_order_not_complete.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module TelegramAlerts
|
|
3
|
+
module Checks
|
|
4
|
+
class PaymentPaidOrderNotComplete < OrderIntegrityAlert
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def anomaly?(order)
|
|
8
|
+
order.payments.completed.exists? && !order.complete?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def emoji
|
|
12
|
+
'⚠️'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def title
|
|
16
|
+
'Payment Paid But Order Not Complete'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module TelegramAlerts
|
|
3
|
+
# Abstract base class for order anomaly checks. Subclasses implement
|
|
4
|
+
# `anomaly?`, `emoji` and `title` to define what they detect and how
|
|
5
|
+
# the alert is presented; this class handles chat resolution, message
|
|
6
|
+
# building and delivery.
|
|
7
|
+
class OrderIntegrityAlert
|
|
8
|
+
prepend ::Spree::ServiceModule::Base
|
|
9
|
+
|
|
10
|
+
def call(order:)
|
|
11
|
+
return success(message: 'No anomaly detected') unless anomaly?(order)
|
|
12
|
+
|
|
13
|
+
chat_id = alert_chat_id
|
|
14
|
+
return failure(nil, 'Order anomaly alert chat ID not configured') if chat_id.blank?
|
|
15
|
+
|
|
16
|
+
TelegramNotificationSenderJob.perform_later(
|
|
17
|
+
chat_id: chat_id,
|
|
18
|
+
message: build_message(order),
|
|
19
|
+
parse_mode: 'HTML'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
success(message: 'Order alert sent successfully')
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
failure(nil, "Failed to send order alert: #{e.message}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def anomaly?(_order)
|
|
30
|
+
raise NotImplementedError, "#{self.class} must implement #anomaly?"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def emoji
|
|
34
|
+
raise NotImplementedError, "#{self.class} must implement #emoji"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def title
|
|
38
|
+
raise NotImplementedError, "#{self.class} must implement #title"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def alert_chat_id
|
|
42
|
+
Spree::Store.default&.preferred_telegram_order_anomaly_alert_chat_id.presence ||
|
|
43
|
+
ENV.fetch('EXCEPTION_NOTIFIER_TELEGRAM_CHANNEL_ID', nil)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build_message(order)
|
|
47
|
+
line_items = order.line_items.includes(:product, :vendor)
|
|
48
|
+
has_payment = order.payments.any?
|
|
49
|
+
|
|
50
|
+
lines = header_lines +
|
|
51
|
+
order_lines(order, has_payment) +
|
|
52
|
+
[''] +
|
|
53
|
+
product_lines(line_items) +
|
|
54
|
+
[''] +
|
|
55
|
+
customer_lines(order) +
|
|
56
|
+
footer_lines(order)
|
|
57
|
+
|
|
58
|
+
lines.compact.join("\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def header_lines
|
|
62
|
+
[
|
|
63
|
+
"#{emoji} <b>ATTENTION — #{title}</b>",
|
|
64
|
+
"🕐 <code>#{Time.current.strftime('%Y-%m-%d %H:%M')}</code>",
|
|
65
|
+
''
|
|
66
|
+
]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def order_lines(order, has_payment)
|
|
70
|
+
lines = [
|
|
71
|
+
'📋 <b>Order</b>',
|
|
72
|
+
"<b>Number:</b> <code>#{order.number}</code>",
|
|
73
|
+
"<b>State:</b> #{order.state}"
|
|
74
|
+
]
|
|
75
|
+
lines << "<b>Payment State:</b> #{order.payment_state}" if has_payment
|
|
76
|
+
lines
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def product_lines(line_items)
|
|
80
|
+
items = line_items.map do |li|
|
|
81
|
+
vendor_name = li.vendor&.name
|
|
82
|
+
entry = "· #{ERB::Util.html_escape(li.product.name)} (x#{li.quantity})"
|
|
83
|
+
entry += " — #{ERB::Util.html_escape(vendor_name)}" if vendor_name.present?
|
|
84
|
+
entry
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
['🎫 <b>Products</b>', *items]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def customer_lines(order)
|
|
91
|
+
[
|
|
92
|
+
'👤 <b>Customer</b>',
|
|
93
|
+
"<b>Name:</b> #{ERB::Util.html_escape(order.name.to_s)}",
|
|
94
|
+
("<b>Email:</b> #{order.email}" if order.email.present?),
|
|
95
|
+
("<b>Phone:</b> #{order.phone_number}" if order.respond_to?(:phone_number) && order.phone_number.present?)
|
|
96
|
+
]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def footer_lines(order)
|
|
100
|
+
['', "🔗 #{order_url(order)}"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def order_url(order)
|
|
104
|
+
Spree::Core::Engine.routes.url_helpers.edit_admin_order_url(
|
|
105
|
+
order,
|
|
106
|
+
host: Rails.application.routes.default_url_options[:host],
|
|
107
|
+
port: Rails.application.routes.default_url_options[:port]
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module TelegramAlerts
|
|
3
|
+
class OrderIntegrityChecksRunner
|
|
4
|
+
prepend ::Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
CHECKS = [
|
|
7
|
+
'SpreeCmCommissioner::TelegramAlerts::Checks::OrderCompletePaymentNotPaid',
|
|
8
|
+
'SpreeCmCommissioner::TelegramAlerts::Checks::PaymentPaidOrderNotComplete'
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
def call(order:)
|
|
12
|
+
CHECKS.each do |check_class|
|
|
13
|
+
result = check_class.constantize.call(order: order)
|
|
14
|
+
log_failure(check_class, order, result.error.to_s) if result.failure?
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
log_failure(check_class, order, e.message)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
success(message: 'Order integrity checks completed')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def log_failure(check_class, order, error)
|
|
25
|
+
CmAppLogger.error(
|
|
26
|
+
label: "#{check_class} failed",
|
|
27
|
+
data: { order_id: order.id, error: error }
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -15,6 +15,20 @@ module SpreeCmCommissioner
|
|
|
15
15
|
# Firestore bounds a batch update by payload size (10 MiB); this batch size leaves 500/commit far under that.
|
|
16
16
|
FIRESTORE_BATCH_SIZE = (ENV['WAITING_ROOM_FIRESTORE_BATCH_SIZE'] || 500).to_i
|
|
17
17
|
|
|
18
|
+
# Above this we leave estimated_wait_seconds nil so the app hides the ETA rather than
|
|
19
|
+
# showing a discouraging "~2 hours". Deep guests just see the coarse "lots ahead" state.
|
|
20
|
+
MAX_WAIT_TO_DISPLAY_SECONDS = (ENV['WAITING_ROOM_MAX_WAIT_TO_DISPLAY_SECONDS'] || 3600).to_i
|
|
21
|
+
|
|
22
|
+
# How often the caller runs and releases one batch. See schedule.yml for WaitingGuestsCallerJob.
|
|
23
|
+
CALLER_INTERVAL_SECONDS = (ENV['WAITING_ROOM_CALLER_INTERVAL_SECONDS'] || 60).to_i
|
|
24
|
+
|
|
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
|
|
27
|
+
# (active_sessions low → available_slots ≈ max_sessions). An uncapped spike collapses hundreds
|
|
28
|
+
# of positions into batch 1 and under-promises the wait, so we bound it to keep ETAs
|
|
29
|
+
# conservative. Tune to roughly the real per-minute release capacity.
|
|
30
|
+
MAX_ETA_BATCH_SIZE = (ENV['WAITING_ROOM_MAX_ETA_BATCH_SIZE'] || 50).to_i
|
|
31
|
+
|
|
18
32
|
def call
|
|
19
33
|
stamp_positions
|
|
20
34
|
success(stamped: @stamped.to_i)
|
|
@@ -26,11 +40,12 @@ module SpreeCmCommissioner
|
|
|
26
40
|
|
|
27
41
|
def stamp_positions
|
|
28
42
|
paths = stamp_paths
|
|
29
|
-
|
|
30
|
-
total = paths.sum { |p| waiting_count(p) }
|
|
43
|
+
total = paths.sum { |path| waiting_count(path) }
|
|
31
44
|
return if total.zero?
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
@slots_per_call = lobby_data[:slots_per_call].to_i
|
|
47
|
+
@flat_wait = lobby_data[:avg_queue_to_enter_seconds].to_i
|
|
48
|
+
@stamped_at = Time.zone.now
|
|
34
49
|
@stamped = 0
|
|
35
50
|
|
|
36
51
|
paths.each do |path|
|
|
@@ -39,41 +54,68 @@ module SpreeCmCommissioner
|
|
|
39
54
|
|
|
40
55
|
docs = waiting_query(path).order('queued_at').limit(remaining).get.to_a
|
|
41
56
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
# (so e.g. 1000 docs = 2 commits, not 1000 round-trips).
|
|
57
|
+
# Commit in FIRESTORE_BATCH_SIZE chunks (one firestore.batch per slice), so e.g.
|
|
58
|
+
# 1000 docs = 2 commits, not 1000 round-trips.
|
|
45
59
|
docs.each_slice(FIRESTORE_BATCH_SIZE) do |slice|
|
|
46
|
-
firestore.batch
|
|
47
|
-
slice.each { |doc| stamp_doc(b, doc, avg_queue_to_enter, stamped_at) }
|
|
48
|
-
end
|
|
60
|
+
firestore.batch { |batch| slice.each { |doc| stamp_doc(batch, doc) } }
|
|
49
61
|
end
|
|
50
62
|
end
|
|
51
63
|
end
|
|
52
64
|
|
|
53
|
-
def stamp_doc(batch, doc
|
|
65
|
+
def stamp_doc(batch, doc)
|
|
54
66
|
@stamped += 1
|
|
55
67
|
|
|
56
|
-
# The app shows a progress bar via (queue_total - position + 1) / queue_total.
|
|
57
|
-
# `position` only ever counts down toward 1, so we keep `queue_total` from ever
|
|
58
|
-
# going down either (max of its old value and the current position) — it becomes
|
|
59
|
-
# the guest's starting depth. That keeps the bar moving only forward, reaching
|
|
60
|
-
# 100% at position 1, instead of jumping as the live waiting count shrinks.
|
|
61
|
-
#
|
|
62
|
-
# Example — a guest who started 8th, with the line draining from 8 to 4 people:
|
|
63
|
-
# live total: position 6 / total 8 = 38%, then position 4 / total 4 = 25% (drops!) ❌
|
|
64
|
-
# this fix: position 6 / total 8 = 38%, then position 4 / total 8 = 63% (climbs) ✅
|
|
65
|
-
# ...continuing to position 1 / total 8 = 100% at the front.
|
|
66
68
|
data = {
|
|
67
69
|
position: @stamped,
|
|
68
|
-
queue_total:
|
|
69
|
-
estimated_wait_seconds:
|
|
70
|
-
status_updated_at: stamped_at
|
|
70
|
+
queue_total: queue_total_for(doc),
|
|
71
|
+
estimated_wait_seconds: estimated_wait_for(@stamped),
|
|
72
|
+
status_updated_at: @stamped_at
|
|
71
73
|
}
|
|
74
|
+
# Set on the first stamp only — records when the guest first reached the front N.
|
|
75
|
+
data[:position_set_at] = @stamped_at unless doc.data[:position_set_at]
|
|
72
76
|
|
|
73
|
-
data[:position_set_at] = stamped_at unless doc.data[:position_set_at]
|
|
74
77
|
batch.update(doc.ref, data)
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
# The app's progress bar is (queue_total - position + 1) / queue_total. `position` only counts
|
|
81
|
+
# down toward 1, so we never let `queue_total` drop either — it freezes at the guest's starting
|
|
82
|
+
# depth (their deepest position). That keeps the bar moving forward to 100% at position 1
|
|
83
|
+
# instead of jumping as the live line shrinks. e.g. starting 8th, line draining 8 → 4:
|
|
84
|
+
# live total: 6/8 = 38%, then 4/4 = 25% (drops!) ❌
|
|
85
|
+
# frozen total: 6/8 = 38%, then 4/8 = 63% (climbs) ✅
|
|
86
|
+
def queue_total_for(doc)
|
|
87
|
+
[doc.data[:queue_total].to_i, @stamped].max
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# ETA written every run (including nil, so a guest whose wait crosses the ceiling has any
|
|
91
|
+
# stale value cleared and the app stops rendering it). nil means "hide the ETA" — over 1 h
|
|
92
|
+
# we show the coarse "lots ahead" state instead of a discouraging number.
|
|
93
|
+
def estimated_wait_for(position)
|
|
94
|
+
seconds = batch_wait(position) || @flat_wait
|
|
95
|
+
return nil unless seconds.positive? && seconds <= MAX_WAIT_TO_DISPLAY_SECONDS
|
|
96
|
+
|
|
97
|
+
seconds
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# 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
|
|
103
|
+
# flat lobby average. e.g. batch size 50, 1 call/min:
|
|
104
|
+
# flat estimate: position 1 and position 50 both see ~20 min ❌
|
|
105
|
+
# batch estimate: positions 1–50 → batch 1 → ~10 min
|
|
106
|
+
# positions 51–100 → batch 2 → ~20 min ✅
|
|
107
|
+
def batch_wait(position)
|
|
108
|
+
return nil unless @slots_per_call.positive?
|
|
109
|
+
|
|
110
|
+
batch_size = [@slots_per_call, MAX_ETA_BATCH_SIZE].min
|
|
111
|
+
batch_number = ((position - 1) / batch_size) + 1
|
|
112
|
+
[batch_number * CALLER_INTERVAL_SECONDS, min_queue_floor].max
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def min_queue_floor
|
|
116
|
+
SpreeCmCommissioner::WaitingGuestsCaller::MIN_QUEUE_TO_ENTER_SECONDS
|
|
117
|
+
end
|
|
118
|
+
|
|
77
119
|
def stamp_paths
|
|
78
120
|
[previous_records_path, records_path]
|
|
79
121
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<% modal_id = "cancelImportModal#{import.id}" %>
|
|
2
|
+
|
|
3
|
+
<div class="modal fade" id="<%= modal_id %>" tabindex="-1" role="dialog" aria-labelledby="<%= modal_id %>Label" aria-hidden="true">
|
|
4
|
+
<div class="modal-dialog" role="document">
|
|
5
|
+
<div class="modal-content">
|
|
6
|
+
<div class="modal-header">
|
|
7
|
+
<h5 class="modal-title" id="<%= modal_id %>Label">Cancel orders from "<%= import.name %>"?</h5>
|
|
8
|
+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
9
|
+
<span aria-hidden="true">×</span>
|
|
10
|
+
</button>
|
|
11
|
+
</div>
|
|
12
|
+
<%= form_with url: spree.cancel_admin_import_new_order_path(import.id), method: :post, local: true do %>
|
|
13
|
+
<div class="modal-body">
|
|
14
|
+
<div class="alert alert-warning mb-3">
|
|
15
|
+
This will cancel <strong>all orders</strong> created by this import, restock inventory, and release seat holds. <strong>This cannot be undone.</strong>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="form-group">
|
|
19
|
+
<label>Type the import name <code class="text-danger"><%= import.name %></code> to confirm</label>
|
|
20
|
+
<%= text_field_tag :confirm_name, nil, class: 'form-control', autocomplete: 'off', required: true %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="form-group mb-0">
|
|
24
|
+
<label>Reason</label>
|
|
25
|
+
<%= text_area_tag :cancellation_reason, nil, class: 'form-control', rows: 4 %>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="modal-footer">
|
|
29
|
+
<button type="button" class="btn btn-secondary" data-dismiss="modal">No, keep them</button>
|
|
30
|
+
<%= button_tag 'Yes, cancel orders', type: 'submit', class: 'btn btn-danger' %>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
<span class="badge badge-success"><%= import.status %></span>
|
|
52
52
|
<% when 'failed' %>
|
|
53
53
|
<span class="badge badge-danger"><%= import.status %></span>
|
|
54
|
+
<% when 'canceling' %>
|
|
55
|
+
<span class="badge badge-warning"><%= import.status %></span>
|
|
56
|
+
<% when 'canceled' %>
|
|
57
|
+
<span class="badge badge-dark"><%= import.status %></span>
|
|
54
58
|
<% else %>
|
|
55
59
|
<span class="badge badge-secondary"><%= import.status %></span>
|
|
56
60
|
<% end %>
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
<%= page_header_back_button spree.admin_import_new_orders_path %>
|
|
3
3
|
<%= @import.name %>
|
|
4
4
|
<% end %>
|
|
5
|
+
|
|
6
|
+
<% if @import.cancellable? %>
|
|
7
|
+
<% content_for :page_actions do %>
|
|
8
|
+
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#cancelImportModal<%= @import.id %>">
|
|
9
|
+
<%= svg_icon name: "cancel.svg", width: '14', height: '14' %>
|
|
10
|
+
Cancel orders
|
|
11
|
+
</button>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
5
15
|
<%= render partial: 'spree/admin/shared/import_order_tabs', locals: { current: :import_new_order } %>
|
|
6
16
|
|
|
7
17
|
<div class="row w-75">
|
|
@@ -33,6 +43,10 @@
|
|
|
33
43
|
<span class="badge badge-success"><%= @import.status %></span>
|
|
34
44
|
<% when 'failed' %>
|
|
35
45
|
<span class="badge badge-danger"><%= @import.status %></span>
|
|
46
|
+
<% when 'canceling' %>
|
|
47
|
+
<span class="badge badge-warning"><%= @import.status %></span>
|
|
48
|
+
<% when 'canceled' %>
|
|
49
|
+
<span class="badge badge-dark"><%= @import.status %></span>
|
|
36
50
|
<% else %>
|
|
37
51
|
<span class="badge badge-secondary"><%= @import.status %></span>
|
|
38
52
|
<% end %>
|
|
@@ -48,8 +62,24 @@
|
|
|
48
62
|
</div>
|
|
49
63
|
<% end %>
|
|
50
64
|
</dd>
|
|
65
|
+
<% if @import.canceled_at.present? %>
|
|
66
|
+
<dt class="col-sm-3">Canceled At:</dt>
|
|
67
|
+
<dd class="col-sm-9"><%= @import.canceled_at.strftime('%A, %d %B %Y %H:%M:%S') %></dd>
|
|
68
|
+
<dt class="col-sm-3">Canceled By:</dt>
|
|
69
|
+
<dd class="col-sm-9">
|
|
70
|
+
<%= [@import.canceled_by&.first_name, @import.canceled_by&.last_name].compact.join(' ').presence || @import.canceled_by&.email %>
|
|
71
|
+
</dd>
|
|
72
|
+
<% if @import.preferred_cancellation_reason.present? %>
|
|
73
|
+
<dt class="col-sm-3">Cancellation Reason:</dt>
|
|
74
|
+
<dd class="col-sm-9"><%= @import.preferred_cancellation_reason %></dd>
|
|
75
|
+
<% end %>
|
|
76
|
+
<% end %>
|
|
51
77
|
</dl>
|
|
52
78
|
</div>
|
|
53
79
|
</div>
|
|
54
80
|
</div>
|
|
55
81
|
</div>
|
|
82
|
+
|
|
83
|
+
<% if @import.cancellable? %>
|
|
84
|
+
<%= render partial: 'cancel_modal', locals: { import: @import } %>
|
|
85
|
+
<% end %>
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
when 'progress' then 'badge badge-warning'
|
|
31
31
|
when 'done' then 'badge badge-success'
|
|
32
32
|
when 'failed' then 'badge badge-danger'
|
|
33
|
+
when 'canceling' then 'badge badge-warning'
|
|
34
|
+
when 'canceled' then 'badge badge-dark'
|
|
33
35
|
else 'badge badge-secondary'
|
|
34
36
|
end %>
|
|
35
37
|
<span class="<%= badge_class %>"><%= import.status %></span>
|
|
@@ -14,12 +14,17 @@
|
|
|
14
14
|
|
|
15
15
|
<%= render 'search' if @holds.any? || params[:q].present? %>
|
|
16
16
|
|
|
17
|
+
<% current_status = params.dig(:q, :status_eq).presence %>
|
|
17
18
|
<div class="flex-wrap mb-3 alert alert-info d-flex">
|
|
19
|
+
<%= link_to admin_inventory_holds_path(q: params.fetch(:q, {}).to_unsafe_h.except('status_eq')), class: 'mr-4' do %>
|
|
20
|
+
<span class="<%= 'font-weight-bold' if current_status.nil? %>">All:</span>
|
|
21
|
+
<span class="badge badge-dark"><%= @status_counts.values.sum %></span>
|
|
22
|
+
<% end %>
|
|
18
23
|
<% SpreeCmCommissioner::InventoryHold.statuses.keys.each do |status| %>
|
|
19
|
-
|
|
20
|
-
<
|
|
24
|
+
<%= link_to admin_inventory_holds_path(q: params.fetch(:q, {}).to_unsafe_h.merge('status_eq' => status)), class: 'mr-4' do %>
|
|
25
|
+
<span class="<%= 'font-weight-bold' if current_status == status %>"><%= status.humanize %>:</span>
|
|
21
26
|
<span class="badge <%= status_badge_classes[status] || 'badge-light' %>"><%= @status_counts[status] || 0 %></span>
|
|
22
|
-
|
|
27
|
+
<% end %>
|
|
23
28
|
<% end %>
|
|
24
29
|
</div>
|
|
25
30
|
|
|
@@ -55,6 +55,7 @@ module Spree
|
|
|
55
55
|
:preferred_telegram_order_request_alert_chat_id,
|
|
56
56
|
:preferred_telegram_new_vendor_alert_chat_id,
|
|
57
57
|
:preferred_telegram_new_event_alert_chat_id,
|
|
58
|
+
:preferred_telegram_order_anomaly_alert_chat_id,
|
|
58
59
|
:preferred_assetlinks,
|
|
59
60
|
:preferred_apple_app_site_association,
|
|
60
61
|
{ default_notification_image_attributes: {} },
|
data/config/locales/en.yml
CHANGED
|
@@ -649,11 +649,11 @@ en:
|
|
|
649
649
|
title: "💸 Order Complete!"
|
|
650
650
|
message: "Your Order numer is %{order_number}"
|
|
651
651
|
payment_incomplete_notification:
|
|
652
|
-
title: "
|
|
653
|
-
message: "
|
|
652
|
+
title: "Complete Your Payment"
|
|
653
|
+
message: "We couldn't process your payment for order %{order_number}."
|
|
654
654
|
release_inventory_item_notification:
|
|
655
|
-
title: "
|
|
656
|
-
message: "
|
|
655
|
+
title: "Ticket Reservation Expired"
|
|
656
|
+
message: "Your ticket reservation for order %{order_number} has expired and the reserved tickets have been released."
|
|
657
657
|
order_accepted_notification:
|
|
658
658
|
title: "Confirm!"
|
|
659
659
|
message: "Your request has been confirmed; please check your upcoming trip."
|
data/config/locales/km.yml
CHANGED
|
@@ -474,11 +474,11 @@ km:
|
|
|
474
474
|
title: "💸 ការបញ្ជាទិញទទួលបានជោគជ័យ!"
|
|
475
475
|
message: "លេខបញ្ជាទិញរបស់អ្នកគឺ %{order_number}"
|
|
476
476
|
payment_incomplete_notification:
|
|
477
|
-
title: "
|
|
478
|
-
message: "
|
|
477
|
+
title: "សូមបញ្ចប់ការទូទាត់របស់អ្នក"
|
|
478
|
+
message: "យើងមិនអាចដំណើរការការទូទាត់សម្រាប់ការបញ្ជាទិញ %{order_number} របស់អ្នកបានទេ។"
|
|
479
479
|
release_inventory_item_notification:
|
|
480
|
-
title: "
|
|
481
|
-
message: "
|
|
480
|
+
title: "ការកក់សំបុត្រត្រូវបានផុតកំណត់"
|
|
481
|
+
message: "ការកក់សំបុត្រសម្រាប់ការបញ្ជាទិញ %{order_number} របស់អ្នកត្រូវបានផុតកំណត់ ហើយសំបុត្រដែលបានកក់ត្រូវបានដោះលែងវិញ។"
|
|
482
482
|
order_accepted_notification:
|
|
483
483
|
title: "បានយល់ព្រម!"
|
|
484
484
|
message: "សំណើរបស់អ្នកត្រូវបានបញ្ជាក់; សូមពិនិត្យមើលការធ្វើដំណើរនាពេលខាងមុខរបស់អ្នក"
|
data/config/routes.rb
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateCmImportedOrders < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :cm_imported_orders, if_not_exists: true do |t|
|
|
4
|
+
t.bigint :order_id, null: false
|
|
5
|
+
t.bigint :import_order_id, null: false
|
|
6
|
+
t.timestamps
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
add_index :cm_imported_orders, :order_id, unique: true, if_not_exists: true
|
|
10
|
+
add_index :cm_imported_orders, :import_order_id, if_not_exists: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class CreateCmIdempotencyKeys < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :cm_idempotency_keys, if_not_exists: true do |t|
|
|
4
|
+
t.string :key, null: false
|
|
5
|
+
t.timestamps
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
add_index :cm_idempotency_keys, :key, unique: true, if_not_exists: true
|
|
9
|
+
add_index :cm_idempotency_keys, :created_at, if_not_exists: true
|
|
10
|
+
end
|
|
11
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_cm_commissioner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.8.
|
|
4
|
+
version: 2.8.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: spree
|
|
@@ -1164,6 +1164,7 @@ files:
|
|
|
1164
1164
|
- app/finders/spree_cm_commissioner/events/find_matches.rb
|
|
1165
1165
|
- app/finders/spree_cm_commissioner/inventory_items/recently_changed_finder.rb
|
|
1166
1166
|
- app/finders/spree_cm_commissioner/orders/find.rb
|
|
1167
|
+
- app/finders/spree_cm_commissioner/orders/find_by_all_state.rb
|
|
1167
1168
|
- app/finders/spree_cm_commissioner/orders/find_by_state.rb
|
|
1168
1169
|
- app/finders/spree_cm_commissioner/payment_methods/group_by_bank.rb
|
|
1169
1170
|
- app/finders/spree_cm_commissioner/places/find_with_route.rb
|
|
@@ -1325,10 +1326,12 @@ files:
|
|
|
1325
1326
|
- app/interactors/spree_cm_commissioner/webhook_subscriber_orders_sender.rb
|
|
1326
1327
|
- app/jobs/application_job.rb
|
|
1327
1328
|
- app/jobs/application_unique_job.rb
|
|
1329
|
+
- app/jobs/concerns/spree_cm_commissioner/idempotent_job.rb
|
|
1328
1330
|
- app/jobs/spree_cm_commissioner/account_deletion_cron_job.rb
|
|
1329
1331
|
- app/jobs/spree_cm_commissioner/application_job.rb
|
|
1330
1332
|
- app/jobs/spree_cm_commissioner/application_job_decorator.rb
|
|
1331
1333
|
- app/jobs/spree_cm_commissioner/application_unique_job.rb
|
|
1334
|
+
- app/jobs/spree_cm_commissioner/cancel_import_order_job.rb
|
|
1332
1335
|
- app/jobs/spree_cm_commissioner/chatrace_order_creator_job.rb
|
|
1333
1336
|
- app/jobs/spree_cm_commissioner/completion_steps/regenerate_for_line_items_job.rb
|
|
1334
1337
|
- app/jobs/spree_cm_commissioner/customer_content_notification_creator_job.rb
|
|
@@ -1342,6 +1345,7 @@ files:
|
|
|
1342
1345
|
- app/jobs/spree_cm_commissioner/export_job.rb
|
|
1343
1346
|
- app/jobs/spree_cm_commissioner/firebase_email_fetcher_job.rb
|
|
1344
1347
|
- app/jobs/spree_cm_commissioner/guests/preload_check_in_session_ids_job.rb
|
|
1348
|
+
- app/jobs/spree_cm_commissioner/idempotency_keys/prune_job.rb
|
|
1345
1349
|
- app/jobs/spree_cm_commissioner/import_order_job.rb
|
|
1346
1350
|
- app/jobs/spree_cm_commissioner/integrations/base_job.rb
|
|
1347
1351
|
- app/jobs/spree_cm_commissioner/integrations/cleanup_sync_sessions_job.rb
|
|
@@ -1383,6 +1387,7 @@ files:
|
|
|
1383
1387
|
- app/jobs/spree_cm_commissioner/state_job.rb
|
|
1384
1388
|
- app/jobs/spree_cm_commissioner/subscription_order_executor_job.rb
|
|
1385
1389
|
- app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb
|
|
1390
|
+
- app/jobs/spree_cm_commissioner/telegram_alerts/order_integrity_check_job.rb
|
|
1386
1391
|
- app/jobs/spree_cm_commissioner/telegram_debug_pin_code_sender_job.rb
|
|
1387
1392
|
- app/jobs/spree_cm_commissioner/telegram_gateway/pin_code_sender_job.rb
|
|
1388
1393
|
- app/jobs/spree_cm_commissioner/telegram_notification_sender_job.rb
|
|
@@ -1516,9 +1521,11 @@ files:
|
|
|
1516
1521
|
- app/models/spree_cm_commissioner/hotel_google_wallet.rb
|
|
1517
1522
|
- app/models/spree_cm_commissioner/icon_decorator.rb
|
|
1518
1523
|
- app/models/spree_cm_commissioner/id_card.rb
|
|
1524
|
+
- app/models/spree_cm_commissioner/idempotency_key.rb
|
|
1519
1525
|
- app/models/spree_cm_commissioner/image_decorator.rb
|
|
1520
1526
|
- app/models/spree_cm_commissioner/image_methods_decorator.rb
|
|
1521
1527
|
- app/models/spree_cm_commissioner/import.rb
|
|
1528
|
+
- app/models/spree_cm_commissioner/imported_order.rb
|
|
1522
1529
|
- app/models/spree_cm_commissioner/imports/import_order.rb
|
|
1523
1530
|
- app/models/spree_cm_commissioner/imports/import_payment_reference.rb
|
|
1524
1531
|
- app/models/spree_cm_commissioner/integration.rb
|
|
@@ -2128,6 +2135,7 @@ files:
|
|
|
2128
2135
|
- app/services/spree_cm_commissioner/homepage_data.rb
|
|
2129
2136
|
- app/services/spree_cm_commissioner/homepage_data_loader.rb
|
|
2130
2137
|
- app/services/spree_cm_commissioner/imports/orders/base.rb
|
|
2138
|
+
- app/services/spree_cm_commissioner/imports/orders/cancel.rb
|
|
2131
2139
|
- app/services/spree_cm_commissioner/imports/orders/create.rb
|
|
2132
2140
|
- app/services/spree_cm_commissioner/imports/orders/update.rb
|
|
2133
2141
|
- app/services/spree_cm_commissioner/integrations/base/sync_manager.rb
|
|
@@ -2222,8 +2230,12 @@ files:
|
|
|
2222
2230
|
- app/services/spree_cm_commissioner/signing/verify_signature.rb
|
|
2223
2231
|
- app/services/spree_cm_commissioner/sprite_data_loader_service.rb
|
|
2224
2232
|
- app/services/spree_cm_commissioner/sprite_import_service.rb
|
|
2233
|
+
- app/services/spree_cm_commissioner/telegram_alerts/checks/order_complete_payment_not_paid.rb
|
|
2234
|
+
- app/services/spree_cm_commissioner/telegram_alerts/checks/payment_paid_order_not_complete.rb
|
|
2225
2235
|
- app/services/spree_cm_commissioner/telegram_alerts/event_creation.rb
|
|
2226
2236
|
- app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb
|
|
2237
|
+
- app/services/spree_cm_commissioner/telegram_alerts/order_integrity_alert.rb
|
|
2238
|
+
- app/services/spree_cm_commissioner/telegram_alerts/order_integrity_checks_runner.rb
|
|
2227
2239
|
- app/services/spree_cm_commissioner/telegram_gateway/pin_code_sender.rb
|
|
2228
2240
|
- app/services/spree_cm_commissioner/ticket_transfers/accept.rb
|
|
2229
2241
|
- app/services/spree_cm_commissioner/ticket_transfers/cancel.rb
|
|
@@ -2360,6 +2372,7 @@ files:
|
|
|
2360
2372
|
- app/views/spree/admin/import_existing_orders/index.html.erb
|
|
2361
2373
|
- app/views/spree/admin/import_existing_orders/new.html.erb
|
|
2362
2374
|
- app/views/spree/admin/import_existing_orders/show.html.erb
|
|
2375
|
+
- app/views/spree/admin/import_new_orders/_cancel_modal.html.erb
|
|
2363
2376
|
- app/views/spree/admin/import_new_orders/_form.html.erb
|
|
2364
2377
|
- app/views/spree/admin/import_new_orders/_import_new_order_instruction.html.erb
|
|
2365
2378
|
- app/views/spree/admin/import_new_orders/index.html.erb
|
|
@@ -3254,6 +3267,9 @@ files:
|
|
|
3254
3267
|
- db/migrate/20260610000001_drop_is_open_dated_from_cm_trips.rb
|
|
3255
3268
|
- db/migrate/20260614000001_change_code_limit_in_cm_pin_codes.rb
|
|
3256
3269
|
- db/migrate/20260615000002_add_index_to_code_on_cm_pin_codes.rb
|
|
3270
|
+
- db/migrate/20260616000001_add_canceled_by_id_to_cm_imports.rb
|
|
3271
|
+
- db/migrate/20260617000002_create_cm_imported_orders.rb
|
|
3272
|
+
- db/migrate/20260619000001_create_cm_idempotency_keys.rb
|
|
3257
3273
|
- docker-compose.yml
|
|
3258
3274
|
- docs/api/scoped-access-token-endpoints.md
|
|
3259
3275
|
- docs/option_types/attr_types.md
|