spree_core 5.4.2 → 5.4.4
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/app/jobs/spree/events/subscriber_job.rb +1 -1
- data/app/models/concerns/spree/publishable.rb +1 -1
- data/app/models/concerns/spree/webhook_payload_redaction.rb +88 -0
- data/app/models/spree/asset.rb +9 -9
- data/app/models/spree/event.rb +6 -6
- data/app/models/spree/export.rb +2 -2
- data/app/models/spree/exports/product_translations.rb +1 -1
- data/app/models/spree/gateway/bogus.rb +6 -1
- data/app/models/spree/line_item.rb +1 -1
- data/app/models/spree/market.rb +25 -0
- data/app/models/spree/order_inventory.rb +24 -2
- data/app/models/spree/payment_setup_sessions/bogus.rb +4 -0
- data/app/models/spree/shipment.rb +10 -4
- data/app/models/spree/subscriber.rb +12 -12
- data/app/models/spree/webhook_delivery.rb +22 -0
- data/app/presenters/spree/csv/formula_sanitizer.rb +28 -0
- data/app/presenters/spree/data_feeds/google_presenter.rb +28 -1
- data/app/services/spree/credit_cards/destroy.rb +1 -1
- data/app/services/spree/payments/handle_webhook.rb +3 -10
- data/app/subscribers/spree/event_log_subscriber.rb +1 -1
- data/config/locales/en.yml +8 -0
- data/db/migrate/20260504103113_add_type_to_spree_payment_setup_sessions.rb +6 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/events/adapters/active_support_notifications.rb +1 -1
- data/lib/spree/events/adapters/base.rb +3 -3
- data/lib/spree/events/registry.rb +1 -1
- data/lib/spree/events.rb +1 -1
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edf33d9d94d2148d28ed9867b3d07a059765d3abe7de12f673c7b1f35d7284d0
|
|
4
|
+
data.tar.gz: e5b6977796350928add77021730bfdbf78dfb32ab245637e709317c505188c83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10da4875414c61bfbccc53c63431393b8202e245b2cd4891ecfef1baea79a3dbb4f7cc865cfed421c524ef19a730b4b28159d7ed4b55bb35c15841f45d6dd67e
|
|
7
|
+
data.tar.gz: 6b6c6d20c23748a2d4203693586ebb6895b70a1424cc51ec51ae4eb25c28bc25f8c61a322d159b70b4830f76c77289d4fc077ff74689ee758d4809b4fd3a3f67
|
|
@@ -10,7 +10,7 @@ module Spree
|
|
|
10
10
|
# @example Direct usage (typically called by the adapter)
|
|
11
11
|
# Spree::Events::SubscriberJob.perform_later(
|
|
12
12
|
# 'MySubscriber',
|
|
13
|
-
# { name: 'order.
|
|
13
|
+
# { name: 'order.completed', payload: {...}, ... }
|
|
14
14
|
# )
|
|
15
15
|
#
|
|
16
16
|
class SubscriberJob < Spree::BaseJob
|
|
@@ -122,7 +122,7 @@ module Spree
|
|
|
122
122
|
|
|
123
123
|
# Publish an event with this model's data
|
|
124
124
|
#
|
|
125
|
-
# @param event_name [String] The event name (e.g., 'order.
|
|
125
|
+
# @param event_name [String] The event name (e.g., 'order.completed')
|
|
126
126
|
# @param payload [Hash, nil] Custom payload (defaults to event_payload)
|
|
127
127
|
# @param metadata [Hash] Additional metadata
|
|
128
128
|
# @return [Spree::Event] The published event
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
# Keeps single-use credentials out of the persisted webhook delivery log.
|
|
5
|
+
#
|
|
6
|
+
# Some events must carry a live credential to their subscriber — a storefront
|
|
7
|
+
# that owns its transactional emails needs the real password reset token to
|
|
8
|
+
# build the email. Delivering it over TLS to a merchant-configured endpoint is
|
|
9
|
+
# intended; keeping a readable copy in `spree_webhook_deliveries.payload` is
|
|
10
|
+
# not, because that column is queryable and is served back through the Admin
|
|
11
|
+
# API delivery log.
|
|
12
|
+
#
|
|
13
|
+
# Sensitive values are therefore replaced with {REDACTION_PLACEHOLDER} before
|
|
14
|
+
# the record is written, and re-attached in memory at send time so the
|
|
15
|
+
# outgoing request body is unchanged.
|
|
16
|
+
module WebhookPayloadRedaction
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
# Payload keys under `data` whose values are live credentials.
|
|
20
|
+
SENSITIVE_PAYLOAD_KEYS = %w[reset_token unsubscribe_token].freeze
|
|
21
|
+
|
|
22
|
+
REDACTION_PLACEHOLDER = '[REDACTED]'
|
|
23
|
+
|
|
24
|
+
# Splits a payload into the version safe to persist and the secrets held
|
|
25
|
+
# back from it.
|
|
26
|
+
#
|
|
27
|
+
# @param payload [Hash] the full event payload
|
|
28
|
+
# @return [Array(Hash, Hash)] redacted payload, and the extracted secrets
|
|
29
|
+
# keyed as they appeared under `data`
|
|
30
|
+
def self.split(payload)
|
|
31
|
+
secrets = {}
|
|
32
|
+
|
|
33
|
+
redacted = transform_data_hashes(payload) do |data|
|
|
34
|
+
data.to_h do |key, value|
|
|
35
|
+
if SENSITIVE_PAYLOAD_KEYS.include?(key.to_s) && value.present?
|
|
36
|
+
secrets[secret_key_for(key)] = value
|
|
37
|
+
[key, REDACTION_PLACEHOLDER]
|
|
38
|
+
else
|
|
39
|
+
[key, value]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
secrets.empty? ? [payload, {}] : [redacted, secrets]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Re-attaches previously extracted secrets to a redacted payload.
|
|
48
|
+
#
|
|
49
|
+
# @param payload [Hash] the redacted payload
|
|
50
|
+
# @param secrets [Hash] secrets returned by {split}
|
|
51
|
+
# @return [Hash] the payload as it should go over the wire
|
|
52
|
+
def self.merge(payload, secrets)
|
|
53
|
+
return payload if secrets.blank?
|
|
54
|
+
|
|
55
|
+
transform_data_hashes(payload) do |data|
|
|
56
|
+
data.to_h do |key, value|
|
|
57
|
+
secret = secrets[secret_key_for(key)]
|
|
58
|
+
[key, secret.presence || value]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Applies +block+ to every `data` hash on the payload.
|
|
64
|
+
#
|
|
65
|
+
# Both key forms are visited rather than only the first match: a payload
|
|
66
|
+
# carrying `:data` *and* `'data'` would otherwise leave one of them
|
|
67
|
+
# unredacted.
|
|
68
|
+
def self.transform_data_hashes(payload)
|
|
69
|
+
return payload unless payload.is_a?(Hash)
|
|
70
|
+
|
|
71
|
+
[:data, 'data'].reduce(payload) do |result, data_key|
|
|
72
|
+
data = result[data_key]
|
|
73
|
+
next result unless data.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
result.merge(data_key => yield(data))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
private_class_method :transform_data_hashes
|
|
79
|
+
|
|
80
|
+
# Secrets are keyed by name alone. They cross an ActiveJob serialization
|
|
81
|
+
# boundary, which coerces symbol keys to strings, so the key form at split
|
|
82
|
+
# time cannot be relied on to still match at merge time.
|
|
83
|
+
def self.secret_key_for(key)
|
|
84
|
+
key.to_s
|
|
85
|
+
end
|
|
86
|
+
private_class_method :secret_key_for
|
|
87
|
+
end
|
|
88
|
+
end
|
data/app/models/spree/asset.rb
CHANGED
|
@@ -153,31 +153,31 @@ module Spree
|
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
def increment_viewable_media_count
|
|
156
|
-
case
|
|
157
|
-
when
|
|
156
|
+
case viewable
|
|
157
|
+
when Spree::Variant
|
|
158
158
|
Spree::Variant.increment_counter(:media_count, viewable_id)
|
|
159
159
|
Spree::Product.increment_counter(:media_count, viewable.product_id)
|
|
160
|
-
when
|
|
160
|
+
when Spree::Product
|
|
161
161
|
Spree::Product.increment_counter(:media_count, viewable_id)
|
|
162
162
|
end
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def decrement_viewable_media_count
|
|
166
|
-
case
|
|
167
|
-
when
|
|
166
|
+
case viewable
|
|
167
|
+
when Spree::Variant
|
|
168
168
|
Spree::Variant.decrement_counter(:media_count, viewable_id)
|
|
169
169
|
Spree::Product.decrement_counter(:media_count, viewable.product_id)
|
|
170
|
-
when
|
|
170
|
+
when Spree::Product
|
|
171
171
|
Spree::Product.decrement_counter(:media_count, viewable_id)
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|
|
175
175
|
def update_viewable_thumbnail
|
|
176
|
-
case
|
|
177
|
-
when
|
|
176
|
+
case viewable
|
|
177
|
+
when Spree::Variant
|
|
178
178
|
viewable.update_thumbnail!
|
|
179
179
|
viewable.product.update_thumbnail!
|
|
180
|
-
when
|
|
180
|
+
when Spree::Product
|
|
181
181
|
viewable.update_thumbnail!
|
|
182
182
|
end
|
|
183
183
|
end
|
data/app/models/spree/event.rb
CHANGED
|
@@ -6,20 +6,20 @@ module Spree
|
|
|
6
6
|
# Events are immutable objects that carry information about something
|
|
7
7
|
# that happened in the system. They contain:
|
|
8
8
|
# - An id (UUID)
|
|
9
|
-
# - A name (e.g., 'order.
|
|
9
|
+
# - A name (e.g., 'order.completed', 'product.created')
|
|
10
10
|
# - A store_id (the store where the event originated)
|
|
11
11
|
# - A payload (serialized data about the event)
|
|
12
12
|
# - Metadata (contextual information like spree_version)
|
|
13
13
|
#
|
|
14
14
|
# @example Creating an event
|
|
15
15
|
# event = Spree::Event.new(
|
|
16
|
-
# name: 'order.
|
|
16
|
+
# name: 'order.completed',
|
|
17
17
|
# payload: order.serializable_hash
|
|
18
18
|
# )
|
|
19
19
|
#
|
|
20
20
|
# @example Accessing event data
|
|
21
21
|
# event.id # => "550e8400-e29b-41d4-a716-446655440000"
|
|
22
|
-
# event.name # => 'order.
|
|
22
|
+
# event.name # => 'order.completed'
|
|
23
23
|
# event.store_id # => 1
|
|
24
24
|
# event.payload # => { 'id' => 1, 'number' => 'R123456' }
|
|
25
25
|
# event.created_at # => 2024-01-15 10:30:00 UTC
|
|
@@ -61,19 +61,19 @@ module Spree
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Returns the resource type from the event name
|
|
64
|
-
# @return [String] The resource type (e.g., 'order' from 'order.
|
|
64
|
+
# @return [String] The resource type (e.g., 'order' from 'order.completed')
|
|
65
65
|
def resource_type
|
|
66
66
|
@resource_type ||= name.to_s.split('.').first
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# Returns the action from the event name
|
|
70
|
-
# @return [String] The action (e.g., '
|
|
70
|
+
# @return [String] The action (e.g., 'completed' from 'order.completed')
|
|
71
71
|
def action
|
|
72
72
|
@action ||= name.to_s.split('.').drop(1).join('.')
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
# Checks if the event matches a pattern
|
|
76
|
-
# Supports wildcards: 'order.*' matches 'order.
|
|
76
|
+
# Supports wildcards: 'order.*' matches 'order.completed', 'order.canceled'
|
|
77
77
|
# @param pattern [String] The pattern to match against
|
|
78
78
|
# @return [Boolean]
|
|
79
79
|
def matches?(pattern)
|
data/app/models/spree/export.rb
CHANGED
|
@@ -83,10 +83,10 @@ module Spree
|
|
|
83
83
|
batch.each do |record|
|
|
84
84
|
if multi_line_csv?
|
|
85
85
|
record.to_csv(store).each do |line|
|
|
86
|
-
csv << line
|
|
86
|
+
csv << Spree::CSV::FormulaSanitizer.row(line)
|
|
87
87
|
end
|
|
88
88
|
else
|
|
89
|
-
csv << record.to_csv(store)
|
|
89
|
+
csv << Spree::CSV::FormulaSanitizer.row(record.to_csv(store))
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
end
|
|
@@ -120,9 +120,14 @@ module Spree
|
|
|
120
120
|
true
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
def payment_setup_session_class
|
|
124
|
+
PaymentSetupSessions::Bogus
|
|
125
|
+
end
|
|
126
|
+
|
|
123
127
|
def create_payment_setup_session(customer:, external_data: {})
|
|
124
|
-
|
|
128
|
+
payment_setup_session_class.create(
|
|
125
129
|
customer: customer,
|
|
130
|
+
payment_method: self,
|
|
126
131
|
status: 'pending',
|
|
127
132
|
external_id: "bogus_seti_#{SecureRandom.hex(12)}",
|
|
128
133
|
external_client_secret: "bogus_seti_secret_#{SecureRandom.hex(8)}",
|
|
@@ -293,7 +293,7 @@ module Spree
|
|
|
293
293
|
end
|
|
294
294
|
|
|
295
295
|
def verify_order_inventory_before_destroy
|
|
296
|
-
Spree::OrderInventory.new(order, self).verify(target_shipment)
|
|
296
|
+
Spree::OrderInventory.new(order, self).verify(target_shipment, removing: true)
|
|
297
297
|
end
|
|
298
298
|
|
|
299
299
|
def update_adjustments
|
data/app/models/spree/market.rb
CHANGED
|
@@ -28,6 +28,7 @@ module Spree
|
|
|
28
28
|
# Callbacks
|
|
29
29
|
#
|
|
30
30
|
before_save :ensure_single_default
|
|
31
|
+
before_destroy :ensure_can_be_deleted
|
|
31
32
|
|
|
32
33
|
#
|
|
33
34
|
# Scopes
|
|
@@ -80,12 +81,36 @@ module Spree
|
|
|
80
81
|
@supported_locales_list ||= (supported_locales.to_s.split(',').map(&:strip) << default_locale).compact.uniq.sort
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
# Returns true when the market is safe to delete. A market cannot be deleted
|
|
85
|
+
# if it is the default market or the only market in the store, since
|
|
86
|
+
# Spree::Current.currency would have no fallback.
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def can_be_deleted?
|
|
90
|
+
!default? && !last_in_store?
|
|
91
|
+
end
|
|
92
|
+
|
|
83
93
|
private
|
|
84
94
|
|
|
95
|
+
def last_in_store?
|
|
96
|
+
!self.class.where(store_id: store_id).where.not(id: id).exists?
|
|
97
|
+
end
|
|
98
|
+
|
|
85
99
|
def ensure_single_default
|
|
86
100
|
return unless default? && default_changed?
|
|
87
101
|
|
|
88
102
|
self.class.where(store_id: store_id, default: true).where.not(id: id).update_all(default: false)
|
|
89
103
|
end
|
|
104
|
+
|
|
105
|
+
def ensure_can_be_deleted
|
|
106
|
+
return if can_be_deleted?
|
|
107
|
+
|
|
108
|
+
if default?
|
|
109
|
+
errors.add(:base, :cannot_destroy_default_market)
|
|
110
|
+
else
|
|
111
|
+
errors.add(:base, :cannot_destroy_last_market)
|
|
112
|
+
end
|
|
113
|
+
throw(:abort)
|
|
114
|
+
end
|
|
90
115
|
end
|
|
91
116
|
end
|
|
@@ -17,13 +17,23 @@ module Spree
|
|
|
17
17
|
# In case shipment is passed the stock location should only unstock or
|
|
18
18
|
# restock items if the order is completed. That is so because stock items
|
|
19
19
|
# are always unstocked when the order is completed through +shipment.finalize+
|
|
20
|
-
def verify(shipment = nil, is_updated: false)
|
|
20
|
+
def verify(shipment = nil, is_updated: false, removing: false)
|
|
21
21
|
return unless order.completed? || shipment.present?
|
|
22
22
|
|
|
23
23
|
units_count = inventory_units.reload.sum(&:quantity)
|
|
24
24
|
line_item_changed = is_updated ? !line_item.saved_changes? : !line_item.changed?
|
|
25
25
|
|
|
26
|
-
if
|
|
26
|
+
if removing
|
|
27
|
+
# When the line item is being destroyed, only remove existing inventory.
|
|
28
|
+
# Adding here would create units that the LineItem `dependent: :destroy`
|
|
29
|
+
# cascade can't see (set_up_inventory writes through shipment.inventory_units,
|
|
30
|
+
# leaving line_item.inventory_units stale), producing an orphaned unit.
|
|
31
|
+
#
|
|
32
|
+
# Bypass `remove` because it routes through `set_quantity_to_remove` which
|
|
33
|
+
# assumes a quantity-change scenario; here we want to drain everything tied
|
|
34
|
+
# to this line item regardless of `line_item.quantity`.
|
|
35
|
+
remove_all_units(units_count, shipment) if units_count.positive?
|
|
36
|
+
elsif units_count < line_item.quantity
|
|
27
37
|
quantity = line_item.quantity - units_count
|
|
28
38
|
|
|
29
39
|
shipment ||= determine_target_shipment
|
|
@@ -49,6 +59,18 @@ module Spree
|
|
|
49
59
|
end
|
|
50
60
|
end
|
|
51
61
|
|
|
62
|
+
def remove_all_units(quantity, target_shipment = nil)
|
|
63
|
+
if target_shipment.present?
|
|
64
|
+
remove_from_shipment(target_shipment, quantity)
|
|
65
|
+
else
|
|
66
|
+
order.shipments.each do |shipment|
|
|
67
|
+
break if quantity.zero?
|
|
68
|
+
|
|
69
|
+
quantity -= remove_from_shipment(shipment, quantity)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
52
74
|
def set_quantity_to_remove(units_count)
|
|
53
75
|
if (units_count - line_item.quantity).zero?
|
|
54
76
|
line_item.quantity
|
|
@@ -272,16 +272,22 @@ module Spree
|
|
|
272
272
|
def manifest
|
|
273
273
|
# Grouping by the ID means that we don't have to call out to the association accessor
|
|
274
274
|
# This makes the grouping by faster because it results in less SQL cache hits.
|
|
275
|
-
inventory_units.group_by(&:variant_id).
|
|
276
|
-
units.group_by(&:line_item_id).
|
|
275
|
+
inventory_units.group_by(&:variant_id).flat_map do |_variant_id, units|
|
|
276
|
+
units.group_by(&:line_item_id).filter_map do |_line_item_id, units|
|
|
277
|
+
line_item = units.first.line_item
|
|
278
|
+
# Defensively skip orphaned inventory units (line item destroyed
|
|
279
|
+
# without cascading) so a single bad row doesn't crash callers that
|
|
280
|
+
# rely on line_item being present (item_cost, item_weight, the admin
|
|
281
|
+
# shipment manifest view, etc.).
|
|
282
|
+
next if line_item.nil?
|
|
283
|
+
|
|
277
284
|
states = {}
|
|
278
285
|
units.group_by(&:state).each { |state, iu| states[state] = iu.sum(&:quantity) }
|
|
279
286
|
|
|
280
|
-
line_item = units.first.line_item
|
|
281
287
|
variant = units.first.variant
|
|
282
288
|
ManifestItem.new(line_item, variant, units.sum(&:quantity), states)
|
|
283
289
|
end
|
|
284
|
-
end
|
|
290
|
+
end
|
|
285
291
|
end
|
|
286
292
|
|
|
287
293
|
def process_order_payments
|
|
@@ -9,7 +9,7 @@ module Spree
|
|
|
9
9
|
#
|
|
10
10
|
# @example Basic subscriber
|
|
11
11
|
# class OrderCompletedNotifier < Spree::Subscriber
|
|
12
|
-
# subscribes_to 'order.
|
|
12
|
+
# subscribes_to 'order.completed'
|
|
13
13
|
#
|
|
14
14
|
# def call(event)
|
|
15
15
|
# order_id = event.payload['id']
|
|
@@ -19,7 +19,7 @@ module Spree
|
|
|
19
19
|
#
|
|
20
20
|
# @example Multi-event subscriber
|
|
21
21
|
# class OrderAuditLogger < Spree::Subscriber
|
|
22
|
-
# subscribes_to 'order.
|
|
22
|
+
# subscribes_to 'order.completed', 'order.canceled', 'order.resumed'
|
|
23
23
|
#
|
|
24
24
|
# def call(event)
|
|
25
25
|
# AuditLog.create!(
|
|
@@ -41,11 +41,11 @@ module Spree
|
|
|
41
41
|
#
|
|
42
42
|
# @example Subscriber with method routing
|
|
43
43
|
# class PaymentSubscriber < Spree::Subscriber
|
|
44
|
-
# subscribes_to 'payment.
|
|
44
|
+
# subscribes_to 'payment.completed', 'payment.voided', 'refund.created'
|
|
45
45
|
#
|
|
46
|
-
# on 'payment.
|
|
47
|
-
# on 'payment.
|
|
48
|
-
# on '
|
|
46
|
+
# on 'payment.completed', :handle_complete
|
|
47
|
+
# on 'payment.voided', :handle_void
|
|
48
|
+
# on 'refund.created', :handle_refund
|
|
49
49
|
#
|
|
50
50
|
# private
|
|
51
51
|
#
|
|
@@ -64,7 +64,7 @@ module Spree
|
|
|
64
64
|
#
|
|
65
65
|
# @example Synchronous subscriber (runs immediately, not via ActiveJob)
|
|
66
66
|
# class CriticalOrderHandler < Spree::Subscriber
|
|
67
|
-
# subscribes_to 'order.
|
|
67
|
+
# subscribes_to 'order.completed', async: false
|
|
68
68
|
#
|
|
69
69
|
# def call(event)
|
|
70
70
|
# # This runs synchronously
|
|
@@ -81,10 +81,10 @@ module Spree
|
|
|
81
81
|
# @return [void]
|
|
82
82
|
#
|
|
83
83
|
# @example
|
|
84
|
-
# subscribes_to 'order.
|
|
85
|
-
# subscribes_to 'order.
|
|
84
|
+
# subscribes_to 'order.completed'
|
|
85
|
+
# subscribes_to 'order.completed', 'order.canceled'
|
|
86
86
|
# subscribes_to 'order.*'
|
|
87
|
-
# subscribes_to 'order.
|
|
87
|
+
# subscribes_to 'order.completed', async: false
|
|
88
88
|
#
|
|
89
89
|
def subscribes_to(*patterns, **options)
|
|
90
90
|
@subscription_patterns ||= []
|
|
@@ -102,8 +102,8 @@ module Spree
|
|
|
102
102
|
# @return [void]
|
|
103
103
|
#
|
|
104
104
|
# @example
|
|
105
|
-
# on 'payment.
|
|
106
|
-
# on 'payment.
|
|
105
|
+
# on 'payment.completed', :handle_complete
|
|
106
|
+
# on 'payment.voided', :handle_void
|
|
107
107
|
#
|
|
108
108
|
def on(pattern, method_name)
|
|
109
109
|
@event_handlers ||= {}
|
|
@@ -7,6 +7,11 @@ module Spree
|
|
|
7
7
|
belongs_to :webhook_endpoint, class_name: 'Spree::WebhookEndpoint'
|
|
8
8
|
delegate :url, to: :webhook_endpoint
|
|
9
9
|
|
|
10
|
+
# Credentials stripped from the persisted payload. Never written to the
|
|
11
|
+
# database — held in memory on a single delivery instance, assigned by
|
|
12
|
+
# WebhookDeliveryJob when it runs. See {Spree::WebhookPayloadRedaction}.
|
|
13
|
+
attr_accessor :payload_secrets
|
|
14
|
+
|
|
10
15
|
validates :event_name, presence: true
|
|
11
16
|
validates :payload, presence: true
|
|
12
17
|
|
|
@@ -64,6 +69,23 @@ module Spree
|
|
|
64
69
|
webhook_endpoint.check_auto_disable! unless is_success
|
|
65
70
|
end
|
|
66
71
|
|
|
72
|
+
# Payload as it should be sent over the wire.
|
|
73
|
+
#
|
|
74
|
+
# Re-attaches any credentials withheld from the persisted column. Once this
|
|
75
|
+
# record has been reloaded from the database those secrets are gone for
|
|
76
|
+
# good, so a redelivery sends the redacted placeholder — single-use tokens
|
|
77
|
+
# are not replayable anyway.
|
|
78
|
+
#
|
|
79
|
+
# This is deliberate: keeping a recoverable copy would put the credential
|
|
80
|
+
# back at rest, which is what redaction exists to prevent. If a delivery is
|
|
81
|
+
# lost (worker crash between creation and delivery), the customer requests
|
|
82
|
+
# a new reset, which mints a fresh token.
|
|
83
|
+
#
|
|
84
|
+
# @return [Hash]
|
|
85
|
+
def deliverable_payload
|
|
86
|
+
Spree::WebhookPayloadRedaction.merge(payload, payload_secrets)
|
|
87
|
+
end
|
|
88
|
+
|
|
67
89
|
# Create a new delivery with the same payload and queue it.
|
|
68
90
|
# Used to retry failed deliveries manually.
|
|
69
91
|
#
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module CSV
|
|
3
|
+
# Neutralizes CSV formula injection (CWE-1236 / OWASP "CSV Injection")
|
|
4
|
+
# by prefixing cells that would otherwise be evaluated as a formula
|
|
5
|
+
# when the exported file is opened in Excel, Google Sheets, LibreOffice,
|
|
6
|
+
# or Numbers.
|
|
7
|
+
#
|
|
8
|
+
# The leading apostrophe is the OWASP-recommended marker — spreadsheets
|
|
9
|
+
# render the cell as plain text without displaying the apostrophe.
|
|
10
|
+
module FormulaSanitizer
|
|
11
|
+
TRIGGERS = ["=", "+", "-", "@", "\t", "\r", "\n"].freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def cell(value)
|
|
16
|
+
return value unless value.is_a?(String)
|
|
17
|
+
return value if value.empty?
|
|
18
|
+
return value unless TRIGGERS.include?(value[0])
|
|
19
|
+
|
|
20
|
+
"'#{value}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def row(values)
|
|
24
|
+
values.map { |v| cell(v) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -3,6 +3,22 @@ require 'nokogiri'
|
|
|
3
3
|
module Spree
|
|
4
4
|
module DataFeeds
|
|
5
5
|
class GooglePresenter < BasePresenter
|
|
6
|
+
# Optional Google Merchant Center product attributes sourced from
|
|
7
|
+
# metafields. See https://support.google.com/merchants/answer/7052112
|
|
8
|
+
OPTIONAL_ATTRIBUTES = %w[
|
|
9
|
+
brand gtin mpn identifier_exists condition adult multipack is_bundle
|
|
10
|
+
age_group color gender material pattern size size_type size_system
|
|
11
|
+
product_length product_width product_height product_weight
|
|
12
|
+
google_product_category product_type sale_price sale_price_effective_date
|
|
13
|
+
cost_of_goods_sold unit_pricing_measure unit_pricing_base_measure
|
|
14
|
+
shipping shipping_label shipping_weight shipping_length shipping_width
|
|
15
|
+
shipping_height ships_from_country transit_time_label max_handling_time
|
|
16
|
+
min_handling_time tax tax_category energy_efficiency_class
|
|
17
|
+
min_energy_efficiency_class max_energy_efficiency_class
|
|
18
|
+
gtin_source expiration_date custom_label_0 custom_label_1 custom_label_2
|
|
19
|
+
custom_label_3 custom_label_4 mobile_link additional_image_link
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
6
22
|
# @return [String] RSS XML feed for Google Merchant Center
|
|
7
23
|
def call
|
|
8
24
|
builder = Nokogiri::XML::Builder.new do |xml|
|
|
@@ -59,10 +75,21 @@ module Spree
|
|
|
59
75
|
|
|
60
76
|
def build_optional_attributes(xml, product)
|
|
61
77
|
product.public_metafields.each do |metafield|
|
|
62
|
-
|
|
78
|
+
key = metafield.metafield_definition.key.parameterize.underscore
|
|
79
|
+
next unless OPTIONAL_ATTRIBUTES.include?(key)
|
|
80
|
+
|
|
81
|
+
append_g_element(xml, key, metafield.value)
|
|
63
82
|
end
|
|
64
83
|
end
|
|
65
84
|
|
|
85
|
+
def append_g_element(xml, name, value)
|
|
86
|
+
parent = xml.parent
|
|
87
|
+
node = Nokogiri::XML::Node.new(name, parent.document)
|
|
88
|
+
node.namespace = parent.namespace_scopes.find { |ns| ns.prefix == 'g' }
|
|
89
|
+
node.content = value
|
|
90
|
+
parent << node
|
|
91
|
+
end
|
|
92
|
+
|
|
66
93
|
def format_title(product, variant)
|
|
67
94
|
parts = [product.name]
|
|
68
95
|
variant.option_values.each do |option_value|
|
|
@@ -11,7 +11,7 @@ module Spree
|
|
|
11
11
|
"#{self.class.name} is deprecated and will be removed in Spree 6.0. " \
|
|
12
12
|
'Use card.destroy directly instead. Payment cleanup is now handled ' \
|
|
13
13
|
'automatically by the before_destroy callback in PaymentSourceConcern.',
|
|
14
|
-
|
|
14
|
+
caller_locations(2)
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
if card.destroy
|
|
@@ -28,6 +28,8 @@ module Spree
|
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
+
# `Spree::Payment#confirm!` honors the payment method's `auto_capture?` setting:
|
|
32
|
+
# auto_capture → complete! + capture_event; otherwise → pend! (auth-only, payment_state=balance_due).
|
|
31
33
|
def handle_success(payment_session, order, metadata)
|
|
32
34
|
order.with_lock do
|
|
33
35
|
# Idempotency: if the session was already completed (by the API
|
|
@@ -36,19 +38,10 @@ module Spree
|
|
|
36
38
|
return success(payment_session)
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
# Ensure payment record exists
|
|
40
41
|
payment = payment_session.find_or_create_payment!(metadata)
|
|
41
|
-
|
|
42
|
-
# Mark payment as completed — the webhook confirms the gateway processed it
|
|
43
|
-
if payment.present? && !payment.completed?
|
|
44
|
-
payment.started_processing! if payment.checkout?
|
|
45
|
-
payment.complete! if payment.can_complete?
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Mark session as completed
|
|
42
|
+
payment.confirm! if payment.present? && !payment.completed?
|
|
49
43
|
payment_session.complete if payment_session.can_complete?
|
|
50
44
|
|
|
51
|
-
# Complete order if not already done
|
|
52
45
|
unless order.reload.completed?
|
|
53
46
|
Spree::Dependencies.carts_complete_service.constantize.call(cart: order)
|
|
54
47
|
end
|
|
@@ -9,7 +9,7 @@ module Spree
|
|
|
9
9
|
# Rails.application.config.filter_parameters.
|
|
10
10
|
#
|
|
11
11
|
# @example Output
|
|
12
|
-
# [Spree Event] order.
|
|
12
|
+
# [Spree Event] order.completed | payload: {"id"=>1} | 0.5ms
|
|
13
13
|
#
|
|
14
14
|
class EventLogSubscriber
|
|
15
15
|
NAMESPACE = 'spree'
|
data/config/locales/en.yml
CHANGED
|
@@ -299,6 +299,11 @@ en:
|
|
|
299
299
|
attributes:
|
|
300
300
|
currency:
|
|
301
301
|
must_match_order_currency: Must match order currency
|
|
302
|
+
spree/market:
|
|
303
|
+
attributes:
|
|
304
|
+
base:
|
|
305
|
+
cannot_destroy_default_market: Cannot delete the default market.
|
|
306
|
+
cannot_destroy_last_market: Cannot delete the only market.
|
|
302
307
|
spree/market_country:
|
|
303
308
|
attributes:
|
|
304
309
|
country:
|
|
@@ -1311,6 +1316,9 @@ en:
|
|
|
1311
1316
|
inventory_error_flash_for_insufficient_quantity: An item in your cart has become unavailable.
|
|
1312
1317
|
inventory_state: Inventory State
|
|
1313
1318
|
invitation_accepted: Invitation accepted!
|
|
1319
|
+
invitation_expired:
|
|
1320
|
+
body: This invitation link is no longer valid. Please ask the person who invited you to send a new one.
|
|
1321
|
+
heading: Invitation invalid or expired
|
|
1314
1322
|
invitation_mailer:
|
|
1315
1323
|
greeting: Hi %{user_name},
|
|
1316
1324
|
invitation_accepted:
|
data/lib/spree/core/version.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Spree
|
|
|
12
12
|
#
|
|
13
13
|
# @example Publishing an event
|
|
14
14
|
# adapter = ActiveSupportNotifications.new(registry)
|
|
15
|
-
# adapter.publish('order.
|
|
15
|
+
# adapter.publish('order.completed', { id: 1 })
|
|
16
16
|
#
|
|
17
17
|
class ActiveSupportNotifications < Base
|
|
18
18
|
NAMESPACE = 'spree'
|
|
@@ -43,13 +43,13 @@ module Spree
|
|
|
43
43
|
|
|
44
44
|
# Publish an event to all matching subscribers.
|
|
45
45
|
#
|
|
46
|
-
# @param event_name [String] the event name (e.g., 'order.
|
|
46
|
+
# @param event_name [String] the event name (e.g., 'order.completed')
|
|
47
47
|
# @param payload [Hash] the event payload (should be serializable)
|
|
48
48
|
# @param metadata [Hash] additional metadata for the event
|
|
49
49
|
# @return [Spree::Event] the published event
|
|
50
50
|
#
|
|
51
51
|
# @example
|
|
52
|
-
# adapter.publish('order.
|
|
52
|
+
# adapter.publish('order.completed', order.serializable_hash, { user_id: 1 })
|
|
53
53
|
#
|
|
54
54
|
def publish(event_name, payload, metadata = {})
|
|
55
55
|
raise NotImplementedError, "#{self.class}#publish must be implemented"
|
|
@@ -67,7 +67,7 @@ module Spree
|
|
|
67
67
|
# @option options [Boolean] :async (true) whether to run async via ActiveJob
|
|
68
68
|
#
|
|
69
69
|
# @example
|
|
70
|
-
# adapter.subscribe('order.
|
|
70
|
+
# adapter.subscribe('order.completed', MySubscriber)
|
|
71
71
|
# adapter.subscribe('order.*', AuditLogger, async: false)
|
|
72
72
|
#
|
|
73
73
|
def subscribe(pattern, subscriber, options = {})
|
|
@@ -15,7 +15,7 @@ module Spree
|
|
|
15
15
|
# @example
|
|
16
16
|
# registry = Spree::Events::Registry.new
|
|
17
17
|
# registry.register('order.*', MySubscriber, async: true)
|
|
18
|
-
# registry.subscriptions_for('order.
|
|
18
|
+
# registry.subscriptions_for('order.completed') # => [Subscription]
|
|
19
19
|
#
|
|
20
20
|
class Registry
|
|
21
21
|
# Immutable subscription data using Ruby 3.2+ Data class
|
data/lib/spree/events.rb
CHANGED
|
@@ -36,7 +36,7 @@ module Spree
|
|
|
36
36
|
|
|
37
37
|
# Publish an event to all matching subscribers
|
|
38
38
|
#
|
|
39
|
-
# @param name [String] The event name (e.g., 'order.
|
|
39
|
+
# @param name [String] The event name (e.g., 'order.completed')
|
|
40
40
|
# @param payload [Hash] The event payload (should be serializable)
|
|
41
41
|
# @param metadata [Hash] Additional metadata for the event
|
|
42
42
|
# @return [Spree::Event] The published event
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.4.
|
|
4
|
+
version: 5.4.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sean Schofield
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date: 2026-
|
|
13
|
+
date: 2026-07-20 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: i18n-tasks
|
|
@@ -922,6 +922,7 @@ files:
|
|
|
922
922
|
- app/models/concerns/spree/user_reporting.rb
|
|
923
923
|
- app/models/concerns/spree/user_roles.rb
|
|
924
924
|
- app/models/concerns/spree/vat_price_calculation.rb
|
|
925
|
+
- app/models/concerns/spree/webhook_payload_redaction.rb
|
|
925
926
|
- app/models/spree/ability.rb
|
|
926
927
|
- app/models/spree/address.rb
|
|
927
928
|
- app/models/spree/adjustable/adjuster/base.rb
|
|
@@ -1052,6 +1053,7 @@ files:
|
|
|
1052
1053
|
- app/models/spree/payment_session.rb
|
|
1053
1054
|
- app/models/spree/payment_sessions/bogus.rb
|
|
1054
1055
|
- app/models/spree/payment_setup_session.rb
|
|
1056
|
+
- app/models/spree/payment_setup_sessions/bogus.rb
|
|
1055
1057
|
- app/models/spree/payment_source.rb
|
|
1056
1058
|
- app/models/spree/permission_sets/base.rb
|
|
1057
1059
|
- app/models/spree/permission_sets/configuration_management.rb
|
|
@@ -1218,6 +1220,7 @@ files:
|
|
|
1218
1220
|
- app/paginators/spree/shared/paginate.rb
|
|
1219
1221
|
- app/presenters/spree/csv/coupon_code_presenter.rb
|
|
1220
1222
|
- app/presenters/spree/csv/customer_presenter.rb
|
|
1223
|
+
- app/presenters/spree/csv/formula_sanitizer.rb
|
|
1221
1224
|
- app/presenters/spree/csv/gift_card_presenter.rb
|
|
1222
1225
|
- app/presenters/spree/csv/metafields_helper.rb
|
|
1223
1226
|
- app/presenters/spree/csv/newsletter_subscriber_presenter.rb
|
|
@@ -1514,6 +1517,7 @@ files:
|
|
|
1514
1517
|
- db/migrate/20260408000001_add_unique_index_to_spree_zone_members.rb
|
|
1515
1518
|
- db/migrate/20260424000001_add_unique_index_to_spree_payments_response_code.rb
|
|
1516
1519
|
- db/migrate/20260424100000_add_processing_groups_to_spree_imports.rb
|
|
1520
|
+
- db/migrate/20260504103113_add_type_to_spree_payment_setup_sessions.rb
|
|
1517
1521
|
- db/sample_data/customers.csv
|
|
1518
1522
|
- db/sample_data/markets.rb
|
|
1519
1523
|
- db/sample_data/metafield_definitions.rb
|
|
@@ -1744,9 +1748,9 @@ licenses:
|
|
|
1744
1748
|
- BSD-3-Clause
|
|
1745
1749
|
metadata:
|
|
1746
1750
|
bug_tracker_uri: https://github.com/spree/spree/issues
|
|
1747
|
-
changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.
|
|
1751
|
+
changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.4
|
|
1748
1752
|
documentation_uri: https://docs.spreecommerce.org/
|
|
1749
|
-
source_code_uri: https://github.com/spree/spree/tree/v5.4.
|
|
1753
|
+
source_code_uri: https://github.com/spree/spree/tree/v5.4.4
|
|
1750
1754
|
post_install_message:
|
|
1751
1755
|
rdoc_options: []
|
|
1752
1756
|
require_paths:
|