spree_cm_commissioner 2.5.0.pre.pre13 → 2.5.0.pre.pre14

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +4 -0
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/spree/admin/base_controller_decorator.rb +7 -33
  5. data/app/controllers/spree/api/v2/storefront/guests_controller.rb +7 -51
  6. data/app/controllers/spree/api/v2/storefront/intercity_taxi/distance_calculator_controller.rb +47 -0
  7. data/app/controllers/spree/api/v2/storefront/intercity_taxi/draft_orders_controller.rb +7 -5
  8. data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +2 -2
  9. data/app/controllers/spree/api/v2/tenant/intercity_taxi/distance_calculator_controller.rb +47 -0
  10. data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +7 -5
  11. data/app/controllers/spree/api/v2/tenant/trip_search_controller.rb +2 -2
  12. data/app/interactors/spree_cm_commissioner/google_routes_distance_calculator.rb +0 -13
  13. data/app/interactors/spree_cm_commissioner/invalidate_cache_request.rb +3 -3
  14. data/app/interactors/spree_cm_commissioner/vehicle_type_updater.rb +41 -0
  15. data/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb +1 -1
  16. data/app/mailers/spree/order_mailer_decorator.rb +22 -1
  17. data/app/models/concerns/spree_cm_commissioner/line_item_seat_selection.rb +40 -0
  18. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +1 -1
  19. data/app/models/concerns/spree_cm_commissioner/vehicle_kind.rb +21 -0
  20. data/app/models/spree_cm_commissioner/adjustable/adjuster/pricing_action.rb +25 -0
  21. data/app/models/spree_cm_commissioner/guest.rb +1 -1
  22. data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +3 -0
  23. data/app/models/spree_cm_commissioner/homepage_background_app_image.rb +1 -1
  24. data/app/models/spree_cm_commissioner/invite_guest.rb +2 -5
  25. data/app/models/spree_cm_commissioner/line_item_decorator.rb +2 -2
  26. data/app/models/spree_cm_commissioner/option_type_decorator.rb +4 -1
  27. data/app/models/spree_cm_commissioner/option_value_decorator.rb +4 -1
  28. data/app/models/spree_cm_commissioner/{option_value_vehicle.rb → option_value_vehicle_type.rb} +2 -2
  29. data/app/models/spree_cm_commissioner/trip.rb +7 -6
  30. data/app/models/spree_cm_commissioner/vehicle.rb +8 -20
  31. data/app/models/spree_cm_commissioner/vehicle_type.rb +28 -0
  32. data/app/models/spree_cm_commissioner/vehicle_type_option_type.rb +6 -0
  33. data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
  34. data/app/overrides/spree/admin/products/edit/clear_cache_button.html.erb.deface +1 -1
  35. data/app/overrides/spree/admin/taxons/edit/clear_cache_button.html.erb.deface +3 -2
  36. data/app/queries/spree_cm_commissioner/trip_query.rb +5 -5
  37. data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_update_schema.rb +2 -0
  38. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +12 -0
  39. data/app/serializers/spree/v2/tenant/line_item_serializer.rb +14 -1
  40. data/app/serializers/spree_cm_commissioner/v2/storefront/distance_serializer.rb +26 -0
  41. data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_line_item_serializer.rb +3 -3
  42. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +2 -2
  43. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_serializer.rb +1 -1
  44. data/app/serializers/spree_cm_commissioner/v2/storefront/{trip_vehicle_serializer.rb → trip_vehicle_type_serializer.rb} +2 -4
  45. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +98 -0
  46. data/app/services/spree_cm_commissioner/calculate_distance.rb +279 -0
  47. data/app/services/spree_cm_commissioner/cart/recalculate_decorator.rb +28 -1
  48. data/app/services/spree_cm_commissioner/guests/claim_invite_guest_service.rb +65 -0
  49. data/app/services/spree_cm_commissioner/intercity_taxi_order/calculate_distance.rb +224 -0
  50. data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +30 -3
  51. data/app/services/spree_cm_commissioner/line_items/apply_pricing_models.rb +27 -0
  52. data/app/services/spree_cm_commissioner/pricing_models/apply.rb +4 -12
  53. data/app/services/spree_cm_commissioner/signing/sign_data.rb +42 -0
  54. data/app/services/spree_cm_commissioner/signing/verify_signature.rb +52 -0
  55. data/app/services/spree_cm_commissioner/transit/export_order.rb +146 -0
  56. data/app/services/spree_cm_commissioner/update_guest_service.rb +129 -0
  57. data/app/views/spree/admin/homepage_section/edit.html.erb +7 -2
  58. data/app/views/spree/order_mailer/cancel_email.html.erb +19 -0
  59. data/app/views/spree_cm_commissioner/cancel_order_mailer/_cancel_email.html.erb +12 -0
  60. data/app/views/spree_cm_commissioner/cancel_order_mailer/_footer.html.erb +8 -0
  61. data/app/views/spree_cm_commissioner/cancel_order_mailer/_header.html.erb +3 -0
  62. data/app/views/spree_cm_commissioner/cancel_order_mailer/_mailer_stylesheets.html.erb +139 -0
  63. data/app/views/spree_cm_commissioner/cancel_order_mailer/_your_booking.html.erb +8 -0
  64. data/app/views/spree_cm_commissioner/cancel_order_mailer/purchased_items/_items.html.erb +53 -0
  65. data/app/views/spree_cm_commissioner/cancel_order_mailer/purchased_items/_summary.html.erb +108 -0
  66. data/app/views/spree_cm_commissioner/cancel_order_mailer/tenant/_cancel_email.html.erb +12 -0
  67. data/app/views/spree_cm_commissioner/cancel_order_mailer/tenant/_footer.html.erb +8 -0
  68. data/app/views/spree_cm_commissioner/cancel_order_mailer/tenant/_header.html.erb +15 -0
  69. data/app/views/spree_cm_commissioner/layouts/cancel_order_mailer.html.erb +16 -0
  70. data/config/locales/en.yml +15 -0
  71. data/config/locales/km.yml +27 -2
  72. data/config/routes.rb +2 -0
  73. data/db/migrate/20251127074809_change_guest_dynamic_fields_value_from_jsonb_to_text.rb +40 -0
  74. data/db/migrate/20251219035243_add_migrations_to_support_vehicle_types.rb +36 -0
  75. data/lib/spree_cm_commissioner/distance.rb +88 -0
  76. data/lib/spree_cm_commissioner/engine.rb +3 -0
  77. data/lib/spree_cm_commissioner/test_helper/factories/guest_factory.rb +2 -2
  78. data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +3 -3
  79. data/lib/spree_cm_commissioner/test_helper/factories/pricing_model_variant_factory.rb +6 -0
  80. data/lib/spree_cm_commissioner/test_helper/factories/seat_layout_factory.rb +1 -1
  81. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +1 -0
  82. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_factory.rb +4 -1
  83. data/lib/spree_cm_commissioner/test_helper/factories/vehicle_type_factory.rb +7 -0
  84. data/lib/spree_cm_commissioner/trip_query_result.rb +1 -1
  85. data/lib/spree_cm_commissioner/trip_result.rb +7 -1
  86. data/lib/spree_cm_commissioner/version.rb +1 -1
  87. data/lib/spree_cm_commissioner.rb +1 -1
  88. metadata +39 -10
  89. data/app/interactors/spree_cm_commissioner/trip_distance_calculator.rb +0 -165
  90. data/app/interactors/spree_cm_commissioner/vehicle_updater.rb +0 -41
  91. data/app/models/concerns/spree_cm_commissioner/vehicle_type.rb +0 -11
  92. data/app/models/spree_cm_commissioner/vehicle_option_type.rb +0 -6
  93. data/lib/spree_cm_commissioner/intercity_taxi/distance.rb +0 -38
  94. data/lib/spree_cm_commissioner/test_helper/factories/vendor_route_factory.rb +0 -7
@@ -1,9 +1,9 @@
1
1
  module SpreeCmCommissioner
2
2
  module PricingModels
3
3
  class Apply
4
- def initialize(line_item_id:, pricing_model_id:)
5
- @line_item_id = line_item_id
6
- @pricing_model_id = pricing_model_id
4
+ def initialize(line_item:, pricing_model:)
5
+ @line_item = line_item
6
+ @pricing_model = pricing_model
7
7
  end
8
8
 
9
9
  def call
@@ -14,15 +14,7 @@ module SpreeCmCommissioner
14
14
 
15
15
  private
16
16
 
17
- attr_reader :line_item_id, :pricing_model_id
18
-
19
- def line_item
20
- @line_item ||= Spree::LineItem.find(line_item_id)
21
- end
22
-
23
- def pricing_model
24
- @pricing_model ||= SpreeCmCommissioner::PricingModel.find(pricing_model_id)
25
- end
17
+ attr_reader :line_item, :pricing_model
26
18
 
27
19
  def apply_group(group, line_item)
28
20
  eligible_rules = group.pricing_rules.select { |r| r.eligible?(line_item) }
@@ -0,0 +1,42 @@
1
+ module SpreeCmCommissioner
2
+ module Signing
3
+ # Signs data hash using HMAC-SHA256 with a signing key.
4
+ # The data must be serialized to JSON in a consistent order for signature verification.
5
+ #
6
+ # == INPUT (keyword args)
7
+ # hash: [Hash] REQUIRED - The hash to sign
8
+ # signing_key: [String] REQUIRED - The signing key to use
9
+ #
10
+ # == OUTPUT (Result object)
11
+ # result.success? => true/false
12
+ # result.failure? => true/false
13
+ # result.value => String (Base64-encoded HMAC signature on success)
14
+ # result.error => String error message (on failure)
15
+ #
16
+ # == Example Usage
17
+ # result = SpreeCmCommissioner::Signing::SignData.call(
18
+ # hash: { 'distance_km' => 314.77 },
19
+ # signing_key: ENV.fetch('DISTANCE_SIGNING_KEY')
20
+ # )
21
+ # if result.success?
22
+ # signature = result.value
23
+ # signed_hash = hash.merge('signature' => signature)
24
+ # end
25
+ class SignData
26
+ prepend ::Spree::ServiceModule::Base
27
+
28
+ def call(hash:, signing_key:)
29
+ return failure(nil, 'Hash is required') if hash.nil?
30
+ return failure(nil, 'Signing key is required') if signing_key.blank?
31
+
32
+ digest = OpenSSL::Digest.new('sha256')
33
+ hmac = OpenSSL::HMAC.digest(digest, signing_key, hash.to_json)
34
+ signature = Base64.strict_encode64(hmac)
35
+
36
+ success(signature)
37
+ rescue StandardError => e
38
+ failure(nil, e.message)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,52 @@
1
+ module SpreeCmCommissioner
2
+ module Signing
3
+ # Verifies if a signed hash's data matches the stored signature.
4
+ # Uses secure comparison to prevent timing attacks.
5
+ #
6
+ # == INPUT (keyword args)
7
+ # signed_hash: [Hash] REQUIRED - Hash containing data and signature
8
+ # signing_key: [String] REQUIRED - The signing key to use for verification
9
+ #
10
+ # == OUTPUT (Result object)
11
+ # result.success? => true/false
12
+ # result.failure? => true/false
13
+ # result.value => Boolean (true if valid, false if invalid)
14
+ # result.error => String error message (on failure)
15
+ #
16
+ # == Example Usage
17
+ # result = SpreeCmCommissioner::Signing::VerifySignature.call(
18
+ # signed_hash: { 'distance_km' => 314.77, 'signature' => 'abc123...' },
19
+ # signing_key: ENV.fetch('DISTANCE_SIGNING_KEY')
20
+ # )
21
+ # if result.success? && result.value
22
+ # # Signature is valid
23
+ # end
24
+ class VerifySignature
25
+ prepend ::Spree::ServiceModule::Base
26
+
27
+ def call(signed_hash:, signing_key:)
28
+ return failure(nil, 'Signed hash is required') if signed_hash.nil?
29
+ return failure(nil, 'Signing key is required') if signing_key.blank?
30
+
31
+ return success(false) if signed_hash['signature'].nil?
32
+
33
+ # Extract signature and data
34
+ hash = signed_hash.dup
35
+ signature = hash.delete('signature')
36
+
37
+ # Re-compute signature from data
38
+ sign_result = SpreeCmCommissioner::Signing::SignData.call(hash: hash, signing_key: signing_key)
39
+ return sign_result unless sign_result.success?
40
+
41
+ expected_signature = sign_result.value
42
+
43
+ # Use secure comparison to prevent timing attacks
44
+ is_valid = ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
45
+
46
+ success(is_valid)
47
+ rescue StandardError => e
48
+ failure(nil, e.message)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,146 @@
1
+ module SpreeCmCommissioner
2
+ module Transit
3
+ class ExportOrder
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(orders:, columns: nil, format: 'csv')
7
+ if format == 'csv'
8
+ csv_data = generate_csv(orders, columns)
9
+ success(csv: csv_data)
10
+ else
11
+ success(orders: orders, columns: columns)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def generate_csv(orders, columns)
18
+ headers = columns.nil? ? default_headers : columns.map { |col| header_name(col) }
19
+ CSV.generate(headers: true) do |csv|
20
+ csv << headers
21
+ orders.find_each do |order|
22
+ attrs = attributes_hash(order)
23
+ csv << (columns.nil? ? attrs.values : columns.map { |col| attrs[col] })
24
+ end
25
+ end
26
+ end
27
+
28
+ def default_headers
29
+ [
30
+ 'Reservation',
31
+ 'Amount',
32
+ 'Trip',
33
+ 'Depart',
34
+ 'Seats',
35
+ 'Pick Up',
36
+ 'Contact',
37
+ 'Ticket Type',
38
+ 'Issuer',
39
+ 'Status',
40
+ 'Issued At'
41
+ ]
42
+ end
43
+
44
+ def header_name(col)
45
+ {
46
+ 'reservation' => 'Reservation',
47
+ 'amount' => 'Amount',
48
+ 'trip' => 'Trip',
49
+ 'depart' => 'Depart',
50
+ 'seats' => 'Seats',
51
+ 'pick_up' => 'Pick Up',
52
+ 'contact' => 'Contact',
53
+ 'ticket_type' => 'Ticket Type',
54
+ 'issuer' => 'Issuer',
55
+ 'status' => 'Status',
56
+ 'issued_at' => 'Issued At'
57
+ }[col] || col
58
+ end
59
+
60
+ def attributes_hash(order)
61
+ {
62
+ 'reservation' => order.number,
63
+ 'amount' => display_total(order),
64
+ 'trip' => trip_name(order),
65
+ 'depart' => depart_datetime(order),
66
+ 'seats' => seat_labels(order),
67
+ 'pick_up' => pick_up_stops(order),
68
+ 'contact' => order&.phone_number,
69
+ 'ticket_type' => ticket_type(order),
70
+ 'issuer' => issuer_name(order),
71
+ 'status' => order.state,
72
+ 'issued_at' => order.created_at.strftime('%d %b %Y - %H:%M:%S')
73
+ }
74
+ end
75
+
76
+ def passengers_name(order)
77
+ names = order.guests.map do |g|
78
+ g.saved_guest&.full_name.presence || g&.full_name.presence
79
+ end.compact
80
+ names.empty? ? 'N/A' : names.join(', ')
81
+ end
82
+
83
+ def display_subtotal(order)
84
+ order.display_item_total.to_s.gsub(',', '').gsub('.00', '').gsub('៛', '')
85
+ end
86
+
87
+ def display_total(order)
88
+ order.display_total.to_s.gsub(',', '').gsub('.00', '').gsub('៛', '')
89
+ end
90
+
91
+ def display_discount(order)
92
+ total_discount = order.adjustments.select { |adj| adj.amount.negative? }.sum(&:amount)
93
+ "-#{total_discount.abs.to_s.gsub(',', '').gsub('.00', '').gsub('៛', '')}"
94
+ end
95
+
96
+ def display_adjustment(order)
97
+ total_adjustment = order.adjustments.select { |adj| adj.amount.positive? }.sum(&:amount)
98
+ total_adjustment.to_s.gsub(',', '').gsub('.00', '').gsub('៛', '')
99
+ end
100
+
101
+ def trip_name(order)
102
+ line_item = order.line_items.first
103
+ return 'N/A' unless line_item
104
+
105
+ trip_id = line_item.private_metadata&.dig('trip_id')
106
+ return 'N/A' unless trip_id
107
+
108
+ trip = SpreeCmCommissioner::Trip.find_by(id: trip_id)
109
+ trip&.product&.name || 'N/A'
110
+ end
111
+
112
+ def depart_datetime(order)
113
+ line_item = order.line_items.first
114
+ line_item&.from_date&.strftime('%d %b %Y - %H:%M:%S') || 'N/A'
115
+ end
116
+
117
+ def seat_labels(order)
118
+ seat_labels = order.guests.map(&:seat_number).compact.join(', ')
119
+ quantity = order.line_items.sum(&:quantity)
120
+ "#{seat_labels} (#{quantity})"
121
+ end
122
+
123
+ def pick_up_stops(order)
124
+ line_item = order.line_items.first
125
+ return 'N/A' unless line_item
126
+
127
+ boarding_id = line_item.boarding_trip_stop_id
128
+ drop_off_id = line_item.drop_off_trip_stop_id
129
+
130
+ boarding_stop = boarding_id ? SpreeCmCommissioner::TripStop.find_by(id: boarding_id)&.stop_name : nil
131
+ drop_off_stop = drop_off_id ? SpreeCmCommissioner::TripStop.find_by(id: drop_off_id)&.stop_name : nil
132
+
133
+ "#{boarding_stop || 'N/A'} - #{drop_off_stop || 'N/A'}"
134
+ end
135
+
136
+ def ticket_type(order)
137
+ line_item = order.line_items.first
138
+ line_item&.product_type&.humanize || 'N/A'
139
+ end
140
+
141
+ def issuer_name(order)
142
+ order.created_by&.full_name || 'N/A'
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,129 @@
1
+ module SpreeCmCommissioner
2
+ # Updates guest records with dynamic fields.
3
+ # Handles create/update/delete of dynamic fields with automatic cleanup of removed fields.
4
+ class UpdateGuestService
5
+ attr_reader :guest, :guest_params
6
+
7
+ def initialize(guest, guest_params)
8
+ @guest = guest
9
+ @guest_params = guest_params
10
+ end
11
+
12
+ # Updates guest within transaction: process dynamic fields → update attributes → move to next stage
13
+ def call
14
+ ActiveRecord::Base.transaction do
15
+ process_dynamic_fields(guest_params[:guest_dynamic_fields_attributes]) if guest_params[:guest_dynamic_fields_attributes]
16
+
17
+ guest.assign_attributes(guest_params.except(:guest_dynamic_fields_attributes))
18
+ guest.save!
19
+
20
+ guest.save_and_move_to_next_stage
21
+ end
22
+ guest
23
+ end
24
+
25
+ private
26
+
27
+ # Processes dynamic fields: destroy marked fields, upsert valid ones, cleanup unlisted fields
28
+ def process_dynamic_fields(dynamic_fields_attributes)
29
+ sent_keys = []
30
+
31
+ dynamic_fields_attributes.each do |raw_attributes|
32
+ attributes = raw_attributes.to_h.symbolize_keys
33
+ should_destroy = ActiveModel::Type::Boolean.new.cast(attributes[:_destroy])
34
+
35
+ if should_destroy
36
+ destroy_guest_dynamic_field(attributes[:id])
37
+ elsif attributes[:id].blank? || guest.guest_dynamic_fields.exists?(attributes[:id])
38
+ field = upsert_guest_dynamic_field(attributes)
39
+ sent_keys << field_key(field)
40
+ end
41
+ end
42
+
43
+ # Auto-cleanup: delete existing fields not in current submission
44
+ guest.guest_dynamic_fields.includes(:dynamic_field).find_each do |field|
45
+ field.destroy! unless sent_keys.include?(field_key(field))
46
+ end
47
+ end
48
+
49
+ def field_key(field)
50
+ if field.dynamic_field.requires_dynamic_field_options?
51
+ [field.dynamic_field_id, field.dynamic_field_option_id]
52
+ else
53
+ [field.dynamic_field_id]
54
+ end
55
+ end
56
+
57
+ # Creates/updates field with validation: prevents dynamic_field_id changes, loads associations, sets options
58
+ def upsert_guest_dynamic_field(attributes)
59
+ field = find_or_initialize_guest_dynamic_field(attributes)
60
+
61
+ if attributes[:id].present? && attributes[:dynamic_field_id].present? && field.dynamic_field_id.to_i != attributes[:dynamic_field_id].to_i
62
+ field.errors.add(:base, "Cannot change dynamic_field_id for existing GuestDynamicField (ID: #{attributes[:id]})")
63
+ raise ActiveRecord::RecordInvalid, field
64
+ end
65
+
66
+ if attributes[:dynamic_field_id].present? && field.dynamic_field.blank?
67
+ field.dynamic_field = SpreeCmCommissioner::DynamicField.find_by(id: attributes[:dynamic_field_id])
68
+ unless field.dynamic_field
69
+ field.errors.add(:dynamic_field, 'not found')
70
+ raise ActiveRecord::RecordInvalid, field
71
+ end
72
+ end
73
+
74
+ field.assign_attributes(attributes.except(:id, :_destroy, :dynamic_field_option_id))
75
+
76
+ field.dynamic_field_option_id = attributes[:dynamic_field_option_id].to_i if attributes[:dynamic_field_option_id].present?
77
+
78
+ associate_dynamic_field_option(field) if field.dynamic_field&.requires_dynamic_field_options?
79
+
80
+ field.save!
81
+ field
82
+ end
83
+
84
+ # Finds and associates option by ID or value, sets field value if empty
85
+ def associate_dynamic_field_option(field)
86
+ return unless field.dynamic_field&.requires_dynamic_field_options?
87
+
88
+ option_id = field.dynamic_field_option_id.presence || field.value
89
+
90
+ option_lookup_id = option_id.to_s.match?(/\A\d+\z/) ? option_id.to_i : option_id
91
+
92
+ option = if option_lookup_id.is_a?(Integer)
93
+ field.dynamic_field.dynamic_field_options.find_by(id: option_lookup_id)
94
+ else
95
+ field.dynamic_field.dynamic_field_options.find_by(value: option_lookup_id)
96
+ end
97
+
98
+ raise ActiveRecord::RecordNotFound, 'Option not found' unless option
99
+
100
+ field.dynamic_field_option = option
101
+ field.value = option.value if field.value.blank?
102
+ end
103
+
104
+ def find_or_initialize_guest_dynamic_field(attributes)
105
+ if attributes[:id].present?
106
+ guest.guest_dynamic_fields.find(attributes[:id])
107
+
108
+ elsif attributes[:dynamic_field_option_id].present?
109
+ guest.guest_dynamic_fields.find_by(
110
+ dynamic_field_id: attributes[:dynamic_field_id],
111
+ dynamic_field_option_id: attributes[:dynamic_field_option_id]
112
+ ) || guest.guest_dynamic_fields.build
113
+
114
+ else
115
+ guest.guest_dynamic_fields.find_by(
116
+ dynamic_field_id: attributes[:dynamic_field_id],
117
+ dynamic_field_option_id: nil
118
+ ) || guest.guest_dynamic_fields.build
119
+ end
120
+ end
121
+
122
+ def destroy_guest_dynamic_field(id)
123
+ return if id.blank?
124
+
125
+ field = guest.guest_dynamic_fields.find_by(id: id)
126
+ field&.destroy!
127
+ end
128
+ end
129
+ end
@@ -1,6 +1,11 @@
1
1
  <% content_for :page_title do %>
2
- <%= link_to plural_resource_name(SpreeCmCommissioner::HomepageSection), spree.admin_homepage_feed_homepage_section_index_url %> /
3
- <%= Spree.t(:edit_section_relatable) %>
2
+ <div class="d-flex justify-content-between">
3
+ <div>
4
+ <%= link_to plural_resource_name(SpreeCmCommissioner::HomepageSection), spree.admin_homepage_feed_homepage_section_index_url %> /
5
+ <%= Spree.t(:edit_section_relatable) %>
6
+ </div>
7
+ <%= button_link_to Spree.t(:clear_cache), admin_invalidate_api_caches_path(model: SpreeCmCommissioner::HomepageSection.name, id: @object.id), method: :post, class: "btn btn-outline-primary" %>
8
+ </div>
4
9
  <% end %>
5
10
 
6
11
  <%= form_with model: @object, url: { action: 'update' }, html: { multipart: true } do |f| %>
@@ -0,0 +1,19 @@
1
+ <tr>
2
+ <td align="center">
3
+ <% if @order.tenant.nil? %>
4
+ <div class="content-wrapper">
5
+ <%= render 'spree_cm_commissioner/cancel_order_mailer/header'%>
6
+ <%= render 'spree_cm_commissioner/cancel_order_mailer/cancel_email', order: @order %>
7
+ <%= render 'spree_cm_commissioner/cancel_order_mailer/your_booking', order: @order, show_booking_header: true %>
8
+ <%= render 'spree_cm_commissioner/cancel_order_mailer/footer', order: @order%>
9
+ </div>
10
+ <% else %>
11
+ <div class="content-wrapper">
12
+ <%= render 'spree_cm_commissioner/cancel_order_mailer/tenant/header', order: @order ,logo: @logo, name: @name %>
13
+ <%= render 'spree_cm_commissioner/cancel_order_mailer/tenant/cancel_email', order: @order %>
14
+ <%= render 'spree_cm_commissioner/cancel_order_mailer/your_booking', order: @order , show_booking_header: true%>
15
+ <%= render 'spree_cm_commissioner/cancel_order_mailer/tenant/footer', order: @order, name: @name %>
16
+ </div>
17
+ <% end %>
18
+ </td>
19
+ </tr>
@@ -0,0 +1,12 @@
1
+ <div class="cancel-greeting">
2
+ <h1>
3
+ <%= I18n.t('mail.order_mailer.hello', full_name: user_full_name(order))%>
4
+ </h1>
5
+ <p class="description">
6
+ <%= I18n.t('mail.order_mailer.cancel_description') %>
7
+ </p>
8
+ <b>
9
+ <%= I18n.t('mail.order_mailer.order_cancelled', order_number: order.number || 'NA') %>
10
+ </b>
11
+ <div style="border-bottom: 1px solid #E5E7EB; margin-top: 1.5rem;">
12
+ </div>
@@ -0,0 +1,8 @@
1
+ <div class="footer">
2
+ <p>
3
+ <%= I18n.t('mail.order_mailer.cancel_thank') %>
4
+ </p>
5
+ <p class="team">
6
+ <%= I18n.t('mail.order_mailer.from_team', store: order.store&.name) %>
7
+ </p>
8
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="header">
2
+ <%= image_tag "mailer/bookme-plus.png", class: "icon-bookme-plus"%>
3
+ </div>
@@ -0,0 +1,139 @@
1
+ <style>
2
+ body, table, td {
3
+ font-family: 'Poppins', Arial, sans-serif;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ .content-wrapper {
9
+ max-width: 500px;
10
+ margin: 0 auto;
11
+ border: 1px solid grey;
12
+ border-top: none;
13
+ }
14
+
15
+ .content-wrapper .header{
16
+ display: flex;
17
+ justify-content: flex-start;
18
+ color: #fff;
19
+ padding: 1.5rem;
20
+ background-size: cover;
21
+ background-position: center;
22
+ background-repeat: no-repeat;
23
+ background: #653ed7;
24
+ background: url(<%= image_path("mailer/event-mailer-bg.png") %>) center center / cover,
25
+ linear-gradient(45deg, #653ed7, rgba(0, 0, 255, 0) 70.71%),
26
+ linear-gradient(225deg, #653ed7, rgba(0, 0, 255, 0) 70.71%),
27
+ linear-gradient(330deg, #35ace0, rgba(0, 0, 255, 0) 70.71%),
28
+ linear-gradient(150deg, #35ace0, rgba(0, 0, 255, 0) 70.71%);
29
+ }
30
+
31
+ .content-wrapper .header .icon-bookme-plus{
32
+ }
33
+
34
+ .content-wrapper .header-tenant{
35
+ color: #fff;
36
+ padding: 1rem;
37
+ background-size: cover;
38
+ background-position: center;
39
+ background-repeat: no-repeat;
40
+ <% if @brand_color.present? %>
41
+ background: <%= @brand_color %>;
42
+ <% end %>
43
+ }
44
+
45
+ .content-wrapper .header-tenant .tenant-name{
46
+ font-weight: bold;
47
+ font-size: 2rem;
48
+ color:#fff;
49
+ }
50
+
51
+ .content-wrapper .header-tenant .logo-tenant img{
52
+ width: 4rem;
53
+ height: 4rem;
54
+ border-radius: 50%;
55
+ display: block;
56
+ }
57
+
58
+ .content-wrapper .cancel-greeting{
59
+ text-align: left;
60
+ padding: 1.5rem;
61
+ }
62
+
63
+ .content-wrapper .cancel-greeting h1{
64
+ font-size: 1.5rem;
65
+ font-weight: bold;
66
+ margin: 0;
67
+ padding: 0;
68
+ }
69
+
70
+ .content-wrapper .cancel-greeting .description{
71
+ font-size: 1rem;
72
+ color: gray;
73
+ }
74
+
75
+ .content-wrapper .cancel-greeting b{
76
+ font-size: 0.9rem;
77
+ }
78
+
79
+ .content-wrapper .purchase {
80
+ width: 100%;
81
+ border-collapse: collapse;
82
+ }
83
+
84
+ .content-wrapper .purchase_image {
85
+ width: 20%;
86
+ }
87
+ .content-wrapper .purchase_item {
88
+ padding: 0 0.625rem;
89
+ width: 60%;
90
+ }
91
+ .content-wrapper .purchase-item-option-text {
92
+ font-size: 0.8rem;
93
+ background-color: #F2F2F2;
94
+ color: #646161;
95
+ white-space: normal;
96
+ overflow: hidden;
97
+ display: inline-block;
98
+ max-width: 100%;
99
+ width: auto;
100
+ padding: 0.375rem 0.875rem;
101
+ margin: 0.625rem 0;
102
+ border-radius: 6.25rem;
103
+ word-wrap: break-word;
104
+ word-break: break-word;
105
+ }
106
+ .content-wrapper .purchase_item_price {
107
+ margin-bottom: 0.5rem; }
108
+ .content-wrapper .purchase-summary-item {
109
+ margin-top: 0.625rem;
110
+ }
111
+ .purchase_total {
112
+ font-weight: normal;
113
+ text-align: right;
114
+ padding-top: 1rem;
115
+ margin: 0;
116
+ }
117
+ .content-wrapper .purchase-summary_sub_total {
118
+ margin-bottom: 0.875rem;
119
+ }
120
+ .content-wrapper .purchase_footer {
121
+ border-top: none;
122
+ }
123
+ .content-wrapper .purchase_total--label {
124
+ text-align: left;
125
+ }
126
+ .content-wrapper .purchase_total-col {
127
+ text-align: right;
128
+ align: right;
129
+ }
130
+ .content-wrapper .footer{
131
+ text-align: center;
132
+ color: gray;
133
+ font-size: 1rem;
134
+ }
135
+ .content-wrapper .footer .team{
136
+ margin: 0;
137
+ padding: 0;
138
+ }
139
+ </style>
@@ -0,0 +1,8 @@
1
+ <table width="100%" cellpadding="0" cellspacing="0">
2
+ <tr>
3
+ <td>
4
+ <%= render 'spree_cm_commissioner/cancel_order_mailer/purchased_items/items', line_items: order.line_items %>
5
+ <%= render 'spree_cm_commissioner/cancel_order_mailer/purchased_items/summary', order: order%>
6
+ </td>
7
+ </tr>
8
+ </table>
@@ -0,0 +1,53 @@
1
+ <table class="purchase order-info">
2
+ <% line_items.each do |line_item| %>
3
+ <tr>
4
+ <td class="purchase_image align-center-vertical">
5
+ <% if line_item.variant.present? && variant_image_url(line_item.variant).present? %>
6
+ <%= link_to(
7
+ image_tag(
8
+ variant_image_url(line_item.variant),
9
+ style: "border-radius: 10px; width: 100%;"),
10
+ custom_product_storefront_resource_url(line_item.product) || '#'
11
+ ) %>
12
+ <% end %>
13
+ </td>
14
+ <td class="purchase_item">
15
+ <% if line_item.name.present? %>
16
+ <p><%= link_to(
17
+ raw(line_item.name),
18
+ custom_product_storefront_resource_url(line_item.product) || '#'
19
+ ) %></p>
20
+ <% end %>
21
+ <% if line_item.variant.options_text.present? %>
22
+ <div class="purchase-item-option-text">
23
+ <%= sanitize(line_item.variant.options_text) %>
24
+ </div>
25
+ <% end %>
26
+ <div> <%= link_to 'View Details', custom_product_line_item_url(line_item) %></div>
27
+ </td>
28
+ <td class="align-right align-center-vertical" width="10%">
29
+ <span>
30
+ <%= line_item.quantity %>x
31
+ </span>
32
+ </td>
33
+ <td class="align-right align-center-vertical" width="20%">
34
+ <div class="f-fallback purchase_item_price">
35
+ <%= line_item.single_money %><br>
36
+ </div>
37
+ <div class="f-fallback purchase_item_price">
38
+ <%= line_item.display_amount %>
39
+ </div>
40
+ </td>
41
+ </tr>
42
+ <tr>
43
+ <td colspan="4">
44
+ <hr style="
45
+ border: none;
46
+ border-top: 1px solid #E5E7EB;
47
+ margin: 1.5rem 0 0 0;
48
+ height: 0;
49
+ ">
50
+ </td>
51
+ </tr>
52
+ <%end%>
53
+ </table>