spree_cm_commissioner 2.3.2 → 2.4.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/api/v2/storefront/intercity_taxi/draft_orders_controller.rb +52 -11
  4. data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +63 -0
  5. data/app/controllers/spree/api/v2/storefront/route_places_controller.rb +68 -0
  6. data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +81 -0
  7. data/app/controllers/spree/api/v2/tenant/trip_places_controller.rb +42 -0
  8. data/app/controllers/spree/api/v2/tenant/trip_search_controller.rb +103 -0
  9. data/app/controllers/spree/api/v2/tenant/trips_controller.rb +32 -0
  10. data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +12 -0
  11. data/app/finders/spree_cm_commissioner/places/find_with_route.rb +43 -0
  12. data/app/finders/spree_cm_commissioner/routes/find_popular.rb +25 -0
  13. data/app/mailers/spree/order_mailer_decorator.rb +8 -6
  14. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +34 -89
  15. data/app/models/concerns/spree_cm_commissioner/service_recommendations.rb +2 -2
  16. data/app/models/spree_cm_commissioner/adjustment_decorator.rb +1 -1
  17. data/app/models/spree_cm_commissioner/calculators/extra_drop_off_distance.rb +15 -0
  18. data/app/models/spree_cm_commissioner/calculators/extra_pick_up_distance.rb +15 -0
  19. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -1
  20. data/app/models/spree_cm_commissioner/place.rb +7 -0
  21. data/app/models/spree_cm_commissioner/pricing_action.rb +27 -0
  22. data/app/models/spree_cm_commissioner/pricing_model.rb +18 -0
  23. data/app/models/spree_cm_commissioner/pricing_model_variant.rb +6 -0
  24. data/app/models/spree_cm_commissioner/pricing_rule.rb +15 -0
  25. data/app/models/spree_cm_commissioner/pricing_rule_group.rb +20 -0
  26. data/app/models/spree_cm_commissioner/pricing_rules/extra_drop_off_distance.rb +10 -0
  27. data/app/models/spree_cm_commissioner/pricing_rules/extra_pick_up_distance.rb +12 -0
  28. data/app/models/spree_cm_commissioner/pricing_rules/nationality.rb +35 -0
  29. data/app/models/spree_cm_commissioner/tenant.rb +4 -0
  30. data/app/models/spree_cm_commissioner/variant_decorator.rb +3 -0
  31. data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
  32. data/app/models/spree_cm_commissioner/vendor_place.rb +9 -0
  33. data/app/queries/spree_cm_commissioner/trip_query.rb +23 -17
  34. data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_schema.rb +10 -0
  35. data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_update_schema.rb +29 -0
  36. data/app/request_schemas/spree_cm_commissioner/route_places_request_schema.rb +12 -0
  37. data/app/serializers/spree/v2/storefront/place_serializer.rb +3 -2
  38. data/app/serializers/spree/v2/tenant/cart_serializer.rb +1 -0
  39. data/app/serializers/spree/v2/tenant/intercity_taxi_cart_serializer.rb +12 -0
  40. data/app/serializers/spree/v2/tenant/line_item_serializer.rb +1 -0
  41. data/app/serializers/spree/v2/tenant/trip_place_serializer.rb +8 -0
  42. data/app/serializers/spree/v2/tenant/trip_query_result_serializer.rb +8 -0
  43. data/app/serializers/spree/v2/tenant/trip_serializer.rb +8 -0
  44. data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_line_item_serializer.rb +17 -18
  45. data/app/serializers/spree_cm_commissioner/v2/storefront/route_serializer.rb +12 -0
  46. data/app/services/spree_cm_commissioner/intercity_taxi_order/create.rb +20 -23
  47. data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +36 -72
  48. data/app/services/spree_cm_commissioner/pricing_models/apply.rb +51 -0
  49. data/app/services/spree_cm_commissioner/pricing_rules/build_params.rb +53 -0
  50. data/app/services/spree_cm_commissioner/pricing_rules/build_template.rb +27 -0
  51. data/app/services/spree_cm_commissioner/pricing_rules/create.rb +10 -0
  52. data/app/services/spree_cm_commissioner/pricing_rules/update.rb +11 -0
  53. data/app/services/spree_cm_commissioner/vendor_places/base.rb +24 -0
  54. data/app/services/spree_cm_commissioner/vendor_places/bulk_create.rb +70 -0
  55. data/app/services/spree_cm_commissioner/vendor_places/create_with_google_map_data.rb +85 -0
  56. data/app/services/spree_cm_commissioner/vendor_places/update.rb +56 -0
  57. data/app/views/spree/order_mailer/confirm_email.html.erb +1 -1
  58. data/app/views/spree_cm_commissioner/order_mailer/_mailer_stylesheets.html.erb +14 -13
  59. data/app/views/spree_cm_commissioner/order_mailer/tenant/_greeting.html.erb +6 -4
  60. data/config/routes.rb +11 -1
  61. data/db/migrate/20250915075830_create_spree_cm_commissioner_pricing_models.rb +17 -0
  62. data/db/migrate/20250915080733_create_spree_cm_commissioner_pricing_model_variants.rb +14 -0
  63. data/db/migrate/20251003082144_create_spree_cm_commissioner_pricing_rule_groups.rb +16 -0
  64. data/db/migrate/20251003093408_create_spree_cm_commissioner_pricing_rule.rb +19 -0
  65. data/db/migrate/20251003093643_create_spree_cm_commissioner_pricing_action.rb +14 -0
  66. data/db/migrate/20251119093000_add_tenant_id_to_cm_vendor_places.rb +8 -0
  67. data/lib/spree_cm_commissioner/intercity_taxi/distance.rb +38 -0
  68. data/lib/spree_cm_commissioner/intercity_taxi/map_place.rb +31 -0
  69. data/lib/spree_cm_commissioner/test_helper/factories/pricing_action_factory.rb +5 -0
  70. data/lib/spree_cm_commissioner/test_helper/factories/pricing_model_factory.rb +8 -0
  71. data/lib/spree_cm_commissioner/test_helper/factories/pricing_rule_factory.rb +17 -0
  72. data/lib/spree_cm_commissioner/test_helper/factories/pricing_rule_group_factory.rb +8 -0
  73. data/lib/spree_cm_commissioner/version.rb +1 -1
  74. data/lib/spree_cm_commissioner.rb +2 -0
  75. metadata +49 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f69009264c3f351003ddd4885f403db68bc0a589a9e28123188a26bdd382b3d6
4
- data.tar.gz: fea37b83621c4eb39ee2936ae4d72c377ee6d0767fb240be01c2de82e63d4c83
3
+ metadata.gz: 5f63b4eb2a25d5dff782d61eb3df12b639c261a418213940ed12d8b9b483b9a1
4
+ data.tar.gz: 6682f9d70eb92effa0345aee38da3d483c8bf555a74da8c88fc52ddb7553a2e5
5
5
  SHA512:
6
- metadata.gz: 8530a8855d325ffae9223d3ed705be0f445bd5ce01c2479ef809f783804af5d4b90cd716a5c10076cbad561f6d57c03becf4d8ccb2e7e33bd57144c29bd1ff5e
7
- data.tar.gz: 9d2bada64c5ac4701bb9f18d233e8948e0e8b6b9cf6fd0e923157007bd06cde7ef79b13ffa0476aad2a3ec955a268e66249b3f49097fce93136a61dfa0856f4f
6
+ metadata.gz: 7abea09634def1e05f839d075bb5fcd1cdfac7bc13ed0392ac6c5aa60e5f9aef3a54600d69afa63778aca0ddf62e09ffc1f5c0200dd88f728b7a1bcd0272faea
7
+ data.tar.gz: f102b65bf2526916e47c728b0e50eebe7ce188b96bc0f206d92be0434499a734fe0c434f5d364554c20d5bcf0cec20f4b2bdb945d321c213ddb03c1678d9f135
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.3.2)
37
+ spree_cm_commissioner (2.4.0)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -4,29 +4,70 @@ module Spree
4
4
  module Storefront
5
5
  module IntercityTaxi
6
6
  class DraftOrdersController < ::Spree::Api::V2::ResourceController
7
+ include Spree::Api::V2::Storefront::OrderConcern
8
+ include SpreeCmCommissioner::OrderConcern
9
+
10
+ before_action :ensure_order, only: :update
11
+
7
12
  def create
8
- order = SpreeCmCommissioner::IntercityTaxiOrder::Create.call(
13
+ result = SpreeCmCommissioner::IntercityTaxiOrder::Create.call(
9
14
  trip_id: params[:trip_id],
10
15
  from_date: params[:from_date],
11
16
  to_date: params[:to_date],
12
- user_id: params[:user_id],
17
+ user: spree_current_user,
13
18
  quantity: params[:quantity]
14
19
  )
15
20
 
16
- render_serialized_payload { serialize_resource(order) }
17
- rescue StandardError => e
18
- render_error_payload(e.message)
21
+ if result.success?
22
+ render_serialized_payload { serialize_resource(result.value) }
23
+ else
24
+ render_error_payload(result.error)
25
+ end
19
26
  end
20
27
 
21
28
  def update
22
- order = SpreeCmCommissioner::IntercityTaxiOrder::Update.call(
23
- order_number: params[:id],
24
- params: params
29
+ result = SpreeCmCommissioner::IntercityTaxiOrder::Update.call(
30
+ order: spree_current_order,
31
+ remark: params[:remark],
32
+ pickup_map_place_attributes: params.require(:pickup_map_place_attributes).permit(
33
+ :place_name,
34
+ :lat,
35
+ :lng,
36
+ :oob_confirmed
37
+ ),
38
+ dropoff_map_place_attributes: params.require(:dropoff_map_place_attributes).permit(
39
+ :place_name,
40
+ :lat,
41
+ :lng,
42
+ :oob_confirmed
43
+ ),
44
+ distance_attributes: params.require(:distance_attributes).permit(
45
+ :distance_km,
46
+ :ordered_points,
47
+ :estimated_time_minutes,
48
+ :base_km,
49
+ :detour_pickup_km,
50
+ :detour_dropoff_km,
51
+ :extra_pickup_km,
52
+ :extra_dropoff_km
53
+ )
25
54
  )
26
55
 
27
- render_serialized_payload { serialize_resource(order) }
28
- rescue StandardError => e
29
- render_error_payload(e.message)
56
+ if result.success?
57
+ render_serialized_payload { serialize_resource(result.value) }
58
+ else
59
+ render_error_payload(result.error)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def required_schema
66
+ if action_name == 'create'
67
+ SpreeCmCommissioner::IntercityTaxiDraftOrderSchema
68
+ else
69
+ SpreeCmCommissioner::IntercityTaxiDraftOrderUpdateSchema
70
+ end
30
71
  end
31
72
 
32
73
  def resource_serializer
@@ -0,0 +1,63 @@
1
+ # API endpoint for retrieving popular routes ordered by order metrics.
2
+ #
3
+ # GET /api/v2/storefront/popular_route_places
4
+ #
5
+ # Returns routes sorted by popularity (fulfilled_order_count, then order_count) with their
6
+ # origin and destination places. Supports optional includes for vendors and nearby_places.
7
+ #
8
+ # Response: Collection of routes serialized with RouteSerializer
9
+ #
10
+ # @example
11
+ # GET /api/v2/storefront/popular_route_places
12
+ module Spree
13
+ module Api
14
+ module V2
15
+ module Storefront
16
+ class PopularRoutePlacesController < ::Spree::Api::V2::ResourceController
17
+ private
18
+
19
+ # override
20
+ def collection
21
+ @collection ||= collection_finder.new.execute
22
+ end
23
+
24
+ # override
25
+ def default_resource_includes
26
+ %w[origin_place destination_place]
27
+ end
28
+
29
+ # override
30
+ def collection_finder
31
+ SpreeCmCommissioner::Routes::FindPopular
32
+ end
33
+
34
+ # override
35
+ def collection_serializer
36
+ resource_serializer
37
+ end
38
+
39
+ # override
40
+ def resource_serializer
41
+ SpreeCmCommissioner::V2::Storefront::RouteSerializer
42
+ end
43
+
44
+ # override
45
+ def serializer_params
46
+ super.merge(
47
+ include_vendors: include_vendors?,
48
+ include_nearby_places: include_nearby_places?
49
+ )
50
+ end
51
+
52
+ def include_vendors?
53
+ resource_includes.include?(:vendors) || false
54
+ end
55
+
56
+ def include_nearby_places?
57
+ resource_includes.include?(:nearby_places) || false
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,68 @@
1
+ # API endpoint for searching places that are used in routes by keyword.
2
+ #
3
+ # GET /api/v2/storefront/route_places
4
+ #
5
+ # Searches for places (origins or destinations) that are used in existing routes,
6
+ # filtered by keyword search on place names.
7
+ #
8
+ # Query Parameters:
9
+ # - query: [String] Required. Keyword to search in place names (case-insensitive)
10
+ # - place_type: [String] Required. Type of route place: 'origin' or 'destination'
11
+ # - include: Optional comma-separated list (e.g., 'vendors,nearby_places')
12
+ #
13
+ # Response: Collection of places serialized with PlaceSerializer
14
+ #
15
+ # Behavior:
16
+ # - Returns empty collection if query is blank or place_type is invalid
17
+ # - UI should only call this endpoint when user has input a search term
18
+ # - For initial/empty state, use GET /api/v2/storefront/popular_route_places instead
19
+ #
20
+ # @example
21
+ # GET /api/v2/storefront/route_places?query=Phnom&place_type=origin
22
+ module Spree
23
+ module Api
24
+ module V2
25
+ module Storefront
26
+ class RoutePlacesController < ::Spree::Api::V2::ResourceController
27
+ private
28
+
29
+ # override
30
+ def collection
31
+ @collection ||= collection_finder.new(keyword: params[:query], place_type: params[:place_type]).execute
32
+ end
33
+
34
+ # override
35
+ def collection_finder
36
+ SpreeCmCommissioner::Places::FindWithRoute
37
+ end
38
+
39
+ # override
40
+ def collection_serializer
41
+ Spree::V2::Storefront::PlaceSerializer
42
+ end
43
+
44
+ # override
45
+ def required_schema
46
+ SpreeCmCommissioner::RoutePlacesRequestSchema
47
+ end
48
+
49
+ # override
50
+ def serializer_params
51
+ super.merge(
52
+ include_vendors: include_vendors?,
53
+ include_nearby_places: include_nearby_places?
54
+ )
55
+ end
56
+
57
+ def include_vendors?
58
+ resource_includes.include?(:vendors) || false
59
+ end
60
+
61
+ def include_nearby_places?
62
+ resource_includes.include?(:nearby_places) || false
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,81 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ module IntercityTaxi
6
+ class DraftOrdersController < BaseController
7
+ include Spree::Api::V2::Storefront::OrderConcern
8
+ include SpreeCmCommissioner::OrderConcern
9
+
10
+ before_action :ensure_order, only: :update
11
+
12
+ def create
13
+ result = SpreeCmCommissioner::IntercityTaxiOrder::Create.call(
14
+ trip_id: params[:trip_id],
15
+ from_date: params[:from_date],
16
+ to_date: params[:to_date],
17
+ quantity: params[:quantity],
18
+ user: spree_current_user
19
+ )
20
+
21
+ if result.success?
22
+ render_serialized_payload { serialize_resource(result.value) }
23
+ else
24
+ render_error_payload(result.error)
25
+ end
26
+ end
27
+
28
+ def update
29
+ result = SpreeCmCommissioner::IntercityTaxiOrder::Update.call(
30
+ order: spree_current_order,
31
+ remark: params[:remark],
32
+ pickup_map_place_attributes: params.require(:pickup_map_place_attributes).permit(
33
+ :place_name,
34
+ :lat,
35
+ :lng,
36
+ :oob_confirmed
37
+ ),
38
+ dropoff_map_place_attributes: params.require(:dropoff_map_place_attributes).permit(
39
+ :place_name,
40
+ :lat,
41
+ :lng,
42
+ :oob_confirmed
43
+ ),
44
+ distance_attributes: params.require(:distance_attributes).permit(
45
+ :distance_km,
46
+ :ordered_points,
47
+ :estimated_time_minutes,
48
+ :base_km,
49
+ :detour_pickup_km,
50
+ :detour_dropoff_km,
51
+ :extra_pickup_km,
52
+ :extra_dropoff_km
53
+ )
54
+ )
55
+
56
+ if result.success?
57
+ render_serialized_payload { serialize_resource(result.value) }
58
+ else
59
+ render_error_payload(result.error)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def required_schema
66
+ if action_name == 'create'
67
+ SpreeCmCommissioner::IntercityTaxiDraftOrderSchema
68
+ else
69
+ SpreeCmCommissioner::IntercityTaxiDraftOrderUpdateSchema
70
+ end
71
+ end
72
+
73
+ def resource_serializer
74
+ Spree::V2::Tenant::IntercityTaxiCartSerializer
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,42 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ class TripPlacesController < BaseController
6
+ private
7
+
8
+ # override
9
+ def collection
10
+ @collection ||= begin
11
+ base_scope = scope
12
+
13
+ if params[:q].present?
14
+ @search = base_scope.ransack(params[:q])
15
+ @search.result
16
+ else
17
+ base_scope
18
+ end
19
+ end
20
+ end
21
+
22
+ # override
23
+ def scope
24
+ model_class.joins(:location_vendor_places)
25
+ .where(location_vendor_places: { tenant_id: @tenant.id })
26
+ .distinct
27
+ end
28
+
29
+ # override
30
+ def model_class
31
+ SpreeCmCommissioner::Place
32
+ end
33
+
34
+ # override
35
+ def collection_serializer
36
+ Spree::V2::Tenant::TripPlaceSerializer
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,103 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ class TripSearchController < BaseController
6
+ CACHE_EXPIRES_IN = 5.minutes
7
+
8
+ def index
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
+ route_type: params[:route_type],
16
+ number_of_guests: params[:number_of_guests],
17
+ tenant_id: @tenant.id,
18
+ params: params
19
+ ).call
20
+
21
+ serialize_collection(trips)
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # override from ContentCacheable
29
+ def max_age
30
+ CACHE_EXPIRES_IN.to_i
31
+ end
32
+
33
+ # override
34
+ def collection_cache_key
35
+ cache_key_parts = [
36
+ 'trip_search',
37
+ @tenant.id,
38
+ params[:origin_id],
39
+ params[:destination_id],
40
+ params[:date],
41
+ params[:route_type],
42
+ params[:number_of_guests],
43
+ resource_includes&.sort&.join(','),
44
+ sparse_fields&.sort&.join(','),
45
+ serializer_params.to_json,
46
+ params[:page]&.to_s&.strip,
47
+ params[:per_page]&.to_s&.strip
48
+ ].compact.join('-')
49
+
50
+ Digest::MD5.hexdigest(cache_key_parts)
51
+ end
52
+
53
+ # override
54
+ def collection_cache_opts
55
+ {
56
+ namespace: Spree::Api::Config[:api_v2_collection_cache_namespace],
57
+ expires_in: CACHE_EXPIRES_IN
58
+ }
59
+ end
60
+
61
+ # override
62
+ def default_resource_includes
63
+ [
64
+ 'trips.vendor',
65
+ 'trips.vendor.logo',
66
+ 'trips.vehicle',
67
+ 'trips.vehicle.vehicle_photos',
68
+ 'trips.amenities'
69
+ ]
70
+ end
71
+
72
+ def serialize_collection(collection)
73
+ serialized_data = Spree::V2::Tenant::TripQueryResultSerializer.new(
74
+ collection,
75
+ include: default_resource_includes,
76
+ params: serializer_params
77
+ ).serializable_hash
78
+ serialized_data[:meta] = {
79
+ count: collection.size,
80
+ total_count: collection.respond_to?(:total_count) ? collection.total_count : collection.size,
81
+ current_page: collection.respond_to?(:current_page) ? collection.current_page : 1,
82
+ per_page: collection.respond_to?(:limit_value) ? collection.limit_value : collection.size,
83
+ pages: collection.respond_to?(:total_pages) ? collection.total_pages : 1,
84
+ next_page: collection.respond_to?(:next_page) ? collection.next_page : nil,
85
+ prev_page: collection.respond_to?(:prev_page) ? collection.prev_page : nil
86
+ }
87
+ serialized_data
88
+ end
89
+
90
+ # override
91
+ def serializer_params
92
+ params.permit(:include).to_hash
93
+ end
94
+
95
+ # override
96
+ def required_schema
97
+ SpreeCmCommissioner::TripSearchRequestSchema
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,32 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Tenant
5
+ class TripsController < BaseController
6
+ private
7
+
8
+ # override
9
+ def resource
10
+ scope.find(params[:id])
11
+ end
12
+
13
+ # override
14
+ def scope
15
+ model_class.joins(:vendor)
16
+ .where(spree_vendors: { tenant_id: @tenant.id })
17
+ end
18
+
19
+ # override
20
+ def model_class
21
+ SpreeCmCommissioner::Trip
22
+ end
23
+
24
+ # override
25
+ def resource_serializer
26
+ Spree::V2::Tenant::TripSerializer
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -19,6 +19,7 @@
19
19
  # Name: Vaneath Awesome
20
20
  # Tel: <code>+85570760548</code>
21
21
  # Email: <code>admin_dev@cm.com</code>
22
+ # Delivery Address: #123, St. 456, Phnom Penh
22
23
 
23
24
  module SpreeCmCommissioner
24
25
  class OrderTelegramMessageFactory < TelegramMessageFactory
@@ -79,6 +80,7 @@ module SpreeCmCommissioner
79
80
  text << "Name: #{order.name}"
80
81
  text << "Tel: #{inline_code(order.intel_phone_number || order.phone_number)}"
81
82
  text << "Email: #{inline_code(order.email)}" if order.email.present?
83
+ text << "Delivery Address: #{formatted_shipping_address.presence || 'N/A'}" if order.delivery_required?
82
84
 
83
85
  if show_details_link && order.guests.any?
84
86
  text << ''
@@ -89,6 +91,16 @@ module SpreeCmCommissioner
89
91
  text.compact.join("\n")
90
92
  end
91
93
 
94
+ def formatted_shipping_address
95
+ return 'N/A' if order.shipping_address.blank?
96
+
97
+ @formatted_shipping_address ||= [
98
+ order.shipping_address.address1,
99
+ order.shipping_address.address2,
100
+ order.shipping_address.state&.name
101
+ ].compact.compact_blank.join(', ')
102
+ end
103
+
92
104
  # Result:
93
105
  # | No. A24 | No. A24 |
94
106
  # | No. A24 | No. A24 |
@@ -0,0 +1,43 @@
1
+ # Finds places that are used in routes (as origin or destination) by keyword search.
2
+ #
3
+ # @param keyword [String] Search term to match against place names (case-insensitive)
4
+ # @param place_type [String] Type of route place: 'origin' or 'destination'
5
+ #
6
+ # @return [ActiveRecord::Relation<SpreeCmCommissioner::Place>] Places matching the keyword
7
+ # that are used as origins or destinations in existing routes. Returns empty relation if
8
+ # keyword is blank or place_type is invalid.
9
+ #
10
+ # @note For default/empty state display, use SpreeCmCommissioner::Routes::FindPopular instead
11
+ # to show popular routes to users before they enter a search term.
12
+ #
13
+ # @example
14
+ # finder = SpreeCmCommissioner::Places::FindWithRoute.new(keyword: 'Phnom', place_type: 'origin')
15
+ # finder.execute # => Returns places with "Phnom" in name that are used as route origins
16
+ module SpreeCmCommissioner
17
+ module Places
18
+ class FindWithRoute
19
+ attr_reader :keyword, :place_type
20
+
21
+ def initialize(keyword:, place_type:)
22
+ @keyword = keyword
23
+ @place_type = place_type
24
+ end
25
+
26
+ def execute
27
+ return SpreeCmCommissioner::Place.none if keyword.blank? || place_type.blank?
28
+
29
+ scope.where('cm_places.name ILIKE ?', "%#{keyword}%")
30
+ end
31
+
32
+ private
33
+
34
+ def scope
35
+ if place_type == 'origin'
36
+ SpreeCmCommissioner::Place.with_routes_as_origin
37
+ else
38
+ SpreeCmCommissioner::Place.with_routes_as_destination
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # Finds routes ordered by popularity based on order metrics.
2
+ #
3
+ # Routes are sorted by:
4
+ # 1. fulfilled_order_count (completed orders) - DESC
5
+ # 2. order_count (total orders including incomplete) - DESC
6
+ #
7
+ # @return [ActiveRecord::Relation<SpreeCmCommissioner::Route>] Routes with origin and destination places loaded,
8
+ # ordered from most to least popular
9
+ #
10
+ # @example
11
+ # finder = SpreeCmCommissioner::Routes::FindPopular.new
12
+ # finder.execute # => Returns routes sorted by fulfillment and order counts
13
+
14
+ module SpreeCmCommissioner
15
+ module Routes
16
+ class FindPopular
17
+ def execute
18
+ SpreeCmCommissioner::Route
19
+ .includes(:origin_place, :destination_place)
20
+ .order(Arel.sql('COALESCE(fulfilled_order_count, 0) DESC'))
21
+ .order(Arel.sql('COALESCE(order_count, 0) DESC'))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -12,14 +12,20 @@ module Spree
12
12
  return false if @order.email.blank?
13
13
 
14
14
  setup_tenant_and_store
15
+ if @tenant.present?
16
+ @name = @order.tenant.name
17
+ @logo = @order.tenant.active_vendor&.logo&.original_url
18
+ @brand_color = @order.tenant.preferred_brand_primary_color
19
+ end
15
20
  @product_type = @order.products.first&.product_type || 'accommodation'
16
21
 
17
22
  subject = build_subject(resend)
18
23
 
19
24
  mail(to: @order.email,
20
- from: from_email_address, subject: subject,
25
+ from: from_email_address,
26
+ subject: subject,
21
27
  store_url: store_url
22
- ) do |format|
28
+ ) do |format|
23
29
  format.html { render layout: 'spree_cm_commissioner/layouts/order_mailer' }
24
30
  format.text
25
31
  end
@@ -38,10 +44,6 @@ module Spree
38
44
  def setup_tenant_and_store
39
45
  @tenant = @order.tenant
40
46
  @current_store = @order.store
41
- return if @tenant.blank?
42
-
43
- @brand_color = @tenant.preferences[:brand_primary_color]
44
- @vendor_logo_url = @tenant.active_vendor&.logo&.original_url
45
47
  end
46
48
 
47
49
  def build_subject(resend)