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.
- checksums.yaml +4 -4
- data/.env.example +4 -0
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/base_controller_decorator.rb +7 -33
- data/app/controllers/spree/api/v2/storefront/guests_controller.rb +7 -51
- data/app/controllers/spree/api/v2/storefront/intercity_taxi/distance_calculator_controller.rb +47 -0
- data/app/controllers/spree/api/v2/storefront/intercity_taxi/draft_orders_controller.rb +7 -5
- data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +2 -2
- data/app/controllers/spree/api/v2/tenant/intercity_taxi/distance_calculator_controller.rb +47 -0
- data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +7 -5
- data/app/controllers/spree/api/v2/tenant/trip_search_controller.rb +2 -2
- data/app/interactors/spree_cm_commissioner/google_routes_distance_calculator.rb +0 -13
- data/app/interactors/spree_cm_commissioner/invalidate_cache_request.rb +3 -3
- data/app/interactors/spree_cm_commissioner/vehicle_type_updater.rb +41 -0
- data/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb +1 -1
- data/app/mailers/spree/order_mailer_decorator.rb +22 -1
- data/app/models/concerns/spree_cm_commissioner/line_item_seat_selection.rb +40 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +1 -1
- data/app/models/concerns/spree_cm_commissioner/vehicle_kind.rb +21 -0
- data/app/models/spree_cm_commissioner/adjustable/adjuster/pricing_action.rb +25 -0
- data/app/models/spree_cm_commissioner/guest.rb +1 -1
- data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +3 -0
- data/app/models/spree_cm_commissioner/homepage_background_app_image.rb +1 -1
- data/app/models/spree_cm_commissioner/invite_guest.rb +2 -5
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +2 -2
- data/app/models/spree_cm_commissioner/option_type_decorator.rb +4 -1
- data/app/models/spree_cm_commissioner/option_value_decorator.rb +4 -1
- data/app/models/spree_cm_commissioner/{option_value_vehicle.rb → option_value_vehicle_type.rb} +2 -2
- data/app/models/spree_cm_commissioner/trip.rb +7 -6
- data/app/models/spree_cm_commissioner/vehicle.rb +8 -20
- data/app/models/spree_cm_commissioner/vehicle_type.rb +28 -0
- data/app/models/spree_cm_commissioner/vehicle_type_option_type.rb +6 -0
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
- data/app/overrides/spree/admin/products/edit/clear_cache_button.html.erb.deface +1 -1
- data/app/overrides/spree/admin/taxons/edit/clear_cache_button.html.erb.deface +3 -2
- data/app/queries/spree_cm_commissioner/trip_query.rb +5 -5
- data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_update_schema.rb +2 -0
- data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +12 -0
- data/app/serializers/spree/v2/tenant/line_item_serializer.rb +14 -1
- data/app/serializers/spree_cm_commissioner/v2/storefront/distance_serializer.rb +26 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_line_item_serializer.rb +3 -3
- data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +2 -2
- data/app/serializers/spree_cm_commissioner/v2/storefront/trip_serializer.rb +1 -1
- data/app/serializers/spree_cm_commissioner/v2/storefront/{trip_vehicle_serializer.rb → trip_vehicle_type_serializer.rb} +2 -4
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +98 -0
- data/app/services/spree_cm_commissioner/calculate_distance.rb +279 -0
- data/app/services/spree_cm_commissioner/cart/recalculate_decorator.rb +28 -1
- data/app/services/spree_cm_commissioner/guests/claim_invite_guest_service.rb +65 -0
- data/app/services/spree_cm_commissioner/intercity_taxi_order/calculate_distance.rb +224 -0
- data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +30 -3
- data/app/services/spree_cm_commissioner/line_items/apply_pricing_models.rb +27 -0
- data/app/services/spree_cm_commissioner/pricing_models/apply.rb +4 -12
- data/app/services/spree_cm_commissioner/signing/sign_data.rb +42 -0
- data/app/services/spree_cm_commissioner/signing/verify_signature.rb +52 -0
- data/app/services/spree_cm_commissioner/transit/export_order.rb +146 -0
- data/app/services/spree_cm_commissioner/update_guest_service.rb +129 -0
- data/app/views/spree/admin/homepage_section/edit.html.erb +7 -2
- data/app/views/spree/order_mailer/cancel_email.html.erb +19 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/_cancel_email.html.erb +12 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/_footer.html.erb +8 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/_header.html.erb +3 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/_mailer_stylesheets.html.erb +139 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/_your_booking.html.erb +8 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/purchased_items/_items.html.erb +53 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/purchased_items/_summary.html.erb +108 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/tenant/_cancel_email.html.erb +12 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/tenant/_footer.html.erb +8 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/tenant/_header.html.erb +15 -0
- data/app/views/spree_cm_commissioner/layouts/cancel_order_mailer.html.erb +16 -0
- data/config/locales/en.yml +15 -0
- data/config/locales/km.yml +27 -2
- data/config/routes.rb +2 -0
- data/db/migrate/20251127074809_change_guest_dynamic_fields_value_from_jsonb_to_text.rb +40 -0
- data/db/migrate/20251219035243_add_migrations_to_support_vehicle_types.rb +36 -0
- data/lib/spree_cm_commissioner/distance.rb +88 -0
- data/lib/spree_cm_commissioner/engine.rb +3 -0
- data/lib/spree_cm_commissioner/test_helper/factories/guest_factory.rb +2 -2
- data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +3 -3
- data/lib/spree_cm_commissioner/test_helper/factories/pricing_model_variant_factory.rb +6 -0
- data/lib/spree_cm_commissioner/test_helper/factories/seat_layout_factory.rb +1 -1
- data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +1 -0
- data/lib/spree_cm_commissioner/test_helper/factories/vehicle_factory.rb +4 -1
- data/lib/spree_cm_commissioner/test_helper/factories/vehicle_type_factory.rb +7 -0
- data/lib/spree_cm_commissioner/trip_query_result.rb +1 -1
- data/lib/spree_cm_commissioner/trip_result.rb +7 -1
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +1 -1
- metadata +39 -10
- data/app/interactors/spree_cm_commissioner/trip_distance_calculator.rb +0 -165
- data/app/interactors/spree_cm_commissioner/vehicle_updater.rb +0 -41
- data/app/models/concerns/spree_cm_commissioner/vehicle_type.rb +0 -11
- data/app/models/spree_cm_commissioner/vehicle_option_type.rb +0 -6
- data/lib/spree_cm_commissioner/intercity_taxi/distance.rb +0 -38
- 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(
|
|
5
|
-
@
|
|
6
|
-
@
|
|
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 :
|
|
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
|
-
|
|
3
|
-
|
|
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,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>
|