spree_cm_commissioner 2.3.2 → 2.4.1

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 +90 -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 +50 -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 +13 -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: 4e32e4116a234b679abcfe3a9d7005bd61890d80936839c32330ecf2b421dadf
4
+ data.tar.gz: a7759f377e09d49281150257a496e44af61e4cabb67a8bec0bacb20a73ea439d
5
5
  SHA512:
6
- metadata.gz: 8530a8855d325ffae9223d3ed705be0f445bd5ce01c2479ef809f783804af5d4b90cd716a5c10076cbad561f6d57c03becf4d8ccb2e7e33bd57144c29bd1ff5e
7
- data.tar.gz: 9d2bada64c5ac4701bb9f18d233e8948e0e8b6b9cf6fd0e923157007bd06cde7ef79b13ffa0476aad2a3ec955a268e66249b3f49097fce93136a61dfa0856f4f
6
+ metadata.gz: 5119cedb570e77e3417bf723ea84926231e4d72000686312a194c1cfb4764061b5df9801923a5db629d8b8adccfd8a70378a238a0c099af46abe8f5c6a2da1d0
7
+ data.tar.gz: adeee4909603c0294be64345ea3de5b310e2dcaa174669137794b56dd23b800ddd9a1f429da636df23953d80154a3b96835cb19a81274fc0908edaae1ebd13d9
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.1)
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,90 @@
1
+ # API endpoint for searching places connected to a specific place via routes.
2
+ #
3
+ # GET /api/v2/storefront/route_places
4
+ #
5
+ # Finds places (origins or destinations) that are connected to a given place through existing routes.
6
+ # Optionally filters results by keyword. Supports two modes:
7
+ #
8
+ # Usage 1: Connected places (requires place_id)
9
+ # - Finds places connected to a specific place via routes
10
+ # - Returns origins/destinations that have routes with the specified place
11
+ #
12
+ # Usage 2: Keyword search (requires query, place_id optional)
13
+ # - Searches all route places by keyword
14
+ # - Returns all origins/destinations matching the keyword
15
+ #
16
+ # Query Parameters:
17
+ # - place_type: [String] Required. Type of route place: 'origin' or 'destination'
18
+ # * 'origin': returns origins that have routes TO the specified place (if place_id provided)
19
+ # * 'destination': returns destinations that have routes FROM the specified place (if place_id provided)
20
+ # - place_id: [Integer] Optional. The place ID to find connected places for
21
+ # - query: [String] Optional. Keyword to filter place names (case-insensitive)
22
+ # - include: Optional comma-separated list (e.g., 'vendors,nearby_places')
23
+ #
24
+ # Response: Collection of places serialized with PlaceSerializer
25
+ #
26
+ # Behavior:
27
+ # - Returns empty collection if place_type is invalid
28
+ # - If place_id provided: returns places connected to that place
29
+ # - If place_id blank: returns all origins/destinations
30
+ # - Filters results by keyword if provided
31
+ #
32
+ # @example Mode 1: Find destinations connected to origin place ID 123
33
+ # GET /api/v2/storefront/route_places?place_id=123&place_type=destination
34
+ #
35
+ # @example Mode 2: Search all destination places by keyword
36
+ # GET /api/v2/storefront/route_places?place_type=destination&query=Phnom
37
+ #
38
+ # @example Combined: Find origins connected to place 456, filtered by keyword
39
+ # GET /api/v2/storefront/route_places?place_id=456&place_type=origin&query=Siem
40
+ module Spree
41
+ module Api
42
+ module V2
43
+ module Storefront
44
+ class RoutePlacesController < ::Spree::Api::V2::ResourceController
45
+ private
46
+
47
+ # override
48
+ def collection
49
+ @collection ||= collection_finder.new(
50
+ place_type: params[:place_type],
51
+ place_id: params[:place_id],
52
+ keyword: params[:query]
53
+ ).execute
54
+ end
55
+
56
+ # override
57
+ def collection_finder
58
+ SpreeCmCommissioner::Places::FindWithRoute
59
+ end
60
+
61
+ # override
62
+ def collection_serializer
63
+ Spree::V2::Storefront::PlaceSerializer
64
+ end
65
+
66
+ # override
67
+ def required_schema
68
+ SpreeCmCommissioner::RoutePlacesRequestSchema
69
+ end
70
+
71
+ # override
72
+ def serializer_params
73
+ super.merge(
74
+ include_vendors: include_vendors?,
75
+ include_nearby_places: include_nearby_places?
76
+ )
77
+ end
78
+
79
+ def include_vendors?
80
+ resource_includes.include?(:vendors) || false
81
+ end
82
+
83
+ def include_nearby_places?
84
+ resource_includes.include?(:nearby_places) || false
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ 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,50 @@
1
+ # Finds places connected via routes with optional filtering.
2
+ #
3
+ # @param place_type [String] Required. 'origin' or 'destination'
4
+ # @param place_id [Integer] Optional. Filter by connected place ID
5
+ # @param keyword [String] Optional. Filter by place name (case-insensitive)
6
+ #
7
+ # @return [ActiveRecord::Relation<SpreeCmCommissioner::Place>]
8
+ #
9
+ # Modes:
10
+ # - place_id only: returns connected places (origins TO or destinations FROM given place)
11
+ # - keyword only: filters by name
12
+ # - both: connected places filtered by name
13
+ # - neither: all origins/destinations in routes
14
+ #
15
+ # @example Origins with routes to place 123
16
+ # FindWithRoute.new(place_type: 'origin', place_id: 123).execute
17
+ #
18
+ # @example Destinations filtered by keyword
19
+ # FindWithRoute.new(place_type: 'destination', keyword: 'Phnom').execute
20
+ module SpreeCmCommissioner
21
+ module Places
22
+ class FindWithRoute
23
+ attr_reader :place_type, :place_id, :keyword
24
+
25
+ def initialize(place_type:, place_id: nil, keyword: nil)
26
+ @place_type = place_type
27
+ @place_id = place_id
28
+ @keyword = keyword
29
+ end
30
+
31
+ def execute
32
+ return SpreeCmCommissioner::Place.none if place_type.blank?
33
+
34
+ result = if place_type == 'origin'
35
+ scope = SpreeCmCommissioner::Place.with_routes_as_origin
36
+ place_id.present? ? scope.where(cm_routes: { destination_place_id: place_id }) : scope
37
+ else
38
+ scope = SpreeCmCommissioner::Place.with_routes_as_destination
39
+ place_id.present? ? scope.where(cm_routes: { origin_place_id: place_id }) : scope
40
+ end
41
+
42
+ return SpreeCmCommissioner::Place.none if place_id.present? && !SpreeCmCommissioner::Place.exists?(place_id)
43
+
44
+ # Apply keyword filter if provided
45
+ result = result.where('cm_places.name ILIKE ?', "%#{keyword}%") if keyword.present?
46
+ result
47
+ end
48
+ end
49
+ end
50
+ 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)