spree_cm_commissioner 2.3.0.pre.pre19 → 2.3.0.pre.pre20

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: ba19c5f6b8ae1e8a80fb0128bf1f58b1a653d6430aa11d46ae3722b13e2d17be
4
- data.tar.gz: 7993749d255ca3800e9d200adc21820bad68ac350a60b88882a5bda4ee054701
3
+ metadata.gz: f73314825dc2db092e1255e49203ceb8e1071e0dac1c11803d8343b8dc37fafa
4
+ data.tar.gz: 32c25a52977a2f20591b8650ece48a69e517845b3431b79e3c57da63cf8ddb50
5
5
  SHA512:
6
- metadata.gz: 83ffc09e4828acf4abb37b4d10e537f2302f967dca20fdf86a806f260fe733dbd9da4097a2f813650e29f2f1420350c538ec4aa8eedc7d9b7543a68e9c535d07
7
- data.tar.gz: 40448782e45555c79e319cca373607a53ca34125ce1c5e13f45a74aa0014992a7294c768fdca9826287e645b2073a2f953eb2956ce983d1147d810f68a31c608
6
+ metadata.gz: 1bd90e765ad3bdd9f28b6199900a7ea4f587dd11732af3a03c8bd3b13d234eb83765740de6619223dd03cbe77cb4b865c82c12c8a1845bc05157977463bf17d6
7
+ data.tar.gz: ef7380007df4d1ee55da2fea5d2d9cb2b3b5d931c44d17a955b9925c338f8a6285df1df59459b643152cd03fda819bcdd264075bc87eaaff5961e93d08b84606
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.3.0.pre.pre19)
37
+ spree_cm_commissioner (2.3.0.pre.pre20)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -2,26 +2,54 @@ module SpreeCmCommissioner
2
2
  class UserRegistrationWithIdToken < BaseInteractor
3
3
  # :id_token
4
4
  def call
5
+ firebase_context = validate_firebase_token!
6
+ return if firebase_context.nil?
7
+
8
+ ActiveRecord::Base.transaction do
9
+ find_or_register_user!(firebase_context.provider[:name], firebase_context.provider[:email])
10
+ link_user_account!(firebase_context.provider)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def validate_firebase_token!
5
17
  firebase_context = SpreeCmCommissioner::FirebaseIdTokenProvider.call(id_token: context.id_token)
18
+ return firebase_context if firebase_context.success?
6
19
 
7
- if firebase_context.success?
8
- ActiveRecord::Base.transaction do
9
- register_user!(firebase_context.provider[:name], firebase_context.provider[:email])
10
- link_user_account!(firebase_context.provider)
11
- end
20
+ context.fail!(message: firebase_context.message)
21
+ nil
22
+ end
23
+
24
+ def find_or_register_user!(name, email)
25
+ if email.present?
26
+ register_user_with_email!(name, email)
12
27
  else
13
- context.fail!(message: firebase_context.message)
28
+ register_user_without_email!(name)
14
29
  end
15
30
  end
16
31
 
17
- def register_user!(name, email)
18
- user = Spree.user_class.new(
19
- password: SecureRandom.base64(16),
20
- email: email,
21
- tenant_id: context.tenant_id,
22
- **name_attributes(name)
23
- )
24
- user.confirmed_at = Time.zone.now
32
+ def register_user_with_email!(name, email)
33
+ user = Spree.user_class.find_by(email: email, tenant_id: context.tenant_id)
34
+ return context.user = ensure_user_confirmed!(user) if user.present?
35
+
36
+ begin
37
+ context.user = Spree.user_class.find_or_create_by!(email: email, tenant_id: context.tenant_id) do |u|
38
+ assign_user_attributes(u, name)
39
+ end
40
+ # Handle potential race condition: Another request might have created the user
41
+ # between our find_by and find_or_create_by calls. If we get a RecordNotUnique,
42
+ # it means the user was created by another process, so we find and return it.
43
+ rescue ActiveRecord::RecordNotUnique
44
+ user = Spree.user_class.find_by!(email: email, tenant_id: context.tenant_id)
45
+ context.user = ensure_user_confirmed!(user)
46
+ end
47
+ end
48
+
49
+ def register_user_without_email!(name)
50
+ user = Spree.user_class.new(email: nil, tenant_id: context.tenant_id)
51
+ assign_user_attributes(user, name)
52
+
25
53
  if user.save(validate: false)
26
54
  context.user = user
27
55
  else
@@ -29,39 +57,32 @@ module SpreeCmCommissioner
29
57
  end
30
58
  end
31
59
 
60
+ def assign_user_attributes(user, name)
61
+ user.password = SecureRandom.base64(16)
62
+ user.confirmed_at = Time.zone.now
63
+ user.assign_attributes(name_attributes(name))
64
+ end
65
+
32
66
  def name_attributes(name)
33
67
  full_name = name&.strip
34
68
  return {} if full_name.blank?
35
69
 
36
- split = full_name.split
37
- first_name = split[0]
38
- last_name = split[1..].join(' ')
39
-
40
- attributes = {}
41
- attributes[:first_name] = first_name if first_name.present?
42
- attributes[:last_name] = last_name if last_name.present?
70
+ parts = full_name.split
71
+ attributes = { first_name: parts[0] }
72
+ attributes[:last_name] = parts[1..].join(' ') if parts.size > 1
43
73
 
44
74
  attributes
45
75
  end
46
76
 
47
- # provider object
48
-
49
- # {
50
- # identity_type: identity_type,
51
- # sub: sub
52
- # }
53
-
54
77
  def link_user_account!(provider)
55
78
  identity_type = SpreeCmCommissioner::UserIdentityProvider.identity_types[provider[:identity_type]]
79
+ user_identity_provider = find_or_initialize_identity_provider(identity_type)
56
80
 
57
- user_identity_provider = SpreeCmCommissioner::UserIdentityProvider.where(
58
- user_id: context.user,
59
- identity_type: identity_type
60
- ).first_or_initialize
61
-
62
- user_identity_provider.sub = provider[:sub]
63
- user_identity_provider.email = provider[:email]
64
- user_identity_provider.name = provider[:name]
81
+ user_identity_provider.assign_attributes(
82
+ sub: provider[:sub],
83
+ email: provider[:email],
84
+ name: provider[:name]
85
+ )
65
86
 
66
87
  if user_identity_provider.save
67
88
  context.user_identity_provider = user_identity_provider
@@ -69,5 +90,20 @@ module SpreeCmCommissioner
69
90
  context.fail!(message: user_identity_provider.errors.full_messages)
70
91
  end
71
92
  end
93
+
94
+ def find_or_initialize_identity_provider(identity_type)
95
+ SpreeCmCommissioner::UserIdentityProvider.where(
96
+ user_id: context.user,
97
+ identity_type: identity_type
98
+ ).first_or_initialize
99
+ end
100
+
101
+ # Ensure user is confirmed when linking with identity provider.
102
+ # Users created via OAuth should be auto-confirmed since they've proven
103
+ # their identity through the OAuth provider.
104
+ def ensure_user_confirmed!(user)
105
+ user.update(confirmed_at: Time.zone.now) if user.confirmed_at.nil?
106
+ user
107
+ end
72
108
  end
73
109
  end
@@ -0,0 +1,13 @@
1
+ module SpreeCmCommissioner
2
+ module Orders
3
+ class BulkArchiveInactiveOrdersJob < ApplicationJob
4
+ # Manual job that archives ALL incomplete orders inactive for 14+ days.
5
+ # Thin wrapper that calls BulkArchiveInactiveOrders service.
6
+ # ApplicationJob handles error logging via around_perform hook.
7
+ # Triggered manually via Sidekiq (not scheduled).
8
+ def perform
9
+ SpreeCmCommissioner::Orders::BulkArchiveInactiveOrders.new.call
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,11 +1,11 @@
1
1
  module SpreeCmCommissioner
2
2
  module Orders
3
- class ArchiveInactiveOrdersJob < ApplicationJob
3
+ class DailyArchiveInactiveOrdersJob < ApplicationJob
4
4
  # Scheduled job that runs daily to archive incomplete orders inactive for 14+ days.
5
- # Thin wrapper that calls ArchiveInactiveOrders service.
5
+ # Thin wrapper that calls DailyArchiveInactiveOrders service.
6
6
  # ApplicationJob handles error logging via around_perform hook.
7
7
  def perform
8
- SpreeCmCommissioner::Orders::ArchiveInactiveOrders.new.call
8
+ SpreeCmCommissioner::Orders::DailyArchiveInactiveOrders.new.call
9
9
  end
10
10
  end
11
11
  end
@@ -0,0 +1,46 @@
1
+ module SpreeCmCommissioner
2
+ module OrderScopes
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # Archiving scopes
7
+ scope :archived, -> { where.not(archived_at: nil) }
8
+ scope :not_archived, -> { where(archived_at: nil) }
9
+
10
+ # Payment and state scopes
11
+ scope :subscription, -> { where.not(subscription_id: nil) }
12
+ scope :paid, -> { where(payment_state: :paid) }
13
+ scope :complete_or_canceled, -> { complete.or(where(state: 'canceled')) }
14
+ scope :payment, -> { incomplete.where(state: 'payment') }
15
+ scope :without_user, -> { where(user_id: nil) }
16
+
17
+ # Inactive orders scopes with parameterized dates
18
+ # Usage: Spree::Order.inactive_incomplete_all(threshold: 14.days.ago)
19
+ scope :inactive_incomplete_all, lambda { |threshold: 14.days.ago|
20
+ where(archived_at: nil, completed_at: nil)
21
+ .where('updated_at < ?', threshold)
22
+ }
23
+
24
+ # Usage: Spree::Order.inactive_incomplete(threshold: 14.days.ago, window: 7.days)
25
+ # Returns orders updated between (threshold - window) and threshold
26
+ scope :inactive_incomplete, lambda { |threshold: 14.days.ago, window: 7.days|
27
+ where(archived_at: nil, completed_at: nil)
28
+ .where('updated_at >= ?', threshold - window)
29
+ .where('updated_at < ?', threshold)
30
+ }
31
+
32
+ # Filter scopes
33
+ scope :filter_by_match_user_contact, lambda { |user|
34
+ complete.where(
35
+ '(email = :email OR intel_phone_number = :intel_phone_number) AND user_id IS NULL',
36
+ email: user.email,
37
+ intel_phone_number: user.intel_phone_number
38
+ )
39
+ }
40
+
41
+ scope :filter_by_vendor, lambda { |vendor|
42
+ joins(:line_items).where(spree_line_items: { vendor_id: vendor }).distinct
43
+ }
44
+ end
45
+ end
46
+ end
@@ -1,31 +1,12 @@
1
1
  module SpreeCmCommissioner
2
2
  module OrderDecorator
3
- def self.prepended(base) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
3
+ def self.prepended(base) # rubocop:disable Metrics/MethodLength
4
4
  base.include SpreeCmCommissioner::StoreMetadata
5
5
  base.include SpreeCmCommissioner::PhoneNumberSanitizer
6
6
  base.include SpreeCmCommissioner::OrderSeatable
7
7
  base.include SpreeCmCommissioner::OrderStateMachine
8
8
  base.include SpreeCmCommissioner::RouteOrderCountable
9
-
10
- base.scope :subscription, -> { where.not(subscription_id: nil) }
11
- base.scope :paid, -> { where(payment_state: :paid) }
12
- base.scope :complete_or_canceled, -> { complete.or(where(state: 'canceled')) }
13
- base.scope :payment, -> { incomplete.where(state: 'payment') }
14
- base.scope :archived, -> { where.not(archived_at: nil) }
15
- base.scope :not_archived, -> { where(archived_at: nil) }
16
- base.scope :without_user, -> { where(user_id: nil) }
17
-
18
- base.scope :filter_by_match_user_contact, lambda { |user|
19
- complete.where(
20
- '(email = :email OR intel_phone_number = :intel_phone_number) AND user_id IS NULL',
21
- email: user.email,
22
- intel_phone_number: user.intel_phone_number
23
- )
24
- }
25
-
26
- base.scope :filter_by_vendor, lambda { |vendor|
27
- joins(:line_items).where(spree_line_items: { vendor_id: vendor }).distinct
28
- }
9
+ base.include SpreeCmCommissioner::OrderScopes
29
10
 
30
11
  base.before_create :link_by_phone_number
31
12
  base.before_create :associate_customer
@@ -0,0 +1,52 @@
1
+ module SpreeCmCommissioner
2
+ module Orders
3
+ class BulkArchiveInactiveOrders
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ # Archives ALL incomplete orders inactive for 14+ days (no time range limit).
7
+ # This is a bulk operation for manual cleanup via Sidekiq.
8
+ # Archived orders are hidden from users (Orders::Find filters them out).
9
+ #
10
+ # Criteria for archiving:
11
+ # - archived_at IS NULL (not already archived)
12
+ # - completed_at IS NULL (incomplete/unfinished orders)
13
+ # - updated_at < 14 days ago (inactive for 2+ weeks)
14
+ #
15
+ # Difference from ArchiveInactiveOrders:
16
+ # - ArchiveInactiveOrders: Runs daily, archives only 1-week window (21-14 days)
17
+ # - BulkArchiveInactiveOrders: Manual job, archives ALL orders older than 14 days
18
+ #
19
+ # Use case: Bulk cleanup when needed, triggered manually via Sidekiq
20
+ def call
21
+ # Archives ALL orders inactive for 14+ days (no time range limit)
22
+ # Uses parameterized scope: threshold=14.days.ago
23
+ inactive_orders = Spree::Order.inactive_incomplete_all(threshold: 14.days.ago)
24
+
25
+ count = inactive_orders.count
26
+
27
+ # Archive all inactive orders with reason in internal_note.
28
+ # We use bulk update_all instead of find_each because:
29
+ # - We rarely use internal_note, so preserving history is not critical
30
+ # - Bulk update is significantly faster (1 query vs N queries)
31
+ # - For bulk operations, performance is critical
32
+ inactive_orders.update_all( # rubocop:disable Rails/SkipsModelValidations
33
+ archived_at: Time.current,
34
+ internal_note: 'Auto-archived: inactive for 14 days',
35
+ updated_at: Time.current
36
+ )
37
+
38
+ CmAppLogger.log(
39
+ label: 'SpreeCmCommissioner::Orders::BulkArchiveInactiveOrders#call completed',
40
+ data: {
41
+ archived_count: count,
42
+ threshold_days: 14
43
+ }
44
+ )
45
+
46
+ success(archived_count: count)
47
+ rescue StandardError => e
48
+ failure(nil, e.message)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,6 +1,6 @@
1
1
  module SpreeCmCommissioner
2
2
  module Orders
3
- class ArchiveInactiveOrders
3
+ class DailyArchiveInactiveOrders
4
4
  prepend ::Spree::ServiceModule::Base
5
5
 
6
6
  # Archives incomplete orders that haven't been updated for 14 days.
@@ -24,14 +24,17 @@ module SpreeCmCommissioner
24
24
  # - Keeps user-facing order history clean (hidden from Orders::Find queries)
25
25
  # - Reduces clutter in active carts/orders
26
26
  def call
27
- inactive_orders = Spree::Order
28
- .where(archived_at: nil)
29
- .where(completed_at: nil)
30
- .where('updated_at < ?', 14.days.ago)
27
+ # Archives orders from the 1-week window (21-14 days ago)
28
+ # Uses parameterized scope: threshold=14.days.ago, window=7.days
29
+ inactive_orders = Spree::Order.inactive_incomplete(threshold: 14.days.ago, window: 7.days)
31
30
 
32
31
  count = inactive_orders.count
33
32
 
34
- # Archive all inactive orders with reason in internal_note
33
+ # Archive all inactive orders with reason in internal_note.
34
+ # We use bulk update_all instead of find_each because:
35
+ # - We rarely use internal_note, so preserving history is not critical
36
+ # - Bulk update is significantly faster (1 query vs N queries)
37
+ # - For typical 100-500 orders/day, bulk update is worth the performance gain
35
38
  inactive_orders.update_all( # rubocop:disable Rails/SkipsModelValidations
36
39
  archived_at: Time.current,
37
40
  internal_note: 'Auto-archived: inactive for 14 days',
@@ -39,7 +42,7 @@ module SpreeCmCommissioner
39
42
  )
40
43
 
41
44
  CmAppLogger.log(
42
- label: 'SpreeCmCommissioner::Orders::ArchiveInactiveOrders#call completed',
45
+ label: 'SpreeCmCommissioner::Orders::DailyArchiveInactiveOrders#call completed',
43
46
  data: {
44
47
  archived_count: count,
45
48
  threshold_days: 14
@@ -0,0 +1,7 @@
1
+ class AddIndexToSpreeOrdersUpdatedAt < ActiveRecord::Migration[7.0]
2
+ def change
3
+ unless index_exists?(:spree_orders, :updated_at)
4
+ add_index :spree_orders, :updated_at, name: 'index_spree_orders_on_updated_at'
5
+ end
6
+ end
7
+ end
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.3.0-pre19'.freeze
2
+ VERSION = '2.3.0-pre20'.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.3.0.pre.pre19
4
+ version: 2.3.0.pre.pre20
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
@@ -1310,7 +1310,8 @@ files:
1310
1310
  - app/jobs/spree_cm_commissioner/option_type_variants_public_metadata_updater_job.rb
1311
1311
  - app/jobs/spree_cm_commissioner/option_value_variants_public_metadata_updater_job.rb
1312
1312
  - app/jobs/spree_cm_commissioner/order_complete_telegram_sender_job.rb
1313
- - app/jobs/spree_cm_commissioner/orders/archive_inactive_orders_job.rb
1313
+ - app/jobs/spree_cm_commissioner/orders/bulk_archive_inactive_orders_job.rb
1314
+ - app/jobs/spree_cm_commissioner/orders/daily_archive_inactive_orders_job.rb
1314
1315
  - app/jobs/spree_cm_commissioner/product_event_id_to_children_syncer_job.rb
1315
1316
  - app/jobs/spree_cm_commissioner/queue_order_webhooks_requests_job.rb
1316
1317
  - app/jobs/spree_cm_commissioner/reports_assigner_job.rb
@@ -1358,6 +1359,7 @@ files:
1358
1359
  - app/models/concerns/spree_cm_commissioner/metafield.rb
1359
1360
  - app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb
1360
1361
  - app/models/concerns/spree_cm_commissioner/option_value_attr_type.rb
1362
+ - app/models/concerns/spree_cm_commissioner/order_scopes.rb
1361
1363
  - app/models/concerns/spree_cm_commissioner/order_seatable.rb
1362
1364
  - app/models/concerns/spree_cm_commissioner/order_state_machine.rb
1363
1365
  - app/models/concerns/spree_cm_commissioner/parameterize_name.rb
@@ -1953,7 +1955,8 @@ files:
1953
1955
  - app/services/spree_cm_commissioner/intercity_taxi_order/update.rb
1954
1956
  - app/services/spree_cm_commissioner/metafields/product_metadata_service.rb
1955
1957
  - app/services/spree_cm_commissioner/order_params_checker.rb
1956
- - app/services/spree_cm_commissioner/orders/archive_inactive_orders.rb
1958
+ - app/services/spree_cm_commissioner/orders/bulk_archive_inactive_orders.rb
1959
+ - app/services/spree_cm_commissioner/orders/daily_archive_inactive_orders.rb
1957
1960
  - app/services/spree_cm_commissioner/orders/generate_commissions_decorator.rb
1958
1961
  - app/services/spree_cm_commissioner/organizer/export_guest_csv_service.rb
1959
1962
  - app/services/spree_cm_commissioner/organizer/export_invite_guest_csv_service.rb
@@ -2844,6 +2847,7 @@ files:
2844
2847
  - db/migrate/20251009033331_add_registered_by_to_spree_users.rb
2845
2848
  - db/migrate/20251009073040_add_lock_version_to_cm_routes.rb
2846
2849
  - db/migrate/20251009073929_add_lock_version_to_cm_vendor_routes.rb
2850
+ - db/migrate/20251113081853_add_index_to_spree_orders_updated_at.rb
2847
2851
  - docker-compose.yml
2848
2852
  - docs/api/scoped-access-token-endpoints.md
2849
2853
  - docs/option_types/attr_types.md