spree_cm_commissioner 2.1.9.pre.pre2 → 2.2.0

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.tool-versions +1 -1
  4. data/Gemfile.lock +2 -1
  5. data/app/controllers/spree/admin/users_controller_decorator.rb +2 -0
  6. data/app/controllers/spree/api/v2/storefront/account/guest_dynamic_fields_controller.rb +3 -5
  7. data/app/controllers/spree/api/v2/storefront/account/mark_guest_info_complete_controller.rb +3 -8
  8. data/app/controllers/spree/api/v2/storefront/guests_controller.rb +37 -18
  9. data/app/interactors/spree_cm_commissioner/account_recover.rb +2 -2
  10. data/app/interactors/spree_cm_commissioner/guest_dynamic_field_notification_sender.rb +11 -0
  11. data/app/interactors/spree_cm_commissioner/notification_reader.rb +3 -1
  12. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
  13. data/app/interactors/spree_cm_commissioner/team_member_adder.rb +37 -0
  14. data/app/interactors/spree_cm_commissioner/team_member_creator.rb +58 -0
  15. data/app/interactors/spree_cm_commissioner/transit/draft_order_creator.rb +4 -0
  16. data/app/interactors/spree_cm_commissioner/trip_clone_creator.rb +2 -21
  17. data/app/interactors/spree_cm_commissioner/user_password_authenticator.rb +2 -2
  18. data/app/interactors/spree_cm_commissioner/user_registration_with_fb_token.rb +1 -1
  19. data/app/interactors/spree_cm_commissioner/user_registration_with_id_token.rb +1 -1
  20. data/app/interactors/spree_cm_commissioner/user_telegram_web_app_authenticator.rb +2 -0
  21. data/app/interactors/spree_cm_commissioner/user_vendor_assigner.rb +39 -0
  22. data/app/interactors/spree_cm_commissioner/vattanac_bank_initiator.rb +2 -0
  23. data/app/jobs/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_job.rb +10 -0
  24. data/app/jobs/spree_cm_commissioner/transit/route_order_count_incrementer_job.rb +10 -0
  25. data/app/jobs/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_job.rb +13 -0
  26. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_decrementer_job.rb +10 -0
  27. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_incrementer_job.rb +10 -0
  28. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +12 -0
  29. data/app/models/concerns/spree_cm_commissioner/route_order_countable.rb +30 -0
  30. data/app/models/concerns/spree_cm_commissioner/route_trip_count_callbacks.rb +48 -0
  31. data/app/models/concerns/spree_cm_commissioner/service_type.rb +35 -0
  32. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +14 -3
  33. data/app/models/spree_cm_commissioner/guest.rb +3 -0
  34. data/app/models/spree_cm_commissioner/inventory_item.rb +1 -1
  35. data/app/models/spree_cm_commissioner/notification.rb +5 -1
  36. data/app/models/spree_cm_commissioner/order_decorator.rb +5 -0
  37. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
  38. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
  39. data/app/models/spree_cm_commissioner/route.rb +12 -0
  40. data/app/models/spree_cm_commissioner/trip.rb +40 -0
  41. data/app/models/spree_cm_commissioner/user_decorator.rb +11 -0
  42. data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
  43. data/app/models/spree_cm_commissioner/vendor_route.rb +9 -0
  44. data/app/notifications/spree_cm_commissioner/guest_dynamic_field_notification.rb +24 -0
  45. data/app/overrides/spree/admin/users/_form/user_add_on.html.erb.deface +27 -0
  46. data/app/services/spree_cm_commissioner/transit/base_route_order_metrics_updater.rb +62 -0
  47. data/app/services/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_service.rb +18 -0
  48. data/app/services/spree_cm_commissioner/transit/route_order_count_incrementer_service.rb +19 -0
  49. data/app/services/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_service.rb +30 -0
  50. data/app/services/spree_cm_commissioner/transit/route_trip_count_decrementer_service.rb +33 -0
  51. data/app/services/spree_cm_commissioner/transit/route_trip_count_incrementer_service.rb +33 -0
  52. data/app/services/spree_cm_commissioner/users/incomplete_guest_checker_service.rb +34 -0
  53. data/app/views/blazer/queries/embed/_content.html.erb +4 -2
  54. data/config/initializers/spree_permitted_attributes.rb +1 -0
  55. data/config/locales/en.yml +20 -0
  56. data/config/locales/km.yml +20 -0
  57. data/db/migrate/20250911042815_create_cm_routes.rb +16 -0
  58. data/db/migrate/20250911045649_create_cm_vendor_routes.rb +12 -0
  59. data/db/migrate/20251008064344_add_route_id_to_cm_trips.rb +7 -0
  60. data/db/migrate/20251009033331_add_registered_by_to_spree_users.rb +6 -0
  61. data/db/migrate/20251009073040_add_lock_version_to_cm_routes.rb +5 -0
  62. data/db/migrate/20251009073929_add_lock_version_to_cm_vendor_routes.rb +5 -0
  63. data/lib/spree_cm_commissioner/engine.rb +4 -0
  64. data/lib/spree_cm_commissioner/test_helper/factories/route_factory.rb +10 -0
  65. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +1 -0
  66. data/lib/spree_cm_commissioner/test_helper/factories/vendor_route_factory.rb +7 -0
  67. data/lib/spree_cm_commissioner/version.rb +1 -1
  68. data/lib/spree_cm_commissioner.rb +8 -14
  69. data/lib/tasks/backfill_confirmed_at.rake +21 -0
  70. data/spree_cm_commissioner.gemspec +1 -0
  71. metadata +50 -4
@@ -0,0 +1,35 @@
1
+ # ServiceType concern provides enum functionality for service type fields with I18n support.
2
+ #
3
+ # How to use:
4
+ # class YourModel < ApplicationRecord
5
+ # include SpreeCmCommissioner::ServiceType
6
+ #
7
+ # service_type_enum :service_type
8
+ # service_type_enum :primary_service_type
9
+ # end
10
+ module SpreeCmCommissioner
11
+ module ServiceType
12
+ extend ActiveSupport::Concern
13
+
14
+ SERVICE_TYPES = %i[organizer transit intercity_taxi].freeze
15
+
16
+ class_methods do
17
+ def service_type_enum(column_name = :service_type)
18
+ enum column_name => SERVICE_TYPES.index_with(&:to_s)
19
+ end
20
+ end
21
+
22
+ # Returns a hash of all service types with their translated names
23
+ # Example: { organizer: "Organizer", transit: "Transit", intercity_taxi: "Intercity Taxi" }
24
+ #
25
+ # Usage: SpreeCmCommissioner::ServiceType.translated_service_types
26
+ def self.translated_service_types
27
+ SERVICE_TYPES.index_with do |type|
28
+ I18n.t("service_types.#{type}",
29
+ scope: 'spree',
30
+ default: type.to_s.humanize
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
@@ -19,10 +19,11 @@
19
19
 
20
20
  # store_public_metadata :completed, :boolean, default: true
21
21
  # store_public_metadata :count, :integer, default: 5
22
+ # store_public_metadata :tags, :array, default: []
22
23
  # store_private_metadata :app_token, :string, default: "XYZ"
23
24
  # end
24
25
  #
25
- # taxon = Spree::Taxon.new(completed: true, app_token: "ABC", count: 10)
26
+ # taxon = Spree::Taxon.new(completed: true, app_token: "ABC", count: 100, tags: ["new"])
26
27
  # taxon.completed # => true, store in public_metadata
27
28
  # taxon.completed? # => true, store in public_metadata
28
29
  # taxon.count # => 10, store in public_metadata
@@ -33,7 +34,7 @@ module SpreeCmCommissioner
33
34
  module StoreMetadata
34
35
  extend ActiveSupport::Concern
35
36
 
36
- class_methods do
37
+ class_methods do # rubocop:disable Metrics/BlockLength
37
38
  def store_private_metadata(key, type, default: nil)
38
39
  store :private_metadata, accessors: [key], coder: JSON
39
40
 
@@ -67,6 +68,8 @@ module SpreeCmCommissioner
67
68
  raw_value.to_i
68
69
  when :string
69
70
  raw_value.to_s
71
+ when :array
72
+ Array(raw_value)
70
73
  else
71
74
  raw_value
72
75
  end
@@ -76,7 +79,7 @@ module SpreeCmCommissioner
76
79
  end
77
80
 
78
81
  # Validates only new assignments (raw JSON values) for type safety
79
- def define_metadata_validation(column_name, key, type)
82
+ def define_metadata_validation(column_name, key, type) # rubocop:disable Metrics/CyclomaticComplexity
80
83
  case type
81
84
  when :boolean
82
85
  validates key, inclusion: { in: [true, false] }, allow_nil: true
@@ -90,6 +93,14 @@ module SpreeCmCommissioner
90
93
  end
91
94
  when :string
92
95
  validates key, length: { maximum: 255 }, allow_nil: true
96
+ when :array
97
+ validate do
98
+ metadata = send(column_name) || {}
99
+ raw_value = metadata[key.to_s]
100
+ next if raw_value.nil?
101
+
102
+ errors.add(key, 'must be an array') unless raw_value.is_a?(Array)
103
+ end
93
104
  end
94
105
  end
95
106
  end
@@ -335,6 +335,7 @@ module SpreeCmCommissioner
335
335
  end
336
336
 
337
337
  def pre_registration_completed?
338
+ return false unless line_item&.order&.completed?
338
339
  return true unless line_item&.product&.dynamic_fields&.pre_registration&.any?
339
340
 
340
341
  required_fields = line_item.product.dynamic_fields.pre_registration.pluck(:id)
@@ -343,6 +344,7 @@ module SpreeCmCommissioner
343
344
  end
344
345
 
345
346
  def post_registration_completed?
347
+ return false unless line_item&.order&.completed?
346
348
  return true unless line_item&.product&.dynamic_fields&.post_registration&.any?
347
349
 
348
350
  required_fields = line_item.product.dynamic_fields.post_registration.pluck(:id)
@@ -351,6 +353,7 @@ module SpreeCmCommissioner
351
353
  end
352
354
 
353
355
  def check_in_completed?
356
+ return false unless line_item&.order&.completed?
354
357
  return true unless line_item&.product&.dynamic_fields&.during_check_in&.any?
355
358
 
356
359
  required_fields = line_item.product.dynamic_fields.during_check_in.pluck(:id)
@@ -48,7 +48,7 @@ module SpreeCmCommissioner
48
48
  end
49
49
 
50
50
  def adjust_quantity_in_redis(quantity)
51
- SpreeCmCommissioner.inventory_redis_pool.with do |redis|
51
+ SpreeCmCommissioner.redis_pool.with do |redis|
52
52
  cached_quantity_available = redis.get(redis_key)
53
53
  # ignore if redis doesn't exist
54
54
  return if cached_quantity_available.nil? # rubocop:disable Lint/NonLocalExitFromIterator
@@ -10,7 +10,7 @@ module SpreeCmCommissioner
10
10
 
11
11
  scope :user_notifications, lambda {
12
12
  where(
13
- type: %w[order_complete_notification customer_notification]
13
+ type: %w[order_complete_notification customer_notification guest_dynamic_field_notification]
14
14
  )
15
15
  }
16
16
 
@@ -20,6 +20,10 @@ module SpreeCmCommissioner
20
20
  )
21
21
  }
22
22
 
23
+ scope :markable_notifications, lambda {
24
+ where(read_at: nil).where.not(type: %w[guest_dynamic_field_notification])
25
+ }
26
+
23
27
  belongs_to :recipient, polymorphic: true
24
28
  belongs_to :notificable, polymorphic: true
25
29
  end
@@ -4,6 +4,8 @@ module SpreeCmCommissioner
4
4
  base.include SpreeCmCommissioner::PhoneNumberSanitizer
5
5
  base.include SpreeCmCommissioner::OrderSeatable
6
6
  base.include SpreeCmCommissioner::OrderStateMachine
7
+ base.include SpreeCmCommissioner::RouteOrderCountable
8
+ base.include SpreeCmCommissioner::StoreMetadata
7
9
 
8
10
  base.scope :subscription, -> { where.not(subscription_id: nil) }
9
11
  base.scope :paid, -> { where(payment_state: :paid) }
@@ -28,6 +30,9 @@ module SpreeCmCommissioner
28
30
  base.before_create :link_by_phone_number
29
31
  base.before_create :associate_customer
30
32
  base.before_create :set_tenant_id
33
+ base.after_commit :increment_route_order_count, on: :create
34
+
35
+ base.store_private_metadata :preload_trip_ids, :array, default: []
31
36
 
32
37
  base.validates :promo_total, base::MONEY_VALIDATION
33
38
  base.validate :validate_channel_prefix, if: :channel_changed?
@@ -12,7 +12,7 @@ module SpreeCmCommissioner
12
12
  keys = inventory_items.map { |item| "inventory:#{item.id}" }
13
13
  return [] unless keys.any?
14
14
 
15
- counts = SpreeCmCommissioner.inventory_redis_pool.with { |redis| redis.mget(*keys) }
15
+ counts = SpreeCmCommissioner.redis_pool.with { |redis| redis.mget(*keys) }
16
16
  inventory_items.map.with_index do |inventory_item, i|
17
17
  ::SpreeCmCommissioner::CachedInventoryItem.new(
18
18
  inventory_key: keys[i],
@@ -30,7 +30,7 @@ module SpreeCmCommissioner
30
30
  return count_in_redis.to_i if count_in_redis.present?
31
31
  return inventory_item.quantity_available unless inventory_item.active?
32
32
 
33
- SpreeCmCommissioner.inventory_redis_pool.with do |redis|
33
+ SpreeCmCommissioner.redis_pool.with do |redis|
34
34
  redis.set(key, inventory_item.quantity_available, ex: inventory_item.redis_expired_in)
35
35
  end
36
36
 
@@ -51,13 +51,13 @@ module SpreeCmCommissioner
51
51
  end
52
52
 
53
53
  def unstock(keys, quantities)
54
- SpreeCmCommissioner.inventory_redis_pool.with do |redis|
54
+ SpreeCmCommissioner.redis_pool.with do |redis|
55
55
  redis.eval(unstock_redis_script, keys: keys, argv: quantities)
56
56
  end.positive?
57
57
  end
58
58
 
59
59
  def restock(keys, quantities)
60
- SpreeCmCommissioner.inventory_redis_pool.with do |redis|
60
+ SpreeCmCommissioner.redis_pool.with do |redis|
61
61
  redis.eval(restock_redis_script, keys: keys, argv: quantities)
62
62
  end.positive?
63
63
  end
@@ -0,0 +1,12 @@
1
+ module SpreeCmCommissioner
2
+ class Route < Base
3
+ has_many :vendor_routes, class_name: 'SpreeCmCommissioner::VendorRoute', dependent: :destroy
4
+ has_many :vendors, through: :vendor_routes
5
+ has_many :trips, inverse_of: :route
6
+
7
+ belongs_to :origin_place, class_name: 'SpreeCmCommissioner::Place'
8
+ belongs_to :destination_place, class_name: 'SpreeCmCommissioner::Place'
9
+
10
+ validates :origin_place_id, uniqueness: { scope: :destination_place_id }
11
+ end
12
+ end
@@ -2,6 +2,7 @@ module SpreeCmCommissioner
2
2
  class Trip < Base
3
3
  include SpreeCmCommissioner::StoreMetadata
4
4
  include SpreeCmCommissioner::RouteType
5
+ include SpreeCmCommissioner::RouteTripCountCallbacks
5
6
 
6
7
  attr_accessor :hours, :minutes, :seconds
7
8
 
@@ -20,6 +21,7 @@ module SpreeCmCommissioner
20
21
 
21
22
  belongs_to :origin_place, class_name: 'SpreeCmCommissioner::Place', optional: false
22
23
  belongs_to :destination_place, class_name: 'SpreeCmCommissioner::Place', optional: false
24
+ belongs_to :route, inverse_of: :trips
23
25
 
24
26
  has_many :trip_blazer_queries, as: :queryable, class_name: 'SpreeCmCommissioner::BlazerQueryable'
25
27
  has_many :blazer_queries, through: :trip_blazer_queries, source: :blazer_query, class_name: 'Blazer::Query'
@@ -38,12 +40,17 @@ module SpreeCmCommissioner
38
40
 
39
41
  # before create, duplicate seat layout from vehicle if present
40
42
  before_validation :duplicate_seat_layout_from_vehicle, if: -> { seat_layout.nil? && vehicle.present? && vehicle.seat_layout.present? }
43
+ before_validation :assign_route_and_vendor_route, if: :changed_route_attributes?
41
44
 
42
45
  accepts_nested_attributes_for :trip_stops, :product, :seat_layout, allow_destroy: true
43
46
 
44
47
  self.whitelisted_ransackable_associations = %w[product vehicle]
45
48
  self.whitelisted_ransackable_attributes = %w[origin_place_id destination_place_id]
46
49
 
50
+ def changed_route_attributes?
51
+ origin_place_id_changed? || destination_place_id_changed? || vendor_id_changed?
52
+ end
53
+
47
54
  def convert_duration_to_seconds
48
55
  return if hours.blank? && minutes.blank? && seconds.blank?
49
56
 
@@ -74,6 +81,9 @@ module SpreeCmCommissioner
74
81
  private
75
82
 
76
83
  def duplicate_seat_layout_from_vehicle
84
+ # Duplicate the vehicle's seat layout into this trip. Copies nested
85
+ # seat sections and blocks while excluding identifying columns and
86
+ # timestamps to avoid collisions when creating new records.
77
87
  original = vehicle.seat_layout
78
88
 
79
89
  self.seat_layout_attributes =
@@ -92,5 +102,35 @@ module SpreeCmCommissioner
92
102
  end
93
103
  )
94
104
  end
105
+
106
+ # Check if any of the route context attributes are changing
107
+ def will_route_context_change?
108
+ # Returns true if any of the route context attributes will change on save.
109
+ will_save_change_to_route_id? ||
110
+ will_save_change_to_origin_place_id? ||
111
+ will_save_change_to_destination_place_id? ||
112
+ will_save_change_to_vendor_id?
113
+ end
114
+
115
+ def changed_route_context?
116
+ saved_change_to_route_id? ||
117
+ saved_change_to_origin_place_id? ||
118
+ saved_change_to_destination_place_id? ||
119
+ saved_change_to_vendor_id?
120
+ end
121
+
122
+ # Find or create the Route for this trip (by origin/destination) and
123
+ # assign it to the trip. Also populate a default route_name from the
124
+ # product when the route is new/blank.
125
+ def assign_route_and_vendor_route
126
+ route = SpreeCmCommissioner::Route.find_or_create_by!(
127
+ origin_place_id: origin_place_id,
128
+ destination_place_id: destination_place_id
129
+ )
130
+ route.route_name = product.name if route.route_name.blank?
131
+ route.save! if route.changed?
132
+
133
+ self.route_id = route.id
134
+ end
95
135
  end
96
136
  end
@@ -6,8 +6,14 @@ module SpreeCmCommissioner
6
6
  base.include SpreeCmCommissioner::UserNotification
7
7
  base.include SpreeCmCommissioner::UserIdentity
8
8
  base.include SpreeCmCommissioner::UserPreference
9
+ base.include SpreeCmCommissioner::StoreMetadata
10
+ base.include SpreeCmCommissioner::ServiceType
9
11
 
10
12
  base.enum gender: %i[male female other]
13
+ base.enum registered_by: {
14
+ self_registered: 0,
15
+ system_registered: 1
16
+ }, _prefix: true
11
17
 
12
18
  base.has_many :subscriptions, through: :customer, class_name: 'SpreeCmCommissioner::Subscription'
13
19
  base.has_many :payments, as: :payable, class_name: 'Spree::Payment', dependent: :nullify
@@ -45,6 +51,11 @@ module SpreeCmCommissioner
45
51
 
46
52
  # Store has_incomplete_guest_info in public_metadata for easy frontend access
47
53
  base.store :public_metadata, accessors: [:has_incomplete_guest_info], coder: JSON
54
+ base.store_public_metadata :primary_service_type, :string
55
+ base.validates :primary_service_type,
56
+ inclusion: { in: SpreeCmCommissioner::ServiceType::SERVICE_TYPES.map(&:to_s) },
57
+ allow_nil: true
58
+ base.store_public_metadata :branch_ids, :array, default: []
48
59
 
49
60
  base.devise :two_factor_authenticatable
50
61
 
@@ -75,6 +75,7 @@ module SpreeCmCommissioner
75
75
  base.has_many :subscriptions, through: :customers, class_name: 'SpreeCmCommissioner::Subscription'
76
76
  base.has_many :subscription_orders, through: :subscriptions, class_name: 'Spree::Order', source: :orders
77
77
  base.has_many :promotion_categories, class_name: 'Spree::PromotionCategory', foreign_key: :vendor_id, dependent: :destroy
78
+ base.has_many :vendor_routes, class_name: 'SpreeCmCommissioner::VendorRoute'
78
79
 
79
80
  base.has_many :homepage_section_relatables,
80
81
  as: :relatable,
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ class VendorRoute < Base
3
+ belongs_to :vendor, class_name: 'Spree::Vendor', optional: false
4
+ belongs_to :route, class_name: 'SpreeCmCommissioner::Route', optional: false
5
+
6
+ has_many :trips, -> (vr) { where(vendor_id: vr.vendor_id, route_id: vr.route_id) }, class_name: 'SpreeCmCommissioner::Trip'
7
+ validates :vendor_id, uniqueness: { scope: :route_id }
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ module SpreeCmCommissioner
2
+ class GuestDynamicFieldNotification < NoticedFcmBase
3
+ def notificable
4
+ guest
5
+ end
6
+
7
+ def guest
8
+ params[:guest]
9
+ end
10
+
11
+ def extra_payload
12
+ {
13
+ guest_id: guest.id,
14
+ line_item_id: guest.line_item_id,
15
+ order_token: guest.line_item.order.token,
16
+ notification_type: type
17
+ }
18
+ end
19
+
20
+ def type
21
+ 'guest_dynamic_field_notification'
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ <!-- insert_bottom "[data-hook='admin_user_form_password_fields']" -->
2
+
3
+ <%
4
+ service_types = SpreeCmCommissioner::ServiceType.translated_service_types.map { |key, label| [label, key.to_s] }
5
+ %>
6
+
7
+ <div data-hook="admin_user_form_user_section" class="p-3 mt-4 rounded border form-group bg-light">
8
+ <strong class="mb-3 d-block"><%= Spree.t(:add_on) %></strong>
9
+
10
+ <%= f.field_container :primary_service_type do %>
11
+ <%= f.label :primary_service_type, Spree.t(:primary_service_type) %>
12
+ <%= f.select :primary_service_type,
13
+ options_for_select(service_types, f.object.primary_service_type),
14
+ { include_blank: Spree.t(:select_service_type) },
15
+ class: 'form-control select2-clear' %>
16
+ <%= f.error_message_on :primary_service_type %>
17
+ <% end %>
18
+
19
+ <div class="form-group">
20
+ <label><%= Spree.t(:registration_source) %></label>
21
+ <div class="form-control" style="background-color: #f8f9fa; cursor: not-allowed;">
22
+ <%= f.object.persisted? ? Spree.t("registered_by.#{f.object.registered_by}") : Spree.t("registered_by.system_registered") %>
23
+ </div>
24
+ <%# Hidden field to submit the default value for new records %>
25
+ <%= f.hidden_field :registered_by, value: 'system_registered' if f.object.new_record? %>
26
+ </div>
27
+ </div>
@@ -0,0 +1,62 @@
1
+ module SpreeCmCommissioner
2
+ module Transit
3
+ class BaseRouteOrderMetricsUpdater
4
+ attr_reader :order, :attribute
5
+
6
+ SUPPORTED_ATTRIBUTES = %i[order_count fulfilled_order_count].freeze
7
+
8
+ def initialize(order:, attribute:)
9
+ @order = order
10
+ @attribute = attribute&.to_sym
11
+ end
12
+
13
+ def self.call(order:, attribute:)
14
+ new(order: order, attribute: attribute).call
15
+ end
16
+
17
+ def call
18
+ return unless order && attribute && SUPPORTED_ATTRIBUTES.include?(attribute)
19
+
20
+ order.line_items.find_each { |li| process_line_item(li) }
21
+ end
22
+
23
+ private
24
+
25
+ def process_line_item(line_item)
26
+ return unless line_item.transit?
27
+
28
+ product = line_item.variant&.product
29
+ return unless product&.transit?
30
+
31
+ trip = product&.trip
32
+ return unless trip
33
+
34
+ route = find_or_create_route_for_trip(trip)
35
+ ensure_trip_route(trip, route)
36
+ increment_route_by(route, line_item.quantity)
37
+ end
38
+
39
+ def find_or_create_route_for_trip(trip)
40
+ SpreeCmCommissioner::Route.find_or_create_by(
41
+ origin_place_id: trip.origin_place_id,
42
+ destination_place_id: trip.destination_place_id
43
+ )
44
+ end
45
+
46
+ def ensure_trip_route(trip, route)
47
+ trip_route_id = trip.respond_to?(:route_id) ? trip.route_id : nil
48
+
49
+ return unless trip.respond_to?(:update_column) && trip_route_id != route.id
50
+
51
+ trip.update_column(:route_id, route.id) # rubocop:disable Rails/SkipsModelValidations
52
+ end
53
+
54
+ def increment_route_by(route, qty)
55
+ route.with_lock do
56
+ current_value = route.public_send(attribute) || 0
57
+ route.update!(attribute => current_value + qty)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,18 @@
1
+ module SpreeCmCommissioner
2
+ module Transit
3
+ class RouteFulfilledOrderCountIncrementerService
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(order:)
7
+ return failure(nil, 'Order not found') unless order
8
+
9
+ SpreeCmCommissioner::Transit::BaseRouteOrderMetricsUpdater
10
+ .call(order: order, attribute: :fulfilled_order_count)
11
+
12
+ success(order: order)
13
+ rescue StandardError => e
14
+ failure(nil, e.message)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module SpreeCmCommissioner
2
+ module Transit
3
+ class RouteOrderCountIncrementerService
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(order:)
7
+ return failure(nil, 'Order not found') unless order
8
+
9
+ SpreeCmCommissioner::Transit::BaseRouteOrderMetricsUpdater
10
+ .new(order: order, attribute: :order_count)
11
+ .call
12
+
13
+ success(order: order)
14
+ rescue StandardError => e
15
+ failure(nil, e.message)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ module SpreeCmCommissioner
2
+ module Transit
3
+ class RoutePreviousTripCountDecrementerService
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(previous_route_id:)
7
+ previous_route = SpreeCmCommissioner::Route.find_by(id: previous_route_id)
8
+ return failure(nil, 'Route not found') unless previous_route
9
+
10
+ if previous_route.vendors.exists?
11
+
12
+ # Build a trip-like object that responds to :route and :vendor so the delegated
13
+ # service can locate vendor routes correctly.
14
+ trip_like = Struct.new(:route, :vendor).new(previous_route, previous_route.vendors.first)
15
+ result = SpreeCmCommissioner::Transit::RouteTripCountDecrementerService.call(trip: trip_like)
16
+ return result if result.failure?
17
+ else
18
+ previous_route.with_lock do
19
+ new_count = [previous_route.trip_count.to_i - 1, 0].max
20
+ previous_route.update!(trip_count: new_count)
21
+ end
22
+ end
23
+
24
+ success(previous_route: previous_route)
25
+ rescue StandardError => e
26
+ failure(nil, e.message)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ module SpreeCmCommissioner
2
+ module Transit
3
+ class RouteTripCountDecrementerService
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(trip:)
7
+ return failure(nil, 'Trip not found') unless trip
8
+
9
+ vendor = trip.vendor
10
+ return failure(nil, 'Vendor not found') unless vendor
11
+
12
+ ActiveRecord::Base.transaction do
13
+ route = trip.route
14
+
15
+ route.update!(trip_count: [route.trip_count - 1, 0].max)
16
+
17
+ vendor_route = locate_vendor_route(vendor: vendor, route: route)
18
+ vendor_route&.update!(trip_count: [vendor_route.trip_count - 1, 0].max)
19
+ end
20
+
21
+ success(trip: trip)
22
+ rescue StandardError => e
23
+ failure(nil, e.message)
24
+ end
25
+
26
+ private
27
+
28
+ def locate_vendor_route(vendor:, route:)
29
+ vendor.vendor_routes.find_by(route_id: route.id)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ module SpreeCmCommissioner
2
+ module Transit
3
+ class RouteTripCountIncrementerService
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(trip:)
7
+ return failure(nil, 'Trip not found') unless trip
8
+
9
+ vendor = trip.vendor
10
+ return failure(nil, 'Vendor not found') unless vendor
11
+
12
+ ActiveRecord::Base.transaction do
13
+ route = trip.route
14
+
15
+ route.update!(trip_count: route.trip_count + 1)
16
+
17
+ vendor_route = find_or_create_vendor_route(vendor: vendor, route: route)
18
+ vendor_route.update!(trip_count: vendor_route.trip_count + 1)
19
+ end
20
+
21
+ success(trip: trip)
22
+ rescue StandardError => e
23
+ failure(nil, e.message)
24
+ end
25
+
26
+ private
27
+
28
+ def find_or_create_vendor_route(vendor:, route:)
29
+ vendor.vendor_routes.find_or_create_by!(route_id: route.id)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ module SpreeCmCommissioner
2
+ module Users
3
+ class IncompleteGuestCheckerService
4
+ attr_reader :user
5
+
6
+ def initialize(user)
7
+ @user = user
8
+ end
9
+
10
+ def call
11
+ return if user.blank? || incomplete_guest_info?
12
+
13
+ user.public_metadata['has_incomplete_guest_info'] = false
14
+ user.save!
15
+ end
16
+
17
+ private
18
+
19
+ def incomplete_guest_info?
20
+ guests =
21
+ SpreeCmCommissioner::Guest
22
+ .joins(line_item: :order)
23
+ .includes(:guest_dynamic_fields, line_item: { product: :dynamic_fields })
24
+ .where(spree_orders: { user_id: user.id, state: 'complete' })
25
+
26
+ guests.any? do |guest|
27
+ guest.pre_registration_completed? &&
28
+ guest.post_registration_fields? &&
29
+ !guest.post_registration_completed?
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -10,8 +10,6 @@
10
10
 
11
11
  <% if @success %>
12
12
  <div id="results" style="font-family: 'Poppins', sans-serif;"></div>
13
- <p class="text-muted">Loading...</p>
14
- </div>
15
13
 
16
14
  <script>
17
15
  function showRun(data) {
@@ -82,4 +80,8 @@
82
80
  height: 15px;
83
81
  fill: <%= theme_colors[:text] %>;
84
82
  }
83
+
84
+ #results table thead.tableFloatingHeaderOriginal {
85
+ top: 60px !important;
86
+ }
85
87
  </style>
@@ -17,6 +17,7 @@ module Spree
17
17
  dob gender
18
18
  phone_number
19
19
  profile
20
+ primary_service_type
20
21
  otp_enabled
21
22
  otp_email
22
23
  otp_phone_number