spree_cm_commissioner 2.9.1.pre.pre1 → 2.9.1.pre.pre2

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/api/v2/storefront/waiting_room_sessions_controller.rb +8 -1
  4. data/app/controllers/spree/api/v2/tenant/user_account_linkages_controller.rb +2 -1
  5. data/app/controllers/spree/api/v2/tenant/voting_contestants_controller.rb +1 -2
  6. data/app/controllers/spree/api/v2/tenant/waiting_room_sessions_controller.rb +8 -1
  7. data/app/controllers/spree_cm_commissioner/admin/orders_controller_decorator.rb +9 -0
  8. data/app/interactors/spree_cm_commissioner/account_linkage.rb +13 -1
  9. data/app/interactors/spree_cm_commissioner/fetch_facebook_user_data_from_jwt.rb +90 -0
  10. data/app/interactors/spree_cm_commissioner/user_fb_jwt_authenticator.rb +37 -0
  11. data/app/interactors/spree_cm_commissioner/user_fb_jwt_checker.rb +20 -0
  12. data/app/interactors/spree_cm_commissioner/user_fb_token_authenticator.rb +1 -0
  13. data/app/interactors/spree_cm_commissioner/waiting_guests_caller.rb +11 -2
  14. data/app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb +31 -6
  15. data/app/models/spree_cm_commissioner/order_decorator.rb +6 -0
  16. data/app/models/spree_cm_commissioner/voting_contestant.rb +6 -5
  17. data/app/models/spree_cm_commissioner/waiting_room_session.rb +14 -0
  18. data/app/overrides/spree/admin/shared/_order_summary/internal_note.html.erb.deface +23 -0
  19. data/app/overrides/spree/admin/shared/_order_summary/internal_note_modal.html.erb.deface +40 -0
  20. data/app/request_schemas/spree_cm_commissioner/user_account_linkage_request_schema.rb +4 -2
  21. data/app/serializers/spree/v2/tenant/show_episode_serializer.rb +1 -9
  22. data/app/services/spree_cm_commissioner/user_authenticator.rb +5 -0
  23. data/config/locales/en.yml +5 -0
  24. data/config/locales/km.yml +5 -0
  25. data/config/routes.rb +1 -0
  26. data/db/migrate/20260701000001_add_device_info_to_cm_waiting_room_sessions.rb +14 -0
  27. data/db/migrate/20260703065239_add_position_to_cm_voting_contestants.rb +6 -0
  28. data/lib/spree_cm_commissioner/version.rb +1 -1
  29. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0545736f33006daa20a5af6f1e283c85343fab4e3a163cdf4e69135ed1a4f0b
4
- data.tar.gz: 0edd768156e510c947f7b938059d757f208eafedc037c9a635b68c43d69c6af8
3
+ metadata.gz: 44f0714e8ec1fab5074038112f26db55308feffc75b103ccda9e9d5d5d38409a
4
+ data.tar.gz: 7f963001ae3a8de9f3f829d03eb83f6a2d310cf5a28ddafc477473e1b2fa9678
5
5
  SHA512:
6
- metadata.gz: 70559b4b0fb659a404d8ea33ec8fd96f69665937ebf325e871cbb42a22189fdd04639eed09e69923348cc91a0e8eabaaa82ec234d5faa8f130aba66fe2793e97
7
- data.tar.gz: a86a66a9bdacd25b9388d53acee40e936d8f9c62aca772ca3b8caed2388d8d19580498f63bb2fe54657766fa45c291f6a988bd7f3e9ab55e7248d31547b4961f
6
+ metadata.gz: 2afbdbb418fbdca1c4ebfb9c6327a5fb0394663b87881e3ce56950c1aca7e4a1467708c6249ad2bab098564349d432187866966b6704e662ca87b34966466760
7
+ data.tar.gz: 569d3213e604558bf4d17134aa51fd8cb2e854780f141187a6bff0af167070fe4c53b5f9eda404f4c38c47659aa79fed702ad1f801dc61d9d62d9ceb775e4480
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.9.1.pre.pre1)
37
+ spree_cm_commissioner (2.9.1.pre.pre2)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -14,7 +14,8 @@ module Spree
14
14
  tenant_id: nil,
15
15
  remote_ip: request.remote_ip,
16
16
  waiting_guest_firebase_doc_id: params[:waiting_guest_firebase_doc_id],
17
- page_path: params[:page_path]
17
+ page_path: params[:page_path],
18
+ device_attributes: permitted_device_attributes
18
19
  )
19
20
 
20
21
  if context.success?
@@ -27,6 +28,12 @@ module Spree
27
28
  def resource_serializer
28
29
  SpreeCmCommissioner::V2::Storefront::WaitingRoomSessionSerializer
29
30
  end
31
+
32
+ private
33
+
34
+ def permitted_device_attributes
35
+ params.permit(*SpreeCmCommissioner::WaitingRoomSessionCreator::PERMITTED_DEVICE_ATTRIBUTES)
36
+ end
30
37
  end
31
38
  end
32
39
  end
@@ -15,7 +15,8 @@ module Spree
15
15
  context = SpreeCmCommissioner::AccountLinkage.call(
16
16
  user: spree_current_user,
17
17
  id_token: params[:id_token],
18
- fb_access_token: params[:fb_access_token]
18
+ fb_access_token: params[:fb_access_token],
19
+ fb_jwt: params[:fb_jwt]
19
20
  )
20
21
 
21
22
  if context.success?
@@ -12,8 +12,7 @@ module Spree
12
12
  .includes(:voting_session, :show_contestant,
13
13
  :show_contestant_images, :show_contestant_videos,
14
14
  :advanced_to, :advanced_from
15
- )
16
- .order(:id)
15
+ ).order(:position)
17
16
  end
18
17
 
19
18
  # override
@@ -10,7 +10,8 @@ module Spree
10
10
  tenant_id: MultiTenant.current_tenant_id,
11
11
  remote_ip: request.remote_ip,
12
12
  waiting_guest_firebase_doc_id: params[:waiting_guest_firebase_doc_id],
13
- page_path: params[:page_path]
13
+ page_path: params[:page_path],
14
+ device_attributes: permitted_device_attributes
14
15
  )
15
16
 
16
17
  if context.success?
@@ -23,6 +24,12 @@ module Spree
23
24
  def resource_serializer
24
25
  Spree::V2::Tenant::WaitingRoomSessionSerializer
25
26
  end
27
+
28
+ private
29
+
30
+ def permitted_device_attributes
31
+ params.permit(*SpreeCmCommissioner::WaitingRoomSessionCreator::PERMITTED_DEVICE_ATTRIBUTES)
32
+ end
26
33
  end
27
34
  end
28
35
  end
@@ -10,6 +10,7 @@ module SpreeCmCommissioner
10
10
  close_adjustments cart channel set_channel
11
11
  accept_all reject_all alert_request_to_vendor
12
12
  notifications fire_notification queue_webhooks_requests update_order_status
13
+ update_internal_note
13
14
  ]
14
15
 
15
16
  base.before_action :initialize_notification_methods, only: %i[
@@ -81,6 +82,14 @@ module SpreeCmCommissioner
81
82
 
82
83
  def notifications; end
83
84
 
85
+ def update_internal_note
86
+ internal_note = params.dig(:order, :internal_note).to_s.strip.presence
87
+ @order.update_columns(internal_note: internal_note, updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
88
+
89
+ flash[:success] = Spree.t(:internal_note_updated, default: 'Internal note updated')
90
+ redirect_back fallback_location: spree.edit_admin_order_url(@order)
91
+ end
92
+
84
93
  def update_order_status
85
94
  if @order.next
86
95
  flash[:success] = I18n.t('orders.update_order_status.success')
@@ -1,7 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  class AccountLinkage
3
3
  include Interactor
4
- delegate :id_token, :fb_access_token, :user, to: :context
4
+ delegate :id_token, :fb_access_token, :fb_jwt, :user, to: :context
5
5
 
6
6
  # id_token:, user:
7
7
  def call
@@ -10,6 +10,8 @@ module SpreeCmCommissioner
10
10
  handle_id_token
11
11
  elsif fb_access_token.present?
12
12
  handle_fb_access_token
13
+ elsif fb_jwt.present?
14
+ handle_fb_jwt
13
15
  else
14
16
  context.fail!(message: 'missing_social_token')
15
17
  end
@@ -37,6 +39,16 @@ module SpreeCmCommissioner
37
39
  end
38
40
  end
39
41
 
42
+ def handle_fb_jwt
43
+ facebook_context = FetchFacebookUserDataFromJwt.call(fb_jwt: fb_jwt)
44
+
45
+ if facebook_context.success?
46
+ build_user_identify_provider(facebook_context.provider)
47
+ else
48
+ context.fail!(message: facebook_context.message)
49
+ end
50
+ end
51
+
40
52
  # {
41
53
  # provider_name: provider_name,
42
54
  # email: email,
@@ -0,0 +1,90 @@
1
+ module SpreeCmCommissioner
2
+ class FetchFacebookUserDataFromJwt < BaseInteractor
3
+ JWKS_URL = 'https://limited.facebook.com/.well-known/oauth/openid/jwks/'.freeze
4
+ EXPECTED_ISSUER = 'https://www.facebook.com'.freeze
5
+
6
+ delegate :fb_jwt, to: :context
7
+
8
+ def call
9
+ context.fail!(message: 'fb_jwt_missing') if fb_jwt.blank?
10
+
11
+ claim = decode_jwt
12
+
13
+ context.provider = {
14
+ identity_type: SpreeCmCommissioner::UserIdentityProvider.identity_types[:facebook],
15
+ sub: claim['sub'],
16
+ name: claim['name'],
17
+ email: nil, # Facebook Limited Login JWT does not include email
18
+ first_name: claim['given_name'],
19
+ last_name: claim['family_name'],
20
+ picture: claim['picture']
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ def decode_jwt
27
+ result = JWT.decode(fb_jwt, nil, true, algorithms: ['RS256']) do |header|
28
+ kid = header['kid']
29
+ jwk_data = find_jwk(fetch_jwks, kid) || find_jwk(refresh_jwks, kid)
30
+ context.fail!(message: 'invalid_facebook_jwt_kid') unless jwk_data
31
+ public_key_from_jwk(jwk_data)
32
+ end
33
+
34
+ claim = result[0]
35
+ context.fail!(message: 'invalid_facebook_issuer') if claim['iss'] != EXPECTED_ISSUER
36
+ claim
37
+ rescue JWT::ExpiredSignature
38
+ context.fail!(message: 'fb_jwt_expired')
39
+ rescue JWT::DecodeError => e
40
+ context.fail!(message: e.message)
41
+ end
42
+
43
+ def find_jwk(jwks, kid)
44
+ jwks['keys'].find { |k| k['kid'] == kid }
45
+ end
46
+
47
+ def public_key_from_jwk(jwk_data)
48
+ n = OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data['n']), 2)
49
+ e = OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data['e']), 2)
50
+
51
+ pkcs1 = OpenSSL::ASN1::Sequence([
52
+ OpenSSL::ASN1::Integer(n),
53
+ OpenSSL::ASN1::Integer(e)
54
+ ]
55
+ )
56
+
57
+ spki = OpenSSL::ASN1::Sequence([
58
+ OpenSSL::ASN1::Sequence([
59
+ OpenSSL::ASN1::ObjectId('rsaEncryption'),
60
+ OpenSSL::ASN1::Null(nil)
61
+ ]
62
+ ),
63
+ OpenSSL::ASN1::BitString(pkcs1.to_der)
64
+ ]
65
+ )
66
+
67
+ OpenSSL::PKey::RSA.new(spki.to_der)
68
+ end
69
+
70
+ def refresh_jwks
71
+ Rails.cache.delete('facebook-limited-jwks')
72
+ fetch_jwks
73
+ end
74
+
75
+ def fetch_jwks
76
+ Rails.cache.fetch('facebook-limited-jwks', expires_in: 1.hour) do
77
+ url = URI(JWKS_URL)
78
+ http = Net::HTTP.new(url.host, url.port)
79
+ http.use_ssl = true
80
+ http.open_timeout = 5
81
+ http.read_timeout = 10
82
+ response = http.get(url.request_uri)
83
+ JSON.parse(response.body)
84
+ end
85
+ rescue StandardError => e
86
+ CmAppLogger.error("Facebook JWKS fetch error: #{e.message}")
87
+ context.fail!(message: 'facebook_jwks_fetch_error')
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,37 @@
1
+ module SpreeCmCommissioner
2
+ class UserFbJwtAuthenticator < BaseInteractor
3
+ delegate :fb_jwt, :tenant_id, to: :context
4
+
5
+ # :fb_jwt, :tenant_id (optional)
6
+ def call
7
+ context.fail!(message: 'fb_jwt_missing') unless fb_jwt
8
+ context.fail!(message: checker.message) if checker.failure?
9
+
10
+ context.user = if checker.user.nil?
11
+ register_user
12
+ else
13
+ checker.user
14
+ end
15
+
16
+ context.fail!(message: 'account_temporarily_deleted') if context.user.soft_deleted?
17
+ end
18
+
19
+ private
20
+
21
+ def checker
22
+ @checker ||= SpreeCmCommissioner::UserFbJwtChecker.call(
23
+ fb_jwt: fb_jwt,
24
+ tenant_id: tenant_id
25
+ )
26
+ end
27
+
28
+ def register_user
29
+ register_context = SpreeCmCommissioner::UserRegistrationWithFbToken.call(
30
+ provider: checker.provider,
31
+ tenant_id: tenant_id
32
+ )
33
+
34
+ register_context.user
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ module SpreeCmCommissioner
2
+ class UserFbJwtChecker < BaseInteractor
3
+ delegate :tenant_id, :fb_jwt, to: :context
4
+
5
+ def call
6
+ fb_data = SpreeCmCommissioner::FetchFacebookUserDataFromJwt.call(fb_jwt: fb_jwt)
7
+ context.fail!(message: fb_data.message) if fb_data.failure?
8
+
9
+ context.provider = fb_data.provider
10
+
11
+ identity_checker = SpreeCmCommissioner::UserIdentityChecker.call(
12
+ identity_type: context.provider[:identity_type],
13
+ sub: context.provider[:sub],
14
+ tenant_id: tenant_id
15
+ )
16
+
17
+ context.user = identity_checker.success? ? identity_checker.user : nil
18
+ end
19
+ end
20
+ end
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
5
5
  # :fb_access_token, :tenant_id (optional)
6
6
  def call
7
7
  context.fail!(message: 'fb_access_token_missing') unless fb_access_token
8
+ context.fail!(message: checker.message) if checker.failure?
8
9
 
9
10
  context.user = if checker.user.nil?
10
11
  register_user
@@ -12,14 +12,23 @@ module SpreeCmCommissioner
12
12
  # Firestore bounds a batch update by payload size (10 MiB); 500 ops/commit leaves us far under that.
13
13
  FIRESTORE_BATCH_SIZE = (ENV['WAITING_ROOM_FIRESTORE_BATCH_SIZE'] || 500).to_i
14
14
 
15
+ # Release at most this many guests per run, even when far more slots are free. A capacity spike
16
+ # (cold start / lull → active_sessions low → available_slots ≈ max_sessions, e.g. 900) would
17
+ # otherwise release every queued guest at once, dumping 900 simultaneous session-token requests
18
+ # on the server. Capping paces entry at ~MAX_CALLS_PER_RUN/min (the caller runs every minute);
19
+ # the rest stay queued and drain over the following runs. Matches the stamper's MAX_ETA_BATCH_SIZE
20
+ # so the per-batch ETA the app shows reflects the real release rate.
21
+ MAX_CALLS_PER_RUN = (ENV['WAITING_ROOM_MAX_CALLS_PER_RUN'] || 50).to_i
22
+
15
23
  def call
16
24
  started_at = Time.zone.now
17
25
  available_slots = fetch_available_slots
26
+ call_limit = [available_slots, MAX_CALLS_PER_RUN].min
18
27
 
19
28
  # Always run through to mark_as — even with no slots — so the lobby keeps a fresh heartbeat
20
29
  # (logs.waiting_guests_caller_job) during peak/full periods instead of going stale
21
30
  # and looking like a dead cron.
22
- long_waiting_guests = available_slots.positive? ? fetch_long_waiting_guests(available_slots) : []
31
+ long_waiting_guests = call_limit.positive? ? fetch_long_waiting_guests(call_limit) : []
23
32
  calling_all(long_waiting_guests) if long_waiting_guests.any?
24
33
 
25
34
  mark_as(
@@ -28,7 +37,7 @@ module SpreeCmCommissioner
28
37
  has_queue: waiting_exists?([previous_records_path, records_path]),
29
38
  avg_wait_to_enter_seconds: compute_avg_wait_to_enter_seconds(long_waiting_guests),
30
39
  avg_queue_to_enter_seconds: compute_avg_queue_to_enter_seconds(long_waiting_guests),
31
- recent_slots_per_call: available_slots.positive? ? available_slots : nil,
40
+ recent_slots_per_call: call_limit.positive? ? call_limit : nil,
32
41
  active_sessions: @active_sessions,
33
42
  max_sessions: @max_sessions,
34
43
  logs: call_logs(started_at: started_at, called_count: long_waiting_guests.size)
@@ -3,10 +3,21 @@ require 'zlib'
3
3
 
4
4
  module SpreeCmCommissioner
5
5
  class WaitingRoomSessionCreator < BaseInteractor
6
- delegate :remote_ip, :waiting_guest_firebase_doc_id, :page_path, :tenant_id, to: :context
6
+ delegate :remote_ip, :waiting_guest_firebase_doc_id, :page_path, :tenant_id,
7
+ :device_attributes, to: :context
7
8
 
8
9
  SESSION_CREATION_LOCK_KEY = 'waiting_room_session_creation'.freeze
9
10
 
11
+ # Device attributes the client may set on the session. device_id and
12
+ # device_fingerprint are real columns; the rest are virtual columns backed by
13
+ # public/private metadata (see WaitingRoomSession). Anything else the client
14
+ # sends is dropped so we never persist arbitrary payloads.
15
+ PERMITTED_DEVICE_ATTRIBUTES = %w[
16
+ device_id device_fingerprint
17
+ os_name os_version device_model device_manufacturer device_type
18
+ device_environment app_version app_build timezone network_type
19
+ ].freeze
20
+
10
21
  def call
11
22
  return context.fail!(message: 'must_provide_waiting_guest_firebase_doc_id') if waiting_guest_firebase_doc_id.blank?
12
23
  return context.fail!(message: 'must_provide_remote_ip') if remote_ip.blank?
@@ -53,15 +64,29 @@ module SpreeCmCommissioner
53
64
  # create or renew
54
65
  context.room_session = SpreeCmCommissioner::WaitingRoomSession.where(guest_identifier: waiting_guest_firebase_doc_id).first_or_initialize
55
66
  context.room_session.assign_attributes(
56
- jwt_token: context.jwt_token,
57
- expired_at: expired_at,
58
- remote_ip: remote_ip,
59
- page_path: page_path,
60
- tenant_id: tenant_id
67
+ {
68
+ jwt_token: context.jwt_token,
69
+ expired_at: expired_at,
70
+ remote_ip: remote_ip,
71
+ page_path: page_path,
72
+ tenant_id: tenant_id
73
+ }.merge(sanitized_device_attributes)
61
74
  )
75
+
62
76
  context.room_session.save!
63
77
  end
64
78
 
79
+ # Whitelisted, flat device attributes. Real columns (device_id,
80
+ # device_fingerprint) and virtual columns (StoreMetadata) are assigned the
81
+ # same way; the model routes each to the right store.
82
+ def sanitized_device_attributes
83
+ attributes = device_attributes
84
+ attributes = attributes.to_unsafe_h if attributes.respond_to?(:to_unsafe_h)
85
+ return {} unless attributes.is_a?(Hash)
86
+
87
+ attributes.symbolize_keys.slice(*PERMITTED_DEVICE_ATTRIBUTES.map(&:to_sym))
88
+ end
89
+
65
90
  def log_to_firebase
66
91
  SpreeCmCommissioner::WaitingRoomSessionFirebaseLoggerJob.perform_later(
67
92
  room_session_id: context.room_session.id,
@@ -71,6 +71,12 @@ module SpreeCmCommissioner
71
71
  end
72
72
  end
73
73
 
74
+ # override
75
+ def after_resume
76
+ super
77
+ update_columns(canceled_at: nil, canceler_id: nil) # rubocop:disable Rails/SkipsModelValidations
78
+ end
79
+
74
80
  # override to allow storing cancellation reason in one DB write
75
81
  def canceled_by(user, cancellation_reason: nil)
76
82
  transaction do
@@ -23,19 +23,20 @@ module SpreeCmCommissioner
23
23
  before_destroy :prevent_if_has_advanced_to
24
24
  before_destroy :prevent_if_pre_vote_session
25
25
 
26
+ acts_as_list scope: :voting_session_id
27
+
26
28
  validates :eliminated_via, presence: true, if: :eliminated?
27
29
 
28
- # Uniqueness validation: vote_number must be unique per contestant per voting_session
29
- # Scope: [show_contestant_id, voting_session_id]
30
+ # Uniqueness validation: vote_number must be unique among contestants within the same voting_session
31
+ # Scope: voting_session_id
30
32
  #
31
33
  # | contestant_id | session_id | vote_number | Status | Reason |
32
34
  # |---------------|-----------|-------------|--------|---------------------------|
33
35
  # | 1 | 1 | 1 | ✓ OK | First record |
34
- # | 1 | 1 | 1 | ✗ FAIL | Duplicate in same scope |
36
+ # | 2 | 1 | 1 | ✗ FAIL | Duplicate in same session |
35
37
  # | 1 | 1 | 2 | ✓ OK | Can update/change number |
36
- # | 2 | 1 | 1 | ✓ OK | Different contestant |
37
38
  # | 1 | 2 | 1 | ✓ OK | Different session |
38
- validates :vote_number, uniqueness: { scope: %i[show_contestant_id voting_session_id], allow_nil: true }
39
+ validates :vote_number, uniqueness: { scope: :voting_session_id, allow_nil: true }
39
40
 
40
41
  delegate :name, :contestant_number, :category, :gender, :eliminated_at, to: :show_contestant, allow_nil: true
41
42
  delegate :can_vote?, to: :voting_session, allow_nil: true
@@ -1,5 +1,19 @@
1
1
  module SpreeCmCommissioner
2
2
  class WaitingRoomSession < Base
3
+ include SpreeCmCommissioner::StoreMetadata
4
+
5
+ store_public_metadata :os_name, :string # e.g. "ios", "android", "web"
6
+ store_public_metadata :os_version, :string # e.g. "17.4", "14"
7
+ store_public_metadata :device_model, :string # e.g. "iPhone15,2", "Pixel 8"
8
+ store_public_metadata :device_manufacturer, :string # e.g. "Apple", "Google", "Samsung"
9
+ store_public_metadata :device_type, :string # e.g. "phone", "tablet", "web", "desktop"
10
+ store_public_metadata :device_environment, :string # e.g. "physical", "emulator", "simulator"
11
+ store_public_metadata :app_version, :string # e.g. "2.3.1"
12
+ store_public_metadata :app_build, :string # e.g. "450"
13
+
14
+ store_private_metadata :timezone, :string # e.g. "ICT", "+07", "UTC"
15
+ store_private_metadata :network_type, :string # e.g. "wifi", "cellular", "ethernet", "none"
16
+
3
17
  validates :jwt_token, presence: true
4
18
  validates :expired_at, presence: true
5
19
  validates :remote_ip, presence: true
@@ -0,0 +1,23 @@
1
+ <!-- insert_after "li:has(#order_channel)" -->
2
+
3
+ <li class="list-group-item d-flex justify-content-between align-items-center" data-hook="admin_order_tab_internal_note">
4
+ <small><%= Spree.t(:internal_note, default: 'Internal Note') %></small>
5
+ <span class="d-flex align-items-center" style="gap: .35rem; min-width: 0;">
6
+ <% if @order.internal_note.present? %>
7
+ <span class="small text-muted text-truncate text-right" style="max-width: 150px;" title="<%= @order.internal_note %>"><%= @order.internal_note %></span>
8
+ <% if can?(:update, @order) %>
9
+ <%= link_to '#', class: 'btn btn-light btn-sm with-tip icon-link p-1',
10
+ title: Spree.t(:edit),
11
+ data: { toggle: 'modal', target: "#internal-note-modal-#{@order.number}" } do %>
12
+ <%= svg_icon name: 'pencil-square.svg', width: '13', height: '13' %>
13
+ <% end %>
14
+ <% end %>
15
+ <% elsif can?(:update, @order) %>
16
+ <%= link_to Spree.t(:add_internal_note, default: 'Add note'), '#',
17
+ class: 'small',
18
+ data: { toggle: 'modal', target: "#internal-note-modal-#{@order.number}" } %>
19
+ <% else %>
20
+ <span class="small text-muted font-italic"><%= Spree.t(:no_internal_note, default: 'No note') %></span>
21
+ <% end %>
22
+ </span>
23
+ </li>
@@ -0,0 +1,40 @@
1
+ <!-- insert_after "#order_tab_summary" -->
2
+
3
+ <% if can?(:update, @order) %>
4
+ <div id="internal-note-modal-<%= @order.number %>" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="internalNoteModalLabel-<%= @order.number %>" aria-hidden="true">
5
+ <div class="modal-dialog" role="document">
6
+ <div class="modal-content">
7
+ <div class="modal-header">
8
+ <h5 class="modal-title mr-auto" id="internalNoteModalLabel-<%= @order.number %>"><%= Spree.t(:internal_note, default: 'Internal Note') %></h5>
9
+ <button type="button" class="close" data-dismiss="modal" aria-label="<%= Spree.t(:close) %>">
10
+ <span aria-hidden="true">&times;</span>
11
+ </button>
12
+ </div>
13
+
14
+ <%= form_with model: @order, url: update_internal_note_admin_order_path(@order), method: :patch, local: true do |f| %>
15
+ <div class="modal-body">
16
+ <div class="form-group text-left text-start">
17
+ <%= f.text_area :internal_note, rows: 5, class: 'form-control' %>
18
+ </div>
19
+ </div>
20
+ <div class="modal-footer d-flex justify-end">
21
+ <button type="button" class="btn btn-light" data-dismiss="modal"><%= Spree.t(:close) %></button>
22
+ <%= f.submit Spree.t(:save_internal_note, default: 'Save Note'), class: 'btn btn-primary' %>
23
+ </div>
24
+ <% end %>
25
+ </div>
26
+ </div>
27
+ </div>
28
+
29
+ <%# The summary renders inside #contextualSideMenu (position: sticky/fixed), whose %>
30
+ <%# stacking context sits below Bootstrap's body-level .modal-backdrop. Relocate the %>
31
+ <%# modal to <body> so it layers above the backdrop and stays interactive. %>
32
+ <script>
33
+ (function () {
34
+ var modal = document.getElementById('internal-note-modal-<%= @order.number %>');
35
+ if (modal && modal.parentNode !== document.body) {
36
+ document.body.appendChild(modal);
37
+ }
38
+ })();
39
+ </script>
40
+ <% end %>
@@ -3,13 +3,15 @@ module SpreeCmCommissioner
3
3
  params do
4
4
  optional(:id_token).maybe(:string)
5
5
  optional(:fb_access_token).maybe(:string)
6
+ optional(:fb_jwt).maybe(:string)
6
7
  end
7
8
 
8
- rule(:id_token, :fb_access_token) do
9
+ rule(:id_token, :fb_access_token, :fb_jwt) do
9
10
  idt = values[:id_token]
10
11
  fbt = values[:fb_access_token]
12
+ fbj = values[:fb_jwt]
11
13
 
12
- key(:base).failure('id_token_or_fb_access_token_required') if blank_or_empty?(idt) && blank_or_empty?(fbt)
14
+ key(:base).failure('id_token_or_fb_access_token_required') if blank_or_empty?(idt) && blank_or_empty?(fbt) && blank_or_empty?(fbj)
13
15
  end
14
16
 
15
17
  private
@@ -2,15 +2,7 @@ module Spree
2
2
  module V2
3
3
  module Tenant
4
4
  class ShowEpisodeSerializer < BaseSerializer
5
- attributes :episode_number, :name, :scheduled_at, :ends_at
6
-
7
- attribute :status do |episode|
8
- if episode.current?
9
- 'current'
10
- elsif episode.upcoming?
11
- 'upcoming'
12
- end
13
- end
5
+ attributes :episode_number, :name, :scheduled_at, :ends_at, :status, :preview
14
6
 
15
7
  has_many :voting_sessions, serializer: Spree::V2::Tenant::VotingSessionSerializer
16
8
  has_many :vote_packages, serializer: Spree::V2::Tenant::VotePackageSerializer
@@ -4,6 +4,7 @@ module SpreeCmCommissioner
4
4
  # :username, :password
5
5
  # :id_token
6
6
  # :fb_access_token
7
+ # :fb_jwt
7
8
  # :telegram_init_data, :tg_bot
8
9
  # :session_id
9
10
 
@@ -42,6 +43,9 @@ module SpreeCmCommissioner
42
43
  when 'facebook_auth'
43
44
  options = { fb_access_token: params[:fb_access_token], tenant_id: tenant_id }
44
45
  SpreeCmCommissioner::UserFbTokenAuthenticator.call(options)
46
+ when 'facebook_jwt_auth'
47
+ options = { fb_jwt: params[:fb_jwt], tenant_id: tenant_id }
48
+ SpreeCmCommissioner::UserFbJwtAuthenticator.call(options)
45
49
  when 'telegram_web_app_auth'
46
50
  options = { telegram_init_data: params[:telegram_init_data], telegram_bot_username: params[:tg_bot] }
47
51
  SpreeCmCommissioner::UserTelegramWebAppAuthenticator.call(options)
@@ -55,6 +59,7 @@ module SpreeCmCommissioner
55
59
  return 'login_auth' if params.key?(:username) && params.key?(:password)
56
60
  return 'social_auth' if params.key?(:id_token)
57
61
  return 'facebook_auth' if params.key?(:fb_access_token)
62
+ return 'facebook_jwt_auth' if params.key?(:fb_jwt)
58
63
  return 'telegram_web_app_auth' if params.key?(:telegram_init_data) && params.key?(:tg_bot)
59
64
  return 'vattanac_bank_web_app_auth' if params.key?(:session_id)
60
65
 
@@ -216,6 +216,11 @@ en:
216
216
  riel: "៛"
217
217
 
218
218
  spree:
219
+ internal_note: "Internal Note"
220
+ add_internal_note: "Add note"
221
+ no_internal_note: "No note"
222
+ save_internal_note: "Save Note"
223
+ internal_note_updated: "Internal note updated"
219
224
  voting_contestant_advancer:
220
225
  reversion_error: "Cannot eliminate here yet. This contestant appears in a later session."
221
226
  is_open_dated: "Is Open Dated Trip?"
@@ -200,6 +200,11 @@ km:
200
200
  riel: "៛"
201
201
 
202
202
  spree:
203
+ internal_note: "ចំណាំផ្ទៃក្នុង"
204
+ add_internal_note: "បន្ថែមចំណាំ"
205
+ no_internal_note: "គ្មានចំណាំ"
206
+ save_internal_note: "រក្សាទុកចំណាំ"
207
+ internal_note_updated: "បានរក្សាទុកចំណាំផ្ទៃក្នុង"
203
208
  voting_contestant_advancer:
204
209
  reversion_error: "មិនអាចដកចេញនៅវគ្គនេះបានទេ។ បេក្ខជននេះមាននៅវគ្គបោះឆ្នោតក្រោយ។"
205
210
  registered_by:
data/config/routes.rb CHANGED
@@ -308,6 +308,7 @@ Spree::Core::Engine.add_routes do
308
308
  put :fire_notification
309
309
  put :queue_webhooks_requests
310
310
  put :update_order_status
311
+ patch :update_internal_note
311
312
  end
312
313
 
313
314
  resources :line_items, only: %i[edit update]
@@ -0,0 +1,14 @@
1
+ class AddDeviceInfoToCmWaitingRoomSessions < ActiveRecord::Migration[7.0]
2
+ def change
3
+ # Indexed identifiers used for dedup / abuse detection.
4
+ add_column :cm_waiting_room_sessions, :device_id, :string, if_not_exists: true
5
+ add_column :cm_waiting_room_sessions, :device_fingerprint, :string, if_not_exists: true
6
+
7
+ # Typed device attributes live here via SpreeCmCommissioner::StoreMetadata.
8
+ add_column :cm_waiting_room_sessions, :public_metadata, :jsonb, default: {}, if_not_exists: true
9
+ add_column :cm_waiting_room_sessions, :private_metadata, :jsonb, default: {}, if_not_exists: true
10
+
11
+ add_index :cm_waiting_room_sessions, :device_id, if_not_exists: true
12
+ add_index :cm_waiting_room_sessions, :device_fingerprint, if_not_exists: true
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ class AddPositionToCmVotingContestants < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :cm_voting_contestants, :position, :integer, default: nil, if_not_exists: true
4
+ add_index :cm_voting_contestants, :position, if_not_exists: true
5
+ end
6
+ end
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.9.1.pre.pre1'.freeze
2
+ VERSION = '2.9.1.pre.pre2'.freeze
3
3
 
4
4
  module_function
5
5
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_cm_commissioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.1.pre.pre1
4
+ version: 2.9.1.pre.pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
@@ -1243,6 +1243,7 @@ files:
1243
1243
  - app/interactors/spree_cm_commissioner/event_qr_generator.rb
1244
1244
  - app/interactors/spree_cm_commissioner/existing_account_checker.rb
1245
1245
  - app/interactors/spree_cm_commissioner/fetch_facebook_user_data.rb
1246
+ - app/interactors/spree_cm_commissioner/fetch_facebook_user_data_from_jwt.rb
1246
1247
  - app/interactors/spree_cm_commissioner/firebase_email_fetcher.rb
1247
1248
  - app/interactors/spree_cm_commissioner/firebase_email_fetcher_cron_executor.rb
1248
1249
  - app/interactors/spree_cm_commissioner/firebase_id_token_provider.rb
@@ -1319,6 +1320,8 @@ files:
1319
1320
  - app/interactors/spree_cm_commissioner/user_contact_updater.rb
1320
1321
  - app/interactors/spree_cm_commissioner/user_device_token_deregister.rb
1321
1322
  - app/interactors/spree_cm_commissioner/user_device_token_register.rb
1323
+ - app/interactors/spree_cm_commissioner/user_fb_jwt_authenticator.rb
1324
+ - app/interactors/spree_cm_commissioner/user_fb_jwt_checker.rb
1322
1325
  - app/interactors/spree_cm_commissioner/user_fb_token_authenticator.rb
1323
1326
  - app/interactors/spree_cm_commissioner/user_fb_token_checker.rb
1324
1327
  - app/interactors/spree_cm_commissioner/user_forgotten_password_updater.rb
@@ -1828,6 +1831,8 @@ files:
1828
1831
  - app/overrides/spree/admin/shared/_main_menu/sidebar_customer_notification.html.erb.deface
1829
1832
  - app/overrides/spree/admin/shared/_main_menu/sidebar_homepage.html.erb.deface
1830
1833
  - app/overrides/spree/admin/shared/_order_summary/adjustment_total.html.erb.deface
1834
+ - app/overrides/spree/admin/shared/_order_summary/internal_note.html.erb.deface
1835
+ - app/overrides/spree/admin/shared/_order_summary/internal_note_modal.html.erb.deface
1831
1836
  - app/overrides/spree/admin/shared/_order_summary/order_status.html.erb.deface
1832
1837
  - app/overrides/spree/admin/shared/_order_summary/request_state.html.erb.deface
1833
1838
  - app/overrides/spree/admin/shared/_order_tabs/guests.html.erb.deface
@@ -3407,6 +3412,8 @@ files:
3407
3412
  - db/migrate/20260623082233_create_cm_claims.rb
3408
3413
  - db/migrate/20260624175658_add_unique_index_to_cm_claims.rb
3409
3414
  - db/migrate/20260629000001_add_term_of_use_to_spree_vendors.rb
3415
+ - db/migrate/20260701000001_add_device_info_to_cm_waiting_room_sessions.rb
3416
+ - db/migrate/20260703065239_add_position_to_cm_voting_contestants.rb
3410
3417
  - docker-compose.yml
3411
3418
  - docs/api/scoped-access-token-endpoints.md
3412
3419
  - docs/option_types/attr_types.md