spree_cm_commissioner 2.6.2.pre.pre.4 → 2.6.2.pre.pre.5

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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test_and_build_gem.yml +0 -2
  3. data/.gitignore +1 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +36 -0
  6. data/Rakefile +9 -0
  7. data/app/controllers/concerns/spree/admin/service_calendars_concern.rb +3 -1
  8. data/app/controllers/concerns/spree_cm_commissioner/adaptive_turnstile.rb +37 -0
  9. data/app/controllers/concerns/spree_cm_commissioner/turnstile_protectable.rb +55 -0
  10. data/app/controllers/spree/admin/inventory_items_controller.rb +2 -2
  11. data/app/controllers/spree/admin/inventory_monitorings_controller.rb +1 -1
  12. data/app/controllers/spree/admin/product_service_calendars_controller.rb +1 -0
  13. data/app/controllers/spree/admin/system/cache_controller.rb +30 -0
  14. data/app/controllers/spree/admin/system/waiting_room_controller.rb +35 -0
  15. data/app/controllers/spree/api/v2/organizer/invite_guests_controller.rb +91 -0
  16. data/app/controllers/spree/api/v2/storefront/access_tokens_controller.rb +3 -0
  17. data/app/controllers/spree/api/v2/storefront/pin_code_generators_controller.rb +4 -0
  18. data/app/controllers/spree/api/v2/storefront/pin_code_otp_generators_controller.rb +4 -0
  19. data/app/controllers/spree/api/v2/storefront/reset_passwords_controller.rb +4 -0
  20. data/app/controllers/spree/api/v2/storefront/saved_guests_controller.rb +77 -0
  21. data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +12 -2
  22. data/app/controllers/spree/api/v2/storefront/user_registration_with_pin_codes_controller.rb +3 -0
  23. data/app/controllers/spree/api/v2/tenant/popular_route_places_controller.rb +8 -2
  24. data/app/controllers/spree/api/v2/tenant/routes_controller.rb +8 -2
  25. data/app/controllers/spree/api/v2/tenant/trip_search_controller.rb +11 -2
  26. data/app/controllers/spree_cm_commissioner/api/v2/storefront/checkout_controller_decorator.rb +3 -0
  27. data/app/errors/spree_cm_commissioner/reserved_blocks/on_hold_by_other_guest_error.rb +21 -0
  28. data/app/errors/spree_cm_commissioner/reserved_blocks/reserved_by_other_guest_error.rb +10 -0
  29. data/app/errors/spree_cm_commissioner/reserved_blocks/reserved_by_same_guest_error.rb +10 -0
  30. data/app/errors/spree_cm_commissioner/reserved_blocks/unable_to_save_reserved_block_error.rb +10 -0
  31. data/app/finders/spree_cm_commissioner/routes/find.rb +8 -3
  32. data/app/finders/spree_cm_commissioner/routes/find_popular.rb +17 -3
  33. data/app/helpers/spree/base_helper_decorator.rb +2 -1
  34. data/app/helpers/spree_cm_commissioner/transit/trip_helper.rb +35 -0
  35. data/app/interactors/spree_cm_commissioner/create_vendor.rb +2 -0
  36. data/app/interactors/spree_cm_commissioner/pin_code_sender.rb +2 -1
  37. data/app/interactors/spree_cm_commissioner/sms.rb +3 -2
  38. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +1 -1
  39. data/app/interactors/spree_cm_commissioner/turnstile_token_validator.rb +62 -0
  40. data/app/jobs/spree_cm_commissioner/cleanup_expired_access_tokens_job.rb +9 -0
  41. data/app/jobs/spree_cm_commissioner/{stock/inventory_items_adjuster_job.rb → inventory_items/bulk_adjust_quantities_by_variant_job.rb} +3 -3
  42. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +15 -0
  43. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_generate_permanent_items_job.rb +12 -0
  44. data/app/jobs/spree_cm_commissioner/inventory_items/generate_inventory_items_job.rb +15 -0
  45. data/app/jobs/spree_cm_commissioner/route_prices/update_price_range_job.rb +12 -0
  46. data/app/jobs/spree_cm_commissioner/sms_pin_code_job.rb +1 -1
  47. data/app/models/concerns/spree_cm_commissioner/order_seatable.rb +6 -8
  48. data/app/models/spree_cm_commissioner/agency.rb +15 -0
  49. data/app/models/spree_cm_commissioner/distribution_agreement.rb +17 -0
  50. data/app/models/spree_cm_commissioner/order_decorator.rb +3 -2
  51. data/app/models/spree_cm_commissioner/pin_code.rb +1 -1
  52. data/app/models/spree_cm_commissioner/price_list.rb +17 -0
  53. data/app/models/spree_cm_commissioner/role_decorator.rb +3 -0
  54. data/app/models/spree_cm_commissioner/route.rb +16 -0
  55. data/app/models/spree_cm_commissioner/route_price.rb +12 -0
  56. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +2 -2
  57. data/app/models/spree_cm_commissioner/taxon_decorator.rb +2 -0
  58. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +3 -0
  59. data/app/models/spree_cm_commissioner/tenant.rb +1 -0
  60. data/app/models/spree_cm_commissioner/trip.rb +37 -4
  61. data/app/models/spree_cm_commissioner/trip_stop.rb +4 -1
  62. data/app/models/spree_cm_commissioner/user_agency.rb +10 -0
  63. data/app/models/spree_cm_commissioner/user_decorator.rb +8 -0
  64. data/app/models/spree_cm_commissioner/vehicle_type.rb +1 -0
  65. data/app/models/spree_cm_commissioner/vendor_decorator.rb +20 -2
  66. data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +269 -0
  67. data/app/queries/spree_cm_commissioner/single_leg_trips_query.rb +134 -0
  68. data/app/queries/spree_cm_commissioner/trip_query.rb +33 -133
  69. data/app/serializers/spree/v2/organizer/invite_guest_serializer.rb +13 -0
  70. data/app/serializers/spree/v2/tenant/line_item_serializer.rb +1 -1
  71. data/app/serializers/spree_cm_commissioner/v2/storefront/price_serializer.rb +21 -0
  72. data/app/serializers/spree_cm_commissioner/v2/storefront/route_price_serializer.rb +11 -0
  73. data/app/serializers/spree_cm_commissioner/v2/storefront/route_serializer.rb +2 -0
  74. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +1 -1
  75. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_stop_serializer.rb +7 -1
  76. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_vehicle_type_serializer.rb +3 -1
  77. data/app/services/spree_cm_commissioner/agency_categories/create.rb +1 -4
  78. data/app/services/spree_cm_commissioner/agency_users/add.rb +80 -0
  79. data/app/services/spree_cm_commissioner/agency_users/create.rb +104 -0
  80. data/app/services/spree_cm_commissioner/agency_users/destroy.rb +20 -0
  81. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +51 -33
  82. data/app/services/spree_cm_commissioner/cleanup_expired_access_tokens.rb +52 -0
  83. data/app/services/spree_cm_commissioner/distribution_agreements/create.rb +28 -0
  84. data/app/services/spree_cm_commissioner/distribution_agreements/update.rb +28 -0
  85. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +3 -3
  86. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities.rb +40 -0
  87. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_by_variant.rb +16 -0
  88. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold.rb +39 -0
  89. data/app/{interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb → services/spree_cm_commissioner/inventory_items/bulk_generate_permanent_items.rb} +12 -7
  90. data/app/services/spree_cm_commissioner/inventory_items/generate_non_permanent_item.rb +13 -0
  91. data/app/{interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb → services/spree_cm_commissioner/inventory_items/reset.rb} +17 -12
  92. data/app/services/spree_cm_commissioner/open_dated_trips/redeem.rb +1 -1
  93. data/app/services/spree_cm_commissioner/price_lists/create.rb +58 -0
  94. data/app/services/spree_cm_commissioner/redis_stock/base.rb +39 -0
  95. data/app/services/spree_cm_commissioner/redis_stock/restock.rb +71 -0
  96. data/app/services/spree_cm_commissioner/redis_stock/unstock.rb +65 -0
  97. data/app/services/spree_cm_commissioner/reserved_blocks/cancel.rb +29 -0
  98. data/app/services/spree_cm_commissioner/reserved_blocks/hold.rb +56 -0
  99. data/app/services/spree_cm_commissioner/reserved_blocks/reserve.rb +50 -0
  100. data/app/services/spree_cm_commissioner/route_prices/update_price_range.rb +36 -0
  101. data/app/services/spree_cm_commissioner/routes/create.rb +1 -1
  102. data/app/services/spree_cm_commissioner/transit/legs_builder_service.rb +13 -13
  103. data/app/services/spree_cm_commissioner/transit_order/create.rb +79 -20
  104. data/app/services/spree_cm_commissioner/trips/clone.rb +25 -31
  105. data/app/services/spree_cm_commissioner/trips/create_multi_leg.rb +355 -0
  106. data/app/services/spree_cm_commissioner/trips/create_open_dated_trip.rb +1 -1
  107. data/app/services/spree_cm_commissioner/trips/create_single_leg.rb +16 -5
  108. data/app/services/spree_cm_commissioner/trips/search.rb +12 -8
  109. data/app/views/spree/admin/exports/index.html.erb +0 -10
  110. data/app/views/spree/admin/product_service_calendars/edit.html.erb +9 -0
  111. data/app/views/spree/admin/product_service_calendars/index.html.erb +1 -0
  112. data/app/views/spree/admin/shared/_system_tabs.html.erb +19 -0
  113. data/app/views/spree/admin/shared/service_calendars/_exception_rules.html.erb +20 -7
  114. data/app/views/spree/admin/system/cache/show.html.erb +18 -0
  115. data/app/views/spree/admin/system/{show.html.erb → waiting_room/show.html.erb} +5 -7
  116. data/config/locales/en.yml +18 -0
  117. data/config/locales/km.yml +8 -0
  118. data/config/routes.rb +18 -6
  119. data/db/migrate/20260226105108_add_trip_type_to_cm_trip.rb +5 -0
  120. data/db/migrate/20260302062116_add_offset_days_to_cm_trip_stops.rb +5 -0
  121. data/db/migrate/20260311105437_add_tenant_to_cm_sms_logs.rb +5 -0
  122. data/db/migrate/20260318081516_create_cm_agencies.rb +18 -0
  123. data/db/migrate/20260319090000_add_role_type_to_spree_roles.rb +5 -0
  124. data/db/migrate/20260320103313_add_index_to_spree_oauth_access_tokens_created_at.rb +7 -0
  125. data/db/migrate/20260325102409_create_cm_user_agencies.rb +16 -0
  126. data/db/migrate/20260326100000_create_cm_price_lists.rb +36 -0
  127. data/db/migrate/20260330032107_create_cm_distribution_agreements.rb +22 -0
  128. data/db/migrate/20260330032108_add_vendor_id_to_spree_taxonomies.rb +9 -0
  129. data/db/migrate/20260401080000_create_cm_route_prices.rb +22 -0
  130. data/lib/spree_cm_commissioner/service_module_throwable.rb +39 -0
  131. data/lib/spree_cm_commissioner/test_helper/factories/agency_factory.rb +7 -0
  132. data/lib/spree_cm_commissioner/test_helper/factories/distribution_agreement_factory.rb +33 -0
  133. data/lib/spree_cm_commissioner/test_helper/factories/price_list_factory.rb +15 -0
  134. data/lib/spree_cm_commissioner/test_helper/factories/user_agency_factory.rb +6 -0
  135. data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +1 -1
  136. data/lib/spree_cm_commissioner/transit/route_stop_collection.rb +18 -5
  137. data/lib/spree_cm_commissioner/transit/service_calendar_form.rb +19 -0
  138. data/lib/spree_cm_commissioner/transit/trip_form.rb +4 -3
  139. data/lib/spree_cm_commissioner/transit/trip_stop_form.rb +20 -5
  140. data/lib/spree_cm_commissioner/trip_query_result.rb +37 -8
  141. data/lib/spree_cm_commissioner/trip_result.rb +71 -8
  142. data/lib/spree_cm_commissioner/version.rb +1 -1
  143. data/lib/spree_cm_commissioner.rb +1 -0
  144. data/lib/tasks/generate_inventory_items.rake +1 -1
  145. metadata +70 -21
  146. data/app/controllers/spree/admin/system_controller.rb +0 -44
  147. data/app/errors/spree_cm_commissioner/seats/blocks_are_on_hold_by_other_guest_error.rb +0 -19
  148. data/app/errors/spree_cm_commissioner/seats/blocks_are_reserved_by_other_guest_error.rb +0 -19
  149. data/app/errors/spree_cm_commissioner/seats/blocks_are_reserved_by_same_guest_error.rb +0 -8
  150. data/app/errors/spree_cm_commissioner/seats/unable_to_save_reserved_block_record_error.rb +0 -19
  151. data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +0 -49
  152. data/app/interactors/spree_cm_commissioner/stock/inventory_items_adjuster.rb +0 -13
  153. data/app/interactors/spree_cm_commissioner/stock/inventory_items_generator.rb +0 -15
  154. data/app/jobs/spree_cm_commissioner/inventory_item_syncer_job.rb +0 -13
  155. data/app/jobs/spree_cm_commissioner/stock/inventory_items_generator_job.rb +0 -11
  156. data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +0 -9
  157. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +0 -143
  158. data/app/models/spree_cm_commissioner/seats/blocks_canceler.rb +0 -31
  159. data/app/models/spree_cm_commissioner/seats/blocks_holder.rb +0 -66
  160. data/app/models/spree_cm_commissioner/seats/blocks_reserver.rb +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c381df113afbf82a9586168ede0e86f1bfee87849e50d3f9aed7a4b5455c2ab
4
- data.tar.gz: 8466ba5865693c20fe012ca0463b584c9be3fe1aed6e25f3d37d31ef3ad76121
3
+ metadata.gz: '0319481a07b3f09bd6c82a11919c71d903c5fba255975a9b8c2125b0d34b8c56'
4
+ data.tar.gz: 4bdb018d9d105aa5a0afb1105f24d4dd5870654528f34c1928256ac4232c61de
5
5
  SHA512:
6
- metadata.gz: e8ad8ac4e47e7a75a68f60244bf4033fb5d035bbd722e7f7d8ecebad7f81d08634990087a47e8702d47fb919bea1c60e3ba81f35fc1f772c2d0e0efa41c59b2f
7
- data.tar.gz: 4a0ae2cd87ccca4f91cd2604a979771192cabffaf63978290a86dae624e25e18c1cf581f2d09a5f9805fa32f159b6e7f4b1982b3bc85958dd53da3b1385764c3
6
+ metadata.gz: 323de4c2e82ca9f643216ca00a8da7f7de5985ffb38da8388ba09549b1cf1b2878634c1ab61c1178f77f9728fd40442b062784db3af7a1ffa694f9308c50b874
7
+ data.tar.gz: 67ade5a70dcbfd1faf99bb89f41ab68db83e7cc6bf51ec997ba16f233c16d696fa92534c15c2d6d8c98d9f68078ef826264f72d9f6a815e99fb375b8676f6702
@@ -244,8 +244,6 @@ jobs:
244
244
  with:
245
245
  path: spec/dummy
246
246
  key: test-app-${{ runner.os }}-${{ hashFiles('Gemfile.lock', 'db/migrate/*.rb') }}
247
- restore-keys: |
248
- test-app-${{ runner.os }}-
249
247
 
250
248
  # Generate dummy Rails app for testing (cacheable part)
251
249
  # This step checks if spec/dummy exists and skips generation if found
data/.gitignore CHANGED
@@ -33,3 +33,4 @@ vendor/bundle/
33
33
 
34
34
  # Cursor
35
35
  .cursor
36
+ dump.rdb
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.6.2.pre.pre.4)
37
+ spree_cm_commissioner (2.6.2.pre.pre.5)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
data/README.md CHANGED
@@ -153,8 +153,38 @@ PIN_CODE_DEBUG_NOTIFIY_TELEGRAM_ENABLE="yes"
153
153
  EXCEPTION_NOTIFY_ENABLE="yes" # yes or no
154
154
  EXCEPTION_TELEGRAM_BOT_TOKEN=""
155
155
  EXCEPTION_NOTIFIER_TELEGRAM_CHANNEL_ID=""
156
+
157
+ TURNSTILE_SECRET_KEY="" # Cloudflare Turnstile secret key (server-side verification)
158
+ TURNSTILE_ENABLED="no" # Kill switch: `yes` to enforce Turnstile validation, `no` to bypass all validation without a code deploy (e.g. during a Cloudflare outage). Default: `no` (disabled)
156
159
  ```
157
160
 
161
+ ### Cloudflare Turnstile
162
+
163
+ Cloudflare Turnstile is used to protect sensitive API endpoints from bots. The server-side validation is handled by `SpreeCmCommissioner::TurnstileTokenValidator` and the concern `SpreeCmCommissioner::TurnstileProtectable`.
164
+
165
+ **Protected endpoints:**
166
+
167
+ - `POST /api/v2/storefront/pin_code_generators`
168
+ - `POST /api/v2/storefront/pin_code_otp_generators`
169
+ - `POST /api/v2/storefront/user_registration_with_pin_codes`
170
+ - `PUT /api/v2/storefront/reset_passwords`
171
+ - `PATCH /api/v2/storefront/checkout/complete`
172
+
173
+ **How it works:**
174
+
175
+ 1. The client sends a Turnstile token via the `X-Turnstile-Token` HTTP header
176
+ 2. `TurnstileProtectable` reads the header and calls `TurnstileTokenValidator`
177
+ 3. The validator POSTs to Cloudflare's siteverify API with the token and `TURNSTILE_SECRET_KEY`
178
+ 4. Returns 403 if verification fails
179
+
180
+ **Configuration:**
181
+
182
+ 1. Set `TURNSTILE_SECRET_KEY` in `.env` (obtained from [Cloudflare Turnstile dashboard](https://dash.cloudflare.com/))
183
+ 2. For local development testing, use Cloudflare's test keys:
184
+ - Always pass: `1x0000000000000000000000000000000AA`
185
+ - Always fail: `2x0000000000000000000000000000000AA`
186
+ - Token spent: `3x0000000000000000000000000000000AA`
187
+
158
188
  ## JSON Format Examples For Store and Tenant Preferences
159
189
 
160
190
  ### Asset Links Format
@@ -234,6 +264,12 @@ bundle update
234
264
  bundle exec rake test_app
235
265
  ```
236
266
 
267
+ To completely remove the dummy app and rebuild it from scratch, you can use:
268
+
269
+ ```sh
270
+ bundle exec rake test_app:reset
271
+ ```
272
+
237
273
  When testing your applications integration with this extension you may use it's factories.
238
274
  Simply add this require statement to your spec_helper:
239
275
 
data/Rakefile CHANGED
@@ -88,6 +88,15 @@ namespace :test_app do
88
88
  puts '✓ Database setup complete'
89
89
  end
90
90
  end
91
+
92
+ desc 'Remove spec/dummy and run test_app'
93
+ task :reset do
94
+ puts 'Removing spec/dummy...'
95
+ FileUtils.rm_rf('spec/dummy')
96
+
97
+ puts 'Rebuilding test app...'
98
+ Rake::Task['test_app'].invoke
99
+ end
91
100
  end
92
101
 
93
102
  desc 'Generates a dummy app for testing'
@@ -44,6 +44,8 @@ module Spree
44
44
  private
45
45
 
46
46
  def build_exception_rules(exception_rules)
47
+ return [] if exception_rules.nil?
48
+
47
49
  exception_rules.values.reject! { |rule| rule['from'].blank? || rule['to'].blank? || rule['type'].blank? } || exception_rules.values
48
50
  end
49
51
 
@@ -57,7 +59,7 @@ module Spree
57
59
  end
58
60
 
59
61
  def set_exception_rules
60
- @exception_rules = [{ from: DateTime.now, to: DateTime.now, type: 'exclusion', reason: nil }]
62
+ @exception_rules = (@object.exception_rules.presence || [{ from: DateTime.now, to: DateTime.now, type: 'exclusion', reason: nil }])
61
63
  end
62
64
 
63
65
  def build_resource
@@ -0,0 +1,37 @@
1
+ module SpreeCmCommissioner
2
+ module AdaptiveTurnstile
3
+ extend ActiveSupport::Concern
4
+ include TurnstileProtectable
5
+
6
+ FAILED_ATTEMPT_THRESHOLD = 3
7
+
8
+ private
9
+
10
+ # Only require Turnstile if the user has exceeded the failed attempt threshold.
11
+ # This reduces latency for normal logins while catching brute force attacks.
12
+ # Only applies to password grant; other OAuth flows (refresh_token, client_credentials) are skipped.
13
+ def verify_turnstile_if_suspicious!
14
+ return unless params[:grant_type] == 'password'
15
+
16
+ user = Spree::User.find_by(email: params[:username]&.downcase&.strip)
17
+
18
+ # No user found -- require Turnstile to prevent email enumeration
19
+ if user.nil?
20
+ verify_turnstile!
21
+ return
22
+ end
23
+
24
+ # User has too many failed attempts -- require Turnstile
25
+ if user.respond_to?(:failed_attempts) && user.failed_attempts >= FAILED_ATTEMPT_THRESHOLD
26
+ verify_turnstile!
27
+ return
28
+ end
29
+
30
+ # Also check Redis for IP-level failed attempt tracking
31
+ return unless defined?(::Rack::Attack) && ::Rack::Attack.respond_to?(:cache)
32
+
33
+ ip_attempts = ::Rack::Attack.cache.count("failed_auth/ip:#{request.remote_ip}", 15.minutes)
34
+ verify_turnstile! if ip_attempts >= FAILED_ATTEMPT_THRESHOLD
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,55 @@
1
+ module SpreeCmCommissioner
2
+ module TurnstileProtectable
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+
7
+ # Verify the Turnstile token from the X-Turnstile-Token header.
8
+ # Renders 403 and halts the request if verification fails.
9
+ #
10
+ # Usage:
11
+ # before_action -> { verify_turnstile! }, only: :create
12
+ #
13
+ def verify_turnstile!
14
+ # Kill switch: set TURNSTILE_ENABLED='no' env var to disable globally.
15
+ return unless turnstile_feature_enabled?
16
+
17
+ token = request.headers['X-Turnstile-Token']
18
+
19
+ # Skip verification if no token and Turnstile is not enforced
20
+ return if token.blank? && turnstile_optional?
21
+
22
+ result = TurnstileTokenValidator.call(
23
+ token: token,
24
+ remote_ip: request.remote_ip
25
+ )
26
+
27
+ return unless result.failure?
28
+
29
+ render json: {
30
+ errors: [{
31
+ status: '403',
32
+ error_code: 'human_verification_failed',
33
+ title: I18n.t('turnstile.verification_failed'),
34
+ detail: result.message
35
+ }
36
+ ]
37
+ }, status: :forbidden
38
+ end
39
+
40
+ # Override in controllers to make Turnstile optional for specific actions.
41
+ # Default: required (returns false).
42
+ def turnstile_optional?
43
+ false
44
+ end
45
+
46
+ # Turnstile verification is enabled only when both conditions are met:
47
+ # 1. TURNSTILE_ENABLED environment variable is set to 'yes'
48
+ # 2. TURNSTILE_SECRET_KEY is present
49
+ # Default: disabled (no). Set TURNSTILE_ENABLED='yes' and provide
50
+ # TURNSTILE_SECRET_KEY to enable verification.
51
+ def turnstile_feature_enabled?
52
+ ENV.fetch('TURNSTILE_ENABLED', 'no') == 'yes' && ENV.fetch('TURNSTILE_SECRET_KEY', '').present?
53
+ end
54
+ end
55
+ end
@@ -66,8 +66,8 @@ module Spree
66
66
 
67
67
  errors = []
68
68
  inventory_items.each do |inventory_item|
69
- result = SpreeCmCommissioner::Stock::InventoryItemResetter.call(inventory_item: inventory_item)
70
- errors << result.message unless result.success?
69
+ result = SpreeCmCommissioner::InventoryItems::Reset.call(inventory_item: inventory_item)
70
+ errors << result.error.to_s unless result.success?
71
71
  end
72
72
 
73
73
  if errors.empty?
@@ -24,7 +24,7 @@ module Spree
24
24
  authorize! :manage, SpreeCmCommissioner::InventoryItem
25
25
 
26
26
  inventory_item = SpreeCmCommissioner::InventoryItem.find(params[:id])
27
- result = SpreeCmCommissioner::Stock::InventoryItemResetter.call(inventory_item: inventory_item)
27
+ result = SpreeCmCommissioner::InventoryItems::Reset.call(inventory_item: inventory_item)
28
28
 
29
29
  if result.success?
30
30
  flash[:success] = "Successfully reset inventory for #{inventory_item.variant.product.name}"
@@ -6,6 +6,7 @@ module Spree
6
6
  before_action :load_product
7
7
  before_action :ensure_product_type_supports_calendar
8
8
  before_action :ensure_product_has_no_calendar, only: %i[new create]
9
+ before_action :set_exception_rules, only: %i[edit update]
9
10
 
10
11
  create.before :set_calendarable
11
12
  update.before :set_calendarable
@@ -0,0 +1,30 @@
1
+ module Spree
2
+ module Admin
3
+ module System
4
+ class CacheController < Spree::Admin::BaseController
5
+ def show; end
6
+
7
+ def invalidate
8
+ @patterns = params[:patterns].to_s
9
+
10
+ patterns = @patterns.split("\n").map(&:strip).compact_blank
11
+
12
+ if patterns.empty?
13
+ flash.now[:error] = 'Please enter at least one pattern.' # rubocop:disable Rails/I18nLocaleTexts
14
+ render :show and return
15
+ end
16
+
17
+ result = SpreeCmCommissioner::InvalidateCacheRequest.call(patterns: patterns)
18
+
19
+ if result.success?
20
+ flash[:success] = "Cache invalidated successfully for #{patterns.size} pattern(s)."
21
+ redirect_to admin_system_cache_path
22
+ else
23
+ flash.now[:error] = result.message
24
+ render :show
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ module Spree
2
+ module Admin
3
+ module System
4
+ class WaitingRoomController < Spree::Admin::BaseController
5
+ def show
6
+ @fetcher = SpreeCmCommissioner::WaitingRoomSystemMetadataFetcher.new
7
+ @fetcher.load_document_data
8
+
9
+ @active_sesions_count = SpreeCmCommissioner::WaitingRoomSession.active.count
10
+ end
11
+
12
+ def modify_multiplier
13
+ modifier = params[:multiplier]&.to_i || 0
14
+ SpreeCmCommissioner::WaitingRoomSystemMetadataSetter.new.modify_multiplier(modifier)
15
+
16
+ redirect_back fallback_location: admin_system_waiting_room_path
17
+ end
18
+
19
+ def modify_max_thread_count
20
+ modifier = params[:max_thread_count]&.to_i || 0
21
+ SpreeCmCommissioner::WaitingRoomSystemMetadataSetter.new.modify_max_thread_count(modifier)
22
+
23
+ redirect_back fallback_location: admin_system_waiting_room_path
24
+ end
25
+
26
+ def force_pull
27
+ SpreeCmCommissioner::WaitingRoomLatestSystemMetadataPullerJob.perform_now
28
+ SpreeCmCommissioner::WaitingGuestsCallerJob.perform_now
29
+
30
+ redirect_back fallback_location: admin_system_waiting_room_path
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,91 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Organizer
5
+ class InviteGuestsController < ::Spree::Api::V2::Organizer::BaseController
6
+ before_action :load_invite_guest_by_token, only: :show
7
+ before_action :load_invite_guest, :assign_line_item_data, only: :update
8
+
9
+ def show
10
+ render_serialized_payload { serialize_resource(@invite_guest) }
11
+ end
12
+
13
+ def update
14
+ return render_error(:revoked) if @invite_guest.revoked?
15
+ return render_error(:closed) if @invite_guest.expired?
16
+ return render_error(:fully_claimed) if @invite_guest.fully_claimed?
17
+
18
+ guest = @line_item.guests.new(guest_params)
19
+ guest.event_id = @invite_guest.event_id
20
+
21
+ if guest.save
22
+ @invite_guest.update(claimed_status: :claimed) if @line_item.guests.count == @invite_guest.quantity
23
+ send_guest_claimed_invitation_telegram_alert_to_vendor(guest) if guest.event.vendor.preferred_telegram_chat_id.present?
24
+
25
+ render json: SpreeCmCommissioner::V2::Storefront::GuestSerializer.new(guest.reload).serializable_hash
26
+ else
27
+ render_error_payload(guest.errors.full_messages.to_sentence)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
34
+ title = '📣 --- [NEW GUEST CLAIMED INVITATION] ---' # Style/StringLiterals
35
+ chat_id = guest.event.vendor.preferred_telegram_chat_id
36
+ return if chat_id.blank?
37
+
38
+ factory = SpreeCmCommissioner::InviteGuestClaimedTelegramMessageFactory.new(
39
+ title: title,
40
+ order: @invite_guest.order,
41
+ guest: guest,
42
+ vendor: guest.event.vendor
43
+ )
44
+ SpreeCmCommissioner::TelegramNotificationSenderJob.perform_later(
45
+ chat_id: chat_id,
46
+ message: factory.message,
47
+ parse_mode: factory.parse_mode
48
+ )
49
+ end
50
+
51
+ def load_invite_guest_by_token
52
+ @invite_guest = SpreeCmCommissioner::InviteGuest.find_by(token: params[:id])
53
+ rescue ActiveRecord::RecordNotFound
54
+ render_error_payload(I18n.t('invite.url_not_found'))
55
+ end
56
+
57
+ def load_invite_guest
58
+ @invite_guest = SpreeCmCommissioner::InviteGuest.find(params[:id])
59
+ end
60
+
61
+ def assign_line_item_data
62
+ @line_item = @invite_guest.order.line_items.first
63
+ @guests = @line_item.guests
64
+ end
65
+
66
+ def render_error(message)
67
+ render json: { errors: message }
68
+ end
69
+
70
+ def guest_params
71
+ params.require(:invite_guest).permit(
72
+ :first_name,
73
+ :last_name,
74
+ :dob,
75
+ :gender,
76
+ :age,
77
+ :emergency_contact,
78
+ :phone_number,
79
+ :address,
80
+ :other_organization
81
+ )
82
+ end
83
+
84
+ def resource_serializer
85
+ ::Spree::V2::Organizer::InviteGuestSerializer
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -3,6 +3,9 @@ module Spree
3
3
  module V2
4
4
  module Storefront
5
5
  class AccessTokensController < Doorkeeper::TokensController
6
+ include SpreeCmCommissioner::AdaptiveTurnstile
7
+
8
+ before_action :verify_turnstile_if_suspicious!, only: :create
6
9
  end
7
10
  end
8
11
  end
@@ -3,6 +3,10 @@ module Spree
3
3
  module V2
4
4
  module Storefront
5
5
  class PinCodeGeneratorsController < ::Spree::Api::V2::ResourceController
6
+ include SpreeCmCommissioner::TurnstileProtectable
7
+
8
+ before_action :verify_turnstile!, only: :create
9
+
6
10
  # :phone_number, :email, :type, :tenant
7
11
  def create
8
12
  context = SpreeCmCommissioner::PinCodeGenerator.call(pin_code_attrs)
@@ -3,6 +3,10 @@ module Spree
3
3
  module V2
4
4
  module Storefront
5
5
  class PinCodeOtpGeneratorsController < ::Spree::Api::V2::ResourceController
6
+ include SpreeCmCommissioner::TurnstileProtectable
7
+
8
+ before_action :verify_turnstile!, only: :create
9
+
6
10
  # :phone_number, :email, :type, :recaptcha_token, :recaptcha_action, :recaptcah_site_key
7
11
  def create
8
12
  options = otp_attrs
@@ -3,6 +3,10 @@ module Spree
3
3
  module V2
4
4
  module Storefront
5
5
  class ResetPasswordsController < Spree::Api::V2::ResourceController
6
+ include SpreeCmCommissioner::TurnstileProtectable
7
+
8
+ before_action :verify_turnstile!, only: :update
9
+
6
10
  def update
7
11
  context = SpreeCmCommissioner::UserForgottenPasswordUpdater.call(update_params)
8
12
 
@@ -0,0 +1,77 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ class SavedGuestsController < ::Spree::Api::V2::ResourceController
6
+ before_action :require_spree_current_user
7
+ before_action :load_saved_guest, only: %i[show update destroy]
8
+
9
+ def collection
10
+ spree_current_user.saved_guests.order(created_at: :desc)
11
+ .page(params[:page])
12
+ .per(params[:per_page])
13
+ end
14
+
15
+ def create
16
+ saved_guest = spree_current_user.saved_guests.new(saved_guest_params)
17
+
18
+ if saved_guest.save
19
+ render_serialized_payload(201) { serialize_resource(saved_guest) }
20
+ else
21
+ render_error_payload(saved_guest.errors.full_messages.to_sentence, 422)
22
+ end
23
+ end
24
+
25
+ def update
26
+ if @saved_guest.update(saved_guest_params)
27
+ render_serialized_payload { serialize_resource(@saved_guest) }
28
+ else
29
+ render_error_payload(@saved_guest.errors.full_messages.to_sentence, 400)
30
+ end
31
+ end
32
+
33
+ def destroy
34
+ @saved_guest.destroy
35
+ head :no_content
36
+ end
37
+
38
+ private
39
+
40
+ def model_class
41
+ SpreeCmCommissioner::SavedGuest
42
+ end
43
+
44
+ def collection_serializer
45
+ resource_serializer
46
+ end
47
+
48
+ def resource_serializer
49
+ SpreeCmCommissioner::V2::Storefront::SavedGuestSerializer
50
+ end
51
+
52
+ def load_saved_guest
53
+ @saved_guest = spree_current_user.saved_guests.find(params[:id])
54
+ end
55
+
56
+ def saved_guest_params
57
+ params.require(:saved_guest).permit(
58
+ :first_name,
59
+ :last_name,
60
+ :dob,
61
+ :age,
62
+ :gender,
63
+ :email,
64
+ :country_code,
65
+ :phone_number,
66
+ :intel_phone_number,
67
+ :nationality_id,
68
+ :occupation_id,
69
+ :nationality_group,
70
+ :age_group
71
+ )
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -15,7 +15,7 @@ module Spree
15
15
  route_type: params[:route_type],
16
16
  vendor_id: params[:vendor_id],
17
17
  number_of_guests: params[:number_of_guests],
18
- params: params
18
+ options: params
19
19
  ).call
20
20
 
21
21
  serialize_collection(trips)
@@ -72,7 +72,7 @@ module Spree
72
72
  def serialize_collection(collection)
73
73
  serialized_data = SpreeCmCommissioner::V2::Storefront::TripQueryResultSerializer.new(
74
74
  collection,
75
- include: default_resource_includes,
75
+ include: resource_includes,
76
76
  params: serializer_params
77
77
  ).serializable_hash
78
78
  serialized_data[:meta] = {
@@ -87,6 +87,16 @@ module Spree
87
87
  serialized_data
88
88
  end
89
89
 
90
+ # From BaseController to always include defaults and merge request includes
91
+ # override
92
+
93
+ def resource_includes
94
+ (Array(super) + default_resource_includes)
95
+ .map { |path| path.to_s.strip }
96
+ .compact_blank
97
+ .uniq
98
+ end
99
+
90
100
  # override
91
101
  def serializer_params
92
102
  params.permit(:include).to_hash
@@ -3,6 +3,9 @@ module Spree
3
3
  module V2
4
4
  module Storefront
5
5
  class UserRegistrationWithPinCodesController < Spree::Api::V2::ResourceController
6
+ include SpreeCmCommissioner::TurnstileProtectable
7
+
8
+ before_action :verify_turnstile!, only: :create
6
9
  before_action :validate_token_client!
7
10
 
8
11
  def validate_token_client!
@@ -9,7 +9,8 @@ module Spree
9
9
  def collection
10
10
  @collection ||= collection_finder.new(
11
11
  tenant: @tenant,
12
- route_type: params[:route_type]
12
+ route_type: params[:route_type],
13
+ include_route_prices: include_route_prices?
13
14
  ).execute
14
15
  end
15
16
 
@@ -37,7 +38,8 @@ module Spree
37
38
  def serializer_params
38
39
  super.merge(
39
40
  include_vendors: include_vendors?,
40
- include_nearby_places: include_nearby_places?
41
+ include_nearby_places: include_nearby_places?,
42
+ include_route_prices: include_route_prices?
41
43
  )
42
44
  end
43
45
 
@@ -53,6 +55,10 @@ module Spree
53
55
  def include_nearby_places?
54
56
  resource_includes.include?(:nearby_places) || false
55
57
  end
58
+
59
+ def include_route_prices?
60
+ resource_includes.include?(:route_prices) || false
61
+ end
56
62
  end
57
63
  end
58
64
  end
@@ -9,7 +9,8 @@ module Spree
9
9
  def collection
10
10
  @collection ||= collection_finder.new(
11
11
  tenant: @tenant,
12
- params: params
12
+ params: params,
13
+ include_route_prices: include_route_prices?
13
14
  ).execute
14
15
  end
15
16
 
@@ -27,7 +28,8 @@ module Spree
27
28
  def serializer_params
28
29
  super.merge(
29
30
  include_vendors: include_vendors?,
30
- include_nearby_places: include_nearby_places?
31
+ include_nearby_places: include_nearby_places?,
32
+ include_route_prices: include_route_prices?
31
33
  )
32
34
  end
33
35
 
@@ -43,6 +45,10 @@ module Spree
43
45
  def include_nearby_places?
44
46
  resource_includes.include?(:nearby_places) || false
45
47
  end
48
+
49
+ def include_route_prices?
50
+ resource_includes.include?(:route_prices) || false
51
+ end
46
52
  end
47
53
  end
48
54
  end
@@ -15,7 +15,7 @@ module Spree
15
15
  route_type: params[:route_type],
16
16
  number_of_guests: params[:number_of_guests],
17
17
  tenant_id: @tenant.id,
18
- params: params
18
+ options: params
19
19
  ).call
20
20
 
21
21
  serialize_collection(trips)
@@ -72,7 +72,7 @@ module Spree
72
72
  def serialize_collection(collection)
73
73
  serialized_data = Spree::V2::Tenant::TripQueryResultSerializer.new(
74
74
  collection,
75
- include: default_resource_includes,
75
+ include: resource_includes,
76
76
  params: serializer_params
77
77
  ).serializable_hash
78
78
  serialized_data[:meta] = {
@@ -87,6 +87,15 @@ module Spree
87
87
  serialized_data
88
88
  end
89
89
 
90
+ # From BaseController to always include defaults and merge request includes
91
+ # override
92
+ def resource_includes
93
+ (Array(super) + default_resource_includes)
94
+ .map { |path| path.to_s.strip }
95
+ .compact_blank
96
+ .uniq
97
+ end
98
+
90
99
  # override
91
100
  def serializer_params
92
101
  params.permit(:include).to_hash