spree_cm_commissioner 2.3.1 → 2.3.2.pre.pre4

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/admin/product_completion_steps_controller.rb +17 -1
  4. data/app/controllers/spree/admin/tenants_controller.rb +1 -1
  5. data/app/controllers/spree/api/v2/storefront/intercity_taxi/draft_orders_controller.rb +52 -11
  6. data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +63 -0
  7. data/app/controllers/spree/api/v2/storefront/route_places_controller.rb +68 -0
  8. data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +81 -0
  9. data/app/controllers/spree/api/v2/tenant/trip_places_controller.rb +42 -0
  10. data/app/controllers/spree/api/v2/tenant/trip_search_controller.rb +103 -0
  11. data/app/controllers/spree/api/v2/tenant/trips_controller.rb +32 -0
  12. data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +12 -0
  13. data/app/finders/spree_cm_commissioner/places/find_with_route.rb +43 -0
  14. data/app/finders/spree_cm_commissioner/routes/find_popular.rb +25 -0
  15. data/app/interactors/spree_cm_commissioner/create_ticket.rb +2 -0
  16. data/app/interactors/spree_cm_commissioner/host_matcher.rb +1 -1
  17. data/app/interactors/spree_cm_commissioner/vendor_creation_telegram_alert_sender.rb +1 -1
  18. data/app/mailers/spree/order_mailer_decorator.rb +12 -11
  19. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +34 -89
  20. data/app/models/concerns/spree_cm_commissioner/service_recommendations.rb +2 -2
  21. data/app/models/concerns/spree_cm_commissioner/store_preference.rb +1 -0
  22. data/app/models/spree_cm_commissioner/adjustment_decorator.rb +1 -1
  23. data/app/models/spree_cm_commissioner/calculators/extra_drop_off_distance.rb +15 -0
  24. data/app/models/spree_cm_commissioner/calculators/extra_pick_up_distance.rb +15 -0
  25. data/app/models/spree_cm_commissioner/place.rb +7 -0
  26. data/app/models/spree_cm_commissioner/pricing_action.rb +27 -0
  27. data/app/models/spree_cm_commissioner/pricing_model.rb +18 -0
  28. data/app/models/spree_cm_commissioner/pricing_model_variant.rb +6 -0
  29. data/app/models/spree_cm_commissioner/pricing_rule.rb +15 -0
  30. data/app/models/spree_cm_commissioner/pricing_rule_group.rb +20 -0
  31. data/app/models/spree_cm_commissioner/pricing_rules/extra_drop_off_distance.rb +10 -0
  32. data/app/models/spree_cm_commissioner/pricing_rules/extra_pick_up_distance.rb +12 -0
  33. data/app/models/spree_cm_commissioner/pricing_rules/nationality.rb +35 -0
  34. data/app/models/spree_cm_commissioner/product_completion_step.rb +4 -1
  35. data/app/models/spree_cm_commissioner/product_completion_step_banner.rb +12 -0
  36. data/app/models/spree_cm_commissioner/product_completion_steps/social_entry_url.rb +86 -19
  37. data/app/models/spree_cm_commissioner/tenant.rb +14 -1
  38. data/app/models/spree_cm_commissioner/variant_decorator.rb +3 -0
  39. data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
  40. data/app/models/spree_cm_commissioner/vendor_place.rb +10 -1
  41. data/app/overrides/spree/admin/stores/_form/store_preferences.html.erb.deface +9 -0
  42. data/app/queries/spree_cm_commissioner/trip_query.rb +23 -17
  43. data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_schema.rb +10 -0
  44. data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_update_schema.rb +29 -0
  45. data/app/request_schemas/spree_cm_commissioner/route_places_request_schema.rb +12 -0
  46. data/app/serializers/spree/v2/storefront/place_serializer.rb +3 -2
  47. data/app/serializers/spree/v2/tenant/cart_serializer.rb +1 -0
  48. data/app/serializers/spree/v2/tenant/intercity_taxi_cart_serializer.rb +12 -0
  49. data/app/serializers/spree/v2/tenant/line_item_serializer.rb +1 -0
  50. data/app/serializers/spree/v2/tenant/trip_place_serializer.rb +8 -0
  51. data/app/serializers/spree/v2/tenant/trip_query_result_serializer.rb +8 -0
  52. data/app/serializers/spree/v2/tenant/trip_serializer.rb +8 -0
  53. data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_line_item_serializer.rb +17 -18
  54. data/app/serializers/spree_cm_commissioner/v2/storefront/route_serializer.rb +12 -0
  55. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_vehicle_serializer.rb +1 -1
  56. data/app/services/spree_cm_commissioner/intercity_taxi_order/create.rb +20 -23
  57. data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +36 -72
  58. data/app/services/spree_cm_commissioner/pricing_models/apply.rb +51 -0
  59. data/app/services/spree_cm_commissioner/pricing_rules/build_params.rb +53 -0
  60. data/app/services/spree_cm_commissioner/pricing_rules/build_template.rb +27 -0
  61. data/app/services/spree_cm_commissioner/pricing_rules/create.rb +10 -0
  62. data/app/services/spree_cm_commissioner/pricing_rules/update.rb +11 -0
  63. data/app/services/spree_cm_commissioner/vendor_places/base.rb +24 -0
  64. data/app/services/spree_cm_commissioner/vendor_places/bulk_create.rb +70 -0
  65. data/app/services/spree_cm_commissioner/vendor_places/create_with_google_map_data.rb +85 -0
  66. data/app/services/spree_cm_commissioner/vendor_places/update.rb +56 -0
  67. data/app/views/shared/_asset_field.html.erb +13 -0
  68. data/app/views/spree/admin/product_completion_steps/_form.html.erb +28 -2
  69. data/app/views/spree/admin/product_completion_steps/_supported_fields.html.erb +33 -0
  70. data/app/views/spree/admin/tenants/_form.html.erb +1 -1
  71. data/app/views/spree/order_mailer/confirm_email.html.erb +1 -1
  72. data/app/views/spree_cm_commissioner/order_mailer/_mailer_stylesheets.html.erb +14 -13
  73. data/app/views/spree_cm_commissioner/order_mailer/tenant/_greeting.html.erb +6 -4
  74. data/config/initializers/spree_permitted_attributes.rb +1 -0
  75. data/config/locales/en.yml +4 -0
  76. data/config/locales/km.yml +65 -61
  77. data/config/routes.rb +15 -1
  78. data/db/migrate/20250915075830_create_spree_cm_commissioner_pricing_models.rb +17 -0
  79. data/db/migrate/20250915080733_create_spree_cm_commissioner_pricing_model_variants.rb +14 -0
  80. data/db/migrate/20251003082144_create_spree_cm_commissioner_pricing_rule_groups.rb +16 -0
  81. data/db/migrate/20251003093408_create_spree_cm_commissioner_pricing_rule.rb +19 -0
  82. data/db/migrate/20251003093643_create_spree_cm_commissioner_pricing_action.rb +14 -0
  83. data/db/migrate/20251119093000_add_tenant_id_to_cm_vendor_places.rb +8 -0
  84. data/lib/spree_cm_commissioner/intercity_taxi/distance.rb +38 -0
  85. data/lib/spree_cm_commissioner/intercity_taxi/map_place.rb +31 -0
  86. data/lib/spree_cm_commissioner/test_helper/factories/pricing_action_factory.rb +5 -0
  87. data/lib/spree_cm_commissioner/test_helper/factories/pricing_model_factory.rb +8 -0
  88. data/lib/spree_cm_commissioner/test_helper/factories/pricing_rule_factory.rb +17 -0
  89. data/lib/spree_cm_commissioner/test_helper/factories/pricing_rule_group_factory.rb +8 -0
  90. data/lib/spree_cm_commissioner/test_helper/factories/product_completion_step_banner_factory.rb +7 -0
  91. data/lib/spree_cm_commissioner/version.rb +1 -1
  92. data/lib/spree_cm_commissioner.rb +2 -0
  93. metadata +54 -4
@@ -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
@@ -37,22 +43,17 @@ module Spree
37
43
 
38
44
  def setup_tenant_and_store
39
45
  @tenant = @order.tenant
40
- if @tenant.present?
41
- @brand_color = @tenant.preferences[:brand_primary_color]
42
- @vendor_logo_url = @tenant.active_vendor&.logo&.original_url
43
- @current_store = @tenant
44
- else
45
- @current_store = @order.store
46
- end
46
+ @current_store = @order.store
47
47
  end
48
48
 
49
49
  def build_subject(resend)
50
50
  prefix = resend ? "[#{Spree.t(:resend).upcase}] " : ''
51
- "#{prefix}#{@current_store&.name} Booking Confirmation ##{@order.number}"
51
+ store_name = @tenant.present? ? @tenant.name : @current_store&.name
52
+ "#{prefix}#{store_name} Booking Confirmation ##{@order.number}"
52
53
  end
53
54
 
54
55
  def store_url
55
- @tenant.present? ? @current_store.host : @current_store.url
56
+ @tenant.present? ? @tenant.url : @current_store.url
56
57
  end
57
58
 
58
59
  def ticket_email(guest, email)
@@ -27,11 +27,6 @@ module SpreeCmCommissioner
27
27
  validates :boarding_trip_stop_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
28
28
  validates :drop_off_trip_stop_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
29
29
 
30
- validates :pickup_lat, numericality: { greater_than_or_equal_to: -90, less_than_or_equal_to: 90 }, allow_nil: true
31
- validates :pickup_lng, numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 }, allow_nil: true
32
- validates :drop_off_lat, numericality: { greater_than_or_equal_to: -90, less_than_or_equal_to: 90 }, allow_nil: true
33
- validates :drop_off_lng, numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 }, allow_nil: true
34
-
35
30
  validates :passenger_count, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
36
31
 
37
32
  # Groups LineItems by trip fields. Some fields can be null,
@@ -61,121 +56,71 @@ module SpreeCmCommissioner
61
56
  def boarding_trip_stop_id = private_metadata['boarding_trip_stop_id']&.to_i
62
57
  def drop_off_trip_stop_id = private_metadata['drop_off_trip_stop_id']&.to_i
63
58
 
64
- def pickup_place_name = public_metadata['pickup_place_name']
65
- def drop_off_place_name = public_metadata['drop_off_place_name']
66
-
67
- def pickup_lat = public_metadata['pickup_lat']&.to_f
68
- def pickup_lng = public_metadata['pickup_lng']&.to_f
69
- def drop_off_lat = public_metadata['drop_off_lat']&.to_f
70
- def drop_off_lng = public_metadata['drop_off_lng']&.to_f
71
59
  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
82
- def pickup_oob_confirmed = private_metadata['pickup_oob_confirmed']
83
- def drop_off_oob_confirmed = private_metadata['drop_off_oob_confirmed']
84
60
 
85
- def direction=(value)
86
- set_private_metadata_value('direction', value)
87
- end
61
+ def pickup_map_place
62
+ pickup_hash = public_metadata&.dig('pickup_map_place')
63
+ return nil if pickup_hash.blank?
88
64
 
89
- def trip_id=(value)
90
- set_private_metadata_value('trip_id', value)
65
+ SpreeCmCommissioner::IntercityTaxi::MapPlace.from_hash(pickup_hash)
91
66
  end
92
67
 
93
- def boarding_trip_stop_id=(value)
94
- set_private_metadata_value('boarding_trip_stop_id', value)
95
- end
68
+ def dropoff_map_place
69
+ dropoff_hash = public_metadata&.dig('dropoff_map_place')
70
+ return nil if dropoff_hash.blank?
96
71
 
97
- def drop_off_trip_stop_id=(value)
98
- set_private_metadata_value('drop_off_trip_stop_id', value)
72
+ SpreeCmCommissioner::IntercityTaxi::MapPlace.from_hash(dropoff_hash)
99
73
  end
100
74
 
101
- def pickup_oob_confirmed=(value)
102
- set_private_metadata_value('pickup_oob_confirmed', value)
75
+ def distance
76
+ distance_hash = public_metadata&.dig('distance')
77
+ return nil if distance_hash.blank?
78
+
79
+ SpreeCmCommissioner::IntercityTaxi::Distance.from_hash(distance_hash)
103
80
  end
104
81
 
105
- def drop_off_oob_confirmed=(value)
106
- set_private_metadata_value('drop_off_oob_confirmed', value)
82
+ def pickup_map_place=(value)
83
+ set_public_metadata_object('pickup_map_place', value)
107
84
  end
108
85
 
109
- def pickup_place_name=(value)
110
- set_public_metadata_value('pickup_place_name', value)
86
+ def dropoff_map_place=(value)
87
+ set_public_metadata_object('dropoff_map_place', value)
111
88
  end
112
89
 
113
- def drop_off_place_name=(value)
114
- set_public_metadata_value('drop_off_place_name', value)
90
+ def distance=(value)
91
+ set_public_metadata_object('distance', value)
115
92
  end
116
93
 
117
- def pickup_lat=(value)
118
- set_public_metadata_value('pickup_lat', value)
94
+ def direction=(value)
95
+ set_private_metadata_value('direction', value)
119
96
  end
120
97
 
121
- def pickup_lng=(value)
122
- set_public_metadata_value('pickup_lng', value)
98
+ def trip_id=(value)
99
+ set_private_metadata_value('trip_id', value)
123
100
  end
124
101
 
125
- def drop_off_lat=(value)
126
- set_public_metadata_value('drop_off_lat', value)
102
+ def boarding_trip_stop_id=(value)
103
+ set_private_metadata_value('boarding_trip_stop_id', value)
127
104
  end
128
105
 
129
- def drop_off_lng=(value)
130
- set_public_metadata_value('drop_off_lng', value)
106
+ def drop_off_trip_stop_id=(value)
107
+ set_private_metadata_value('drop_off_trip_stop_id', value)
131
108
  end
132
109
 
133
110
  def passenger_count=(value)
134
111
  set_public_metadata_value('passenger_count', value)
135
112
  end
136
113
 
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
114
+ private
172
115
 
173
- def estimated_time_minutes=(value)
174
- set_public_metadata_value('estimated_time_minutes', value)
116
+ def set_public_metadata_object(key, value)
117
+ if value.nil?
118
+ public_metadata.delete(key)
119
+ else
120
+ public_metadata[key] = value.to_h.stringify_keys
121
+ end
175
122
  end
176
123
 
177
- private
178
-
179
124
  def set_public_metadata_value(key, value)
180
125
  if value.nil?
181
126
  public_metadata.delete(key)
@@ -50,9 +50,9 @@ module SpreeCmCommissioner
50
50
  private
51
51
 
52
52
  def tenant_link_or_default(tenant, default_link)
53
- return default_link unless tenant.respond_to?(:host)
53
+ return default_link unless tenant.respond_to?(:formatted_url)
54
54
 
55
- site = tenant.host.to_s.strip
55
+ site = tenant.formatted_url.to_s.strip
56
56
  site.presence || default_link
57
57
  rescue StandardError
58
58
  default_link
@@ -6,6 +6,7 @@ module SpreeCmCommissioner
6
6
  preference :sms_sender_id, :string
7
7
  preference :telegram_order_alert_chat_id, :string
8
8
  preference :telegram_order_request_alert_chat_id, :string
9
+ preference :telegram_new_vendor_alert_chat_id, :string
9
10
  preference :assetlinks, :string, default: ''
10
11
  preference :apple_app_site_association, :string, default: ''
11
12
  end
@@ -2,7 +2,7 @@ module SpreeCmCommissioner
2
2
  module AdjustmentDecorator
3
3
  def self.prepended(base)
4
4
  base.belongs_to :payable, polymorphic: true, optional: true
5
-
5
+ base.scope :pricing_action, -> { where(source_type: 'SpreeCmCommissioner::PricingAction') }
6
6
  base.whitelisted_ransackable_attributes |= %w[payable_id]
7
7
  end
8
8
 
@@ -0,0 +1,15 @@
1
+ module SpreeCmCommissioner
2
+ module Calculators
3
+ class ExtraDropOffDistance < ExtraPickUpDistance
4
+ preference :base_price, :decimal, default: 0
5
+ preference :amount, :decimal, default: 0
6
+
7
+ def compute(line_item)
8
+ distance_km = line_item.distance.extra_dropoff_km
9
+ return nil if distance_km.nil?
10
+
11
+ preferred_base_price + (distance_km * preferred_amount)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module SpreeCmCommissioner
2
+ module Calculators
3
+ class ExtraPickUpDistance < Spree::Calculator
4
+ preference :base_price, :decimal, default: 0
5
+ preference :amount, :decimal, default: 0
6
+
7
+ def compute(line_item)
8
+ distance_km = line_item.distance.extra_pickup_km
9
+ return nil if distance_km.nil?
10
+
11
+ preferred_base_price + (distance_km * preferred_amount)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -21,6 +21,13 @@ module SpreeCmCommissioner
21
21
  has_many :taxon_places, class_name: 'SpreeCmCommissioner::TaxonPlace', dependent: :destroy
22
22
  has_many :taxons, through: :taxon_places
23
23
 
24
+ has_many :routes_as_origin, class_name: 'SpreeCmCommissioner::Route', foreign_key: :origin_place_id
25
+ has_many :routes_as_destination, class_name: 'SpreeCmCommissioner::Route', foreign_key: :destination_place_id
26
+
27
+ # Scopes for route-based filtering
28
+ scope :with_routes_as_origin, -> { joins(:routes_as_origin).distinct }
29
+ scope :with_routes_as_destination, -> { joins(:routes_as_destination).distinct }
30
+
24
31
  def self.ransackable_attributes(auth_object = nil)
25
32
  super & %w[name code]
26
33
  end
@@ -0,0 +1,27 @@
1
+ module SpreeCmCommissioner
2
+ class PricingAction < Base
3
+ belongs_to :pricing_rule_group, class_name: 'SpreeCmCommissioner::PricingRuleGroup'
4
+
5
+ has_one :calculator, as: :calculable, class_name: 'Spree::Calculator', dependent: :destroy
6
+ accepts_nested_attributes_for :calculator
7
+
8
+ def perform(line_item)
9
+ return if pricing_rule_group.pricing_rules.blank?
10
+
11
+ amount = calculator&.compute(line_item)
12
+
13
+ line_item.adjustments.create!(
14
+ amount: amount,
15
+ source: self,
16
+ adjustable: line_item,
17
+ order: line_item.order,
18
+ label: pricing_rule_group.pricing_model.name,
19
+ mandatory: true
20
+ )
21
+ end
22
+
23
+ def self.available_calculator_types
24
+ SpreeCmCommissioner::Calculators.constants.map(&:to_s)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module SpreeCmCommissioner
2
+ class PricingModel < Base
3
+ acts_as_list column: :position, scope: :vendor_id
4
+
5
+ belongs_to :vendor, class_name: 'Spree::Vendor'
6
+
7
+ has_many :pricing_model_variants, class_name: 'SpreeCmCommissioner::PricingModelVariant', dependent: :destroy
8
+ has_many :variants, through: :pricing_model_variants, class_name: 'Spree::Variant'
9
+ has_many :pricing_rule_groups, class_name: 'SpreeCmCommissioner::PricingRuleGroup', dependent: :destroy
10
+ has_many :pricing_rules, through: :pricing_rule_groups, class_name: 'SpreeCmCommissioner::PricingRule'
11
+
12
+ accepts_nested_attributes_for :pricing_rule_groups, allow_destroy: true
13
+
14
+ validates :name, presence: true, uniqueness: { scope: :vendor_id }
15
+
16
+ enum status: { draft: 0, active: 1, archived: 2 }
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ module SpreeCmCommissioner
2
+ class PricingModelVariant < Base
3
+ belongs_to :pricing_model, class_name: 'SpreeCmCommissioner::PricingModel'
4
+ belongs_to :variant, class_name: 'Spree::Variant'
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ module SpreeCmCommissioner
2
+ class PricingRule < Base
3
+ acts_as_list column: :position, scope: :pricing_rule_group_id
4
+
5
+ belongs_to :pricing_rule_group, class_name: 'SpreeCmCommissioner::PricingRuleGroup'
6
+
7
+ def eligible?(_line_item)
8
+ raise 'eligible? should be implemented in a sub-class of SpreeCmCommissioner::PricingRule'
9
+ end
10
+
11
+ def self.available_rule_types
12
+ SpreeCmCommissioner::PricingRules.constants.map(&:to_s)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module SpreeCmCommissioner
2
+ class PricingRuleGroup < Base
3
+ MATCH_TYPES = %w[all any none].freeze
4
+
5
+ acts_as_list column: :position, scope: :pricing_model_id
6
+ store_accessor :private_metadata, :template_key
7
+ store_accessor :private_metadata, :match_type
8
+
9
+ belongs_to :pricing_model, class_name: 'SpreeCmCommissioner::PricingModel'
10
+ has_many :pricing_rules, class_name: 'SpreeCmCommissioner::PricingRule', dependent: :destroy
11
+ has_one :pricing_action, class_name: 'SpreeCmCommissioner::PricingAction', dependent: :destroy
12
+
13
+ accepts_nested_attributes_for :pricing_rules, allow_destroy: true
14
+ accepts_nested_attributes_for :pricing_action, allow_destroy: true
15
+
16
+ validates :name, presence: true
17
+
18
+ enum status: { draft: 0, active: 1, archived: 2 }
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ module SpreeCmCommissioner
2
+ module PricingRules
3
+ class ExtraDropOffDistance < ExtraPickUpDistance
4
+ def eligible?(line_item)
5
+ distance_km = line_item.distance&.extra_dropoff_km || 0
6
+ distance_km > min_distance.to_f
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ module SpreeCmCommissioner
2
+ module PricingRules
3
+ class ExtraPickUpDistance < SpreeCmCommissioner::PricingRule
4
+ store_accessor :private_metadata, :min_distance
5
+
6
+ def eligible?(line_item)
7
+ distance_km = line_item.distance&.extra_pickup_km || 0
8
+ distance_km > min_distance.to_f
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,35 @@
1
+ module SpreeCmCommissioner
2
+ module PricingRules
3
+ class Nationality < SpreeCmCommissioner::PricingRule
4
+ RULE_TYPES = %w[all any none].freeze
5
+
6
+ store_accessor :private_metadata, :rule_type, :nationalities
7
+
8
+ def eligible?(line_item) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
9
+ return false if nationalities.blank? || line_item.guests.blank?
10
+
11
+ targets = Array.wrap(nationalities).map(&:downcase)
12
+ guest_nationalities = line_item.guests.map { |g| guest_nationality(g) }.compact
13
+
14
+ return false if guest_nationalities.empty?
15
+
16
+ case rule_type
17
+ when 'all'
18
+ guest_nationalities.all? { |n| targets.include?(n) }
19
+ when 'any'
20
+ guest_nationalities.any? { |n| targets.include?(n) }
21
+ when 'none'
22
+ guest_nationalities.none? { |n| targets.include?(n) }
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def guest_nationality(guest)
31
+ guest.nationality&.name.to_s.downcase
32
+ end
33
+ end
34
+ end
35
+ end
@@ -4,6 +4,8 @@ module SpreeCmCommissioner
4
4
 
5
5
  belongs_to :product, class_name: '::Spree::Product', optional: false
6
6
 
7
+ has_one :banner, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::ProductCompletionStepBanner'
8
+
7
9
  # When a completion step is changed, regenerate completion steps for all line items
8
10
  # of the product so they reflect the latest step configuration.
9
11
  after_destroy :regenerate_line_items_completion_steps
@@ -28,7 +30,8 @@ module SpreeCmCommissioner
28
30
  action_label: action_label,
29
31
  action_url: action_url_for(line_item),
30
32
  completed_at: existing_data&.dig('completed_at'),
31
- completed: completed?(line_item)
33
+ completed: completed?(line_item),
34
+ banner_url: banner.present? ? banner.original_url : nil
32
35
  }
33
36
  end
34
37
 
@@ -0,0 +1,12 @@
1
+ module SpreeCmCommissioner
2
+ class ProductCompletionStepBanner < Asset
3
+ # 16x9 aspect ratio
4
+ def asset_styles
5
+ {
6
+ mini: '160x90>',
7
+ small: '480x270>',
8
+ medium: '960x540>'
9
+ }
10
+ end
11
+ end
12
+ end
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
5
5
  # - https://t.me/ThePlatformKHBot?start={order.number}
6
6
  # - https://example.com?guest_name={guests[0].first_name}&phone={guests[0].phone_number}
7
7
  # - https://example.com?line_item_id={line_item.id}&quantity={line_item.quantity}
8
+ # - https://example.com?bill_address.first_name={bill_address.first_name}&bill_address.lastname={bill_address.lastname}
8
9
  preference :entry_point_link, :string
9
10
 
10
11
  # Allowed fields per object (excludes private/reference fields like *_id, *_by_id, metadata, etc.)
@@ -14,7 +15,11 @@ module SpreeCmCommissioner
14
15
  state
15
16
  email
16
17
  total
18
+ display_total
17
19
  item_total
20
+ display_item_total
21
+ adjustment_total
22
+ display_adjustment_total
18
23
  created_at
19
24
  completed_at
20
25
  token
@@ -23,12 +28,31 @@ module SpreeCmCommissioner
23
28
  currency
24
29
  item_count
25
30
  channel
31
+ special_instructions
32
+ ].freeze
33
+
34
+ BILL_ADDRESS_ALLOWED_FIELDS = %w[
35
+ first_name
36
+ last_name
37
+ full_name
38
+ address1
39
+ address2
40
+ city
41
+ zipcode
42
+ phone
43
+ state_name
44
+ alternative_phone
45
+ company
46
+ label
47
+ age
48
+ gender
26
49
  ].freeze
27
50
 
28
51
  LINE_ITEM_ALLOWED_FIELDS = %w[
29
52
  qr_data
30
53
  quantity
31
54
  price
55
+ display_price
32
56
  from_date
33
57
  to_date
34
58
  number
@@ -45,7 +69,6 @@ module SpreeCmCommissioner
45
69
  dob
46
70
  gender
47
71
  age
48
- email
49
72
  phone_number
50
73
  address
51
74
  seat_number
@@ -60,9 +83,6 @@ module SpreeCmCommissioner
60
83
  expectation
61
84
  other_occupation
62
85
  other_organization
63
- entry_type
64
- upload_later
65
- data_fill_stage_phase
66
86
  created_at
67
87
  updated_at
68
88
  ].freeze
@@ -72,32 +92,79 @@ module SpreeCmCommissioner
72
92
  return nil if preferred_entry_point_link.blank?
73
93
 
74
94
  url = preferred_entry_point_link.dup
95
+ url = replace_order_placeholders(url, line_item)
96
+ url = replace_bill_address_placeholders(url, line_item)
97
+ url = replace_line_item_placeholders(url, line_item)
98
+
99
+ replace_guest_placeholders(url, line_item)
100
+ end
101
+
102
+ def completed?(line_item)
103
+ return false if line_item.completion_steps.blank?
104
+
105
+ step_data = line_item.completion_steps.find { |step| step['position'].to_s == position.to_s }
106
+ step_data&.dig('completed_at').present?
107
+ end
108
+
109
+ # Returns a hash of all supported placeholder fields grouped by object type.
110
+ # Used by the admin UI to display available placeholders when configuring the entry_point_link template.
111
+ # Example usage in template: {order.number}, {bill_address.first_name}, {line_item.quantity}, {guests[0].email}
112
+ #
113
+ # @return [Hash] Hash with keys :order, :bill_address, :line_item, :guests
114
+ # Each key maps to an array of allowed field names for that object type
115
+ def supported_fields
116
+ {
117
+ order: ORDER_ALLOWED_FIELDS,
118
+ bill_address: BILL_ADDRESS_ALLOWED_FIELDS,
119
+ line_item: LINE_ITEM_ALLOWED_FIELDS,
120
+ guests: GUEST_ALLOWED_FIELDS
121
+ }
122
+ end
123
+
124
+ private
75
125
 
76
- # Replace order placeholders: {order.field_name}
77
- line_item.order.attributes.each do |field, value|
78
- url.gsub!("{order.#{field}}", value.to_s) if ORDER_ALLOWED_FIELDS.include?(field)
126
+ def replace_order_placeholders(url, line_item)
127
+ return url if line_item.order.blank?
128
+
129
+ ORDER_ALLOWED_FIELDS.each do |field|
130
+ value = line_item.order.public_send(field)
131
+ url.gsub!("{order.#{field}}", value.to_s) if value.present?
79
132
  end
80
133
 
81
- # Replace line_item placeholders: {line_item.field_name}
82
- line_item.attributes.each do |field, value|
83
- url.gsub!("{line_item.#{field}}", value.to_s) if LINE_ITEM_ALLOWED_FIELDS.include?(field)
134
+ url
135
+ end
136
+
137
+ def replace_bill_address_placeholders(url, line_item)
138
+ return url if line_item.order&.bill_address.blank?
139
+
140
+ BILL_ADDRESS_ALLOWED_FIELDS.each do |field|
141
+ value = line_item.order.bill_address.public_send(field)
142
+ url.gsub!("{bill_address.#{field}}", value.to_s) if value.present?
84
143
  end
85
144
 
86
- # Replace guest placeholders: {guests[index].field_name}
87
- line_item.guests.each_with_index do |guest, index|
88
- guest.attributes.each do |field, value|
89
- url.gsub!("{guests[#{index}].#{field}}", value.to_s) if GUEST_ALLOWED_FIELDS.include?(field)
90
- end
145
+ url
146
+ end
147
+
148
+ def replace_line_item_placeholders(url, line_item)
149
+ LINE_ITEM_ALLOWED_FIELDS.each do |field|
150
+ value = line_item.public_send(field)
151
+ url.gsub!("{line_item.#{field}}", value.to_s) if value.present?
91
152
  end
92
153
 
93
154
  url
94
155
  end
95
156
 
96
- def completed?(line_item)
97
- return false if line_item.completion_steps.blank?
157
+ def replace_guest_placeholders(url, line_item)
158
+ return url if line_item.guests.blank?
98
159
 
99
- step_data = line_item.completion_steps.find { |step| step['position'].to_s == position.to_s }
100
- step_data&.dig('completed_at').present?
160
+ line_item.guests.each_with_index do |guest, index|
161
+ GUEST_ALLOWED_FIELDS.each do |field|
162
+ value = guest.public_send(field)
163
+ url.gsub!("{guests[#{index}].#{field}}", value.to_s) if value.present?
164
+ end
165
+ end
166
+
167
+ url
101
168
  end
102
169
  end
103
170
  end