spree_cm_commissioner 2.9.3.pre.pre1 → 2.9.4
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/concerns/spree_cm_commissioner/load_test_account_scope.rb +14 -21
- data/app/controllers/concerns/spree_cm_commissioner/load_test_bypassable.rb +29 -0
- data/app/controllers/spree/api/chatrace/guests_controller.rb +1 -1
- data/app/controllers/spree/api/v2/storefront/account/guest_dynamic_fields_controller.rb +19 -48
- data/app/controllers/spree/api/v2/storefront/account/mark_guest_info_complete_controller.rb +2 -2
- data/app/controllers/spree/api/v2/storefront/account/orders_controller_decorator.rb +5 -15
- data/app/controllers/spree/api/v2/storefront/account/saved_guests_controller.rb +65 -0
- data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +3 -0
- data/app/controllers/spree/api/v2/storefront/guests_controller.rb +10 -9
- data/app/controllers/spree/api/v2/storefront/order_histories_controller.rb +0 -1
- data/app/controllers/spree/api/v2/tenant/account/saved_guests_controller.rb +70 -0
- data/app/controllers/spree/api/v2/tenant/guests_controller.rb +11 -21
- data/app/controllers/spree/api/v2/tenant/order_histories_controller.rb +0 -1
- data/app/controllers/spree_cm_commissioner/api/v2/storefront/checkout_controller_decorator.rb +3 -14
- data/app/jobs/spree_cm_commissioner/maintenance_tasks/guest_data_fill_stage_batch_job.rb +28 -0
- data/app/models/concerns/spree_cm_commissioner/guest_data_fill_stage_concern.rb +82 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +4 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +1 -1
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +13 -31
- data/app/models/spree_cm_commissioner/current.rb +6 -3
- data/app/models/spree_cm_commissioner/dynamic_field.rb +12 -0
- data/app/models/spree_cm_commissioner/guest.rb +1 -144
- data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +43 -71
- data/app/models/spree_cm_commissioner/maintenance_tasks/guest_data_fill_stage.rb +21 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +26 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +0 -22
- data/app/models/spree_cm_commissioner/product_dynamic_field.rb +17 -0
- data/app/models/spree_cm_commissioner/request_guard.rb +19 -0
- data/app/models/spree_cm_commissioner/user_decorator.rb +26 -1
- data/app/queries/spree_cm_commissioner/event_chart_queries.rb +2 -2
- data/app/queries/spree_cm_commissioner/pie_chart_event_aggregator_queries.rb +2 -2
- data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +2 -2
- data/app/serializers/spree/v2/storefront/user_serializer_decorator.rb +3 -1
- data/app/serializers/spree/v2/tenant/saved_guest_serializer.rb +23 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +1 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/guest_serializer.rb +3 -6
- data/app/serializers/spree_cm_commissioner/v2/storefront/ticket_transfer_serializer.rb +2 -2
- data/app/services/spree_cm_commissioner/{cart/create_guest.rb → guests/create.rb} +12 -10
- data/app/services/spree_cm_commissioner/guests/refresh_data_fill_stage.rb +20 -0
- data/app/services/spree_cm_commissioner/guests/send_dynamic_field_notification.rb +15 -0
- data/app/services/spree_cm_commissioner/guests/update.rb +68 -0
- data/app/services/spree_cm_commissioner/inventory_holds/validate_limits.rb +8 -3
- data/app/services/spree_cm_commissioner/load_test/destroy_accounts.rb +7 -10
- data/app/services/spree_cm_commissioner/order/guests/update.rb +5 -8
- data/config/initializers/spree_permitted_attributes.rb +19 -0
- data/config/locales/en.yml +0 -5
- data/config/routes.rb +3 -1
- data/db/migrate/20260714002626_rename_data_fill_stage_phase_on_cm_guests.rb +5 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/tasks/recompute_guest_data_fill_stage.rake +19 -0
- metadata +18 -14
- data/app/controllers/spree/api/v2/storefront/saved_guests_controller.rb +0 -77
- data/app/interactors/spree_cm_commissioner/guest_dynamic_field_notification_sender.rb +0 -11
- data/app/interactors/spree_cm_commissioner/guest_dynamic_fields_manager.rb +0 -33
- data/app/serializers/spree/v2/storefront/order_serializer_decorator.rb +0 -63
- data/app/serializers/spree_cm_commissioner/v2/storefront/guest_minimal_serializer.rb +0 -30
- data/app/serializers/spree_cm_commissioner/v2/storefront/line_item_minimal_serializer.rb +0 -57
- data/app/serializers/spree_cm_commissioner/v2/storefront/ticket_transfer_minimal_serializer.rb +0 -17
- data/app/services/spree_cm_commissioner/update_guest_service.rb +0 -129
- data/app/services/spree_cm_commissioner/users/incomplete_guest_checker_service.rb +0 -34
|
@@ -88,7 +88,7 @@ module SpreeCmCommissioner
|
|
|
88
88
|
|
|
89
89
|
# public_metadata override for a guest count, only when it's a non-negative integer;
|
|
90
90
|
# nil otherwise so callers fall back to the variant default. Keeps number_of_guests >= 0
|
|
91
|
-
# (a negative would make custom_guest_count? false and spin the bump loop in
|
|
91
|
+
# (a negative would make custom_guest_count? false and spin the bump loop in Guests::Create).
|
|
92
92
|
def metadata_guest_count(key)
|
|
93
93
|
return nil unless valid_integer_metadata?(key)
|
|
94
94
|
|
|
@@ -1,10 +1,12 @@
|
|
|
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
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
included do # rubocop:disable Metrics/BlockLength
|
|
8
|
+
after_commit :clear_has_incomplete_guest_info_cache, on: %i[create update]
|
|
9
|
+
|
|
8
10
|
state_machine.before_transition to: :address, do: :hold_order_holds
|
|
9
11
|
|
|
10
12
|
state_machine.before_transition to: :complete, do: :request, if: :need_confirmation?
|
|
@@ -19,7 +21,7 @@ module SpreeCmCommissioner
|
|
|
19
21
|
|
|
20
22
|
state_machine.around_transition to: :complete, do: :unstock_inventory!
|
|
21
23
|
|
|
22
|
-
state_machine.after_transition to: :complete, do: :
|
|
24
|
+
state_machine.after_transition to: :complete, do: :send_dynamic_field_notification_to_guests
|
|
23
25
|
state_machine.after_transition to: :complete, do: :send_transaction_email_to_user, if: :user_has_email?
|
|
24
26
|
|
|
25
27
|
state_machine.before_transition to: :resumed, do: :clear_cancellation_details
|
|
@@ -118,29 +120,6 @@ module SpreeCmCommissioner
|
|
|
118
120
|
end
|
|
119
121
|
end
|
|
120
122
|
|
|
121
|
-
def mark_user_as_has_incomplete_guest_info
|
|
122
|
-
user&.public_metadata&.[]=('has_incomplete_guest_info', true)
|
|
123
|
-
user&.save!
|
|
124
|
-
|
|
125
|
-
line_items.each do |line_item|
|
|
126
|
-
line_item.guests.each do |guest|
|
|
127
|
-
guest.user ||= user
|
|
128
|
-
guest.save!(validate: false) if guest.changed?
|
|
129
|
-
guest.save_and_move_to_next_stage
|
|
130
|
-
# send notification only for guests that still have incomplete dynamic fields
|
|
131
|
-
SpreeCmCommissioner::GuestDynamicFieldNotificationSender.call(guest: guest) if guest_incomplete?(guest)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def has_incomplete_guest_info? # rubocop:disable Naming/PredicateName
|
|
137
|
-
line_items.any? do |line_item|
|
|
138
|
-
line_item.guests.any? do |guest|
|
|
139
|
-
guest_incomplete?(guest)
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
123
|
# allow authorized user to accept all requested line items
|
|
145
124
|
def accepted_by(user)
|
|
146
125
|
transaction do
|
|
@@ -152,12 +131,6 @@ module SpreeCmCommissioner
|
|
|
152
131
|
end
|
|
153
132
|
end
|
|
154
133
|
|
|
155
|
-
def guest_incomplete?(guest)
|
|
156
|
-
(guest.pre_registration_fields? && !guest.pre_registration_completed?) ||
|
|
157
|
-
(guest.post_registration_fields? && !guest.post_registration_completed?) ||
|
|
158
|
-
(guest.during_check_in_fields? && !guest.check_in_completed?)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
134
|
def unstock_inventory!
|
|
162
135
|
ActiveRecord::Base.transaction do
|
|
163
136
|
yield if block_given? # Equal to block.call
|
|
@@ -285,6 +258,15 @@ module SpreeCmCommissioner
|
|
|
285
258
|
@telegram_alert_enabled = line_items.any?(&:enable_telegram_alert?)
|
|
286
259
|
end
|
|
287
260
|
|
|
261
|
+
def send_dynamic_field_notification_to_guests
|
|
262
|
+
incomplete_guests = line_items.flat_map(&:guests).select(&:incomplete?)
|
|
263
|
+
return if incomplete_guests.empty?
|
|
264
|
+
|
|
265
|
+
incomplete_guests.each do |guest|
|
|
266
|
+
SpreeCmCommissioner::Guests::SendDynamicFieldNotification.call(guest: guest)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
288
270
|
def send_order_complete_telegram_alert_to_vendors
|
|
289
271
|
return unless telegram_alert_enabled?
|
|
290
272
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class Current < ActiveSupport::CurrentAttributes
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
3
|
+
# True during a load-test request, letting deep services (e.g.
|
|
4
|
+
# InventoryHolds::ValidateLimits) skip anti-abuse checks without threading
|
|
5
|
+
# the request through them.
|
|
6
|
+
#
|
|
7
|
+
# Thread-safe: CurrentAttributes keeps values per thread/fiber and Rails
|
|
8
|
+
# resets them each request, so concurrent requests never share this value.
|
|
6
9
|
attribute :load_test_bypass
|
|
7
10
|
end
|
|
8
11
|
end
|
|
@@ -18,6 +18,8 @@ module SpreeCmCommissioner
|
|
|
18
18
|
validates :configurations, presence: true, allow_blank: false
|
|
19
19
|
validate :validate_data_fill_stage_phase
|
|
20
20
|
|
|
21
|
+
after_commit :enqueue_data_fill_stage_maintenance_for_products, on: %i[update], if: :saved_change_to_data_fill_stage?
|
|
22
|
+
|
|
21
23
|
scope :with_dynamic_field_options, -> { includes(dynamic_field_options: :dynamic_field) }
|
|
22
24
|
|
|
23
25
|
def requires_dynamic_field_options?
|
|
@@ -45,6 +47,16 @@ module SpreeCmCommissioner
|
|
|
45
47
|
|
|
46
48
|
private
|
|
47
49
|
|
|
50
|
+
# Reassigning which phase this field belongs to affects every product that has it enabled,
|
|
51
|
+
# not just one — same recompute/notify need as ProductDynamicField create.
|
|
52
|
+
def enqueue_data_fill_stage_maintenance_for_products
|
|
53
|
+
product_dynamic_fields.distinct.pluck(:product_id).each do |product_id|
|
|
54
|
+
SpreeCmCommissioner::MaintenanceTasks::GuestDataFillStage
|
|
55
|
+
.pending
|
|
56
|
+
.find_or_create_by(maintainable_type: 'Spree::Product', maintainable_id: product_id)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
48
60
|
def validate_data_fill_stage_phase
|
|
49
61
|
return if data_fill_stage.blank?
|
|
50
62
|
|
|
@@ -4,6 +4,7 @@ module SpreeCmCommissioner
|
|
|
4
4
|
include SpreeCmCommissioner::KycBitwise
|
|
5
5
|
include SpreeCmCommissioner::PhoneNumberSanitizer
|
|
6
6
|
include SpreeCmCommissioner::Integrations::IntegrationMappable
|
|
7
|
+
include SpreeCmCommissioner::GuestDataFillStageConcern
|
|
7
8
|
|
|
8
9
|
delegate :kyc, to: :line_item, allow_nil: true
|
|
9
10
|
delegate :allowed_upload_later?, to: :line_item, allow_nil: true
|
|
@@ -68,7 +69,6 @@ module SpreeCmCommissioner
|
|
|
68
69
|
after_update :preload_order_block_ids, if: :saved_change_to_block_id?
|
|
69
70
|
before_destroy :cancel_reserved_block!, if: -> { reserved_block.present? }
|
|
70
71
|
after_destroy :preload_order_block_ids
|
|
71
|
-
after_commit :update_user_incomplete_guest_info_status, if: :should_update_incomplete_guest_info?
|
|
72
72
|
|
|
73
73
|
validates :bib_index, uniqueness: true, allow_nil: true
|
|
74
74
|
|
|
@@ -368,154 +368,11 @@ module SpreeCmCommissioner
|
|
|
368
368
|
)
|
|
369
369
|
end
|
|
370
370
|
|
|
371
|
-
def all_required_dynamic_fields_completed?(stage)
|
|
372
|
-
missing_dynamic_fields_for_stage(stage).blank?
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
state_machine :data_fill_stage_phase, initial: 'pre_registration' do
|
|
376
|
-
state 'pre_registration', value: 'pre_registration'
|
|
377
|
-
state 'post_registration', value: 'post_registration'
|
|
378
|
-
state 'during_check_in', value: 'during_check_in'
|
|
379
|
-
state 'completed', value: 'completed'
|
|
380
|
-
|
|
381
|
-
event :complete_pre_registration do
|
|
382
|
-
transition 'pre_registration' => 'post_registration',
|
|
383
|
-
if: -> (guest) { guest.pre_registration_completed? && guest.post_registration_fields? }
|
|
384
|
-
transition 'pre_registration' => 'during_check_in',
|
|
385
|
-
if: -> (guest) { guest.pre_registration_completed? && !guest.post_registration_fields? && guest.during_check_in_fields? }
|
|
386
|
-
transition 'pre_registration' => 'completed',
|
|
387
|
-
if: -> (guest) { guest.pre_registration_completed? && !guest.post_registration_fields? && !guest.during_check_in_fields? }
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
event :complete_post_registration do
|
|
391
|
-
transition 'post_registration' => 'during_check_in',
|
|
392
|
-
if: -> (guest) { guest.post_registration_completed? && guest.during_check_in_fields? }
|
|
393
|
-
transition 'post_registration' => 'completed',
|
|
394
|
-
if: -> (guest) { guest.post_registration_completed? && !guest.during_check_in_fields? }
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
event :complete_check_in do
|
|
398
|
-
transition 'during_check_in' => 'completed',
|
|
399
|
-
if: -> (guest) { guest.check_in_completed? }
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
def save_and_move_to_next_stage(_transition = nil)
|
|
404
|
-
# Trigger state machine transitions based on completion status
|
|
405
|
-
case data_fill_stage_phase
|
|
406
|
-
when 'pre_registration'
|
|
407
|
-
complete_pre_registration! if pre_registration_completed?
|
|
408
|
-
when 'post_registration'
|
|
409
|
-
complete_post_registration! if post_registration_completed?
|
|
410
|
-
when 'during_check_in'
|
|
411
|
-
complete_check_in! if check_in_completed?
|
|
412
|
-
end
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
def completed_phases
|
|
416
|
-
phases = []
|
|
417
|
-
phases << 'pre_registration' if pre_registration_completed?
|
|
418
|
-
phases << 'post_registration' if post_registration_completed?
|
|
419
|
-
phases << 'during_check_in' if check_in_completed?
|
|
420
|
-
phases
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
def pre_registration_completed?
|
|
424
|
-
return false unless line_item&.order&.completed?
|
|
425
|
-
return true unless line_item&.product&.dynamic_fields&.pre_registration&.any?
|
|
426
|
-
|
|
427
|
-
required_fields = line_item.product.dynamic_fields.pre_registration.pluck(:id)
|
|
428
|
-
filled_fields = guest_dynamic_fields.where(dynamic_field_id: required_fields).pluck(:dynamic_field_id)
|
|
429
|
-
(required_fields - filled_fields).empty?
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
def post_registration_completed?
|
|
433
|
-
return false unless line_item&.order&.completed?
|
|
434
|
-
return true unless line_item&.product&.dynamic_fields&.post_registration&.any?
|
|
435
|
-
|
|
436
|
-
required_fields = line_item.product.dynamic_fields.post_registration.pluck(:id)
|
|
437
|
-
filled_fields = guest_dynamic_fields.where(dynamic_field_id: required_fields).pluck(:dynamic_field_id)
|
|
438
|
-
(required_fields - filled_fields).empty?
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
def check_in_completed?
|
|
442
|
-
return false unless line_item&.order&.completed?
|
|
443
|
-
return true unless line_item&.product&.dynamic_fields&.during_check_in&.any?
|
|
444
|
-
|
|
445
|
-
required_fields = line_item.product.dynamic_fields.during_check_in.pluck(:id)
|
|
446
|
-
filled_fields = guest_dynamic_fields.where(dynamic_field_id: required_fields).pluck(:dynamic_field_id)
|
|
447
|
-
(required_fields - filled_fields).empty?
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
def pre_registration_fields?
|
|
451
|
-
line_item&.product&.dynamic_fields&.pre_registration&.any?
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
def post_registration_fields?
|
|
455
|
-
line_item&.product&.dynamic_fields&.post_registration&.any?
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
def during_check_in_fields?
|
|
459
|
-
line_item&.product&.dynamic_fields&.during_check_in&.any?
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
def product_dynamic_fields
|
|
463
|
-
@product_dynamic_fields ||= line_item.product.dynamic_fields
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
def missing_dynamic_fields_for_stage(stage)
|
|
467
|
-
return [] if line_item.blank? || line_item.product.blank?
|
|
468
|
-
|
|
469
|
-
# Cache product dynamic fields to avoid multiple queries
|
|
470
|
-
@product_dynamic_fields ||= line_item.product.dynamic_fields
|
|
471
|
-
required_fields = @product_dynamic_fields.where(data_fill_stage: stage)
|
|
472
|
-
filled_ids = guest_dynamic_fields&.pluck(:dynamic_field_id) || []
|
|
473
|
-
|
|
474
|
-
required_fields.reject { |field| filled_ids.include?(field.id) }
|
|
475
|
-
end
|
|
476
|
-
|
|
477
371
|
def reject_guest_dynamic_field?(attributes)
|
|
478
372
|
# Reject if dynamic_field_id is blank or if _destroy is true
|
|
479
373
|
attributes[:dynamic_field_id].blank? || ActiveModel::Type::Boolean.new.cast(attributes[:_destroy])
|
|
480
374
|
end
|
|
481
375
|
|
|
482
|
-
def update_user_incomplete_guest_info_status
|
|
483
|
-
return unless user
|
|
484
|
-
return unless line_item&.order&.completed?
|
|
485
|
-
|
|
486
|
-
user.reload
|
|
487
|
-
|
|
488
|
-
has_incomplete = user.orders.complete.any? do |order|
|
|
489
|
-
order_has_incomplete_guests?(order)
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
current_status = user.public_metadata['has_incomplete_guest_info'] || false
|
|
493
|
-
|
|
494
|
-
return unless current_status != has_incomplete
|
|
495
|
-
|
|
496
|
-
user.public_metadata['has_incomplete_guest_info'] = has_incomplete
|
|
497
|
-
user.save!
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
def order_has_incomplete_guests?(order)
|
|
501
|
-
order.line_items.any? do |line_item|
|
|
502
|
-
line_item.guests.any? do |guest|
|
|
503
|
-
guest_incomplete?(guest)
|
|
504
|
-
end
|
|
505
|
-
end
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
def guest_incomplete?(guest)
|
|
509
|
-
(guest.pre_registration_fields? && !guest.pre_registration_completed?) ||
|
|
510
|
-
(guest.post_registration_fields? && !guest.post_registration_completed?) ||
|
|
511
|
-
(guest.during_check_in_fields? && !guest.check_in_completed?)
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
def should_update_incomplete_guest_info?
|
|
515
|
-
saved_change_to_data_fill_stage_phase? ||
|
|
516
|
-
(guest_dynamic_fields.any? && (guest_dynamic_fields.any?(&:saved_changes?) || guest_dynamic_fields.any?(&:destroyed?)))
|
|
517
|
-
end
|
|
518
|
-
|
|
519
376
|
# Ticket Transfer validations
|
|
520
377
|
def allows_transfer?
|
|
521
378
|
line_item&.product&.allow_transfer?
|
|
@@ -4,26 +4,49 @@ module SpreeCmCommissioner
|
|
|
4
4
|
belongs_to :dynamic_field, class_name: 'SpreeCmCommissioner::DynamicField', optional: false
|
|
5
5
|
belongs_to :dynamic_field_option, class_name: 'SpreeCmCommissioner::DynamicFieldOption', optional: true
|
|
6
6
|
|
|
7
|
+
before_validation :assign_dynamic_field_option, if: :requires_dynamic_field_options?
|
|
8
|
+
|
|
9
|
+
validates :value, presence: true
|
|
10
|
+
|
|
7
11
|
validate :validate_value_format, if: -> { value.present? && dynamic_field.present? }
|
|
8
|
-
validate :
|
|
9
|
-
validate :
|
|
10
|
-
validate :check_required_fields_based_on_phase
|
|
11
|
-
validate :reject_empty_string_for_text_fields
|
|
12
|
+
validate :validate_phase_sequence, if: -> { dynamic_field.present? && guest.present? }
|
|
13
|
+
validate :dynamic_field_unchanged, on: :update
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
# Fires once per changed field in a batch (e.g. Guests::Update saving N rows in one transaction).
|
|
16
|
+
# Each call recomputes the same already-correct value since all N changes are committed by the time
|
|
17
|
+
# any after_commit runs, so only the first call's write is real. Accepted: the alternative is either manual
|
|
18
|
+
# call sites (the exact bug this design replaced) or a fragile transaction-scoped dedup guard.
|
|
19
|
+
after_commit { guest&.refresh_data_fill_stage }
|
|
14
20
|
|
|
15
|
-
scope :with_dynamic_field_and_options,
|
|
16
|
-
includes(:dynamic_field, :dynamic_field_option)
|
|
17
|
-
}
|
|
21
|
+
scope :with_dynamic_field_and_options, -> { includes(:dynamic_field, :dynamic_field_option) }
|
|
18
22
|
|
|
19
23
|
self.whitelisted_ransackable_associations = %w[guest]
|
|
20
24
|
self.whitelisted_ransackable_attributes = %w[value]
|
|
21
25
|
|
|
26
|
+
def requires_dynamic_field_options?
|
|
27
|
+
return false if dynamic_field.blank?
|
|
28
|
+
|
|
29
|
+
dynamic_field.requires_dynamic_field_options?
|
|
30
|
+
end
|
|
31
|
+
|
|
22
32
|
private
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
# If no option is set yet, look it up from `value` -- by option id first, then by
|
|
35
|
+
# option value (unifies the two shapes callers submit a chosen option in: `value: "3"`
|
|
36
|
+
# vs `value: "red"`). Then overwrite `value` with the option's canonical value, so it
|
|
37
|
+
# stays a uniform human-readable string for consumers that read it generically
|
|
38
|
+
# regardless of field type (chatrace payloads, CSV export) instead of a raw option id.
|
|
39
|
+
def assign_dynamic_field_option
|
|
40
|
+
if dynamic_field_option.blank?
|
|
41
|
+
self.dynamic_field_option =
|
|
42
|
+
dynamic_field.dynamic_field_options.find_by(id: value) ||
|
|
43
|
+
dynamic_field.dynamic_field_options.find_by(value: value)
|
|
44
|
+
end
|
|
26
45
|
|
|
46
|
+
self.value = dynamic_field_option.value if dynamic_field_option.present?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate_value_format # rubocop:disable Metrics/CyclomaticComplexity
|
|
27
50
|
case dynamic_field.data_type.to_s
|
|
28
51
|
when 'text', 'textarea'
|
|
29
52
|
errors.add(:value, "can't be blank") if value.is_a?(String) && value.strip == ''
|
|
@@ -32,74 +55,23 @@ module SpreeCmCommissioner
|
|
|
32
55
|
when 'boolean'
|
|
33
56
|
errors.add(:value, 'must be true or false') unless %w[true false].include?(value.to_s.downcase)
|
|
34
57
|
when 'checkbox', 'radio', 'selection'
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def validate_option_value
|
|
40
|
-
if dynamic_field_option.present?
|
|
41
|
-
self.value ||= dynamic_field_option.value
|
|
42
|
-
else
|
|
43
|
-
valid_options = dynamic_field.dynamic_field_options.pluck(:id).map(&:to_s)
|
|
44
|
-
errors.add(:value, 'is not a valid option') unless valid_options.include?(value)
|
|
58
|
+
valid_option = dynamic_field_option.present? && dynamic_field_option.dynamic_field_id == dynamic_field_id
|
|
59
|
+
errors.add(:value, 'is not a valid option') unless valid_option
|
|
45
60
|
end
|
|
46
61
|
end
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return if dynamic_field_option.dynamic_field_id == dynamic_field_id
|
|
51
|
-
|
|
52
|
-
# Only validate if both dynamic_field and dynamic_field_option are persisted
|
|
53
|
-
# This prevents validation errors during creation when associations might not be fully loaded
|
|
54
|
-
return if dynamic_field.new_record? || dynamic_field_option.new_record?
|
|
55
|
-
|
|
56
|
-
errors.add(:dynamic_field_option, 'must belong to the same dynamic field')
|
|
57
|
-
end
|
|
58
|
-
|
|
63
|
+
# Blocks answering a later-stage field (e.g. post_registration) until every earlier stage
|
|
64
|
+
# (e.g. pre_registration) is complete for this guest.
|
|
59
65
|
def validate_phase_sequence
|
|
60
|
-
|
|
66
|
+
stages = GuestDataFillStageConcern::STAGES
|
|
67
|
+
prior_stages = stages.first(stages.index(dynamic_field.data_fill_stage))
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
errors.add(:base, 'Must complete pre_registration fields first') unless guest.pre_registration_completed?
|
|
65
|
-
when :during_check_in
|
|
66
|
-
unless guest.post_registration_completed? || !guest.post_registration_fields?
|
|
67
|
-
errors.add(:base, 'Must complete post_registration fields first')
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def field_required_in_current_phase?
|
|
73
|
-
return false if dynamic_field.blank? || guest.blank?
|
|
74
|
-
|
|
75
|
-
case dynamic_field.data_fill_stage.to_sym
|
|
76
|
-
when :pre_registration, :post_registration
|
|
77
|
-
guest.line_item.order.completed? && guest.check_ins.empty?
|
|
78
|
-
when :during_check_in
|
|
79
|
-
guest.check_ins.exists?
|
|
80
|
-
else
|
|
81
|
-
false
|
|
82
|
-
end
|
|
69
|
+
incomplete = prior_stages.find { |stage| !guest.stage_completed?(stage) }
|
|
70
|
+
errors.add(:base, "Must complete #{incomplete} fields first") if incomplete
|
|
83
71
|
end
|
|
84
72
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
return unless field_required_in_current_phase?
|
|
88
|
-
|
|
89
|
-
errors.add(:value, I18n.t("guest_dynamic_field.errors.#{dynamic_field.data_fill_stage}_required"))
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def sync_guest_data_fill_stage_phase
|
|
93
|
-
guest&.save_and_move_to_next_stage
|
|
94
|
-
guest&.update_user_incomplete_guest_info_status
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def reject_empty_string_for_text_fields
|
|
98
|
-
return if dynamic_field.blank?
|
|
99
|
-
return unless %w[text textarea].include?(dynamic_field.data_type.to_s)
|
|
100
|
-
return unless value.is_a?(String)
|
|
101
|
-
|
|
102
|
-
errors.add(:value, "can't be blank") if value.strip == ''
|
|
73
|
+
def dynamic_field_unchanged
|
|
74
|
+
errors.add(:dynamic_field_id, 'cannot be changed') if dynamic_field_id_changed?
|
|
103
75
|
end
|
|
104
76
|
end
|
|
105
77
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
class GuestDataFillStage < MaintenanceTask
|
|
4
|
+
BATCH_SIZE = ENV.fetch('MAINTENANCE_TASKS_GUEST_DATA_FILL_STAGE_BATCH_SIZE', '300').to_i
|
|
5
|
+
|
|
6
|
+
def maintain
|
|
7
|
+
product = maintainable
|
|
8
|
+
|
|
9
|
+
SpreeCmCommissioner::Guest
|
|
10
|
+
.joins(line_item: %i[variant order])
|
|
11
|
+
.where(spree_variants: { product_id: product.id })
|
|
12
|
+
.merge(Spree::Order.complete)
|
|
13
|
+
.order(:id)
|
|
14
|
+
.pluck(:id)
|
|
15
|
+
.each_slice(BATCH_SIZE) do |guest_ids|
|
|
16
|
+
SpreeCmCommissioner::MaintenanceTasks::GuestDataFillStageBatchJob.perform_later(guest_ids: guest_ids, notify: true)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -259,6 +259,32 @@ module SpreeCmCommissioner
|
|
|
259
259
|
guests.exists?(state: %i[locked transferred])
|
|
260
260
|
end
|
|
261
261
|
|
|
262
|
+
def has_incomplete_guest_info? # rubocop:disable Naming/PredicateName
|
|
263
|
+
# expires_in is a safety net, not the primary invalidation path — clear_has_incomplete_guest_info_cache
|
|
264
|
+
# is still called explicitly wherever the underlying guest data can change. This just bounds
|
|
265
|
+
# how long a missed invalidation could leave a stale value in place.
|
|
266
|
+
#
|
|
267
|
+
# Reads the persisted data_fill_stage column via a join instead of Guest#incomplete? (which
|
|
268
|
+
# recomputes live per guest). post_registration is the only stage that matters here: a
|
|
269
|
+
# completed order already forces pre_registration complete (guest_data_fill_stage_concern.rb),
|
|
270
|
+
# and during_check_in is check-in crew's concern, not something this banner surfaces —
|
|
271
|
+
# matches find_orders_with_incomplete_guest_info in orders_controller_decorator.rb.
|
|
272
|
+
Rails.cache.fetch(incomplete_guest_info_cache_key, expires_in: 1.hour) do
|
|
273
|
+
guests.post_registration.exists?
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Clears this order's own cache plus the purchaser's cross-order aggregate cache —
|
|
278
|
+
# anything that can change the former can also change the latter.
|
|
279
|
+
def clear_has_incomplete_guest_info_cache
|
|
280
|
+
Rails.cache.delete(incomplete_guest_info_cache_key)
|
|
281
|
+
user&.clear_has_incomplete_guest_info_cache
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def incomplete_guest_info_cache_key
|
|
285
|
+
"order/#{id}/has_incomplete_guest_info"
|
|
286
|
+
end
|
|
287
|
+
|
|
262
288
|
def generate_svg_qr
|
|
263
289
|
qrcode = RQRCode::QRCode.new(qr_data)
|
|
264
290
|
qrcode.as_svg(
|
|
@@ -183,28 +183,6 @@ module SpreeCmCommissioner
|
|
|
183
183
|
end
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
def dynamic_fields_by_collection_phase
|
|
187
|
-
{
|
|
188
|
-
pre_registration: dynamic_fields.pre_registration.includes(:dynamic_field_options).order(:position),
|
|
189
|
-
post_registration: dynamic_fields.post_registration.includes(:dynamic_field_options).order(:position),
|
|
190
|
-
during_check_in: dynamic_fields.during_check_in.includes(:dynamic_field_options).order(:position)
|
|
191
|
-
}
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def required_dynamic_fields_completed?(guest, phase)
|
|
195
|
-
required_fields = dynamic_fields.where(data_fill_stage: phase)
|
|
196
|
-
return true if required_fields.empty?
|
|
197
|
-
|
|
198
|
-
filled_ids = guest.guest_dynamic_fields.where(dynamic_field_id: required_fields.select(:id)).pluck(:dynamic_field_id)
|
|
199
|
-
(required_fields.pluck(:id) - filled_ids).empty?
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def required_fields_for_guest(guest)
|
|
203
|
-
dynamic_fields_by_collection_phase.transform_values do |fields|
|
|
204
|
-
fields.select { |field| guest.guest_dynamic_fields.where(dynamic_field: field).empty? }
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
186
|
private
|
|
209
187
|
|
|
210
188
|
def set_tenant
|
|
@@ -4,5 +4,22 @@ module SpreeCmCommissioner
|
|
|
4
4
|
belongs_to :dynamic_field, class_name: 'SpreeCmCommissioner::DynamicField', optional: false
|
|
5
5
|
|
|
6
6
|
validates :dynamic_field_id, uniqueness: { scope: :product_id }
|
|
7
|
+
|
|
8
|
+
after_commit :enqueue_data_fill_stage_maintenance, on: %i[create]
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Only newly-added fields need active recompute: adding a field can make an
|
|
13
|
+
# already-complete guest newly incomplete, which needs a persisted-column fix and a
|
|
14
|
+
# catch-up notification. Removing a field can only shrink the required set, which
|
|
15
|
+
# stage_completed? already reflects correctly the instant it's checked live (no
|
|
16
|
+
# GuestDynamicField answers get destroyed by unlinking a field from a product) — no guest
|
|
17
|
+
# is ever made worse off, and nothing needs notifying, so it's not worth an async trigger.
|
|
18
|
+
# The persisted data_fill_stage column self-heals on that guest's next natural write.
|
|
19
|
+
def enqueue_data_fill_stage_maintenance
|
|
20
|
+
SpreeCmCommissioner::MaintenanceTasks::GuestDataFillStage
|
|
21
|
+
.pending
|
|
22
|
+
.find_or_create_by(maintainable_type: 'Spree::Product', maintainable_id: product_id)
|
|
23
|
+
end
|
|
7
24
|
end
|
|
8
25
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
# Runs a protected block only for normal requests, so callers don't touch
|
|
3
|
+
# Current.load_test_bypass directly. Exempt requests (e.g. trusted load tests)
|
|
4
|
+
# skip the block and get nil back.
|
|
5
|
+
#
|
|
6
|
+
# include SpreeCmCommissioner::RequestGuard
|
|
7
|
+
#
|
|
8
|
+
# with_request_guard { validate_ip_rate_limit!(order) }
|
|
9
|
+
module RequestGuard
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Runs the block unless the request is exempt; returns its value, or nil.
|
|
13
|
+
def with_request_guard
|
|
14
|
+
return nil if SpreeCmCommissioner::Current.load_test_bypass
|
|
15
|
+
|
|
16
|
+
yield
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -52,7 +52,6 @@ module SpreeCmCommissioner
|
|
|
52
52
|
base.scope :by_non_tenant, -> { where(tenant_id: nil) }
|
|
53
53
|
base.scope :vendor_users, -> (vendor_id) { joins(:role_users => :role).where(spree_roles: { vendor_id: vendor_id }).distinct }
|
|
54
54
|
|
|
55
|
-
base.store :public_metadata, accessors: [:has_incomplete_guest_info], coder: JSON
|
|
56
55
|
base.store :private_metadata, accessors: %i[otp_secret otp_required_for_login consumed_timestep], coder: JSON
|
|
57
56
|
|
|
58
57
|
base.store_public_metadata :primary_service_type, :string
|
|
@@ -66,6 +65,15 @@ module SpreeCmCommissioner
|
|
|
66
65
|
# the id_token param. Nothing token-related is stored on the account itself.
|
|
67
66
|
base.store_private_metadata :load_test_account, :boolean, default: false
|
|
68
67
|
|
|
68
|
+
# SQL counterpart of `load_test_account?`, so the flag can be filtered in the DB instead of
|
|
69
|
+
# loading rows into Ruby to call the predicate. private_metadata is stored double-encoded (a
|
|
70
|
+
# JSON string scalar, not a native jsonb object — see store_metadata), so unwrap the outer
|
|
71
|
+
# string with `#>> '{}'` before keying into it. Keeps the storage detail in one place next to
|
|
72
|
+
# the declaration above rather than duplicating the raw SQL in every caller.
|
|
73
|
+
base.scope :load_test_accounts, lambda {
|
|
74
|
+
where("(private_metadata #>> '{}')::jsonb ->> 'load_test_account' = 'true'")
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
# Validations
|
|
70
78
|
base.validates :primary_service_type,
|
|
71
79
|
inclusion: { in: SpreeCmCommissioner::ServiceType::SERVICE_TYPES.map(&:to_s) },
|
|
@@ -223,6 +231,23 @@ module SpreeCmCommissioner
|
|
|
223
231
|
super(request)
|
|
224
232
|
end
|
|
225
233
|
end
|
|
234
|
+
|
|
235
|
+
def has_incomplete_guest_info? # rubocop:disable Naming/PredicateName
|
|
236
|
+
# expires_in is a safety net, not the primary invalidation path — see the matching comment
|
|
237
|
+
# on Order#has_incomplete_guest_info?. Joins straight to guests.post_registration instead of
|
|
238
|
+
# calling Order#has_incomplete_guest_info? per order (which would be N+1 query calls).
|
|
239
|
+
Rails.cache.fetch(incomplete_guest_info_cache_key, expires_in: 1.hour) do
|
|
240
|
+
orders.complete.joins(:guests).merge(SpreeCmCommissioner::Guest.post_registration).exists?
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def clear_has_incomplete_guest_info_cache
|
|
245
|
+
Rails.cache.delete(incomplete_guest_info_cache_key)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def incomplete_guest_info_cache_key
|
|
249
|
+
"user/#{id}/has_incomplete_guest_info"
|
|
250
|
+
end
|
|
226
251
|
end
|
|
227
252
|
end
|
|
228
253
|
|
|
@@ -59,7 +59,7 @@ module SpreeCmCommissioner
|
|
|
59
59
|
WHEN cm_check_ins.id IS NOT NULL THEN 'show'
|
|
60
60
|
ELSE 'no_show'
|
|
61
61
|
END AS name,
|
|
62
|
-
COUNT(
|
|
62
|
+
COUNT(DISTINCT cm_guests.id) AS total"
|
|
63
63
|
)
|
|
64
64
|
.group('spree_products.id, CASE WHEN cm_check_ins.id IS NOT NULL THEN \'show\' ELSE \'no_show\' END')
|
|
65
65
|
.group_by(&:product_id)
|
|
@@ -98,7 +98,7 @@ module SpreeCmCommissioner
|
|
|
98
98
|
WHEN cm_check_ins.entry_type = 0 THEN 'normal'
|
|
99
99
|
WHEN cm_check_ins.entry_type = 1 THEN 'VIP'
|
|
100
100
|
END AS name,
|
|
101
|
-
COUNT(
|
|
101
|
+
COUNT(DISTINCT cm_guests.id) AS total"
|
|
102
102
|
)
|
|
103
103
|
.group('spree_products.id, cm_check_ins.entry_type')
|
|
104
104
|
.group_by(&:product_id)
|
|
@@ -58,7 +58,7 @@ module SpreeCmCommissioner
|
|
|
58
58
|
WHEN cm_check_ins.id IS NOT NULL THEN 'show'
|
|
59
59
|
ELSE 'no_show'
|
|
60
60
|
END AS name,
|
|
61
|
-
COUNT(
|
|
61
|
+
COUNT(DISTINCT cm_guests.id) AS total"
|
|
62
62
|
)
|
|
63
63
|
.group('spree_products.id, CASE WHEN cm_check_ins.id IS NOT NULL THEN \'show\' ELSE \'no_show\' END')
|
|
64
64
|
.group_by(&:product_id)
|
|
@@ -110,7 +110,7 @@ module SpreeCmCommissioner
|
|
|
110
110
|
CASE WHEN cm_check_ins.entry_type = 0 THEN 'normal'
|
|
111
111
|
WHEN cm_check_ins.entry_type = 1 THEN 'VIP'
|
|
112
112
|
ELSE NULL END AS name,
|
|
113
|
-
COUNT(
|
|
113
|
+
COUNT(DISTINCT cm_guests.id) AS total"
|
|
114
114
|
)
|
|
115
115
|
.where.not(cm_check_ins: { id: nil })
|
|
116
116
|
.group('spree_products.id, cm_check_ins.entry_type')
|