spree_cm_commissioner 2.12.0 → 2.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10936d8185cc09d4bd8eca8c3c2626cb9bb113d2e94695030be009f9b38edbcc
4
- data.tar.gz: 8b1493341611af89e52c06017d1abc3ad6adc838a541dd3ec93dd76877985eaf
3
+ metadata.gz: 518b50594502cdd95eb7710824cb6f8a104974d44e9602f2babb3235bcc17559
4
+ data.tar.gz: 4906dc6a8f57707ebf5fcb9712b6a074bbef1af0437e14923f61f2ff6606c32e
5
5
  SHA512:
6
- metadata.gz: f945b868565fa7e4f699c07c35f20bb324b4c87d910f96738d4d4049e5b17d7c1455fd3227c4ab699bf85a449349ab4bc0cdd52b1b3d55bd519c48762da39ab6
7
- data.tar.gz: d6fd25adafc7a2ad5e710043fbd8dc062008292df46d4819598a0a45333fd4dbbccae1428b6bc389ec6c1bcde2ffd9f7ddd1665a5974ff14989148622fabcadd
6
+ metadata.gz: 67a54630258eda9e5bf72e26ce22177d6b1ce8da399d71434319403f1df8d4a71469ec115f7015b7b9064544688fe833f3230329e456e09eeb88b4f8a488212c
7
+ data.tar.gz: 6aa524ac8653f151fd629c4bef58258212ee3eb67f5f1bba328f7e3e964c4efb9f67ec9692c359c80f4b2d8abac088541b7b692503d02bb8afecedc21e550072
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.12.0)
37
+ spree_cm_commissioner (2.12.2)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -3,6 +3,7 @@ module Spree
3
3
  module UsersControllerDecorator
4
4
  def self.prepended(base)
5
5
  base.before_action :build_profile, only: %i[create update]
6
+ base.before_action :downcase_normalized_login_search_param, only: :index
6
7
  end
7
8
 
8
9
  # override
@@ -81,6 +82,14 @@ module Spree
81
82
 
82
83
  @user.build_profile(attachment: permitted_resource_params.delete(:profile))
83
84
  end
85
+
86
+ # normalized_login is stored lowercase, so a mixed-case search term would never
87
+ # match under a plain LIKE/ILIKE — downcase it before Ransack builds the query.
88
+ def downcase_normalized_login_search_param
89
+ return unless params[:q]&.key?(:normalized_login_cont)
90
+
91
+ params[:q][:normalized_login_cont] = params[:q][:normalized_login_cont].to_s.downcase
92
+ end
84
93
  end
85
94
  end
86
95
  end
@@ -3,8 +3,8 @@ module Spree
3
3
  module V2
4
4
  module Storefront
5
5
  class QueueDraftOrdersController < ::Spree::Api::V2::ResourceController
6
- # Creating the order (once the job runs) acquires the inventory hold, so honor the
7
- # load-test bypass here too, same as DraftOrdersController.
6
+ # Sets Current.load_test_bypass from the X-Request-Verify secret. The hold is acquired later
7
+ # in ProcessJob, so `create` passes this flag through options for the job to restore there.
8
8
  include SpreeCmCommissioner::LoadTestBypassable
9
9
 
10
10
  # Accepts a draft order request without building it: writes a queued Firestore status
@@ -27,7 +27,9 @@ module Spree
27
27
  options: {
28
28
  device_id: device_id,
29
29
  last_ip_address: request.remote_ip,
30
- private_sale_token: params[:st]
30
+ private_sale_token: params[:st],
31
+ # Carried into the job so it can bypass hold limits for a load test (see ProcessJob).
32
+ load_test_bypass: SpreeCmCommissioner::Current.load_test_bypass
31
33
  }
32
34
  },
33
35
  locale: I18n.locale.to_s
@@ -13,9 +13,13 @@ module SpreeCmCommissioner
13
13
 
14
14
  def perform(document_path, create_type, creation_params, locale = nil)
15
15
  I18n.with_locale(locale.presence || I18n.default_locale) do
16
- SpreeCmCommissioner::QueueDraftOrders::Process.call(
17
- document_path: document_path, create_type: create_type, creation_params: creation_params
18
- )
16
+ # Current doesn't cross into the job, so restore the load-test-bypass flag here — otherwise
17
+ # ValidateLimits would reject a trusted load test.
18
+ SpreeCmCommissioner::Current.set(load_test_bypass: creation_params.dig(:options, :load_test_bypass) || false) do
19
+ SpreeCmCommissioner::QueueDraftOrders::Process.call(
20
+ document_path: document_path, create_type: create_type, creation_params: creation_params
21
+ )
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -21,8 +21,10 @@ module SpreeCmCommissioner
21
21
  validates :phone_number, uniqueness: { scope: :tenant_id, allow_blank: true }
22
22
  validates :phone_number, format: { with: /^\d{8,12}$/, :multiline => true, allow_blank: true }
23
23
 
24
+ before_validation :normalize_login
25
+
24
26
  validates :login, presence: true, if: -> (u) { u.require_login_identity_all_blank_for(:login) && !u.registering_via_social_network }
25
- validates :login, uniqueness: { scope: :tenant_id, case_sensitive: false, allow_blank: true }
27
+ validate :validate_normalized_login_uniqueness
26
28
 
27
29
  _validators.reject! { |key, _| key == :email }
28
30
  _validate_callbacks.each { |c| c.filter.attributes.delete(:email) if c.filter.respond_to?(:attributes) }
@@ -46,7 +48,7 @@ module SpreeCmCommissioner
46
48
  elsif login =~ URI::MailTo::EMAIL_REGEXP
47
49
  scope.find_by(email: login)
48
50
  else
49
- scope.find_by(login: login)
51
+ scope.find_by(normalized_login: login)
50
52
  end
51
53
  end
52
54
  end
@@ -58,6 +60,23 @@ module SpreeCmCommissioner
58
60
 
59
61
  protected
60
62
 
63
+ # Keeps a plain, indexable lowercase copy of `login` so uniqueness and lookups can
64
+ # use a straight equality match instead of LOWER(login) everywhere, without ever
65
+ # rewriting the user's own `login` value (shown to them as a real handle elsewhere).
66
+ def normalize_login
67
+ self.normalized_login = login.present? ? login.downcase : nil
68
+ end
69
+
70
+ # Attaches the error to :login (the field shown to and edited by the user), not
71
+ # :normalized_login, so existing forms/specs keyed on `errors[:login]` keep working.
72
+ def validate_normalized_login_uniqueness
73
+ return if normalized_login.blank?
74
+
75
+ scope = self.class.where(tenant_id: tenant_id, normalized_login: normalized_login)
76
+ scope = scope.where.not(id: id) if persisted?
77
+ errors.add(:login, :taken) if scope.exists?
78
+ end
79
+
61
80
  def require_login_identity_all_blank_for(field)
62
81
  required_fields = %i[phone_number email user_identity_providers login].reject { |item| item == field }
63
82
  required_fields.each do |required_field|
@@ -195,18 +195,10 @@ module SpreeCmCommissioner
195
195
  end
196
196
  end
197
197
 
198
- # override
199
- # [spree_core] only checks active?/deleted?, so a scheduled product (available_on in the future)
200
- # reports available even though it isn't purchasable yet. Since Variant#available? delegates
201
- # here, this closes the gap for every caller (cart, checkout, Stock::AvailabilityChecker) in
202
- # one place — mirrors VariantDecorator#discontinued?'s window check.
203
- def available?
204
- return false unless active?
205
- return false if deleted?
206
- return false if discontinued?
207
- return false if available_on.present? && available_on > Time.current
208
-
209
- true
198
+ # Not yet on sale. Kept separate from available? so a scheduled product's real stock still
199
+ # shows via in_stock? Stock::AvailabilityValidatorDecorator checks this to block add-to-cart.
200
+ def scheduled?
201
+ available_on.present? && available_on > Time.current
210
202
  end
211
203
 
212
204
  private
@@ -15,6 +15,11 @@ module SpreeCmCommissioner
15
15
  # skipping it here means we never even build a checker for them.
16
16
  return if line_item.order&.ticket_transfer?
17
17
 
18
+ # scheduled? isn't checked anywhere else (product.available?/variant.discontinued? are, via
19
+ # can_supply? below) — a scheduled product must still read in_stock? true, so its sale
20
+ # window is enforced here instead.
21
+ return add_inactive_product_error(line_item) if !line_item.product.available? || line_item.product.scheduled?
22
+
18
23
  checker = SpreeCmCommissioner::Stock::LineItemAvailabilityChecker.new(line_item).checker
19
24
  return if checker.can_supply?(quantity)
20
25
 
@@ -94,7 +94,7 @@ module SpreeCmCommissioner
94
94
  base.before_save :update_otp_enabled
95
95
 
96
96
  base.attr_accessor :assigned_roles
97
- base.whitelisted_ransackable_attributes = %w[email first_name last_name gender phone_number blocked_at]
97
+ base.whitelisted_ransackable_attributes = %w[email first_name last_name gender phone_number blocked_at normalized_login]
98
98
  base.devise :two_factor_authenticatable
99
99
  def base.end_users
100
100
  joins('LEFT JOIN spree_vendor_users ON spree_users.id = spree_vendor_users.user_id').where(spree_vendor_users: { user_id: nil })
@@ -4,13 +4,13 @@
4
4
  service_types = SpreeCmCommissioner::ServiceType.translated_service_types.map { |key, label| [label, key.to_s] }
5
5
  %>
6
6
 
7
- <div data-hook="admin_user_form_user_section" class="p-3 mt-4 rounded border form-group bg-light">
7
+ <div data-hook="admin_user_form_user_section" class="p-3 mt-4 border rounded form-group bg-light">
8
8
  <strong class="mb-3 d-block"><%= Spree.t(:add_on) %></strong>
9
9
 
10
10
  <%= f.field_container :primary_service_type do %>
11
11
  <%= f.label :primary_service_type, Spree.t(:primary_service_type) %>
12
12
  <%= f.select :primary_service_type,
13
- options_for_select(service_types, f.object.primary_service_type),
13
+ options_for_select(service_types, f.object.primary_service_type || service_types.first&.last),
14
14
  { include_blank: Spree.t(:select_service_type) },
15
15
  class: 'form-control select2-clear' %>
16
16
  <%= f.error_message_on :primary_service_type %>
@@ -7,6 +7,9 @@
7
7
  <span class="badge badge-danger text-uppercase"><%= I18n.t('spree_user.blocked') %></span>
8
8
  <% end %>
9
9
  </td>
10
+ <td class='login'>
11
+ <%= user.login %>
12
+ </td>
10
13
  <td class='first_name'>
11
14
  <%= user.first_name %>
12
15
  </td>
@@ -4,6 +4,9 @@
4
4
  <th>
5
5
  <%= sort_link @search, :email, Spree.t(:email), {}, {title: 'users_email_title'} %>
6
6
  </th>
7
+ <th>
8
+ <%= sort_link @search, :login, Spree.t(:login), {}, {title: 'login_title'} %>
9
+ </th>
7
10
  <th>
8
11
  <%= sort_link @search, :first_name, Spree.t(:first_name), {}, {title: 'first_name_title'} %>
9
12
  </th>
@@ -39,6 +39,12 @@
39
39
  <%= f.telephone_field :phone_number_cont, class: 'form-control js-filterable', autofocus: true, placeholder: 'xxx-xxx-xxxx'%>
40
40
  </div>
41
41
  </div>
42
+ <div class="col-12 col-lg-6">
43
+ <div class="form-group">
44
+ <%= f.label :normalized_login_cont, Spree.t(:login) %>
45
+ <%= f.text_field :normalized_login_cont, class: 'form-control js-filterable' %>
46
+ </div>
47
+ </div>
42
48
  <div class="col-12 col-lg-6">
43
49
  <div class="form-group">
44
50
  <%# blocked_at_null: false = blocked, true = active. Ransack drops the blank option, so an
@@ -0,0 +1,48 @@
1
+ module SpreeCmCommissioner
2
+ module Users
3
+ class DedupeLogins
4
+ prepend Spree::ServiceModule::Base
5
+
6
+ def call
7
+ renamed_count = duplicate_groups.sum { |tenant_id, normalized_login| rename_duplicates(tenant_id, normalized_login) }
8
+ success(renamed_count: renamed_count)
9
+ rescue StandardError => e
10
+ failure(nil, e.message)
11
+ end
12
+
13
+ private
14
+
15
+ # login uniqueness is scoped to tenant_id, so duplicates must be grouped the same way
16
+ # or users in different tenants would be falsely flagged as colliding.
17
+ def duplicate_groups
18
+ Spree::User.where.not(normalized_login: [nil, ''])
19
+ .group(:tenant_id, :normalized_login)
20
+ .having('count(*) > 1')
21
+ .pluck(:tenant_id, :normalized_login)
22
+ end
23
+
24
+ def rename_duplicates(tenant_id, normalized_login)
25
+ # keep the oldest account's login untouched; rename the rest
26
+ duplicates = Spree::User.where(tenant_id: tenant_id, normalized_login: normalized_login)
27
+ .order(:created_at)
28
+ .offset(1)
29
+ .to_a
30
+
31
+ duplicates.each { |user| rename_user(user, tenant_id) }
32
+ duplicates.size
33
+ end
34
+
35
+ def rename_user(user, tenant_id)
36
+ old_login = user.login
37
+ new_login = SpreeCmCommissioner::Users::Username::Generate.call(user: user)
38
+ # update_column skips callbacks, so normalized_login must be set explicitly here too.
39
+ user.update_columns(login: new_login, normalized_login: new_login.downcase) # rubocop:disable Rails/SkipsModelValidations
40
+
41
+ Rails.logger.info do
42
+ "[SpreeCmCommissioner::Users::DedupeLogins] user_id=#{user.id} tenant_id=#{tenant_id.inspect} " \
43
+ "login=#{old_login.inspect} -> #{new_login.inspect}"
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -18,7 +18,7 @@ module SpreeCmCommissioner
18
18
  login = payload['login']
19
19
 
20
20
  # we don't reveal whether user exists or not in error messages for security reasons.
21
- user = Spree::User.find_by(login: login)
21
+ user = Spree::User.find_by(normalized_login: login.to_s.downcase)
22
22
  raise JWT::DecodeError, 'QR code is invalid' if user.nil?
23
23
 
24
24
  JWT.decode(qr_data, user.secure_token, true, { algorithm: 'HS256' })
@@ -27,7 +27,7 @@ module SpreeCmCommissioner
27
27
  def call
28
28
  @max_attempts.times do
29
29
  username = generate_clean_username
30
- return username unless Spree::User.exists?(login: username)
30
+ return username unless Spree::User.exists?(normalized_login: username.downcase)
31
31
  end
32
32
 
33
33
  # Last resort: use extended number range
@@ -1,15 +1,15 @@
1
1
  <%= render partial: 'spree/admin/users/tabs', locals: { current: :security } %>
2
2
 
3
- <div class="card shadow-sm">
3
+ <div class="shadow-sm card">
4
4
  <div class="card-header bg-light">
5
- <h5 class="card-title mb-0 h6">
5
+ <h5 class="mb-0 card-title h6">
6
6
  <%= Spree.t(:two_factor_authentication) %>
7
7
  </h5>
8
8
  </div>
9
9
 
10
- <div class="card-body text-center">
10
+ <div class="text-center card-body">
11
11
  <% if @user.otp_secret.present? && @user.otp_required_for_login == true %>
12
- <p class="text-muted mb-3">
12
+ <p class="mb-3 text-muted">
13
13
  Two-factor authentication is currently active for this user.
14
14
  </p>
15
15
  <%= button_to disable_authenticator_admin_user_security_path, method: :delete, class: "btn btn-danger btn-md",
@@ -17,9 +17,50 @@
17
17
  Disable 2FA
18
18
  <% end %>
19
19
  <% else %>
20
- <p class="text-muted mb-0">
20
+ <p class="mb-0 text-muted">
21
21
  Two-factor authentication is <strong>not enabled</strong> for this user.
22
22
  </p>
23
23
  <% end %>
24
24
  </div>
25
25
  </div>
26
+
27
+ <div data-hook="admin_user_account_status" class="mt-3 shadow-sm card">
28
+ <div class="card-header bg-light">
29
+ <h5 class="mb-0 card-title h6">
30
+ <%= I18n.t('spree_user.account_status') %>
31
+ </h5>
32
+ </div>
33
+
34
+ <div class="card-body">
35
+ <% if @user.blocked? %>
36
+ <p>
37
+ <span class="badge badge-danger text-uppercase"><%= I18n.t('spree_user.blocked') %></span>
38
+ <span class="ml-2 text-muted"><%= I18n.t('spree_user.blocked_at', time: l(@user.blocked_at, format: :long)) %></span>
39
+ </p>
40
+
41
+ <% if @user.blocked_reason.present? %>
42
+ <p class="text-muted"><%= @user.blocked_reason %></p>
43
+ <% end %>
44
+
45
+ <%= form_tag spree.unblock_admin_user_path(@user), method: :put do %>
46
+ <%= button_tag I18n.t('spree_user.unblock_user'),
47
+ type: 'submit',
48
+ class: 'btn btn-primary',
49
+ data: { confirm: I18n.t('spree_user.unblock_confirmation') } %>
50
+ <% end %>
51
+ <% else %>
52
+ <%= form_tag spree.block_admin_user_path(@user), method: :put do %>
53
+ <div class="form-group">
54
+ <%= label_tag :blocked_reason, I18n.t('spree_user.blocked_reason') %>
55
+ <%= text_field_tag :blocked_reason, nil, class: 'form-control' %>
56
+ <small class="form-text text-muted"><%= I18n.t('spree_user.blocked_reason_hint') %></small>
57
+ </div>
58
+
59
+ <%= button_tag I18n.t('spree_user.block_user'),
60
+ type: 'submit',
61
+ class: 'btn btn-danger',
62
+ data: { confirm: I18n.t('spree_user.block_confirmation') } %>
63
+ <% end %>
64
+ <% end %>
65
+ </div>
66
+ </div>
@@ -497,6 +497,7 @@ en:
497
497
  # Overrides Spree core's "selected of %{item} is not available." Must stay under
498
498
  # `spree:` -- Spree::Stock::AvailabilityValidator looks it up as Spree.t(:selected_quantity_not_available).
499
499
  selected_quantity_not_available: "Sorry, not enough %{item} left. Please choose fewer or try again in a few minutes."
500
+ only_active_products_can_be_added_to_cart: "Sorry, this product isn't available for purchase right now."
500
501
  auto_apply: "Auto apply"
501
502
  auto_apply_info: "Path & code will be removed"
502
503
  transit:
@@ -769,11 +770,11 @@ en:
769
770
  attributes:
770
771
  base:
771
772
  last_contestant_in_parent_session: 'cannot remove the last contestant from "%{session_name}" because child sessions depend on it'
772
- advanced_from_session: 'was advanced from a previous session and cannot be removed directly. Clear the advancement in the source session instead.'
773
- pre_vote_session: 'cannot be removed from a pre-vote session.'
774
- has_advanced_to_session: 'has been assigned to advance to another session. Clear the advancement first before removing this contestant.'
773
+ advanced_from_session: "was advanced from a previous session and cannot be removed directly. Clear the advancement in the source session instead."
774
+ pre_vote_session: "cannot be removed from a pre-vote session."
775
+ has_advanced_to_session: "has been assigned to advance to another session. Clear the advancement first before removing this contestant."
775
776
  advanced_to_id:
776
- same_as_advanced_from: 'cannot be the same as the contestant it was advanced from'
777
+ same_as_advanced_from: "cannot be the same as the contestant it was advanced from"
777
778
  spree_cm_commissioner/voting_credit:
778
779
  attributes:
779
780
  free_votes_amount:
@@ -403,6 +403,7 @@ km:
403
403
  upsupported_payment: "មិនស្គាល់ការបង់ប្រាក់"
404
404
  selected_item_not_available_on_date: "Selected item not available on %{date}"
405
405
  selected_quantity_not_available: "សូមអភ័យទោស %{item} នៅសល់មិនគ្រប់គ្រាន់។ សូមរើសចំនួនតិច ឬព្យាយាមម្តងទៀត។"
406
+ only_active_products_can_be_added_to_cart: "សូមអភ័យទោស ទំនិញនេះមិនទាន់អាចទិញបានទេនៅពេលនេះ។"
406
407
  auto_apply: "ដាក់ប្រើដោយស្វ័យប្រវត្តិ"
407
408
  auto_apply_info: "Path & code will be removed"
408
409
  device_tokens: "Device Tokens"
@@ -0,0 +1,39 @@
1
+ class AddNormalizedLoginToSpreeUsers < ActiveRecord::Migration[7.0]
2
+ def up
3
+ add_column :spree_users, :normalized_login, :string unless column_exists?(:spree_users, :normalized_login)
4
+
5
+ execute <<~SQL.squish
6
+ UPDATE spree_users SET normalized_login = LOWER(login)
7
+ WHERE login IS NOT NULL AND login != ''
8
+ SQL
9
+
10
+ # Case-insensitive login uniqueness has only ever been enforced by the Rails
11
+ # validation, not the DB, so case-variant duplicates already exist. Clean them
12
+ # up first or the unique index below fails to create.
13
+ result = SpreeCmCommissioner::Users::DedupeLogins.call
14
+ raise "Failed to dedupe user logins before indexing: #{result.error}" if result.failure?
15
+
16
+ unless index_exists?(:spree_users, %i[tenant_id normalized_login], name: 'index_spree_users_on_tenant_id_and_normalized_login')
17
+ add_index :spree_users, %i[tenant_id normalized_login],
18
+ unique: true,
19
+ where: 'tenant_id IS NOT NULL AND normalized_login IS NOT NULL',
20
+ name: 'index_spree_users_on_tenant_id_and_normalized_login'
21
+ end
22
+
23
+ # tenant_id IS NULL rows aren't covered by the composite index above (NULL never
24
+ # equals NULL in a unique index), but the validation's `scope: :tenant_id` treats
25
+ # all null-tenant users as one group, so they need their own partial index to match.
26
+ return if index_exists?(:spree_users, :normalized_login, name: 'index_spree_users_on_normalized_login_when_tenant_id_null')
27
+
28
+ add_index :spree_users, :normalized_login,
29
+ unique: true,
30
+ where: 'tenant_id IS NULL AND normalized_login IS NOT NULL',
31
+ name: 'index_spree_users_on_normalized_login_when_tenant_id_null'
32
+ end
33
+
34
+ def down
35
+ remove_index :spree_users, name: 'index_spree_users_on_tenant_id_and_normalized_login', if_exists: true
36
+ remove_index :spree_users, name: 'index_spree_users_on_normalized_login_when_tenant_id_null', if_exists: true
37
+ remove_column :spree_users, :normalized_login if column_exists?(:spree_users, :normalized_login)
38
+ end
39
+ end
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.12.0'.freeze
2
+ VERSION = '2.12.2'.freeze
3
3
 
4
4
  module_function
5
5
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_cm_commissioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12.0
4
+ version: 2.12.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-27 00:00:00.000000000 Z
11
+ date: 2026-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree
@@ -1938,7 +1938,6 @@ files:
1938
1938
  - app/overrides/spree/admin/users/_form/roles_fields.html.erb.deface
1939
1939
  - app/overrides/spree/admin/users/_form/user_add_on.html.erb.deface
1940
1940
  - app/overrides/spree/admin/users/_tabs/tabs.html.erb.deface
1941
- - app/overrides/spree/admin/users/edit/account_status.html.erb.deface
1942
1941
  - app/overrides/spree/admin/users/index/body.html.erb.deface
1943
1942
  - app/overrides/spree/admin/users/index/headers.html.erb.deface
1944
1943
  - app/overrides/spree/admin/users/index/ransack.html.erb.deface
@@ -2424,6 +2423,7 @@ files:
2424
2423
  - app/services/spree_cm_commissioner/user_counters_service.rb
2425
2424
  - app/services/spree_cm_commissioner/user_roles_assigner.rb
2426
2425
  - app/services/spree_cm_commissioner/users/block.rb
2426
+ - app/services/spree_cm_commissioner/users/dedupe_logins.rb
2427
2427
  - app/services/spree_cm_commissioner/users/qr_data/extract_login.rb
2428
2428
  - app/services/spree_cm_commissioner/users/qr_data/generate.rb
2429
2429
  - app/services/spree_cm_commissioner/users/qr_data/invalidate.rb
@@ -3547,6 +3547,7 @@ files:
3547
3547
  - db/migrate/20260820100000_create_cm_private_sales.rb
3548
3548
  - db/migrate/20260824120000_add_user_id_to_cm_waiting_room_sessions.rb
3549
3549
  - db/migrate/20260825092416_add_live_stock_window_to_spree_taxons.rb
3550
+ - db/migrate/20260828080349_add_normalized_login_to_spree_users.rb
3550
3551
  - docker-compose.yml
3551
3552
  - docs/api/scoped-access-token-endpoints.md
3552
3553
  - docs/option_types/attr_types.md
@@ -1,42 +0,0 @@
1
- <!-- insert_after "[data-hook='admin_user_edit_general_settings']" -->
2
-
3
- <div data-hook="admin_user_account_status" class="card mb-3">
4
- <div class="card-header">
5
- <h5 class="card-title mb-0 h6">
6
- <%= I18n.t('spree_user.account_status') %>
7
- </h5>
8
- </div>
9
-
10
- <div class="card-body">
11
- <% if @user.blocked? %>
12
- <p>
13
- <span class="badge badge-danger text-uppercase"><%= I18n.t('spree_user.blocked') %></span>
14
- <span class="text-muted ml-2"><%= I18n.t('spree_user.blocked_at', time: l(@user.blocked_at, format: :long)) %></span>
15
- </p>
16
-
17
- <% if @user.blocked_reason.present? %>
18
- <p class="text-muted"><%= @user.blocked_reason %></p>
19
- <% end %>
20
-
21
- <%= form_tag spree.unblock_admin_user_path(@user), method: :put do %>
22
- <%= button_tag I18n.t('spree_user.unblock_user'),
23
- type: 'submit',
24
- class: 'btn btn-primary',
25
- data: { confirm: I18n.t('spree_user.unblock_confirmation') } %>
26
- <% end %>
27
- <% else %>
28
- <%= form_tag spree.block_admin_user_path(@user), method: :put do %>
29
- <div class="form-group">
30
- <%= label_tag :blocked_reason, I18n.t('spree_user.blocked_reason') %>
31
- <%= text_field_tag :blocked_reason, nil, class: 'form-control' %>
32
- <small class="form-text text-muted"><%= I18n.t('spree_user.blocked_reason_hint') %></small>
33
- </div>
34
-
35
- <%= button_tag I18n.t('spree_user.block_user'),
36
- type: 'submit',
37
- class: 'btn btn-danger',
38
- data: { confirm: I18n.t('spree_user.block_confirmation') } %>
39
- <% end %>
40
- <% end %>
41
- </div>
42
- </div>