spree_cm_commissioner 2.3.0.pre.pre16 → 2.3.0.pre.pre17

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +33 -0
  4. data/app/controllers/blazer/base_controller_decorator.rb +20 -1
  5. data/app/controllers/spree/api/v2/storefront/intercity_taxi/draft_orders_controller.rb +40 -0
  6. data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +47 -9
  7. data/app/controllers/spree_cm_commissioner/admin/variants_controller_decorator.rb +1 -1
  8. data/app/factory/spree_cm_commissioner/vendor_telegram_message_factory.rb +65 -0
  9. data/app/helpers/spree/base_helper_decorator.rb +2 -19
  10. data/app/interactors/spree_cm_commissioner/vendor_creation_telegram_alert_sender.rb +28 -0
  11. data/app/jobs/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_job.rb +1 -1
  12. data/app/jobs/spree_cm_commissioner/transit/route_order_count_incrementer_job.rb +1 -1
  13. data/app/jobs/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_job.rb +1 -1
  14. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_decrementer_job.rb +1 -1
  15. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_incrementer_job.rb +1 -1
  16. data/app/jobs/spree_cm_commissioner/vendor_creation_telegram_alert_sender_job.rb +10 -0
  17. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +51 -0
  18. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +7 -0
  19. data/app/models/concerns/spree_cm_commissioner/option_value_attr_type.rb +25 -0
  20. data/app/models/concerns/spree_cm_commissioner/route_order_countable.rb +3 -14
  21. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +18 -4
  22. data/app/models/spree_cm_commissioner/block.rb +19 -0
  23. data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
  24. data/app/models/spree_cm_commissioner/variant_decorator.rb +0 -5
  25. data/app/models/spree_cm_commissioner/variant_options.rb +8 -0
  26. data/app/models/spree_cm_commissioner/vendor_decorator.rb +6 -0
  27. data/app/overrides/spree/admin/variants/_form/add_permanent_stock.html.erb.deface +2 -1
  28. data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_cart_serializer.rb +12 -0
  29. data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_line_item_serializer.rb +31 -0
  30. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +11 -0
  31. data/app/services/spree_cm_commissioner/intercity_taxi_order/create.rb +67 -0
  32. data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +79 -0
  33. data/app/services/spree_cm_commissioner/{transit/base_route_order_metrics_updater.rb → routes/base_update_order_metrics.rb} +2 -2
  34. data/app/services/spree_cm_commissioner/{transit/route_previous_trip_count_decrementer_service.rb → routes/decrement_previous_trip_count.rb} +3 -3
  35. data/app/services/spree_cm_commissioner/{transit/route_trip_count_decrementer_service.rb → routes/decrement_trip_count.rb} +2 -2
  36. data/app/services/spree_cm_commissioner/{transit/route_fulfilled_order_count_incrementer_service.rb → routes/increment_fulfilled_order_count.rb} +3 -3
  37. data/app/services/spree_cm_commissioner/{transit/route_order_count_incrementer_service.rb → routes/increment_order_count.rb} +3 -3
  38. data/app/services/spree_cm_commissioner/{transit/route_trip_count_incrementer_service.rb → routes/increment_trip_count.rb} +2 -2
  39. data/app/services/spree_cm_commissioner/trips/search.rb +65 -0
  40. data/app/views/blazer/queries/embed/_content.html.erb +51 -2
  41. data/app/views/spree/admin/option_types/_color_field.html.erb +21 -0
  42. data/app/views/spree/admin/option_types/_option_value_fields.html.erb +2 -0
  43. data/app/views/spree/admin/variants/_color_field.html.erb +28 -0
  44. data/app/views/spree/admin/variants/_date_field.html.erb +11 -1
  45. data/app/views/spree/admin/variants/_default_field.html.erb +7 -3
  46. data/app/views/spree/admin/variants/_option_values.html.erb +5 -1
  47. data/app/views/spree/admin/variants/_time_field.html.erb +7 -1
  48. data/app/views/spree/order_mailer/_adjustment.html.erb +7 -0
  49. data/app/views/spree_cm_commissioner/event_transactional_mailer/_event_banner.html.erb +9 -3
  50. data/config/locales/km.yml +64 -0
  51. data/config/routes.rb +4 -1
  52. data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +18 -0
  53. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +30 -0
  54. data/lib/spree_cm_commissioner/trip_result.rb +15 -0
  55. data/lib/spree_cm_commissioner/version.rb +1 -1
  56. metadata +20 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 654baee3237bba36ebcc85458b09f73b076c6d1ffb782351cf697fa7ab287c7d
4
- data.tar.gz: ef9275e9c3d748cdd7947780dca206ea2ae75b4362731e9d9d1b04b2eab04aee
3
+ metadata.gz: 76d6f1a36e751b1723a6a6f7d6ba016dcf6df4c24d4e66322fadd7cb6d99f133
4
+ data.tar.gz: 5ee3fb8b1c36b92b04b349218a72db179b9e5afffe18342b73c6bafea2375600
5
5
  SHA512:
6
- metadata.gz: b35dd2fe6fc1815f72f0c1e33a88eb629437040de5ea73fc48f8cb249b6f322aedfe2be423847b74ef609a78783f62de13399b0c17c6fdebdf0c54c4a826cdf8
7
- data.tar.gz: 11131275c0fc9afec3d0ed082e10ac0471490e07fc04ebbc64ccb10d01ac1442acb102fd4178bfedad3a0e685939df1ad42459f5c62494dd168c699cbb105b57
6
+ metadata.gz: d402702c7778ddf7a44be7b6370fb943c7e091ee1d9cc5214ac4ffa97fde73a2347f072692661a815daf85fd1a18441d44b79c2e4246fce686b5f9822554a5b5
7
+ data.tar.gz: 3adbb3c9b40e612c65be67ae0853047bd5f1b1c82ac5daf7e91dc494a1f16d51f005eca9dbc48adf83467d3edb25e466114d9716abec8d43b0e7edff1b0753f2
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.pre16)
37
+ spree_cm_commissioner (2.3.0.pre.pre17)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
data/README.md CHANGED
@@ -32,6 +32,39 @@ If your server was running, restart it so that it can find the assets properly.
32
32
 
33
33
  Update the semantic version number in `lib/spree_cm_commissioner/version.rb`, then create and push a new tag to GitHub. When the tag is pushed, the GitHub Actions workflow will automatically build the gem and publish it to RubyGems.
34
34
 
35
+ Here's a cleaner and more polished version of your section:
36
+
37
+ ## PostgreSQL Configuration
38
+
39
+ By default, this Rails engine uses your current system login to connect to the PostgreSQL database. To allow passwordless connections, you need to configure PostgreSQL to trust local connections by editing `pg_hba.conf`:
40
+
41
+ ```conf
42
+ # pg_hba.conf
43
+ local all all trust
44
+ ```
45
+
46
+ With this `trust` configuration, PostgreSQL expects a role that matches your current system username.
47
+
48
+ To check your system username:
49
+
50
+ ```sh
51
+ whoami # e.g., superdev
52
+ ```
53
+
54
+ Then create a matching PostgreSQL role:
55
+
56
+ ```sh
57
+ createuser -U postgres -s superdev
58
+ ```
59
+
60
+ Or simply run the following to create a role matching your current system user:
61
+
62
+ ```sh
63
+ createuser -U postgres -s $(whoami)
64
+ ```
65
+
66
+ > 💡 We recommend using [Postgres.app](https://postgresapp.com) for local development. It supports multiple PostgreSQL versions and provides a developer-friendly setup out of the box.
67
+
35
68
  ## Config
36
69
 
37
70
  ### Rake tasks
@@ -12,11 +12,30 @@ module Blazer
12
12
  end
13
13
 
14
14
  def restrict_organizer_access
15
- return unless spree_current_user.organizer? && !spree_current_user.admin?
15
+ return if spree_current_user.admin?
16
+
17
+ return unless spree_current_user.organizer? || vendor_permissions?
16
18
  return if controller_name == 'queries' && %w[show run].include?(action_name)
17
19
 
18
20
  raise ActionController::RoutingError, 'Unauthorized'
19
21
  end
22
+
23
+ private
24
+
25
+ def current_vendor
26
+ @current_vendor ||= vendors&.find_by(id: session[:vendor_id]) || vendors&.first
27
+ end
28
+
29
+ def vendors
30
+ @vendors ||= spree_current_user&.vendors
31
+ end
32
+
33
+ def vendor_permissions?
34
+ return false unless current_vendor
35
+
36
+ permissions = spree_current_user.permissions_for_vendor(current_vendor.id)
37
+ permissions.exists?(entry: 'shared_console/reports', action: 'view_reports')
38
+ end
20
39
  end
21
40
  end
22
41
 
@@ -0,0 +1,40 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ module IntercityTaxi
6
+ class DraftOrdersController < ::Spree::Api::V2::ResourceController
7
+ def create
8
+ order = SpreeCmCommissioner::IntercityTaxiOrder::Create.call(
9
+ trip_id: params[:trip_id],
10
+ from_date: params[:from_date],
11
+ to_date: params[:to_date],
12
+ user_id: params[:user_id],
13
+ quantity: params[:quantity]
14
+ )
15
+
16
+ render_serialized_payload { serialize_resource(order) }
17
+ rescue StandardError => e
18
+ render_error_payload(e.message)
19
+ end
20
+
21
+ def update
22
+ order = SpreeCmCommissioner::IntercityTaxiOrder::Update.call(
23
+ order_number: params[:id],
24
+ params: params
25
+ )
26
+
27
+ render_serialized_payload { serialize_resource(order) }
28
+ rescue StandardError => e
29
+ render_error_payload(e.message)
30
+ end
31
+
32
+ def resource_serializer
33
+ SpreeCmCommissioner::V2::Storefront::IntercityTaxiCartSerializer
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,21 +3,59 @@ module Spree
3
3
  module V2
4
4
  module Storefront
5
5
  class TripSearchController < ::Spree::Api::V2::ResourceController
6
+ CACHE_EXPIRES_IN = 5.minutes
7
+
6
8
  def index
7
- trips = SpreeCmCommissioner::TripQuery.new(
8
- origin_id: params[:origin_id],
9
- destination_id: params[:destination_id],
10
- date: params[:date],
11
- vendor_id: params[:vendor_id],
12
- number_of_guests: params[:number_of_guests],
13
- params: params
14
- ).call
9
+ render_serialized_payload do
10
+ Rails.cache.fetch(collection_cache_key, collection_cache_opts) do
11
+ trips = SpreeCmCommissioner::TripQuery.new(
12
+ origin_id: params[:origin_id],
13
+ destination_id: params[:destination_id],
14
+ date: params[:date],
15
+ vendor_id: params[:vendor_id],
16
+ number_of_guests: params[:number_of_guests],
17
+ params: params
18
+ ).call
15
19
 
16
- render_serialized_payload { serialize_collection(trips) }
20
+ serialize_collection(trips)
21
+ end
22
+ end
17
23
  end
18
24
 
19
25
  private
20
26
 
27
+ # override from ContentCacheable
28
+ def max_age
29
+ CACHE_EXPIRES_IN.to_i
30
+ end
31
+
32
+ # override
33
+ def collection_cache_key
34
+ cache_key_parts = [
35
+ 'trip_search',
36
+ params[:origin_id],
37
+ params[:destination_id],
38
+ params[:date],
39
+ params[:vendor_id],
40
+ params[:number_of_guests],
41
+ resource_includes&.sort&.join(','),
42
+ sparse_fields&.sort&.join(','),
43
+ serializer_params.to_json,
44
+ params[:page]&.to_s&.strip,
45
+ params[:per_page]&.to_s&.strip
46
+ ].compact.join('-')
47
+
48
+ Digest::MD5.hexdigest(cache_key_parts)
49
+ end
50
+
51
+ # override
52
+ def collection_cache_opts
53
+ {
54
+ namespace: Spree::Api::Config[:api_v2_collection_cache_namespace],
55
+ expires_in: CACHE_EXPIRES_IN
56
+ }
57
+ end
58
+
21
59
  # override
22
60
  def default_resource_includes
23
61
  [
@@ -17,7 +17,7 @@ module SpreeCmCommissioner
17
17
  end
18
18
 
19
19
  # construct option values base on name & create new option value when not exist.
20
- # then set to variant.
20
+ # then set to variant. Empty values will remove the option value from variant.
21
21
  def build_option_values
22
22
  option_values = permitted_resource_params.delete(:option_values_attributes).to_h.values
23
23
  return if option_values.blank?
@@ -0,0 +1,65 @@
1
+ # 📋 ---[New Vendor]---
2
+ #
3
+ # 📝 Name: {{vendor.name}}
4
+ #
5
+ # ✉️ Email: <code>{{vendor.notification_email || vendor.support_email}}</code>
6
+ # 📱 Phone Number: {{vendor.phone_number}}
7
+ #
8
+ # 👤 Created by: {{first_user.name || 'Admin'}}
9
+ # 🗓️ Created at: {{vendor.created_at}}
10
+ #
11
+ # 🔗 URL: {{vendor.organizer_url}}
12
+ module SpreeCmCommissioner
13
+ class VendorTelegramMessageFactory < TelegramMessageFactory
14
+ attr_reader :vendor
15
+
16
+ def initialize(vendor:)
17
+ @vendor = vendor
18
+
19
+ title = '✨ New Vendor ✨'
20
+
21
+ super(title: title)
22
+ end
23
+
24
+ def body
25
+ [
26
+ "📝 Name: #{vendor.name}",
27
+ '',
28
+ email_line,
29
+ phone_line,
30
+ '',
31
+ creator_line,
32
+ created_at_line,
33
+ '',
34
+ url_line
35
+ ].compact.join("\n")
36
+ end
37
+
38
+ private
39
+
40
+ def email_line
41
+ return unless vendor.notification_email.present? || vendor.support_email.present?
42
+
43
+ "✉️ Email: #{inline_code(vendor.notification_email || vendor.support_email)}"
44
+ end
45
+
46
+ def phone_line
47
+ return if vendor.contact_us.blank?
48
+
49
+ "📱 Phone Number: #{vendor.contact_us}"
50
+ end
51
+
52
+ def creator_line
53
+ creator_email = vendor.users.first&.email
54
+ "👤 Created by: #{creator_email}" if creator_email.present?
55
+ end
56
+
57
+ def created_at_line
58
+ "🗓️ Created at: #{pretty_date(vendor.created_at)}" if vendor.created_at.present?
59
+ end
60
+
61
+ def url_line
62
+ "🔗 URL: #{vendor.organizer_url}" if vendor.organizer_url.present?
63
+ end
64
+ end
65
+ end
@@ -20,25 +20,8 @@ module Spree
20
20
  end
21
21
  end
22
22
 
23
- def custom_product_line_item_url(line_item, options = {})
24
- if defined?(locale_param) && locale_param.present?
25
- options.merge!(locale: locale_param)
26
- end
27
-
28
- localize = if options[:locale].present?
29
- "/#{options[:locale]}"
30
- else
31
- ''
32
- end
33
-
34
- line_item = Spree::LineItem.find(line_item.id)
35
- payload = { order_number: line_item.order.number, line_item_id: line_item.id }
36
-
37
- jwt_token = SpreeCmCommissioner::LineItemJwtToken.encode(payload, line_item.order.token)
38
-
39
- return if line_item.number.blank? && jwt_token.blank?
40
-
41
- "#{current_store.formatted_url + localize}/a/#{line_item.qr_data}"
23
+ def custom_product_line_item_url(line_item)
24
+ main_app.url_for("/a/#{line_item.qr_data}")
42
25
  end
43
26
  end
44
27
  end
@@ -0,0 +1,28 @@
1
+ module SpreeCmCommissioner
2
+ class VendorCreationTelegramAlertSender < BaseInteractor
3
+ delegate :vendor, to: :context
4
+
5
+ def call
6
+ return if admin_chat_id.blank?
7
+
8
+ send_alert
9
+ end
10
+
11
+ def send_alert
12
+ TelegramNotificationSender.call(
13
+ chat_id: admin_chat_id,
14
+ message: alert_message,
15
+ parse_mode: 'HTML'
16
+ )
17
+ end
18
+
19
+ def alert_message
20
+ factory = SpreeCmCommissioner::VendorTelegramMessageFactory.new(vendor: vendor)
21
+ factory.message
22
+ end
23
+
24
+ def admin_chat_id
25
+ ENV.fetch('EXCEPTION_NOTIFIER_TELEGRAM_CHANNEL_ID', nil)
26
+ end
27
+ end
28
+ end
@@ -3,7 +3,7 @@ module SpreeCmCommissioner
3
3
  class RouteFulfilledOrderCountIncrementerJob < ApplicationUniqueJob
4
4
  def perform(order_id:)
5
5
  order = Spree::Order.find(order_id)
6
- SpreeCmCommissioner::Transit::RouteFulfilledOrderCountIncrementerService.call(order: order)
6
+ SpreeCmCommissioner::Routes::IncrementFulfilledOrderCount.call(order: order)
7
7
  end
8
8
  end
9
9
  end
@@ -3,7 +3,7 @@ module SpreeCmCommissioner
3
3
  class RouteOrderCountIncrementerJob < ApplicationUniqueJob
4
4
  def perform(order_id:)
5
5
  order = Spree::Order.find(order_id)
6
- SpreeCmCommissioner::Transit::RouteOrderCountIncrementerService.call(order: order)
6
+ SpreeCmCommissioner::Routes::IncrementOrderCount.call(order: order)
7
7
  end
8
8
  end
9
9
  end
@@ -6,7 +6,7 @@ module SpreeCmCommissioner
6
6
  queue_as :default
7
7
 
8
8
  def perform(previous_route_id:)
9
- SpreeCmCommissioner::Transit::RoutePreviousTripCountDecrementerService.call(previous_route_id: previous_route_id)
9
+ SpreeCmCommissioner::Routes::DecrementPreviousTripCount.call(previous_route_id: previous_route_id)
10
10
  end
11
11
  end
12
12
  end
@@ -3,7 +3,7 @@ module SpreeCmCommissioner
3
3
  class RouteTripCountDecrementerJob < ApplicationUniqueJob
4
4
  def perform(trip_id:)
5
5
  trip = SpreeCmCommissioner::Trip.find(trip_id)
6
- SpreeCmCommissioner::Transit::RouteTripCountDecrementerService.call(trip: trip)
6
+ SpreeCmCommissioner::Routes::DecrementTripCount.call(trip: trip)
7
7
  end
8
8
  end
9
9
  end
@@ -3,7 +3,7 @@ module SpreeCmCommissioner
3
3
  class RouteTripCountIncrementerJob < ApplicationUniqueJob
4
4
  def perform(trip_id:)
5
5
  trip = SpreeCmCommissioner::Trip.find(trip_id)
6
- SpreeCmCommissioner::Transit::RouteTripCountIncrementerService.call(trip: trip)
6
+ SpreeCmCommissioner::Routes::IncrementTripCount.call(trip: trip)
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,10 @@
1
+ module SpreeCmCommissioner
2
+ class VendorCreationTelegramAlertSenderJob < ApplicationUniqueJob
3
+ queue_as :telegram_bot
4
+
5
+ def perform(vendor_id)
6
+ vendor = Spree::Vendor.find(vendor_id)
7
+ SpreeCmCommissioner::VendorCreationTelegramAlertSender.call(vendor: vendor)
8
+ end
9
+ end
10
+ end
@@ -63,11 +63,22 @@ module SpreeCmCommissioner
63
63
 
64
64
  def pickup_place_name = public_metadata['pickup_place_name']
65
65
  def drop_off_place_name = public_metadata['drop_off_place_name']
66
+
66
67
  def pickup_lat = public_metadata['pickup_lat']&.to_f
67
68
  def pickup_lng = public_metadata['pickup_lng']&.to_f
68
69
  def drop_off_lat = public_metadata['drop_off_lat']&.to_f
69
70
  def drop_off_lng = public_metadata['drop_off_lng']&.to_f
70
71
  def passenger_count = public_metadata['passenger_count']&.to_i
72
+ def distance_km = public_metadata['distance_km']&.to_f
73
+ def ordered_points = public_metadata['ordered_points']
74
+ def base_km = public_metadata['base_km']&.to_f
75
+ def detour_pickup_km = public_metadata['detour_pickup_km']&.to_f
76
+ def detour_dropoff_km = public_metadata['detour_dropoff_km']&.to_f
77
+ def extra_pickup_km = public_metadata['extra_pickup_km']&.to_f
78
+ def extra_dropoff_km = public_metadata['extra_dropoff_km']&.to_f
79
+ def extra_pickup_charge_usd = public_metadata['extra_pickup_charge_usd']&.to_f
80
+ def extra_dropoff_charge_usd = public_metadata['extra_dropoff_charge_usd']&.to_f
81
+ def estimated_time_minutes = public_metadata['estimated_time_minutes']&.to_i
71
82
  def pickup_oob_confirmed = private_metadata['pickup_oob_confirmed']
72
83
  def drop_off_oob_confirmed = private_metadata['drop_off_oob_confirmed']
73
84
 
@@ -123,6 +134,46 @@ module SpreeCmCommissioner
123
134
  set_public_metadata_value('passenger_count', value)
124
135
  end
125
136
 
137
+ def distance_km=(value)
138
+ set_public_metadata_value('distance_km', value)
139
+ end
140
+
141
+ def ordered_points=(value)
142
+ set_public_metadata_value('ordered_points', value)
143
+ end
144
+
145
+ def base_km=(value)
146
+ set_public_metadata_value('base_km', value)
147
+ end
148
+
149
+ def detour_pickup_km=(value)
150
+ set_public_metadata_value('detour_pickup_km', value)
151
+ end
152
+
153
+ def detour_dropoff_km=(value)
154
+ set_public_metadata_value('detour_dropoff_km', value)
155
+ end
156
+
157
+ def extra_pickup_km=(value)
158
+ set_public_metadata_value('extra_pickup_km', value)
159
+ end
160
+
161
+ def extra_dropoff_km=(value)
162
+ set_public_metadata_value('extra_dropoff_km', value)
163
+ end
164
+
165
+ def extra_pickup_charge_usd=(value)
166
+ set_public_metadata_value('extra_pickup_charge_usd', value)
167
+ end
168
+
169
+ def extra_dropoff_charge_usd=(value)
170
+ set_public_metadata_value('extra_dropoff_charge_usd', value)
171
+ end
172
+
173
+ def estimated_time_minutes=(value)
174
+ set_public_metadata_value('estimated_time_minutes', value)
175
+ end
176
+
126
177
  private
127
178
 
128
179
  def set_public_metadata_value(key, value)
@@ -10,6 +10,7 @@ module SpreeCmCommissioner
10
10
  array
11
11
  date
12
12
  time
13
+ color
13
14
  coordinate
14
15
  state_selection
15
16
  payment_option
@@ -41,6 +42,8 @@ module SpreeCmCommissioner
41
42
  'bib-display-prefix' => 'boolean',
42
43
  'bib-pre-generation-on-create' => 'boolean',
43
44
  'seat-number-positions' => 'array',
45
+ 'color' => 'color',
46
+ 'ticket-type' => 'string',
44
47
  'seat-type' => 'string',
45
48
  'intercity-taxi' => 'string'
46
49
  }.freeze
@@ -71,6 +74,10 @@ module SpreeCmCommissioner
71
74
  name.in?(RESERVED_OPTIONS.keys)
72
75
  end
73
76
 
77
+ def ticket_type?
78
+ name == 'ticket-type'
79
+ end
80
+
74
81
  def set_reverved_options_attributes
75
82
  self.attr_type = RESERVED_OPTIONS[name]
76
83
  self.kind = :variant
@@ -13,6 +13,8 @@ module SpreeCmCommissioner
13
13
 
14
14
  before_validation :construct_time, if: :attr_type_time?
15
15
  before_validation :construct_date, if: :attr_type_date?
16
+ before_validation :construct_color, if: :attr_type_color?
17
+ before_validation :construct_ticket_type, if: :ticket_type?
16
18
  before_validation :normalize_items, if: :attr_type_array?
17
19
 
18
20
  after_save :update_variants_metadata, if: :saved_change_to_name?
@@ -27,6 +29,10 @@ module SpreeCmCommissioner
27
29
  end
28
30
  end
29
31
 
32
+ def ticket_type?
33
+ option_type.present? && option_type&.ticket_type?
34
+ end
35
+
30
36
  def items
31
37
  return nil unless attr_type_array?
32
38
  return nil if name.nil?
@@ -92,6 +98,13 @@ module SpreeCmCommissioner
92
98
  self.name = name.split(',').map(&:strip).join(',')
93
99
  end
94
100
 
101
+ # Ticket types are case-sensitive: "STANDARD" and "Standard" are distinct ticket types.
102
+ # Only strip whitespace; preserve the exact case entered by the user.
103
+ def construct_ticket_type
104
+ self.name = name&.strip&.presence
105
+ self.presentation = name&.strip&.presence
106
+ end
107
+
95
108
  def construct_time
96
109
  hour, minute = extract_time_from_time_select
97
110
  hour, minute = extract_time_from_default_format if hour.nil? || minute.nil?
@@ -141,5 +154,17 @@ module SpreeCmCommissioner
141
154
  self.name = nil if parse_date(name).blank?
142
155
  self.presentation = name if parse_date(presentation) != parse_date(name)
143
156
  end
157
+
158
+ def construct_color
159
+ self.name = parse_hex_color(name)
160
+ self.presentation = name if parse_hex_color(presentation) != parse_hex_color(name)
161
+ end
162
+
163
+ def parse_hex_color(value)
164
+ return nil if value.nil?
165
+ return nil unless value.match?(/^#[0-9A-Fa-f]{6}$/)
166
+
167
+ value.upcase
168
+ end
144
169
  end
145
170
  end
@@ -1,30 +1,19 @@
1
1
  module SpreeCmCommissioner
2
2
  module RouteOrderCountable
3
- def should_update_count?
3
+ def has_trip_ids? # rubocop:disable Naming/PredicateName
4
4
  preload_trip_ids.any?
5
5
  end
6
6
 
7
7
  def increment_route_fulfilled_order_count
8
- return unless should_update_count?
8
+ return unless has_trip_ids?
9
9
 
10
10
  SpreeCmCommissioner::Transit::RouteFulfilledOrderCountIncrementerJob.perform_later(order_id: id)
11
11
  end
12
12
 
13
13
  def increment_route_order_count
14
- return unless should_update_count?
14
+ return unless has_trip_ids?
15
15
 
16
16
  SpreeCmCommissioner::Transit::RouteOrderCountIncrementerJob.perform_later(order_id: id)
17
17
  end
18
-
19
- # Calling `.trip_ids` directly can cause many slow database queries (N+1 problem)
20
- # every time `.should_update_trip_ids?` or `.preload_trip_ids` runs.
21
- # To avoid this, we store a precomputed list of trip IDs in `private_metadata`.
22
- def preload_trip_ids=(preload_trip_ids = [])
23
- self.private_metadata = (private_metadata || {}).merge('preload_trip_ids' => preload_trip_ids)
24
- end
25
-
26
- def preload_trip_ids
27
- private_metadata&.fetch('preload_trip_ids', []) || []
28
- end
29
18
  end
30
19
  end
@@ -29,10 +29,18 @@ module SpreeCmCommissioner
29
29
  :bib_pre_generation_on_create?,
30
30
  :seat_number_positions,
31
31
  :seat_number_layouts,
32
+ :color,
33
+ :ticket_type,
32
34
  :seat_type,
33
35
  to: :options
34
36
  end
35
37
 
38
+ # Override variant.rb to return cached formatted options text, avoiding repeated database queries.
39
+ # Falls back to database queries & computing the format if preload data is unavailable.
40
+ def options_text
41
+ @options_text ||= public_metadata[:preload_options_text].presence || Spree::Variants::VisableOptionsPresenter.new(self).to_sentence
42
+ end
43
+
36
44
  def options
37
45
  @options ||= VariantOptions.new(self)
38
46
  end
@@ -87,17 +95,23 @@ module SpreeCmCommissioner
87
95
  options.payment_option == 'post-paid'
88
96
  end
89
97
 
90
- # save optins to public_metadata so we don't have to query option types & option values when needed them.
91
- # once variant changed, we update metadata.
92
98
  def set_options_to_public_metadata
93
99
  self.public_metadata ||= {}
94
100
 
95
- latest_options_in_hash = option_values.each_with_object({}) do |option_value, hash|
101
+ # Cache option values as a hash to avoid N+1 queries when accessing options.
102
+ # Stores {option_type_name => option_value_name} pairs that can be retrieved
103
+ # without loading option_types and option_values associations.
104
+ # Example: "Red, 256GB" - formatted options for quick display
105
+ self.public_metadata[:cm_options] = option_values.each_with_object({}) do |option_value, hash|
96
106
  option_type_name = option_value.option_type.name
97
107
  hash[option_type_name] = find_option_value_name_for(option_type_name: option_type_name)
98
108
  end
99
109
 
100
- self.public_metadata[:cm_options] = latest_options_in_hash
110
+ # Cache formatted options text for quick retrieval via #options_text.
111
+ # Precomputes the human-readable format (e.g., "Red, 256GB") to avoid
112
+ # repeated formatting and association queries.
113
+ # Example: {"color" => "red", "storage" => "256GB"} - option_type_name => option_value_name pairs
114
+ self.public_metadata[:preload_options_text] = Spree::Variants::VisableOptionsPresenter.new(self).to_sentence
101
115
  end
102
116
 
103
117
  def set_options_to_public_metadata!
@@ -25,6 +25,8 @@ module SpreeCmCommissioner
25
25
  validates :y, presence: true, numericality: true
26
26
  validates :rotation, presence: true, numericality: { greater_than_or_equal_to: -360, less_than_or_equal_to: 360 }
27
27
 
28
+ validate :cannot_unassign_variant_with_active_blocks
29
+
28
30
  before_validation :assign_layout_from_section, if: -> { seat_layout.nil? && seat_section.present? }
29
31
 
30
32
  def label_required?
@@ -50,5 +52,22 @@ module SpreeCmCommissioner
50
52
  def assign_layout_from_section
51
53
  self.seat_layout = seat_section.seat_layout
52
54
  end
55
+
56
+ private
57
+
58
+ def cannot_unassign_variant_with_active_blocks
59
+ return if variant_id_was.blank? || variant_id.present?
60
+
61
+ active_blocks = all_reserved_blocks.reserved_or_on_hold
62
+ return unless active_blocks.exists?
63
+
64
+ block_count = active_blocks.count
65
+ block_statuses = active_blocks.pluck(:status).uniq.map { |s| s.humanize.downcase }
66
+
67
+ errors.add(
68
+ :variant,
69
+ "cannot be unassigned because #{block_count} #{block_count == 1 ? 'block is' : 'blocks are'} #{block_statuses.join(' and ')} by guests"
70
+ )
71
+ end
53
72
  end
54
73
  end
@@ -7,6 +7,7 @@ module SpreeCmCommissioner
7
7
  base.include SpreeCmCommissioner::Metafield
8
8
  base.include SpreeCmCommissioner::TenantUpdatable
9
9
  base.include SpreeCmCommissioner::ServiceType
10
+ base.include SpreeCmCommissioner::ServiceRecommendations
10
11
 
11
12
  base.has_many :variant_kind_option_types, -> { where(kind: :variant).order(:position) },
12
13
  through: :product_option_types, source: :option_type