spree_cm_commissioner 2.1.7 → 2.1.9.pre.pre1

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +75 -29
  3. data/Gemfile.lock +8 -1
  4. data/app/controllers/spree/admin/user_security_controller.rb +25 -0
  5. data/app/controllers/spree/api/v2/storefront/access_tokens_controller.rb +10 -0
  6. data/app/controllers/spree/api/v2/storefront/account/guest_dynamic_fields_controller.rb +82 -0
  7. data/app/controllers/spree/api/v2/storefront/account/mark_guest_info_complete_controller.rb +39 -0
  8. data/app/controllers/spree/api/v2/storefront/account/orders_controller_decorator.rb +34 -5
  9. data/app/controllers/spree/api/v2/storefront/seat_layouts_controller.rb +1 -1
  10. data/app/controllers/spree/api/v2/tenant/access_tokens_controller.rb +10 -0
  11. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
  12. data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +39 -0
  13. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +27 -21
  14. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +97 -0
  15. data/app/models/spree_cm_commissioner/guest.rb +38 -4
  16. data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +1 -0
  17. data/app/models/spree_cm_commissioner/inventory_item.rb +1 -1
  18. data/app/models/spree_cm_commissioner/invite_team.rb +8 -0
  19. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
  20. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
  21. data/app/models/spree_cm_commissioner/seat_layout.rb +3 -3
  22. data/app/models/spree_cm_commissioner/taxon_decorator.rb +2 -20
  23. data/app/models/spree_cm_commissioner/trip.rb +5 -7
  24. data/app/models/spree_cm_commissioner/user_decorator.rb +16 -0
  25. data/app/models/spree_cm_commissioner/vehicle.rb +5 -7
  26. data/app/overrides/spree/admin/taxons/_form/background_color_and_foreground_color.html.erb.deface +6 -6
  27. data/app/overrides/spree/admin/taxons/_form/check_in_flows.html.erb.deface +14 -8
  28. data/app/overrides/spree/admin/users/_tabs/tabs.html.erb.deface +6 -0
  29. data/app/serializers/spree/v2/storefront/order_serializer_decorator.rb +63 -0
  30. data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +1 -1
  31. data/app/serializers/spree_cm_commissioner/v2/operator/dashboard_crew_event_serializer.rb +1 -0
  32. data/app/views/spree/admin/user_security/show.html.erb +25 -0
  33. data/config/initializers/spree_permitted_attributes.rb +5 -4
  34. data/config/locales/en.yml +1 -0
  35. data/config/routes.rb +11 -0
  36. data/docs/api/scoped-access-token-endpoints.md +84 -0
  37. data/lib/spree_cm_commissioner/version.rb +1 -1
  38. data/lib/spree_cm_commissioner.rb +35 -12
  39. data/spree_cm_commissioner.gemspec +1 -0
  40. metadata +28 -5
  41. data/app/models/concerns/spree_cm_commissioner/event_check_in_flowable.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7e5168cb588dc6b931bee2b2711b37886b364323bf2e7215390fde1e46479b8
4
- data.tar.gz: ed7571aca12c59e7385bd7e34fcde1105b0c8c04484d652bc5c4974f4c90e2ca
3
+ metadata.gz: a278c011c4129a11c968c695ac8ae1a22996991ab69f8700bdafa6d9002c470e
4
+ data.tar.gz: 5354f593b70e02b4956f27006a82cc06341a2630ee45306b9e8f33a4c246eb9f
5
5
  SHA512:
6
- metadata.gz: e744361324c16633f8e3648c90b5af642e9deae632e3e9d92681c6d05ffdeca616663b7c535f8ea0f530d640232d4498389f7109d668d92f0f3fcc21afc57038
7
- data.tar.gz: c428e0893d04a933352fe9965f3f1f0e3b270715652f598bbcdf2b12dbf30cb15194eb5e3e5adff6e321d532d0faddae9e40f881060f3cc6cce403031bbbb457
6
+ metadata.gz: 140d7641c8e2542f82e64f8d8bf5758b31b5c3a6daf9232bd57bc9835c27685aba70182cf173df43fb805edc08b0968c80612d726469fd1780ad05a05d38d596
7
+ data.tar.gz: fe162b2e261ace9661ea9b2e05a699617ff5da1a707dab0cad139b72c6fb0242966a9728c37e2a653213665c34767177ab8a4ee1d11c8c02ecf82088631386bb
data/.env.example CHANGED
@@ -1,29 +1,75 @@
1
- # aws
2
- AWS_ACCESS_KEY_ID=""
3
- AWS_BUCKET_NAME=""
4
- AWS_REGION=""
5
- AWS_SECRET_ACCESS_KEY=
6
- AWS_CLUSTER_NAME="" # fetch all tasks
7
-
8
- AWS_CF_MEDIA_DOMAIN="medias.bookme.plus"
9
- AWS_CF_PUBLIC_KEY_ID="KKC****QFJ2"
10
- AWS_OUTPUT_BUCKET_NAME="output-production-cm"
11
-
12
- AWS_CF_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
13
- -----END PRIVATE KEY-----"
14
-
15
- # for GET request with following host, it will be cached.
16
- CONTENT_HOST_URL=https://content.bookme.plus
17
- CONTENT_CACHE_MAX_AGE=7200
18
-
19
- WAITING_ROOM_SESSION_SIGNATURE="e6b2********************26e3"
20
- WAITING_ROOM_SESSION_EXPIRE_DURATION_IN_SECOND=
21
- WAITING_ROOM_MIN_SESSIONS_COUNT=5
22
- WAITING_ROOM_DISABLED=no
23
-
24
- # Vattanac Bank
25
- VATTANAC_AES_SECRET_KEY= ""
26
- VATTANAC_PUBLIC_KEY=""
27
-
28
- # Organizer URL
29
- ORGANIZER_URL=http://127.0.0.1:4000/organizer
1
+ # Rails Configuration
2
+ RAILS_ENV=development # Rails environment (development, test, production)
3
+ RAILS_MASTER_KEY= # Rails master key for encrypted credentials
4
+ RAILS_MIN_THREADS=5 # Minimum number of threads for Puma server
5
+ RAILS_MAX_THREADS=5 # Maximum number of threads for Puma server
6
+ RAILS_SERVE_STATIC_FILES= # Enable static file serving (useful for production)
7
+ RAILS_LOG_TO_STDOUT=enabled # Enable Rails logging to STDOUT
8
+
9
+ # Server Configuration
10
+ PORT=3000 # Port for the server to listen on
11
+ PIDFILE=tmp/pids/server.pid # Process ID file location
12
+ WEB_CONCURRENCY=2 # Number of web workers for Puma
13
+ DEFAULT_URL_HOST=http://localhost:4000 # Default host URL for the application
14
+ ORGANIZER_URL= # Organizer platform URL (e.g., https://organizer.bookme.plus)
15
+
16
+ # Redis Configuration
17
+ REDIS_URL=redis://localhost:6379/12 # Redis connection URL
18
+ REDIS_POOL_SIZE=5 # Redis connection pool size
19
+ REDIS_TIMEOUT=5 # Redis connection timeout in seconds
20
+
21
+ # AWS Configuration
22
+ AWS_REGION=ap-southeast-1 # AWS region for services
23
+ AWS_ACCESS_KEY_ID= # AWS access key ID for authentication
24
+ AWS_SECRET_ACCESS_KEY= # AWS secret access key for authentication
25
+ AWS_BUCKET_NAME=contigoasia-production # AWS S3 bucket name for storage
26
+ AWS_OUTPUT_BUCKET_NAME=output-production-cm # AWS S3 output bucket name for media processing
27
+ AWS_CLUSTER_NAME=production-cm-web # AWS ECS cluster name for waiting room system
28
+
29
+ # AWS CloudFront Configuration
30
+ ASSETS_SYNC_CF_DIST_ID=E3DV5NICAX5NCH # CloudFront distribution ID for asset synchronization
31
+ AWS_CF_MEDIA_DOMAIN=medias.bookme.plus # CloudFront media domain (e.g., medias.bookme.plus)
32
+ AWS_CF_PUBLIC_KEY_ID= # CloudFront public key ID for signed URLs
33
+ AWS_CF_PRIVATE_KEY= # CloudFront private key for signed URLs (PEM format)
34
+
35
+ # Cache Configuration
36
+ CONTENT_CACHE_MAX_AGE=180 # Maximum age for CDN content cache in seconds (default: 86400)
37
+
38
+ # Email Configuration
39
+ NO_REPLY_EMAIL=noreply@example.com # No-reply email address for outgoing emails
40
+
41
+ # SMS Configuration
42
+ SMS_SENDER_ID=SMS Info # Sender ID for SMS messages (default: 'SMS Info')
43
+
44
+ # Payment Gateway Configuration
45
+ VATTANAC_AES_SECRET_KEY= # Vattanac Bank AES secret key for encryption
46
+ BOOKMEPLUS_PRIVATE_KEY= # BookMe+ private key for Vattanac Bank integration
47
+ VATTANAC_PUBLIC_KEY= # Vattanac Bank public key for signature verification
48
+
49
+ # Google Services Configuration
50
+ GOOGLE_MAP_KEY=AIzaSyBt-OSnDZze6hxGjfiapSxpcyGzBo_COv8 # Google Maps API key for nearby places and location services
51
+ ISSUER_ID=3388000000022336356 # Google Wallet issuer ID for digital passes
52
+
53
+ # Waiting Room System Configuration
54
+ WAITING_ROOM_DISABLED=no # Disable waiting room functionality (set to 'yes' to disable)
55
+ WAITING_ROOM_SESSION_SIGNATURE=4234f0a3-c42f-40b5-9057-7c006793cea4 # JWT signature key for waiting room session tokens
56
+ WAITING_ROOM_SESSION_EXPIRE_DURATION_IN_SECOND=180 # Session expiration duration in seconds (default: 180 / 3 minutes)
57
+ WAITING_ROOM_MIN_SESSIONS_COUNT=5 # Minimum sessions count for waiting room calculation (default: 5)
58
+ WAITING_ROOM_SERVERS_COUNT=2 # Number of running servers for capacity calculation (default: 2)
59
+ WAITING_ROOM_MAX_THREAD_COUNT=10 # Maximum thread count per server (default: 10)
60
+ WAITING_ROOM_MULTIPLIER=150 # Multiplier for waiting room capacity calculation (default: 150)
61
+
62
+ # Exception Notification Configuration
63
+ EXCEPTION_NOTIFY_ENABLE=yes # Enable exception notifications (set to 'yes' to enable)
64
+ EXCEPTION_TELEGRAM_BOT_TOKEN=6441690414:AAFBpevRdBaRTXmalJR2vcSdPNzYoDnMEFk # Telegram bot token for exception notifications
65
+ EXCEPTION_NOTIFIER_TELEGRAM_CHANNEL_ID=-1001807686211 # Telegram channel ID for exception notifications
66
+
67
+ # Telegram Bot Configuration
68
+ DEFAULT_TELEGRAM_BOT_API_TOKEN=6441690414:AAFBpevRdBaRTXmalJR2vcSdPNzYoDnMEFk # Default Telegram bot API token
69
+ PIN_CODE_DEBUG_NOTIFIY_TELEGRAM_ENABLE=yes # Enable PIN code debug notifications via Telegram (set to 'yes' to enable)
70
+
71
+ # Business Rules Configuration
72
+ ACCOMMODATION_MAX_STAY_DAYS=10 # Maximum number of days allowed for accommodation stays (default: 10)
73
+
74
+ # Development/Testing Configuration
75
+ BUNDLE_GEMFILE= # Gemfile location for bundler
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.1.7)
37
+ spree_cm_commissioner (2.1.9.pre.pre1)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -44,6 +44,7 @@ PATH
44
44
  byebug
45
45
  connection_pool
46
46
  counter_culture (~> 3.2)
47
+ devise-two-factor
47
48
  dry-validation (~> 1.10)
48
49
  elasticsearch (~> 8.5)
49
50
  exception_notification
@@ -291,6 +292,11 @@ GEM
291
292
  warden (~> 1.2.3)
292
293
  devise-encryptable (0.2.0)
293
294
  devise (>= 2.1.0)
295
+ devise-two-factor (6.1.0)
296
+ activesupport (>= 7.0, < 8.1)
297
+ devise (~> 4.0)
298
+ railties (>= 7.0, < 8.1)
299
+ rotp (~> 6.0)
294
300
  diff-lcs (1.5.0)
295
301
  docile (1.4.0)
296
302
  domain_name (0.5.20190701)
@@ -701,6 +707,7 @@ GEM
701
707
  railties (>= 5.2)
702
708
  retriable (3.1.2)
703
709
  rexml (3.2.6)
710
+ rotp (6.3.0)
704
711
  rqrcode (2.2.0)
705
712
  chunky_png (~> 1.0)
706
713
  rqrcode_core (~> 1.0)
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ module Admin
3
+ class UserSecurityController < Spree::Admin::BaseController
4
+ before_action :load_user
5
+
6
+ def disable_authenticator
7
+ @user.otp_secret = nil
8
+ @user.otp_required_for_login = false
9
+ begin
10
+ @user.save!
11
+ flash[:success] = I18n.t('authenticator.success_disabled_2fa')
12
+ rescue StandardError => e
13
+ flash[:error] = "Failed to disable 2FA: #{e.message}"
14
+ end
15
+ redirect_to admin_user_security_path(@user)
16
+ end
17
+
18
+ private
19
+
20
+ def load_user
21
+ @user = Spree::User.find(params[:user_id])
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ class AccessTokensController < Doorkeeper::TokensController
6
+ end
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,82 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ module Account
6
+ class GuestDynamicFieldsController < ::Spree::Api::V2::ResourceController
7
+ before_action :require_spree_current_user
8
+ before_action :load_guest
9
+
10
+ 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, phase)
26
+ end
27
+
28
+ @guest.save_and_move_to_next_stage
29
+ 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
+ end
37
+
38
+ private
39
+
40
+ def dynamic_fields_params
41
+ params.require(:dynamic_fields).map do |field|
42
+ field.permit(:id, :value)
43
+ end
44
+ end
45
+
46
+ def load_guest
47
+ @guest = SpreeCmCommissioner::Guest.find(params[:guest_id])
48
+
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, phase)
56
+ dynamic_field = SpreeCmCommissioner::DynamicField.find(field[:id])
57
+ guest_field = @guest.guest_dynamic_fields.find_or_initialize_by(dynamic_field_id: dynamic_field.id)
58
+
59
+ return if guest_field.new_record? && dynamic_field.data_fill_stage != phase
60
+
61
+ if dynamic_field.requires_dynamic_field_options?
62
+ option = dynamic_field.dynamic_field_options.find_by(value: field[:value])
63
+ raise ActiveRecord::RecordNotFound, "Invalid value for dynamic field #{dynamic_field.label}" unless option
64
+
65
+ guest_field.dynamic_field_option = option
66
+ guest_field.value = option.value
67
+ else
68
+ guest_field.value = field[:value]
69
+ end
70
+
71
+ guest_field.save!
72
+ end
73
+
74
+ def serialize_guest(guest)
75
+ SpreeCmCommissioner::V2::Storefront::GuestSerializer.new(guest).serializable_hash
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,39 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ module Account
6
+ class MarkGuestInfoCompleteController < ::Spree::Api::V2::ResourceController
7
+ before_action :require_spree_current_user
8
+
9
+ def update
10
+ update_user_metadata
11
+ render_success_response
12
+ end
13
+
14
+ private
15
+
16
+ def update_user_metadata
17
+ spree_current_user.public_metadata['has_incomplete_guest_info'] = false
18
+ spree_current_user.save!
19
+ end
20
+
21
+ def render_success_response
22
+ render_serialized_payload(200) do
23
+ {
24
+ success: true,
25
+ message: 'Guest information marked as complete',
26
+ has_incomplete_guest_info: false
27
+ }
28
+ end
29
+ end
30
+
31
+ def model_class
32
+ Spree::User
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -4,12 +4,41 @@ module Spree
4
4
  module Storefront
5
5
  module Account
6
6
  module OrdersControllerDecorator
7
+ def self.prepended(base)
8
+ base.include Spree::Api::V2::Storefront::OrderConcern
9
+ end
10
+
7
11
  def collection
8
- if params[:request_state].present?
9
- spree_current_user.orders.filter_by_request_state
10
- else
11
- collection_finder.new(user: spree_current_user, store: current_store, state: params.delete(:state)).execute
12
- end
12
+ @collection =
13
+ if params[:incomplete_guest_info] == 'true'
14
+ find_orders_with_incomplete_guest_info
15
+ elsif params[:request_state].present?
16
+ spree_current_user.orders.filter_by_request_state
17
+ else
18
+ collection_finder.new(user: spree_current_user, store: current_store, state: params.delete(:state)).execute
19
+ end
20
+
21
+ @collection.page(params[:page]).per(params[:per_page])
22
+ end
23
+
24
+ private
25
+
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)
13
42
  end
14
43
 
15
44
  def collection_finder
@@ -1,4 +1,4 @@
1
- # This endpoint is only used for events (via `event.seat_layout_id` to call this API).
1
+ # This endpoint is only used for events (via `event.preload_seat_layout_id` to call this API).
2
2
  # For transit, seat layouts are included directly in the Trip API (no separate call needed).
3
3
  #
4
4
  # In both cases, seat availability must always be checked using:
@@ -0,0 +1,10 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ class AccessTokensController < Doorkeeper::TokensController
6
+ end
7
+ end
8
+ end
9
+ end
10
+ end
@@ -35,7 +35,7 @@ module SpreeCmCommissioner
35
35
  end
36
36
 
37
37
  def clear_inventory_cache
38
- SpreeCmCommissioner.redis_pool.with do |redis|
38
+ SpreeCmCommissioner.inventory_redis_pool.with do |redis|
39
39
  redis.del(inventory_item.redis_key)
40
40
  end
41
41
  end
@@ -0,0 +1,39 @@
1
+ module SpreeCmCommissioner
2
+ module EventMetadata
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ store_public_metadata :background_color, :string
7
+ store_public_metadata :foreground_color, :string
8
+
9
+ store_public_metadata :group_check_in_enabled, :boolean, default: true
10
+ store_public_metadata :individual_check_in_enabled, :boolean, default: true
11
+
12
+ # By default, for most events, manual search is allowed for operators.
13
+ # For certain large events, admin can disable this manually.
14
+ store_public_metadata :allow_manual_search_for_operator, :boolean, default: true
15
+
16
+ # This model has no seat_layout column (polymorphic association), so we store the preload_seat_layout_id ID in public_metadata.
17
+ # This lets us check if a seat layout exists without triggering a database query.
18
+ # The ID is automatically updated whenever the seat_layout is saved.
19
+ store_public_metadata :preload_seat_layout_id, :integer
20
+
21
+ before_validation :validate_at_least_one_check_in_flow_presence
22
+ end
23
+
24
+ def check_in_flows
25
+ flows = []
26
+ flows << 'group' if group_check_in_enabled?
27
+ flows << 'individual' if individual_check_in_enabled?
28
+ flows
29
+ end
30
+
31
+ private
32
+
33
+ def validate_at_least_one_check_in_flow_presence
34
+ return if check_in_flows.any?
35
+
36
+ errors.add(:check_in_flows, 'must have at least one flow selected')
37
+ end
38
+ end
39
+ end
@@ -74,17 +74,40 @@ module SpreeCmCommissioner
74
74
  end
75
75
  end
76
76
 
77
+ def mark_user_as_has_incomplete_guest_info
78
+ user&.public_metadata&.[]=('has_incomplete_guest_info', true)
79
+ user&.save!
80
+ end
81
+
77
82
  def has_incomplete_guest_info? # rubocop:disable Naming/PredicateName
78
83
  line_items.any? do |line_item|
79
84
  line_item.guests.any? do |guest|
80
- !guest.post_registration_completed?
85
+ guest_incomplete?(guest)
81
86
  end
82
87
  end
83
88
  end
84
89
 
85
- def mark_user_as_has_incomplete_guest_info
86
- user&.public_metadata&.[]=('has_incomplete_guest_info', true)
87
- user&.save!
90
+ # overrided not to send email yet to user if order needs confirmation
91
+ # it will send after vendors accepted.
92
+ def confirmation_delivered?
93
+ confirmation_delivered || need_confirmation?
94
+ end
95
+
96
+ # allow authorized user to accept all requested line items
97
+ def accepted_by(user)
98
+ transaction do
99
+ accept!
100
+
101
+ line_items.each do |line_item|
102
+ line_item.accepted_by(user)
103
+ end
104
+ end
105
+ end
106
+
107
+ def guest_incomplete?(guest)
108
+ (guest.pre_registration_fields? && !guest.pre_registration_completed?) ||
109
+ (guest.post_registration_fields? && !guest.post_registration_completed?) ||
110
+ (guest.during_check_in_fields? && !guest.check_in_completed?)
88
111
  end
89
112
 
90
113
  def unstock_inventory!
@@ -159,23 +182,6 @@ module SpreeCmCommissioner
159
182
  user.present? && user.email.present?
160
183
  end
161
184
 
162
- # allow authorized user to accept all requested line items
163
- def accepted_by(user)
164
- transaction do
165
- accept!
166
-
167
- line_items.each do |line_item|
168
- line_item.accepted_by(user)
169
- end
170
- end
171
- end
172
-
173
- # overrided not to send email yet to user if order needs confirmation
174
- # it will send after vendors accepted.
175
- def confirmation_delivered?
176
- confirmation_delivered || need_confirmation?
177
- end
178
-
179
185
  # can_accepted? already use by ransack/visitor.rb
180
186
  def can_accept_all?
181
187
  approved? && requested?
@@ -0,0 +1,97 @@
1
+ # Provides a DSL for defining typed key/value attributes stored in JSON columns
2
+ # using ActiveRecord's `store`.
3
+ #
4
+ # Enhancements over ActiveRecord::Store:
5
+ # - Supports default values when a key is missing or nil
6
+ # - Applies type casting when reading from the database
7
+ # - Adds predicate helpers (`foo?`) for boolean attributes
8
+ # - Validates new data types only (does not validate existing DB values)
9
+ #
10
+ # Supported types:
11
+ # - boolean
12
+ # - string
13
+ # - integer
14
+ #
15
+ # Example usage:
16
+ # ```
17
+ # class Spree::Taxon < ApplicationRecord
18
+ # include SpreeCmCommissioner::StoreMetadata
19
+
20
+ # store_public_metadata :completed, :boolean, default: true
21
+ # store_public_metadata :count, :integer, default: 5
22
+ # store_private_metadata :app_token, :string, default: "XYZ"
23
+ # end
24
+ #
25
+ # taxon = Spree::Taxon.new(completed: true, app_token: "ABC", count: 10)
26
+ # taxon.completed # => true, store in public_metadata
27
+ # taxon.completed? # => true, store in public_metadata
28
+ # taxon.count # => 10, store in public_metadata
29
+ # taxon.app_token # => "ABC", store in private_metadata
30
+ # ```
31
+
32
+ module SpreeCmCommissioner
33
+ module StoreMetadata
34
+ extend ActiveSupport::Concern
35
+
36
+ class_methods do
37
+ def store_private_metadata(key, type, default: nil)
38
+ store :private_metadata, accessors: [key], coder: JSON
39
+
40
+ define_metadata_reader(:private_metadata, key, type, default)
41
+ define_metadata_validation(:private_metadata, key, type)
42
+ end
43
+
44
+ def store_public_metadata(key, type, default: nil)
45
+ store :public_metadata, accessors: [key], coder: JSON
46
+
47
+ define_metadata_reader(:public_metadata, key, type, default)
48
+ define_metadata_validation(:public_metadata, key, type)
49
+ end
50
+
51
+ private
52
+
53
+ # Override the store reader to:
54
+ # - Return default if nil
55
+ # - Cast to correct type
56
+ # - Add `?` predicate for booleans
57
+ def define_metadata_reader(column_name, key, type, default)
58
+ define_method(key) do
59
+ metadata = send(column_name) || {}
60
+ raw_value = metadata[key.to_s]
61
+ return default if raw_value.nil?
62
+
63
+ case type
64
+ when :boolean
65
+ ActiveModel::Type::Boolean.new.cast(raw_value)
66
+ when :integer
67
+ raw_value.to_i
68
+ when :string
69
+ raw_value.to_s
70
+ else
71
+ raw_value
72
+ end
73
+ end
74
+
75
+ define_method("#{key}?") { send(key) } if type == :boolean
76
+ end
77
+
78
+ # Validates only new assignments (raw JSON values) for type safety
79
+ def define_metadata_validation(column_name, key, type)
80
+ case type
81
+ when :boolean
82
+ validates key, inclusion: { in: [true, false] }, allow_nil: true
83
+ when :integer
84
+ validate do
85
+ metadata = send(column_name) || {}
86
+ raw_value = metadata[key.to_s]
87
+ next if raw_value.nil?
88
+
89
+ errors.add(key, 'is not a number') unless raw_value.to_s =~ /\A-?\d+\z/
90
+ end
91
+ when :string
92
+ validates key, length: { maximum: 255 }, allow_nil: true
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -57,10 +57,9 @@ module SpreeCmCommissioner
57
57
  before_create :generate_bib, if: -> { line_item.reload && variant.bib_pre_generation_on_create? }
58
58
  after_create :preload_order_block_ids, if: -> { block_id.present? }
59
59
  after_update :preload_order_block_ids, if: :saved_change_to_block_id?
60
-
61
60
  before_destroy :cancel_reserved_block!, if: -> { reserved_block.present? && reserved_block.active_on_hold? }
62
-
63
61
  after_destroy :preload_order_block_ids
62
+ after_commit :update_user_incomplete_guest_info_status, if: :should_update_incomplete_guest_info?
64
63
 
65
64
  validates :bib_index, uniqueness: true, allow_nil: true
66
65
 
@@ -396,8 +395,6 @@ module SpreeCmCommissioner
396
395
  eligible_check_in_sessions.include?(session)
397
396
  end
398
397
 
399
- private
400
-
401
398
  def product_dynamic_fields
402
399
  @product_dynamic_fields ||= line_item.product.dynamic_fields
403
400
  end
@@ -417,6 +414,43 @@ module SpreeCmCommissioner
417
414
  # Reject if dynamic_field_id is blank or if _destroy is true
418
415
  attributes[:dynamic_field_id].blank? || ActiveModel::Type::Boolean.new.cast(attributes[:_destroy])
419
416
  end
417
+
418
+ def update_user_incomplete_guest_info_status
419
+ return unless user
420
+ return unless line_item&.order&.completed?
421
+
422
+ user.reload
423
+
424
+ has_incomplete = user.orders.complete.any? do |order|
425
+ order_has_incomplete_guests?(order)
426
+ end
427
+
428
+ current_status = user.public_metadata['has_incomplete_guest_info'] || false
429
+
430
+ return unless current_status != has_incomplete
431
+
432
+ user.public_metadata['has_incomplete_guest_info'] = has_incomplete
433
+ user.save!
434
+ end
435
+
436
+ def order_has_incomplete_guests?(order)
437
+ order.line_items.any? do |line_item|
438
+ line_item.guests.any? do |guest|
439
+ guest_incomplete?(guest)
440
+ end
441
+ end
442
+ end
443
+
444
+ def guest_incomplete?(guest)
445
+ (guest.pre_registration_fields? && !guest.pre_registration_completed?) ||
446
+ (guest.post_registration_fields? && !guest.post_registration_completed?) ||
447
+ (guest.during_check_in_fields? && !guest.check_in_completed?)
448
+ end
449
+
450
+ def should_update_incomplete_guest_info?
451
+ saved_change_to_data_fill_stage_phase? ||
452
+ (guest_dynamic_fields.any? && (guest_dynamic_fields.any?(&:saved_changes?) || guest_dynamic_fields.any?(&:destroyed?)))
453
+ end
420
454
  end
421
455
  end
422
456
  # rubocop:enable Metrics/ClassLength
@@ -88,6 +88,7 @@ module SpreeCmCommissioner
88
88
 
89
89
  def sync_guest_data_fill_stage_phase
90
90
  guest&.save_and_move_to_next_stage
91
+ guest&.update_user_incomplete_guest_info_status
91
92
  end
92
93
 
93
94
  def reject_empty_string_for_text_fields