spree_core 5.5.2 → 5.5.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/imports/process_group_job.rb +20 -9
- data/app/jobs/spree/imports/process_rows_job.rb +12 -4
- data/app/models/concerns/spree/webhook_payload_redaction.rb +88 -0
- data/app/models/spree/import.rb +9 -0
- data/app/models/spree/webhook_delivery.rb +22 -0
- data/app/presenters/spree/data_feeds/google_presenter.rb +28 -1
- data/app/services/spree/imports/row_processors/base.rb +9 -0
- data/app/services/spree/imports/row_processors/product_variant.rb +37 -19
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/permitted_attributes.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1bfb936ec246953ec3805bd017b32594f42bb168c5373dd3ed86d4a219efd6d
|
|
4
|
+
data.tar.gz: 273695ba130dafae60be7bb880cb6bae32722d82bf8dc3d49ee11943049c1c65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 412b093fc9621b695691bcbd3b3b9cb4335d42f17fe62b81b820ed67acabdabbc9b16954ae4331cac03d58fabb43217b72d68dd1bc4992d6ee13f203fcad4b71
|
|
7
|
+
data.tar.gz: 9e325093542d1a77466ff4780e61aae47d309daeba2f583723f00bb83c906f26ee085512b1bb85caae53d4c731f1a2ff309541878c675a2736f397e98931001d
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Imports
|
|
3
3
|
class ProcessGroupJob < Spree::Imports::BaseJob
|
|
4
|
+
# Rows are loaded in slices so a large group never holds every ImportRow
|
|
5
|
+
# (and its raw CSV data) in memory for the whole job.
|
|
6
|
+
ROWS_BATCH_SIZE = 100
|
|
7
|
+
|
|
4
8
|
def perform(import_id, row_ids)
|
|
5
9
|
import = Spree::Import.find(import_id)
|
|
6
10
|
Spree::Current.store = import.store
|
|
@@ -8,16 +12,23 @@ module Spree
|
|
|
8
12
|
mappings = import.mappings.mapped.to_a
|
|
9
13
|
schema_fields = import.schema_fields
|
|
10
14
|
large = import.large_import?
|
|
11
|
-
# Skip rows already completed on a prior attempt so retries don't double-process them.
|
|
12
|
-
rows = import.rows.where(id: row_ids).pending_and_failed.order(:row_number)
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
row_ids.each_slice(ROWS_BATCH_SIZE) do |ids|
|
|
17
|
+
# Skip rows already completed on a prior attempt so retries don't double-process them.
|
|
18
|
+
rows = import.rows.where(id: ids).pending_and_failed.order(:row_number).to_a
|
|
19
|
+
# Share the already-loaded import across rows: each row's processor reads
|
|
20
|
+
# `row.import` (store, ability, lookup cache), and without this every row
|
|
21
|
+
# lazily loads its own Import instance and rebuilds all of that per row.
|
|
22
|
+
rows.each { |row| row.association(:import).target = import }
|
|
23
|
+
|
|
24
|
+
if large
|
|
25
|
+
Spree::Events.disable do
|
|
26
|
+
rows.each { |row| row.bulk_process!(mappings: mappings, schema_fields: schema_fields) }
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
rows.each do |row|
|
|
30
|
+
row.process!(mappings: mappings, schema_fields: schema_fields)
|
|
31
|
+
end
|
|
21
32
|
end
|
|
22
33
|
end
|
|
23
34
|
|
|
@@ -2,6 +2,7 @@ module Spree
|
|
|
2
2
|
module Imports
|
|
3
3
|
class ProcessRowsJob < Spree::Imports::BaseJob
|
|
4
4
|
BATCH_SIZE = 100
|
|
5
|
+
UNGROUPED_KEY = '__ungrouped__'.freeze
|
|
5
6
|
|
|
6
7
|
def perform(import_id)
|
|
7
8
|
import = Spree::Import.find(import_id)
|
|
@@ -27,20 +28,27 @@ module Spree
|
|
|
27
28
|
|
|
28
29
|
import.rows.pending_and_failed.order(:row_number).pluck(:id, :data).each do |id, data|
|
|
29
30
|
parsed = JSON.parse(data)
|
|
30
|
-
key = parsed[file_column].to_s.strip.downcase.presence ||
|
|
31
|
+
key = parsed[file_column].to_s.strip.downcase.presence || UNGROUPED_KEY
|
|
31
32
|
groups[key] << id
|
|
32
33
|
rescue JSON::ParserError
|
|
33
|
-
groups[
|
|
34
|
+
groups[UNGROUPED_KEY] << id
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
# Rows without a group value don't depend on each other, so they don't have
|
|
38
|
+
# to share a single job — split them into bounded batches. Real groups stay
|
|
39
|
+
# intact: their rows must run sequentially (product row before variant rows).
|
|
40
|
+
ungrouped = groups.delete(UNGROUPED_KEY)
|
|
41
|
+
batches = groups.values
|
|
42
|
+
ungrouped&.each_slice(BATCH_SIZE) { |row_ids| batches << row_ids }
|
|
43
|
+
|
|
36
44
|
# Set count before enqueuing so workers can't complete prematurely
|
|
37
45
|
import.update_columns(
|
|
38
|
-
processing_groups_count:
|
|
46
|
+
processing_groups_count: batches.size,
|
|
39
47
|
completed_groups_count: 0,
|
|
40
48
|
updated_at: Time.current
|
|
41
49
|
)
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
batches.each { |row_ids| ProcessGroupJob.perform_later(import.id, row_ids) }
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
def dispatch_batched(import)
|
|
@@ -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/import.rb
CHANGED
|
@@ -245,6 +245,15 @@ module Spree
|
|
|
245
245
|
@current_ability ||= Spree.ability_class.new(user, { store: store })
|
|
246
246
|
end
|
|
247
247
|
|
|
248
|
+
# Per-instance cache shared by row processors within a single processing job.
|
|
249
|
+
# Group jobs funnel every row through the same Import instance, so lookups of
|
|
250
|
+
# shared records (tax/shipping categories, option types, metafield definitions)
|
|
251
|
+
# resolve once per job instead of once per row.
|
|
252
|
+
# @return [Hash]
|
|
253
|
+
def row_lookup_cache
|
|
254
|
+
@row_lookup_cache ||= {}
|
|
255
|
+
end
|
|
256
|
+
|
|
248
257
|
def event_serializer_class
|
|
249
258
|
'Spree::Api::V3::ImportSerializer'.safe_constantize
|
|
250
259
|
end
|
|
@@ -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
|
#
|
|
@@ -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|
|
|
@@ -20,6 +20,15 @@ module Spree
|
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
+
# Memoizes a shared-record lookup (including nil misses) in the import's
|
|
24
|
+
# per-job cache so repeated rows don't re-run the same query.
|
|
25
|
+
def cached_lookup(*key)
|
|
26
|
+
cache = import.row_lookup_cache
|
|
27
|
+
return cache[key] if cache.key?(key)
|
|
28
|
+
|
|
29
|
+
cache[key] = yield
|
|
30
|
+
end
|
|
31
|
+
|
|
23
32
|
def build_schema_hash(row, mappings, schema_fields)
|
|
24
33
|
attributes = {}
|
|
25
34
|
schema_fields.each do |field|
|
|
@@ -123,12 +123,16 @@ module Spree
|
|
|
123
123
|
|
|
124
124
|
def prepare_shipping_category
|
|
125
125
|
shipping_category_name = attributes['shipping_category'].strip
|
|
126
|
-
|
|
126
|
+
cached_lookup(:shipping_category, shipping_category_name) do
|
|
127
|
+
Spree::ShippingCategory.find_by(name: shipping_category_name)
|
|
128
|
+
end
|
|
127
129
|
end
|
|
128
130
|
|
|
129
131
|
def prepare_tax_category
|
|
130
132
|
tax_category_name = attributes['tax_category'].strip
|
|
131
|
-
|
|
133
|
+
cached_lookup(:tax_category, tax_category_name) do
|
|
134
|
+
Spree::TaxCategory.find_by(name: tax_category_name)
|
|
135
|
+
end
|
|
132
136
|
end
|
|
133
137
|
|
|
134
138
|
def prepare_option_value_variants
|
|
@@ -171,27 +175,39 @@ module Spree
|
|
|
171
175
|
# surfaces via the DB unique index (RecordNotUnique) or the AR uniqueness
|
|
172
176
|
# validator (RecordInvalid with a :taken error on the relevant attribute).
|
|
173
177
|
def find_or_create_option_type!(presentation)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
cached_lookup(:option_type, presentation) do
|
|
179
|
+
begin
|
|
180
|
+
Spree::OptionType.search_by_name(presentation).first || Spree::OptionType.create!(presentation: presentation)
|
|
181
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
182
|
+
raise unless uniqueness_conflict?(e, :name)
|
|
177
183
|
|
|
178
|
-
|
|
184
|
+
Spree::OptionType.search_by_name(presentation).first!
|
|
185
|
+
end
|
|
186
|
+
end
|
|
179
187
|
end
|
|
180
188
|
|
|
181
189
|
def find_or_create_option_value!(option_type, presentation)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
cached_lookup(:option_value, option_type.id, presentation) do
|
|
191
|
+
begin
|
|
192
|
+
option_type.option_values.search_by_name(presentation).first || option_type.option_values.create!(presentation: presentation)
|
|
193
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
194
|
+
raise unless uniqueness_conflict?(e, :name)
|
|
185
195
|
|
|
186
|
-
|
|
196
|
+
option_type.option_values.search_by_name(presentation).first!
|
|
197
|
+
end
|
|
198
|
+
end
|
|
187
199
|
end
|
|
188
200
|
|
|
189
201
|
def find_or_create_product_option_type!(option_type)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
202
|
+
cached_lookup(:product_option_type, product.id, option_type.id) do
|
|
203
|
+
begin
|
|
204
|
+
Spree::ProductOptionType.find_or_create_by!(product: product, option_type: option_type)
|
|
205
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
|
|
206
|
+
raise unless uniqueness_conflict?(e, :product_id)
|
|
193
207
|
|
|
194
|
-
|
|
208
|
+
Spree::ProductOptionType.find_by!(product: product, option_type: option_type)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
195
211
|
end
|
|
196
212
|
|
|
197
213
|
# RecordNotUnique is always a uniqueness conflict; RecordInvalid only when the
|
|
@@ -245,11 +261,13 @@ module Spree
|
|
|
245
261
|
namespace, key = product.extract_namespace_and_key(full_key)
|
|
246
262
|
|
|
247
263
|
# Find or initialize metafield definition
|
|
248
|
-
metafield_definition =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
264
|
+
metafield_definition = cached_lookup(:metafield_definition, namespace, key, product.class.name) do
|
|
265
|
+
Spree::MetafieldDefinition.find_by(
|
|
266
|
+
namespace: namespace,
|
|
267
|
+
key: key,
|
|
268
|
+
resource_type: product.class.name
|
|
269
|
+
)
|
|
270
|
+
end
|
|
253
271
|
|
|
254
272
|
next unless metafield_definition
|
|
255
273
|
|
data/lib/spree/core/version.rb
CHANGED
|
@@ -256,7 +256,7 @@ module Spree
|
|
|
256
256
|
@@stock_item_attributes = [:variant_id, :stock_location_id, :backorderable, :count_on_hand, { metadata: {} }]
|
|
257
257
|
|
|
258
258
|
@@stock_location_attributes = [
|
|
259
|
-
:name, :active, :address1, :address2, :city, :zipcode, :company,
|
|
259
|
+
:name, :admin_name, :active, :address1, :address2, :city, :zipcode, :company,
|
|
260
260
|
:backorderable_default, :state_name, :state_id, :country_id, :phone,
|
|
261
261
|
:propagate_all_variants
|
|
262
262
|
]
|
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.5.
|
|
4
|
+
version: 5.5.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-07-
|
|
13
|
+
date: 2026-07-20 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: i18n-tasks
|
|
@@ -930,6 +930,7 @@ files:
|
|
|
930
930
|
- app/models/concerns/spree/user_reporting.rb
|
|
931
931
|
- app/models/concerns/spree/user_roles.rb
|
|
932
932
|
- app/models/concerns/spree/vat_price_calculation.rb
|
|
933
|
+
- app/models/concerns/spree/webhook_payload_redaction.rb
|
|
933
934
|
- app/models/spree/ability.rb
|
|
934
935
|
- app/models/spree/address.rb
|
|
935
936
|
- app/models/spree/adjustable/adjuster/base.rb
|
|
@@ -1818,9 +1819,9 @@ licenses:
|
|
|
1818
1819
|
- BSD-3-Clause
|
|
1819
1820
|
metadata:
|
|
1820
1821
|
bug_tracker_uri: https://github.com/spree/spree/issues
|
|
1821
|
-
changelog_uri: https://github.com/spree/spree/releases/tag/v5.5.
|
|
1822
|
+
changelog_uri: https://github.com/spree/spree/releases/tag/v5.5.4
|
|
1822
1823
|
documentation_uri: https://docs.spreecommerce.org/
|
|
1823
|
-
source_code_uri: https://github.com/spree/spree/tree/v5.5.
|
|
1824
|
+
source_code_uri: https://github.com/spree/spree/tree/v5.5.4
|
|
1824
1825
|
post_install_message:
|
|
1825
1826
|
rdoc_options: []
|
|
1826
1827
|
require_paths:
|