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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/concerns/spree_cm_commissioner/load_test_account_scope.rb +14 -21
  4. data/app/controllers/concerns/spree_cm_commissioner/load_test_bypassable.rb +29 -0
  5. data/app/controllers/spree/api/chatrace/guests_controller.rb +1 -1
  6. data/app/controllers/spree/api/v2/storefront/account/guest_dynamic_fields_controller.rb +19 -48
  7. data/app/controllers/spree/api/v2/storefront/account/mark_guest_info_complete_controller.rb +2 -2
  8. data/app/controllers/spree/api/v2/storefront/account/orders_controller_decorator.rb +5 -15
  9. data/app/controllers/spree/api/v2/storefront/account/saved_guests_controller.rb +65 -0
  10. data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +3 -0
  11. data/app/controllers/spree/api/v2/storefront/guests_controller.rb +10 -9
  12. data/app/controllers/spree/api/v2/storefront/order_histories_controller.rb +0 -1
  13. data/app/controllers/spree/api/v2/tenant/account/saved_guests_controller.rb +70 -0
  14. data/app/controllers/spree/api/v2/tenant/guests_controller.rb +11 -21
  15. data/app/controllers/spree/api/v2/tenant/order_histories_controller.rb +0 -1
  16. data/app/controllers/spree_cm_commissioner/api/v2/storefront/checkout_controller_decorator.rb +3 -14
  17. data/app/jobs/spree_cm_commissioner/maintenance_tasks/guest_data_fill_stage_batch_job.rb +28 -0
  18. data/app/models/concerns/spree_cm_commissioner/guest_data_fill_stage_concern.rb +82 -0
  19. data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +4 -0
  20. data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +1 -1
  21. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +13 -31
  22. data/app/models/spree_cm_commissioner/current.rb +6 -3
  23. data/app/models/spree_cm_commissioner/dynamic_field.rb +12 -0
  24. data/app/models/spree_cm_commissioner/guest.rb +1 -144
  25. data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +43 -71
  26. data/app/models/spree_cm_commissioner/maintenance_tasks/guest_data_fill_stage.rb +21 -0
  27. data/app/models/spree_cm_commissioner/order_decorator.rb +26 -0
  28. data/app/models/spree_cm_commissioner/product_decorator.rb +0 -22
  29. data/app/models/spree_cm_commissioner/product_dynamic_field.rb +17 -0
  30. data/app/models/spree_cm_commissioner/request_guard.rb +19 -0
  31. data/app/models/spree_cm_commissioner/user_decorator.rb +26 -1
  32. data/app/queries/spree_cm_commissioner/event_chart_queries.rb +2 -2
  33. data/app/queries/spree_cm_commissioner/pie_chart_event_aggregator_queries.rb +2 -2
  34. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +2 -2
  35. data/app/serializers/spree/v2/storefront/user_serializer_decorator.rb +3 -1
  36. data/app/serializers/spree/v2/tenant/saved_guest_serializer.rb +23 -0
  37. data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +1 -0
  38. data/app/serializers/spree_cm_commissioner/v2/storefront/guest_serializer.rb +3 -6
  39. data/app/serializers/spree_cm_commissioner/v2/storefront/ticket_transfer_serializer.rb +2 -2
  40. data/app/services/spree_cm_commissioner/{cart/create_guest.rb → guests/create.rb} +12 -10
  41. data/app/services/spree_cm_commissioner/guests/refresh_data_fill_stage.rb +20 -0
  42. data/app/services/spree_cm_commissioner/guests/send_dynamic_field_notification.rb +15 -0
  43. data/app/services/spree_cm_commissioner/guests/update.rb +68 -0
  44. data/app/services/spree_cm_commissioner/inventory_holds/validate_limits.rb +8 -3
  45. data/app/services/spree_cm_commissioner/load_test/destroy_accounts.rb +7 -10
  46. data/app/services/spree_cm_commissioner/order/guests/update.rb +5 -8
  47. data/config/initializers/spree_permitted_attributes.rb +19 -0
  48. data/config/locales/en.yml +0 -5
  49. data/config/routes.rb +3 -1
  50. data/db/migrate/20260714002626_rename_data_fill_stage_phase_on_cm_guests.rb +5 -0
  51. data/lib/spree_cm_commissioner/version.rb +1 -1
  52. data/lib/tasks/recompute_guest_data_fill_stage.rake +19 -0
  53. metadata +18 -14
  54. data/app/controllers/spree/api/v2/storefront/saved_guests_controller.rb +0 -77
  55. data/app/interactors/spree_cm_commissioner/guest_dynamic_field_notification_sender.rb +0 -11
  56. data/app/interactors/spree_cm_commissioner/guest_dynamic_fields_manager.rb +0 -33
  57. data/app/serializers/spree/v2/storefront/order_serializer_decorator.rb +0 -63
  58. data/app/serializers/spree_cm_commissioner/v2/storefront/guest_minimal_serializer.rb +0 -30
  59. data/app/serializers/spree_cm_commissioner/v2/storefront/line_item_minimal_serializer.rb +0 -57
  60. data/app/serializers/spree_cm_commissioner/v2/storefront/ticket_transfer_minimal_serializer.rb +0 -17
  61. data/app/services/spree_cm_commissioner/update_guest_service.rb +0 -129
  62. data/app/services/spree_cm_commissioner/users/incomplete_guest_checker_service.rb +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cde283f01b472fecef2447fa4f2ecfbd2e89126a5c0100f6eb0da02d8470a81
4
- data.tar.gz: 115e564d261168faef7c209fc743b7f57ef2914afbfaac75de7668d1d1579865
3
+ metadata.gz: 73795eb4d207d3bf219b7c5913512b6b82ff71b71f5f9c5ea41acd18a016ade6
4
+ data.tar.gz: 44ed0c0b84771995b3f858d6d3db3e5c8feb4cb872a5d23981022837c2c8f847
5
5
  SHA512:
6
- metadata.gz: 61475e20da1c597866e2a2bf80d05ec60cbb5b26082c8103f002abde91d129738eb7d483c0955d66c0b1045d689ac4f68430888905ef638ddea162cf48d847b8
7
- data.tar.gz: 659074571ef7ae8f01b2fe5737a4f569edb7986bd7d33bbb6e99342956bc211c6d0c85894feb5e4511557b22670711874d8316bcdc5f9deac5ffc63d446bc1c6
6
+ metadata.gz: 56fa3d866b2de53c7f58978403eb1a04c26a0da14a95d4a8447502dfed4123db703e84ef733385aa0b271774940031a362096a10b3606f0b6dda63dbcb3136ba
7
+ data.tar.gz: 8343f2ec6ff570e80a833272763431b699d68b926648f5b0a98a8929fd321aa4df7636461cbd2b94c8685dfaef162131440cdd5a02a3610ab1a87ea090d7a02d
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.9.3.pre.pre1)
37
+ spree_cm_commissioner (2.9.4)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -10,37 +10,30 @@ module SpreeCmCommissioner
10
10
  raise NotImplementedError, "#{self.class} must implement #load_test_tenant"
11
11
  end
12
12
 
13
- # Scan cap for the tenant-scoped candidate set below — bounds the query while still comfortably
14
- # covering realistic load-test pool sizes.
15
- EXPORT_SCAN_LIMIT = 2000
16
-
17
- DEFAULT_EXPORT_PER_PAGE = 20
13
+ DEFAULT_EXPORT_PER_PAGE = 10
18
14
 
19
15
  # Queried fresh from the DB every time, so it's always in sync with the current pool rather than
20
16
  # a stashed file or session state.
21
17
  #
22
- # `load_test_account` lives in the `private_metadata` jsonb store (see UserDecorator's
23
- # `store_private_metadata`), not a real column, so it can't be filtered in SQL directly
24
- # `store ..., coder: JSON` on top of an already-jsonb column double-encodes the value as a JSON
25
- # string scalar rather than a native object, which breaks Postgres's `@>`/`->>` jsonb operators.
26
- # Filter with the model's own `load_test_account?` in Ruby instead, which deserializes it
27
- # correctly regardless of that storage quirk.
18
+ # Filters the `load_test_account` flag in SQL via the model's `load_test_accounts` scope (see
19
+ # UserDecorator), so only the tenant's load-test accounts are considered never the whole users
20
+ # table. Unlike the old `order(:id).limit(2000).select(&:...)` approach, there's no scan cap:
21
+ # newly created accounts (highest ids, ordered first below) always show up even on tenants with
22
+ # far more than 2,000 users.
28
23
  #
29
- # Filtering happens in Ruby, so this is a plain Array by the time it needs paging — wrap it
30
- # with Kaminari.paginate_array (same pattern as VoteLogsController) to get the same
31
- # `page`/`per_page` params and `paginate` view helper every other index page in this app uses,
32
- # instead of hand-rolled pagination just for this one page.
24
+ # Kaminari pages on the relation itself, so the DB only ever loads the current page's rows
25
+ # (`LIMIT`/`OFFSET`) never the full pool into memory — via the same `page`/`per_page` params
26
+ # and `paginate` view helper every other index page in this app uses.
33
27
  #
34
28
  # Shared between LoadTestAccountManagement (lists/mutates the pool) and
35
29
  # LoadTestLoginTokenGeneration (mints tokens for exactly the page shown), so both controllers
36
30
  # resolve the same page the same way.
37
31
  def export_accounts
38
- matches = Spree::User.where(tenant_id: load_test_tenant&.id)
39
- .order(:id)
40
- .limit(EXPORT_SCAN_LIMIT)
41
- .select(&:load_test_account?)
42
-
43
- Kaminari.paginate_array(matches).page(params[:page]).per(params[:per_page] || DEFAULT_EXPORT_PER_PAGE)
32
+ Spree::User.where(tenant_id: load_test_tenant&.id)
33
+ .load_test_accounts
34
+ .order(id: :desc)
35
+ .page(params[:page])
36
+ .per(params[:per_page] || DEFAULT_EXPORT_PER_PAGE)
44
37
  end
45
38
  end
46
39
  end
@@ -0,0 +1,29 @@
1
+ module SpreeCmCommissioner
2
+ # Sets Current.load_test_bypass when the request carries the correct X-Request-Verify secret, so
3
+ # anti-abuse services (e.g. InventoryHolds::ValidateLimits) can skip their checks for trusted
4
+ # load tests. No-op unless LOAD_TEST_BYPASS_KEY is set. Include in any controller that can
5
+ # trigger such a check.
6
+ module LoadTestBypassable
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ before_action :set_load_test_bypass_current
11
+ end
12
+
13
+ private
14
+
15
+ # Always assigns the flag (false unless the request carries the correct
16
+ # secret), so its value never leaks from a previous request.
17
+ def set_load_test_bypass_current
18
+ SpreeCmCommissioner::Current.load_test_bypass = load_test_bypass_requested?
19
+ end
20
+
21
+ def load_test_bypass_requested?
22
+ key = ENV.fetch('LOAD_TEST_BYPASS_KEY', nil)
23
+ return false if key.blank?
24
+
25
+ provided = request.headers.fetch('X-Request-Verify', nil).to_s
26
+ ActiveSupport::SecurityUtils.secure_compare(provided, key)
27
+ end
28
+ end
29
+ end
@@ -53,7 +53,7 @@ module Spree
53
53
  preferences upload_later social_contact_platform user_id
54
54
  bib_number bib_prefix bib_index public_metadata private_metadata
55
55
  seat_number intel_phone_number block_id saved_guest_id
56
- data_fill_stage_phase
56
+ data_fill_stage
57
57
  ].freeze
58
58
 
59
59
  def guest_base_columns
@@ -4,69 +4,40 @@ module Spree
4
4
  module Storefront
5
5
  module Account
6
6
  class GuestDynamicFieldsController < ::Spree::Api::V2::ResourceController
7
+ ALLOWED_PHASES = %w[pre_registration post_registration].freeze
8
+
7
9
  before_action :require_spree_current_user
8
10
  before_action :load_guest
9
11
 
10
12
  def update_dynamic_field
11
- phase = params[:phase]
12
-
13
- return render_error_payload('Invalid phase', 422) unless %w[pre_registration post_registration].include?(phase)
14
-
15
- begin
16
- fields = dynamic_fields_params
17
- rescue ActionController::ParameterMissing
18
- return render_error_payload('No dynamic fields provided', 422)
19
- end
20
-
21
- return render_error_payload('No dynamic fields provided', 422) if fields.blank?
22
-
23
- ActiveRecord::Base.transaction do
24
- fields.each do |field|
25
- process_dynamic_field(field)
26
- end
27
-
28
- @guest.save_and_move_to_next_stage
13
+ return render_error_payload('Invalid phase') unless ALLOWED_PHASES.include?(params[:phase])
14
+ return render_error_payload('No dynamic fields provided') if dynamic_fields_params.blank?
15
+
16
+ result = SpreeCmCommissioner::Guests::Update.call(
17
+ guest: @guest,
18
+ guest_params: {
19
+ guest_dynamic_fields_attributes: dynamic_fields_params.map { |f| { dynamic_field_id: f[:id], value: f[:value] } }
20
+ }
21
+ )
22
+
23
+ if result.success?
24
+ render_serialized_payload { serialize_guest(result.value[:guest]) }
25
+ else
26
+ render_error_payload(result.error)
29
27
  end
30
-
31
- render_serialized_payload { serialize_guest(@guest) }
32
- rescue ActiveRecord::RecordInvalid => e
33
- render_error_payload(e.record.errors, 422)
34
- rescue ActiveRecord::RecordNotFound => e
35
- render_error_payload(e.message, 404)
36
28
  end
37
29
 
38
30
  private
39
31
 
40
32
  def dynamic_fields_params
41
- params.require(:dynamic_fields).map do |field|
42
- field.permit(:id, :value)
43
- end
33
+ Array(params[:dynamic_fields]).map { |field| field.permit(:id, :value) }
44
34
  end
45
35
 
46
36
  def load_guest
47
37
  @guest = SpreeCmCommissioner::Guest.find(params[:guest_id])
48
38
 
49
- unless spree_current_user.guests.exists?(id: @guest.id) ||
50
- @guest.line_item.order.user == spree_current_user
51
- render_error_payload('Unauthorized', 401)
52
- end
53
- end
54
-
55
- def process_dynamic_field(field)
56
- dynamic_field = SpreeCmCommissioner::DynamicField.find(field[:id])
57
- guest_field = @guest.guest_dynamic_fields.build(dynamic_field_id: dynamic_field.id)
58
-
59
- if dynamic_field.requires_dynamic_field_options?
60
- option = dynamic_field.dynamic_field_options.find_by(value: field[:value])
61
- raise ActiveRecord::RecordNotFound, "Invalid value for dynamic field #{dynamic_field.label}" unless option
62
-
63
- guest_field.dynamic_field_option = option
64
- guest_field.value = option.value
65
- else
66
- guest_field.value = field[:value]
67
- end
68
-
69
- guest_field.save!
39
+ raise CanCan::AccessDenied unless spree_current_user.guests.exists?(id: @guest.id) ||
40
+ @guest.line_item.order.user == spree_current_user
70
41
  end
71
42
 
72
43
  def serialize_guest(guest)
@@ -7,7 +7,7 @@ module Spree
7
7
  before_action :require_spree_current_user
8
8
 
9
9
  def update
10
- SpreeCmCommissioner::Users::IncompleteGuestCheckerService.new(spree_current_user).call
10
+ spree_current_user.clear_has_incomplete_guest_info_cache
11
11
  render_success
12
12
  end
13
13
 
@@ -18,7 +18,7 @@ module Spree
18
18
  {
19
19
  success: true,
20
20
  message: 'Guest information marked as complete',
21
- has_incomplete_guest_info: false
21
+ has_incomplete_guest_info: spree_current_user.has_incomplete_guest_info?
22
22
  }
23
23
  end
24
24
  end
@@ -24,21 +24,11 @@ module Spree
24
24
  private
25
25
 
26
26
  def find_orders_with_incomplete_guest_info
27
- orders = spree_current_user.orders
28
- .complete
29
- .includes(line_items: { guests: [:guest_dynamic_fields] })
30
-
31
- incomplete_order_ids = orders.select do |order|
32
- order.line_items.any? do |line_item|
33
- line_item.guests.any? do |guest|
34
- guest.pre_registration_completed? &&
35
- guest.post_registration_fields? &&
36
- !guest.post_registration_completed?
37
- end
38
- end
39
- end.map(&:id)
40
-
41
- spree_current_user.orders.where(id: incomplete_order_ids)
27
+ spree_current_user.orders
28
+ .complete
29
+ .joins(:guests)
30
+ .merge(SpreeCmCommissioner::Guest.post_registration)
31
+ .distinct
42
32
  end
43
33
 
44
34
  def collection_finder
@@ -0,0 +1,65 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ module Account
6
+ class SavedGuestsController < ::Spree::Api::V2::ResourceController
7
+ before_action :require_spree_current_user
8
+ before_action :load_saved_guest, only: %i[show update destroy]
9
+
10
+ def collection
11
+ spree_current_user.saved_guests.order(created_at: :desc)
12
+ .page(params[:page])
13
+ .per(params[:per_page])
14
+ end
15
+
16
+ def create
17
+ saved_guest = spree_current_user.saved_guests.new(saved_guest_params)
18
+
19
+ if saved_guest.save
20
+ render_serialized_payload(201) { serialize_resource(saved_guest) }
21
+ else
22
+ render_error_payload(saved_guest.errors.full_messages.to_sentence, 422)
23
+ end
24
+ end
25
+
26
+ def update
27
+ if @saved_guest.update(saved_guest_params)
28
+ render_serialized_payload { serialize_resource(@saved_guest) }
29
+ else
30
+ render_error_payload(@saved_guest.errors.full_messages.to_sentence, 422)
31
+ end
32
+ end
33
+
34
+ def destroy
35
+ @saved_guest.destroy
36
+ head :no_content
37
+ end
38
+
39
+ private
40
+
41
+ def model_class
42
+ SpreeCmCommissioner::SavedGuest
43
+ end
44
+
45
+ def collection_serializer
46
+ resource_serializer
47
+ end
48
+
49
+ def resource_serializer
50
+ SpreeCmCommissioner::V2::Storefront::SavedGuestSerializer
51
+ end
52
+
53
+ def load_saved_guest
54
+ @saved_guest = spree_current_user.saved_guests.find(params[:id])
55
+ end
56
+
57
+ def saved_guest_params
58
+ params.require(:saved_guest).permit(*::Spree::PermittedAttributes.saved_guest_attributes)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -3,6 +3,9 @@ module Spree
3
3
  module V2
4
4
  module Storefront
5
5
  class DraftOrdersController < ::Spree::Api::V2::ResourceController
6
+ # Creating the order acquires the inventory hold, so honor the load-test bypass here too.
7
+ include SpreeCmCommissioner::LoadTestBypassable
8
+
6
9
  # Creates a generic draft order from a locally-assembled cart in a single request.
7
10
  # Endpoint: POST /api/v2/storefront/draft_orders
8
11
  # Params:
@@ -31,26 +31,27 @@ module Spree
31
31
  def create
32
32
  spree_authorize! :update, spree_current_order, order_token
33
33
 
34
- result = SpreeCmCommissioner::Cart::CreateGuest.call(
34
+ result = SpreeCmCommissioner::Guests::Create.call(
35
35
  line_item: parent,
36
- options: guest_params
36
+ guest_params: guest_params
37
37
  )
38
38
 
39
39
  if result.success?
40
- render_serialized_payload(201) { serialize_resource(result.value) }
40
+ render_serialized_payload(201) { serialize_resource(result.value[:guest]) }
41
41
  else
42
- render_error_payload(result.value.errors, 422)
42
+ render_error_payload(result.error || result.value.errors, 422)
43
43
  end
44
44
  end
45
45
 
46
46
  def update
47
47
  spree_authorize! :update, spree_current_order, order_token unless guest_for_update.pre_registration?
48
48
 
49
- begin
50
- SpreeCmCommissioner::UpdateGuestService.new(guest_for_update, guest_params).call
51
- render_serialized_payload { serialize_resource(guest_for_update) }
52
- rescue ActiveRecord::RecordInvalid => e
53
- render_error_payload(e.record.errors, 422)
49
+ result = SpreeCmCommissioner::Guests::Update.call(guest: guest_for_update, guest_params: guest_params)
50
+
51
+ if result.success?
52
+ render_serialized_payload { serialize_resource(result.value[:guest]) }
53
+ else
54
+ render_error_payload(result.error || result.value&.errors, 422)
54
55
  end
55
56
  end
56
57
 
@@ -18,7 +18,6 @@ module Spree
18
18
  order = Spree::Order.not_archived.find_by!(token: order_token)
19
19
 
20
20
  spree_authorize! :update, order, order_token
21
- raise CanCan::AccessDenied unless order.payment?
22
21
 
23
22
  if order.mark_as_archive
24
23
  render_serialized_payload { serialize_resource(order) }
@@ -0,0 +1,70 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ module Account
6
+ class SavedGuestsController < BaseController
7
+ around_action :wrap_with_multitenant_without
8
+ before_action :require_spree_current_user
9
+ before_action :load_saved_guest, only: %i[show update destroy]
10
+
11
+ def collection
12
+ spree_current_user.saved_guests.order(created_at: :desc)
13
+ .page(params[:page])
14
+ .per(params[:per_page])
15
+ end
16
+
17
+ def create
18
+ saved_guest = spree_current_user.saved_guests.new(saved_guest_params)
19
+
20
+ if saved_guest.save
21
+ render_serialized_payload(201) { serialize_resource(saved_guest) }
22
+ else
23
+ render_error_payload(saved_guest.errors.full_messages.to_sentence, 422)
24
+ end
25
+ end
26
+
27
+ def update
28
+ if @saved_guest.update(saved_guest_params)
29
+ render_serialized_payload { serialize_resource(@saved_guest) }
30
+ else
31
+ render_error_payload(@saved_guest.errors.full_messages.to_sentence, 422)
32
+ end
33
+ end
34
+
35
+ def destroy
36
+ @saved_guest.destroy
37
+ head :no_content
38
+ end
39
+
40
+ private
41
+
42
+ def wrap_with_multitenant_without(&block)
43
+ MultiTenant.without(&block)
44
+ end
45
+
46
+ def model_class
47
+ SpreeCmCommissioner::SavedGuest
48
+ end
49
+
50
+ def collection_serializer
51
+ resource_serializer
52
+ end
53
+
54
+ def resource_serializer
55
+ Spree::V2::Tenant::SavedGuestSerializer
56
+ end
57
+
58
+ def load_saved_guest
59
+ @saved_guest = spree_current_user.saved_guests.find(params[:id])
60
+ end
61
+
62
+ def saved_guest_params
63
+ params.require(:saved_guest).permit(*::Spree::PermittedAttributes.saved_guest_attributes)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -30,28 +30,25 @@ module Spree
30
30
  def create
31
31
  spree_authorize! :update, spree_current_order, order_token
32
32
 
33
- resource = scope.new(guest_params.except(:guest_dynamic_fields_attributes))
33
+ result = SpreeCmCommissioner::Guests::Create.call(line_item: parent, guest_params: guest_params)
34
34
 
35
- ActiveRecord::Base.transaction do
36
- resource.save!
37
- run_dynamic_fields(resource)
38
- resource.save_and_move_to_next_stage
35
+ if result.success?
36
+ render_serialized_payload(201) { serialize_resource(result.value[:guest]) }
37
+ else
38
+ render_error_payload(result.error || result.value&.errors, 422)
39
39
  end
40
-
41
- render_serialized_payload(201) { serialize_resource(resource) }
42
40
  end
43
41
 
44
42
  def update
45
43
  spree_authorize! :update, spree_current_order, order_token
46
44
 
47
- ActiveRecord::Base.transaction do
48
- run_dynamic_fields(resource) if guest_params[:guest_dynamic_fields_attributes]
49
- resource.assign_attributes(guest_params.except(:guest_dynamic_fields_attributes))
50
- resource.save!
51
- resource.save_and_move_to_next_stage
52
- end
45
+ result = SpreeCmCommissioner::Guests::Update.call(guest: resource, guest_params: guest_params)
53
46
 
54
- render_serialized_payload { serialize_resource(resource) }
47
+ if result.success?
48
+ render_serialized_payload { serialize_resource(result.value[:guest]) }
49
+ else
50
+ render_error_payload(result.error || result.value&.errors, 422)
51
+ end
55
52
  end
56
53
 
57
54
  private
@@ -99,13 +96,6 @@ module Spree
99
96
  )
100
97
  end
101
98
 
102
- def run_dynamic_fields(resource)
103
- SpreeCmCommissioner::GuestDynamicFieldsManager.call(
104
- resource: resource,
105
- dynamic_fields_attributes: guest_params[:guest_dynamic_fields_attributes]
106
- )
107
- end
108
-
109
99
  def handle_record_invalid(exception)
110
100
  render_error_payload(exception.record.errors, 422)
111
101
  end
@@ -21,7 +21,6 @@ module Spree
21
21
  order = Spree::Order.not_archived.find_by!(token: order_token)
22
22
 
23
23
  spree_authorize! :update, order, order_token
24
- raise CanCan::AccessDenied unless order.payment?
25
24
 
26
25
  if order.mark_as_archive
27
26
  render_serialized_payload { serialize_resource(order) }
@@ -5,22 +5,11 @@ module SpreeCmCommissioner
5
5
  module CheckoutControllerDecorator
6
6
  def self.prepended(base)
7
7
  base.include SpreeCmCommissioner::OrderConcern
8
-
9
- base.before_action :set_load_test_bypass_current
10
- end
11
-
12
- private
13
-
14
- def set_load_test_bypass_current
15
- key = ENV.fetch('LOAD_TEST_BYPASS_KEY', nil)
16
- return if key.blank?
17
-
18
- provided = request.headers.fetch('X-Request-Verify', nil).to_s
19
- SpreeCmCommissioner::Current.load_test_bypass = ActiveSupport::SecurityUtils.secure_compare(provided, key)
8
+ # Sets Current.load_test_bypass from the X-Request-Verify secret so
9
+ # InventoryHolds::ValidateLimits can skip anti-abuse checks for trusted load tests.
10
+ base.include SpreeCmCommissioner::LoadTestBypassable
20
11
  end
21
12
 
22
- public
23
-
24
13
  # Override Spree's default behavior:
25
14
  # By default, Spree raises an error if `complete` is called on a completed or confirmed order.
26
15
  # We want to allow this call to succeed, so the API can be used both to:
@@ -0,0 +1,28 @@
1
+ module SpreeCmCommissioner
2
+ module MaintenanceTasks
3
+ class GuestDataFillStageBatchJob < ApplicationJob
4
+ queue_as :maintenance
5
+
6
+ # Refreshes data_fill_stage (+ clears has_incomplete_guest_info caches, and optionally
7
+ # notifies newly-incomplete guests) for a bounded batch of guests. Enqueued in slices by
8
+ # MaintenanceTasks::GuestDataFillStage#maintain and by the recompute_guest_data_fill_stage
9
+ # rake task instead of looping over every guest inline, so neither a product with many
10
+ # completed-order guests nor a full-table backfill blocks a single job for a long time.
11
+ def perform(guest_ids:, notify: true)
12
+ SpreeCmCommissioner::Guest
13
+ .where(id: guest_ids)
14
+ .includes(line_item: :order)
15
+ .find_each do |guest|
16
+ result = SpreeCmCommissioner::Guests::RefreshDataFillStage.call(guest: guest, notify: notify)
17
+
18
+ next if result.success?
19
+
20
+ CmAppLogger.log(
21
+ label: "#{self.class.name} failed",
22
+ data: { guest_id: guest.id, error: result.error.to_s }
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,82 @@
1
+ module SpreeCmCommissioner
2
+ module GuestDataFillStageConcern
3
+ extend ActiveSupport::Concern
4
+
5
+ STAGES = %w[pre_registration post_registration during_check_in].freeze
6
+
7
+ included do
8
+ enum data_fill_stage: {
9
+ pre_registration: 'pre_registration',
10
+ post_registration: 'post_registration',
11
+ during_check_in: 'during_check_in',
12
+ completed: 'completed'
13
+ }
14
+
15
+ has_one :product, through: :line_item, class_name: 'Spree::Product'
16
+
17
+ STAGES.each do |stage|
18
+ has_many :"#{stage}_fields", -> { where(data_fill_stage: stage) },
19
+ through: :product, source: :dynamic_fields, class_name: 'SpreeCmCommissioner::DynamicField'
20
+ end
21
+
22
+ after_commit :refresh_data_fill_stage, on: %i[create update]
23
+ end
24
+
25
+ def refresh_data_fill_stage
26
+ new_stage = computed_data_fill_stage
27
+ return if data_fill_stage == new_stage
28
+
29
+ update_column(:data_fill_stage, new_stage) # rubocop:disable Rails/SkipsModelValidations
30
+
31
+ # Guests are also refreshed one-off outside the maintenance-task/batch-job path (e.g. a
32
+ # guest filling out a dynamic field via the API), so the cache clear has to live here
33
+ # rather than only in the caller — otherwise has_incomplete_guest_info? could keep
34
+ # returning a stale cached value after an individual guest update.
35
+ line_item&.order&.clear_has_incomplete_guest_info_cache
36
+ end
37
+
38
+ # Recomputes live rather than trusting the persisted data_fill_stage column, staying fully
39
+ # compatible with guests whose stage was set before this concern existed. Callers (Order,
40
+ # user-facing reads) sit behind caching, so the extra queries here aren't a hot-path concern.
41
+ def incomplete?
42
+ STAGES.any? { |stage| !stage_completed?(stage) }
43
+ end
44
+
45
+ # Already true when a stage has no required fields, so any_fields_exist_for? isn't needed
46
+ # first. Uses pluck/exists?, not any?/map, so it always queries fresh instead of reusing an
47
+ # association target cached from an earlier call.
48
+ def stage_completed?(stage)
49
+ # Pre-registration is considered complete if the order is completed
50
+ return true if stage.to_s == 'pre_registration' && line_item&.order&.completed? == true
51
+
52
+ # Once the line item's own duration has ended, pre/post-registration info is moot — don't
53
+ # keep asking for it. during_check_in is deliberately excluded: staff may still log
54
+ # check-in-stage data after the event occurs.
55
+ return true if %w[pre_registration post_registration].include?(stage.to_s) && line_item&.ended? == true
56
+
57
+ required_field_ids = fields_for_stage(stage).pluck(:id)
58
+ return true if required_field_ids.empty?
59
+
60
+ filled_field_ids = guest_dynamic_fields.pluck(:dynamic_field_id)
61
+ required_field_ids.to_set.subset?(filled_field_ids.to_set)
62
+ end
63
+
64
+ def any_fields_exist_for?(stage)
65
+ fields_for_stage(stage).exists?
66
+ end
67
+
68
+ private
69
+
70
+ def fields_for_stage(stage)
71
+ case stage.to_s
72
+ when 'pre_registration' then pre_registration_fields
73
+ when 'post_registration' then post_registration_fields
74
+ when 'during_check_in' then during_check_in_fields
75
+ end
76
+ end
77
+
78
+ def computed_data_fill_stage
79
+ STAGES.find { |stage| !stage_completed?(stage) } || 'completed'
80
+ end
81
+ end
82
+ end
@@ -46,6 +46,10 @@ module SpreeCmCommissioner
46
46
  to_date&.to_datetime
47
47
  end
48
48
 
49
+ def ended?
50
+ checkout_date.present? && checkout_date <= Time.current
51
+ end
52
+
49
53
  private
50
54
 
51
55
  def set_event_id