spree_core 6.0.0.rc1 → 6.0.0.rc2
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/README.md +3 -3
- data/app/jobs/spree/search_provider/index_job.rb +4 -0
- data/app/jobs/spree/uploads/purge_unattached_job.rb +49 -0
- data/app/mailers/spree/base_mailer.rb +4 -3
- data/app/models/concerns/spree/admin_user_methods.rb +2 -0
- data/app/models/concerns/spree/calculated_adjustments.rb +41 -13
- data/app/models/concerns/spree/calculator/currency_amounts.rb +1 -1
- data/app/models/concerns/spree/calculator/tiers.rb +1 -14
- data/app/models/concerns/spree/canonical_decimals.rb +61 -0
- data/app/models/concerns/spree/customer_methods.rb +8 -4
- data/app/models/concerns/spree/derived_number.rb +11 -13
- data/app/models/concerns/spree/preference_schema/json_schema.rb +139 -0
- data/app/models/concerns/spree/preference_schema.rb +59 -21
- data/app/models/concerns/spree/product_scopes.rb +5 -1
- data/app/models/concerns/spree/purchase/payment_processing.rb +1 -1
- data/app/models/concerns/spree/purchase/purchase_order.rb +7 -2
- data/app/models/concerns/spree/purchase/store_credits.rb +36 -1
- data/app/models/concerns/spree/purchase/totals.rb +1 -8
- data/app/models/concerns/spree/ransackable_attributes.rb +46 -0
- data/app/models/concerns/spree/signed_id_attachments.rb +54 -0
- data/app/models/concerns/spree/stores/email_branding.rb +2 -1
- data/app/models/concerns/spree/stores/markets.rb +5 -2
- data/app/models/concerns/spree/typed_adjustment_line.rb +4 -5
- data/app/models/concerns/spree/typed_associations.rb +6 -18
- data/app/models/concerns/spree/vat_price_calculation.rb +5 -6
- data/app/models/spree/address.rb +1 -1
- data/app/models/spree/adjusters/largest_remainder.rb +14 -1
- data/app/models/spree/adjusters/promotion.rb +3 -4
- data/app/models/spree/allowed_origin.rb +3 -0
- data/app/models/spree/base.rb +29 -0
- data/app/models/spree/calculator/flat_percent_item_total.rb +1 -1
- data/app/models/spree/calculator/flat_rate.rb +2 -2
- data/app/models/spree/calculator/flexi_rate.rb +3 -3
- data/app/models/spree/calculator/percent_on_line_item.rb +1 -1
- data/app/models/spree/calculator/price_sack.rb +4 -4
- data/app/models/spree/calculator/shipping/digital_delivery.rb +2 -2
- data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +6 -4
- data/app/models/spree/calculator/shipping/flat_rate.rb +4 -4
- data/app/models/spree/calculator/shipping/flexi_rate.rb +3 -3
- data/app/models/spree/calculator/shipping/per_item.rb +2 -2
- data/app/models/spree/calculator/shipping/price_sack.rb +4 -4
- data/app/models/spree/calculator/tiered_flat_rate.rb +2 -2
- data/app/models/spree/calculator/tiered_percent.rb +1 -1
- data/app/models/spree/catalog.rb +1 -0
- data/app/models/spree/catalog_price.rb +0 -7
- data/app/models/spree/catalog_price_tier.rb +0 -8
- data/app/models/spree/category.rb +3 -1
- data/app/models/spree/channel/gating.rb +2 -1
- data/app/models/spree/channel.rb +2 -0
- data/app/models/spree/claim.rb +1 -0
- data/app/models/spree/collection.rb +2 -0
- data/app/models/spree/commission_rate.rb +5 -1
- data/app/models/spree/commission_rules/category_rule.rb +2 -5
- data/app/models/spree/commission_rules/item_total_rule.rb +2 -2
- data/app/models/spree/commission_rules/seller_rule.rb +2 -5
- data/app/models/spree/company.rb +1 -0
- data/app/models/spree/custom_field_definition.rb +11 -12
- data/app/models/spree/customer_group.rb +1 -0
- data/app/models/spree/data_request.rb +1 -0
- data/app/models/spree/delivery_method.rb +6 -1
- data/app/models/spree/delivery_method_rules/channel_rule.rb +2 -5
- data/app/models/spree/delivery_method_rules/item_total_rule.rb +2 -2
- data/app/models/spree/delivery_profile.rb +1 -0
- data/app/models/spree/delivery_rate.rb +3 -3
- data/app/models/spree/delivery_zone.rb +1 -1
- data/app/models/spree/exchange.rb +1 -0
- data/app/models/spree/exports/report.rb +2 -2
- data/app/models/spree/file_upload.rb +155 -0
- data/app/models/spree/fulfillment.rb +13 -3
- data/app/models/spree/fulfillment_group.rb +6 -1
- data/app/models/spree/gateway/bogus.rb +2 -0
- data/app/models/spree/gateway.rb +1 -1
- data/app/models/spree/gift_card.rb +6 -7
- data/app/models/spree/gift_card_batch.rb +3 -4
- data/app/models/spree/import.rb +3 -0
- data/app/models/spree/integration.rb +1 -1
- data/app/models/spree/line_item.rb +2 -2
- data/app/models/spree/market.rb +7 -2
- data/app/models/spree/media.rb +1 -9
- data/app/models/spree/newsletter_subscriber.rb +1 -0
- data/app/models/spree/option_type.rb +2 -1
- data/app/models/spree/option_value.rb +2 -1
- data/app/models/spree/order.rb +5 -5
- data/app/models/spree/order_group.rb +7 -2
- data/app/models/spree/package_type.rb +1 -0
- data/app/models/spree/payment/gateway_options.rb +1 -1
- data/app/models/spree/payment.rb +3 -10
- data/app/models/spree/payment_method/check.rb +2 -0
- data/app/models/spree/payment_method/store_credit.rb +11 -9
- data/app/models/spree/payment_method.rb +42 -2
- data/app/models/spree/policy.rb +1 -0
- data/app/models/spree/price.rb +17 -14
- data/app/models/spree/price_adjustment_tier.rb +4 -0
- data/app/models/spree/price_list.rb +13 -7
- data/app/models/spree/price_rules/channel_rule.rb +2 -5
- data/app/models/spree/price_rules/customer_group_rule.rb +2 -5
- data/app/models/spree/price_rules/market_rule.rb +2 -5
- data/app/models/spree/price_rules/user_rule.rb +1 -3
- data/app/models/spree/product/channels.rb +9 -2
- data/app/models/spree/product.rb +23 -23
- data/app/models/spree/product_type.rb +1 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +1 -1
- data/app/models/spree/promotion/rules/category.rb +1 -1
- data/app/models/spree/promotion/rules/channel.rb +2 -5
- data/app/models/spree/promotion/rules/country.rb +3 -4
- data/app/models/spree/promotion/rules/currency.rb +1 -1
- data/app/models/spree/promotion/rules/customer_group.rb +2 -5
- data/app/models/spree/promotion/rules/item_total.rb +2 -2
- data/app/models/spree/promotion/rules/market.rb +2 -5
- data/app/models/spree/promotion/rules/option_value.rb +2 -3
- data/app/models/spree/promotion/rules/product.rb +1 -1
- data/app/models/spree/promotion.rb +1 -0
- data/app/models/spree/purchase_order.rb +2 -0
- data/app/models/spree/refund.rb +13 -15
- data/app/models/spree/return.rb +1 -0
- data/app/models/spree/saved_report.rb +1 -0
- data/app/models/spree/search_provider/base.rb +4 -0
- data/app/models/spree/seller.rb +8 -1
- data/app/models/spree/seller_payout.rb +1 -0
- data/app/models/spree/seller_requirement.rb +1 -0
- data/app/models/spree/seller_requirement_submission.rb +1 -0
- data/app/models/spree/seller_transfer.rb +1 -0
- data/app/models/spree/shipping_label.rb +1 -0
- data/app/models/spree/stock/estimator.rb +1 -1
- data/app/models/spree/stock_level.rb +2 -0
- data/app/models/spree/stock_location.rb +1 -0
- data/app/models/spree/stock_transfer.rb +2 -0
- data/app/models/spree/store.rb +37 -12
- data/app/models/spree/store_credit.rb +5 -4
- data/app/models/spree/supplier.rb +1 -0
- data/app/models/spree/tax_category.rb +1 -0
- data/app/models/spree/tax_exemption_certificate.rb +1 -0
- data/app/models/spree/tax_provider/internal.rb +12 -9
- data/app/models/spree/tax_rate.rb +29 -6
- data/app/models/spree/taxon.rb +1 -1
- data/app/models/spree/update_status.rb +25 -0
- data/app/models/spree/variant.rb +6 -9
- data/app/models/spree/webhook_delivery.rb +1 -0
- data/app/models/spree/webhook_endpoint.rb +36 -0
- data/app/presenters/spree/csv/order_line_item_presenter.rb +10 -6
- data/app/presenters/spree/csv/price_list_price_presenter.rb +3 -8
- data/app/presenters/spree/csv/product_variant_presenter.rb +17 -8
- data/app/presenters/spree/csv/purchase_order_item_presenter.rb +1 -3
- data/app/services/spree/carts/update.rb +3 -3
- data/app/services/spree/imports/row_processors/product_variant.rb +12 -0
- data/app/services/spree/locales.rb +39 -5
- data/app/services/spree/orders/create.rb +3 -3
- data/app/services/spree/orders/discounts/create.rb +8 -8
- data/app/services/spree/prices/bulk_upsert.rb +32 -25
- data/app/services/spree/seeds/stores.rb +1 -1
- data/app/services/spree/stores/provision_defaults.rb +13 -16
- data/app/services/spree/update_check.rb +160 -0
- data/app/services/spree/uploads/assign_stores.rb +92 -0
- data/app/validators/spree/attachment_content_type_validator.rb +19 -2
- data/app/validators/spree/locale_codes_validator.rb +22 -0
- data/app/workflows/spree/fulfillments/create.rb +2 -2
- data/app/workflows/spree/payments/capture.rb +20 -5
- data/app/workflows/spree/payments/process.rb +3 -1
- data/app/workflows/spree/price_lists/update.rb +4 -4
- data/app/workflows/spree/purchase_orders/create.rb +3 -1
- data/app/workflows/spree/shipping_labels/record.rb +2 -2
- data/app/workflows/spree/stock_transfers/create.rb +3 -1
- data/config/currency_exponents.json +180 -0
- data/config/locales/content_locales.rb +9 -0
- data/config/locales/en.yml +10 -2
- data/db/migrate/20261009000001_change_money_columns_to_decimal_19_4.rb +116 -0
- data/db/migrate/20261010000001_add_store_id_to_active_storage_blobs.rb +12 -0
- data/lib/generators/spree/dummy/templates/rails/test.rb +21 -2
- data/lib/spree/core/configuration.rb +18 -0
- data/lib/spree/core/engine.rb +26 -0
- data/lib/spree/core/preferences/invalid_preferences.rb +18 -0
- data/lib/spree/core/preferences/json_conversion.rb +5 -1
- data/lib/spree/core/preferences/masking.rb +51 -1
- data/lib/spree/core/preferences/preferable.rb +212 -26
- data/lib/spree/core/preferences/preferable_class_methods.rb +121 -4
- data/lib/spree/core/ransack_configuration.rb +13 -2
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +6 -3
- data/lib/spree/iso_data.rb +7 -0
- data/lib/spree/localized_number.rb +6 -0
- data/lib/spree/migrations.rb +4 -1
- data/lib/spree/money.rb +196 -15
- data/lib/spree/reporting/adapters/base.rb +7 -6
- data/lib/spree/reporting/registry.rb +13 -0
- data/lib/spree/testing_support/factories/product_factory.rb +9 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +22 -1
- data/lib/spree/uploads.rb +101 -0
- data/lib/tasks/markets.rake +2 -1
- data/lib/tasks/migrate_users_to_customers.rake +23 -18
- data/lib/tasks/money.rake +46 -0
- data/lib/tasks/seller_sample_data.rake +2 -2
- data/lib/tasks/store_settings.rake +13 -6
- data/lib/tasks/typed_adjustments_migration.rake +1 -1
- data/lib/tasks/uploads.rake +19 -0
- metadata +35 -6
- data/app/jobs/spree/products/touch_categories_job.rb +0 -15
- data/app/jobs/spree/products/touch_taxons_job.rb +0 -13
|
@@ -45,7 +45,7 @@ module Spree
|
|
|
45
45
|
rescue ActiveRecord::RecordInvalid => e
|
|
46
46
|
failure(e.record, e.record.errors.full_messages.to_sentence)
|
|
47
47
|
rescue ActiveStorage::FileNotFoundError
|
|
48
|
-
# An abandoned
|
|
48
|
+
# An abandoned presigned upload leaves the blob row without its bytes.
|
|
49
49
|
# ActiveStorage raises a message-less error for that, which would
|
|
50
50
|
# otherwise be an unhandled 500 on the Admin API.
|
|
51
51
|
order ||= @store.orders.new
|
|
@@ -71,8 +71,8 @@ module Spree
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
# The buyer's purchase order, when it arrived with the paperwork. A
|
|
74
|
-
# signed
|
|
75
|
-
attrs[:
|
|
74
|
+
# signed id, so it assigns like any other attribute.
|
|
75
|
+
attrs[:po_document_signed_id] = @params[:po_document_signed_id] if @params[:po_document_signed_id].present?
|
|
76
76
|
|
|
77
77
|
attrs[:market] = resolve_market if @params[:market_id].present?
|
|
78
78
|
attrs[:channel] = resolve_channel if @params[:channel_id].present?
|
|
@@ -17,8 +17,8 @@ module Spree
|
|
|
17
17
|
# @param line_item [Spree::LineItem, nil] nil distributes order-level
|
|
18
18
|
# @return [Spree::ServiceModule::Result] value is the created rows
|
|
19
19
|
def call(order:, label:, value:, value_type: 'flat', line_item: nil)
|
|
20
|
-
value =
|
|
21
|
-
return failure(nil, I18n.t('spree.errors.messages.discount_value_must_be_positive')) unless value
|
|
20
|
+
value = Spree::Money::Rounding.parse_decimal(value)
|
|
21
|
+
return failure(nil, I18n.t('spree.errors.messages.discount_value_must_be_positive')) unless value&.positive?
|
|
22
22
|
return failure(nil, I18n.t('spree.errors.messages.discount_value_type_invalid')) unless %w[flat percent].include?(value_type)
|
|
23
23
|
|
|
24
24
|
rows = order.with_lock do
|
|
@@ -39,7 +39,7 @@ module Spree
|
|
|
39
39
|
|
|
40
40
|
def line_item_row(order, line_item, label, value, value_type)
|
|
41
41
|
base = discountable_base(line_item)
|
|
42
|
-
amount = -[amount_for(base, value, value_type), base].min
|
|
42
|
+
amount = -[amount_for(base, value, value_type, order.currency), base].min
|
|
43
43
|
return if amount.zero?
|
|
44
44
|
|
|
45
45
|
create_row(order, line_item, label, amount, value, value_type)
|
|
@@ -51,19 +51,19 @@ module Spree
|
|
|
51
51
|
bases_sum = bases.sum
|
|
52
52
|
return [] if bases_sum <= 0
|
|
53
53
|
|
|
54
|
-
total = [amount_for(bases_sum, value, value_type), bases_sum].min
|
|
55
|
-
shares = Spree::Adjusters::LargestRemainder.
|
|
54
|
+
total = [amount_for(bases_sum, value, value_type, order.currency), bases_sum].min
|
|
55
|
+
shares = Spree::Adjusters::LargestRemainder.apportion(total, bases, order.currency)
|
|
56
56
|
|
|
57
57
|
line_items.each_with_index.filter_map do |line_item, index|
|
|
58
|
-
amount = -
|
|
58
|
+
amount = -shares[index]
|
|
59
59
|
next if amount.zero?
|
|
60
60
|
|
|
61
61
|
create_row(order, line_item, label, amount, value, value_type)
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def amount_for(base, value, value_type)
|
|
66
|
-
value_type == 'percent' ?
|
|
65
|
+
def amount_for(base, value, value_type, currency)
|
|
66
|
+
Spree::Money::Rounding.to_currency(value_type == 'percent' ? base * value / 100 : value, currency)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# Remaining discountable base: amount net of already-applied discounts.
|
|
@@ -44,9 +44,12 @@ module Spree
|
|
|
44
44
|
# (docs/plans/6.0-volume-pricing.md).
|
|
45
45
|
def call(rows:)
|
|
46
46
|
rows = Array(rows).map { |r| r.with_indifferent_access }
|
|
47
|
-
# Amounts are parsed once, here; everything below reads numbers.
|
|
48
47
|
keyed = rows.select { |r| r[:variant_id].present? && r[:currency].present? }
|
|
49
|
-
|
|
48
|
+
unreadable = rows_with_malformed_amount(keyed)
|
|
49
|
+
return failure(nil, malformed_amounts: unreadable) if unreadable.any?
|
|
50
|
+
|
|
51
|
+
# Amounts are parsed once, here; everything below reads numbers.
|
|
52
|
+
keyed = keyed.map { |r| r.merge(amount: Spree::Money::Rounding.parse_decimal(r[:amount]), compare_at_amount: Spree::Money::Rounding.parse_decimal(r[:compare_at_amount])) }
|
|
50
53
|
# Checked before anything is deduped: `row_key` coerces the quantity,
|
|
51
54
|
# so two malformed rows would collapse into one and the batch would be
|
|
52
55
|
# judged on a shape the caller never sent.
|
|
@@ -98,12 +101,13 @@ module Spree
|
|
|
98
101
|
upsert_batch(base_rows, BASE_UNIQUE_BY)
|
|
99
102
|
end
|
|
100
103
|
upsert_batch(override_rows, OVERRIDE_UNIQUE_BY)
|
|
101
|
-
sweep(affected_keys, clear_rows)
|
|
104
|
+
cleared_variant_ids = sweep(affected_keys, clear_rows)
|
|
102
105
|
# `upsert_all` and `delete_all` both skip AR callbacks, so the
|
|
103
106
|
# `Price -> Variant -> Product` `touch:` chain never fires —
|
|
104
107
|
# downstream caches (`cache_key_with_version`) would stay stale.
|
|
105
|
-
#
|
|
106
|
-
|
|
108
|
+
# A blank row clearing a rung that had no amount changes nothing,
|
|
109
|
+
# so only priced rows and cleared amounts touch their variant.
|
|
110
|
+
touch_variants((upsert_rows.map { |r| r[:variant_id].to_s } + cleared_variant_ids).uniq)
|
|
107
111
|
end
|
|
108
112
|
|
|
109
113
|
success(price_count: payload.length)
|
|
@@ -167,6 +171,18 @@ module Spree
|
|
|
167
171
|
Integer(raw.to_s.strip, 10, exception: false)
|
|
168
172
|
end
|
|
169
173
|
|
|
174
|
+
# Rows whose amount is not a number or plain decimal text ("1,599.99",
|
|
175
|
+
# `false`), refused before a malformed value can read as "clear".
|
|
176
|
+
def rows_with_malformed_amount(rows)
|
|
177
|
+
rows.each_with_index.filter_map do |row, index|
|
|
178
|
+
Spree::Money::Rounding.parse_decimal(row[:amount])
|
|
179
|
+
Spree::Money::Rounding.parse_decimal(row[:compare_at_amount])
|
|
180
|
+
nil
|
|
181
|
+
rescue Spree::Money::InvalidFormat
|
|
182
|
+
{ index: index }
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
170
186
|
# Rows pricing below zero. This path runs no model validations, so the
|
|
171
187
|
# sign is checked here for every caller rather than in each of them.
|
|
172
188
|
def rows_with_negative_amount(rows)
|
|
@@ -370,15 +386,6 @@ module Spree
|
|
|
370
386
|
[row[:variant_id].to_s, row[:currency], row[:price_list_id].to_s]
|
|
371
387
|
end
|
|
372
388
|
|
|
373
|
-
# Parses locale-aware decimal input ("1.234,56" in DE, "1,234.56"
|
|
374
|
-
# in en-US). Numeric values pass through; blank values become nil.
|
|
375
|
-
def parse_amount(value)
|
|
376
|
-
return nil if value.blank?
|
|
377
|
-
return value if value.is_a?(Numeric)
|
|
378
|
-
|
|
379
|
-
Spree::LocalizedNumber.parse(value)
|
|
380
|
-
end
|
|
381
|
-
|
|
382
389
|
def sweep(affected_keys, clear_rows)
|
|
383
390
|
cleared_keys = clear_rows.map { |r| row_key(r) }.to_set
|
|
384
391
|
affected_set = affected_keys.to_set
|
|
@@ -392,16 +399,14 @@ module Spree
|
|
|
392
399
|
)
|
|
393
400
|
.pluck(:id, :variant_id, :currency, :price_list_id, :min_quantity, :amount)
|
|
394
401
|
|
|
395
|
-
|
|
402
|
+
doomed = candidates.select do |_, variant_id, currency, price_list_id, min_quantity, amount|
|
|
396
403
|
key = [variant_id.to_s, currency, price_list_id&.to_s, min_quantity]
|
|
397
|
-
|
|
398
|
-
next id if amount.nil?
|
|
399
|
-
next id if cleared_keys.include?(key)
|
|
400
|
-
|
|
401
|
-
nil
|
|
404
|
+
affected_set.include?(key) && (amount.nil? || cleared_keys.include?(key))
|
|
402
405
|
end
|
|
403
406
|
|
|
404
|
-
Spree::Price.where(id:
|
|
407
|
+
Spree::Price.where(id: doomed.map(&:first)).delete_all if doomed.any?
|
|
408
|
+
|
|
409
|
+
doomed.filter_map { |_, variant_id, *, amount| variant_id.to_s if amount }
|
|
405
410
|
end
|
|
406
411
|
|
|
407
412
|
# Bumps `updated_at` on the affected variants and their parent
|
|
@@ -410,15 +415,17 @@ module Spree
|
|
|
410
415
|
# `Variant -> Product` `touch:` chains otherwise.
|
|
411
416
|
#
|
|
412
417
|
# We use `touch_all` rather than per-record `Variant#touch`: it bumps
|
|
413
|
-
# every affected row in a single UPDATE
|
|
414
|
-
# the
|
|
418
|
+
# every affected row in a single UPDATE. It also skips the product's
|
|
419
|
+
# commit callbacks, so the search index, which stores prices, is
|
|
420
|
+
# refreshed here.
|
|
415
421
|
def touch_variants(variant_ids)
|
|
416
422
|
return if variant_ids.empty?
|
|
417
423
|
|
|
418
424
|
variants = Spree::Variant.where(id: variant_ids)
|
|
419
|
-
|
|
425
|
+
products = Spree::Product.where(id: variants.select(:product_id))
|
|
420
426
|
variants.touch_all
|
|
421
|
-
|
|
427
|
+
products.touch_all
|
|
428
|
+
products.find_each(&:enqueue_search_index)
|
|
422
429
|
end
|
|
423
430
|
|
|
424
431
|
# MySQL infers conflict targets from its own unique indexes and
|
|
@@ -18,8 +18,9 @@ module Spree
|
|
|
18
18
|
|
|
19
19
|
# @param store [Spree::Store] the store to provision, already persisted
|
|
20
20
|
# @param country [Spree::Country] where the shop ships from
|
|
21
|
-
# @param locale [String, nil] storefront locale
|
|
22
|
-
#
|
|
21
|
+
# @param locale [String, nil] storefront locale, a code from
|
|
22
|
+
# `Spree::Locales::ALL` (another spelling or region moves to the listed
|
|
23
|
+
# one); defaults to the country's own language, then English
|
|
23
24
|
# @param currency [String, nil] ISO 4217 code; defaults to the country's
|
|
24
25
|
# own currency. Merchants who ship from one country and price in
|
|
25
26
|
# another currency pass it explicitly.
|
|
@@ -55,21 +56,17 @@ module Spree
|
|
|
55
56
|
found&.iso_code || country.default_currency.presence || 'USD'
|
|
56
57
|
end
|
|
57
58
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
59
|
+
# A store's locale is a content locale, so it comes from
|
|
60
|
+
# `Spree::Locales::ALL` whether or not Spree ships translations for it:
|
|
61
|
+
# the storefront and dashboard carry their own. An explicit locale is
|
|
62
|
+
# kept when it is listed, or moved to its listed language (`en-IE`
|
|
63
|
+
# becomes `en`); one Spree cannot place, or none, gives the country's
|
|
64
|
+
# first official language Spree lists.
|
|
63
65
|
def resolve_locale(requested, country)
|
|
64
|
-
|
|
66
|
+
listed = Spree::Locales.listed(requested)
|
|
67
|
+
return listed if listed
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
return 'en' if derived.blank?
|
|
68
|
-
|
|
69
|
-
locales = Spree.available_locales
|
|
70
|
-
Spree.locale_for_language(derived, locales) ||
|
|
71
|
-
country.official_locales.lazy.filter_map { |language| Spree.locale_for_language(language, locales) }.first ||
|
|
72
|
-
'en'
|
|
69
|
+
[country.default_locale, *country.official_locales].lazy.filter_map { |language| Spree::Locales.for_language(language) }.first || 'en'
|
|
73
70
|
end
|
|
74
71
|
|
|
75
72
|
# The store's own country/currency/locale columns are not the source of
|
|
@@ -260,7 +257,7 @@ module Spree
|
|
|
260
257
|
calculator = delivery_method.calculator
|
|
261
258
|
next unless calculator.respond_to?(:preferred_currency)
|
|
262
259
|
next if calculator.preferred_currency == currency
|
|
263
|
-
next unless calculator.preferences[:amount]
|
|
260
|
+
next unless Spree::Money::Rounding.blank_or_zero?(calculator.preferences[:amount])
|
|
264
261
|
|
|
265
262
|
calculator.update!(preferences: calculator.preferences.merge(currency: currency))
|
|
266
263
|
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
# Asks spreecloud.io which Spree releases are newer than the running one,
|
|
5
|
+
# and reports usage with the same request. The answer is cached for a day,
|
|
6
|
+
# so at most one admin request a day waits on it, for no more than the two
|
|
7
|
+
# one-second timeouts. A failed check is cached for an hour, so a passing
|
|
8
|
+
# outage costs one slow request an hour and never hides a release for long.
|
|
9
|
+
#
|
|
10
|
+
# The +version+, +environment+, +url+ and +install_id+ query keys are what
|
|
11
|
+
# Spree 5 sent, and the endpoint still serves 5.x installs with them. They
|
|
12
|
+
# are never renamed; new keys only ever go beside them.
|
|
13
|
+
class UpdateCheck
|
|
14
|
+
ENDPOINT = 'https://spreecloud.io/updates.json'.freeze
|
|
15
|
+
RELEASES_URL = 'https://github.com/spree/spree/releases/tag/'.freeze
|
|
16
|
+
RELEASE_TAG = /\Av\d+(\.[0-9a-z]+)*\z/
|
|
17
|
+
CACHE_TTL = 1.day
|
|
18
|
+
FAILURE_TTL = 1.hour
|
|
19
|
+
OPEN_TIMEOUT = 1
|
|
20
|
+
READ_TIMEOUT = 1
|
|
21
|
+
# The read timeout applies to each read, not the whole response, so a
|
|
22
|
+
# slowly trickling answer needs an overall deadline as well.
|
|
23
|
+
TOTAL_TIMEOUT = 2
|
|
24
|
+
# Order-of-magnitude buckets: enough to tell a hobby install from a large
|
|
25
|
+
# merchant, never an exact figure.
|
|
26
|
+
BUCKETS = {
|
|
27
|
+
0..0 => '0',
|
|
28
|
+
1..9 => '1-9',
|
|
29
|
+
10..99 => '10-99',
|
|
30
|
+
100..999 => '100-999',
|
|
31
|
+
1_000..9_999 => '1k-9k',
|
|
32
|
+
10_000..99_999 => '10k-99k'
|
|
33
|
+
}.freeze
|
|
34
|
+
LARGEST_BUCKET = '100k+'.freeze
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# @return [Boolean] false when the operator turned the check off
|
|
38
|
+
def enabled?
|
|
39
|
+
Spree::Config.update_check_enabled && !env_flag?('SPREE_UPDATE_CHECK_DISABLED')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# When false, the check still runs but sends the running version only.
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def telemetry_enabled?
|
|
46
|
+
Spree::Config.telemetry_enabled &&
|
|
47
|
+
!env_flag?('SPREE_TELEMETRY_DISABLED') &&
|
|
48
|
+
!env_flag?('DO_NOT_TRACK')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The cached answer, fetched from the endpoint when there is none.
|
|
52
|
+
#
|
|
53
|
+
# @return [Spree::UpdateStatus]
|
|
54
|
+
def status
|
|
55
|
+
return Spree::UpdateStatus.new(current_version: Spree.version, enabled: false) unless enabled?
|
|
56
|
+
|
|
57
|
+
result = Rails.cache.read(cache_key) || refresh
|
|
58
|
+
|
|
59
|
+
Spree::UpdateStatus.new(current_version: Spree.version, checked_at: result[:checked_at], releases: result[:releases])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Hash] the query string the endpoint receives
|
|
63
|
+
def query
|
|
64
|
+
return { version: Spree.version } unless telemetry_enabled?
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
version: Spree.version,
|
|
68
|
+
environment: Rails.env,
|
|
69
|
+
url: Spree::Store.default&.storefront_url,
|
|
70
|
+
install_id: Spree.install_id,
|
|
71
|
+
ruby: RUBY_VERSION,
|
|
72
|
+
rails: Rails.version,
|
|
73
|
+
database: ActiveRecord::Base.connection_db_config.adapter,
|
|
74
|
+
gems: Gem.loaded_specs.keys.grep(/\Aspree_/).sort.join(','),
|
|
75
|
+
dashboard: dashboard_served?,
|
|
76
|
+
stores: bucket(Spree::Store.count),
|
|
77
|
+
products: bucket(Spree::Product.count),
|
|
78
|
+
orders: bucket(Spree::Order.complete.count),
|
|
79
|
+
sellers: seller_bucket
|
|
80
|
+
}.compact
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def refresh
|
|
86
|
+
releases = fetch_releases
|
|
87
|
+
result = { releases: releases || [], checked_at: Time.current }
|
|
88
|
+
Rails.cache.write(cache_key, result, expires_in: releases ? CACHE_TTL : FAILURE_TTL)
|
|
89
|
+
result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Nil when the check failed, for whatever reason: an update notice is
|
|
93
|
+
# never worth failing the admin's request over, including a database
|
|
94
|
+
# error while counting for the usage fields.
|
|
95
|
+
def fetch_releases
|
|
96
|
+
uri = URI(ENDPOINT)
|
|
97
|
+
uri.query = URI.encode_www_form(query)
|
|
98
|
+
|
|
99
|
+
response = Timeout.timeout(TOTAL_TIMEOUT) do
|
|
100
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true, max_retries: 0,
|
|
101
|
+
open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT) do |http|
|
|
102
|
+
http.get(uri.request_uri)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
return unless response.is_a?(Net::HTTPSuccess)
|
|
106
|
+
|
|
107
|
+
newer_releases(JSON.parse(response.body))
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
Rails.error.report(e, handled: true, severity: :warning, source: 'spree.update_check')
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The endpoint already filters by version; this does it again and
|
|
114
|
+
# builds each link from the tag, so whatever comes back over the wire
|
|
115
|
+
# never reaches the dashboard as a URL. Release candidates are only
|
|
116
|
+
# offered to installs already running one.
|
|
117
|
+
def newer_releases(payload)
|
|
118
|
+
current = Gem::Version.new(Spree.version)
|
|
119
|
+
|
|
120
|
+
Array(payload).filter_map { |release|
|
|
121
|
+
tag = release.is_a?(Hash) ? release['tag_name'].to_s : ''
|
|
122
|
+
next unless tag.match?(RELEASE_TAG)
|
|
123
|
+
|
|
124
|
+
version = Gem::Version.new(tag.delete_prefix('v'))
|
|
125
|
+
next unless version > current
|
|
126
|
+
next if version.prerelease? && !current.prerelease?
|
|
127
|
+
|
|
128
|
+
{ version: version.to_s, url: "#{RELEASES_URL}#{tag}", published_at: parse_time(release['published_at']) }
|
|
129
|
+
}.sort_by { |release| Gem::Version.new(release[:version]) }.reverse
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def parse_time(value)
|
|
133
|
+
Time.iso8601(value.to_s)
|
|
134
|
+
rescue ArgumentError
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def bucket(count)
|
|
139
|
+
BUCKETS.find { |range, _| range.cover?(count) }&.last || LARGEST_BUCKET
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def seller_bucket
|
|
143
|
+
count = Spree::Seller.count
|
|
144
|
+
bucket(count) if count.positive?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def dashboard_served?
|
|
148
|
+
(defined?(Spree::Dashboard) && Spree::Dashboard.dist_path.present?) || Spree::Config.dashboard_url.present?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def env_flag?(name)
|
|
152
|
+
ActiveModel::Type::Boolean.new.cast(ENV[name].presence) == true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def cache_key
|
|
156
|
+
"spree/update_check/#{Spree.version}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Uploads
|
|
3
|
+
# Gives every attached file that has no store the store of the record it
|
|
4
|
+
# is attached to (the 6.0 upgrade, `spree:uploads:assign_stores`).
|
|
5
|
+
#
|
|
6
|
+
# Owners with a `store_id` column are handled with one set-based update per
|
|
7
|
+
# attachment type; the rest (a variant's digital file, a seller's
|
|
8
|
+
# submission) ask {Spree::Uploads.store_id_for} record by record. Files
|
|
9
|
+
# attached nowhere keep no store, and the daily purge leaves them alone.
|
|
10
|
+
#
|
|
11
|
+
# Safe to re-run: only files without a store are touched.
|
|
12
|
+
class AssignStores
|
|
13
|
+
prepend Spree::ServiceModule::Base
|
|
14
|
+
|
|
15
|
+
BATCH_SIZE = 1_000
|
|
16
|
+
|
|
17
|
+
# @return [Spree::ServiceModule::Result] a hash with `assigned`, the
|
|
18
|
+
# number of files given a store, and `conflicts`, the ids of files
|
|
19
|
+
# attached to records of more than one store, which are reported and
|
|
20
|
+
# left for a person to resolve
|
|
21
|
+
def call
|
|
22
|
+
assigned = record_types.sum do |record_type|
|
|
23
|
+
owner_class = record_type.safe_constantize
|
|
24
|
+
next 0 if owner_class.nil?
|
|
25
|
+
|
|
26
|
+
# Renditions and previews are blobs themselves: a set-based update
|
|
27
|
+
# would join the table it updates, which MySQL refuses.
|
|
28
|
+
if owner_class.column_names.include?('store_id') && !record_type.start_with?('ActiveStorage::')
|
|
29
|
+
assign_from_store_column(record_type, owner_class)
|
|
30
|
+
else
|
|
31
|
+
assign_record_by_record(record_type)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
success(assigned: assigned, conflicts: conflicting_blob_ids)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Renditions and previews take their store from their original file, so
|
|
41
|
+
# they go last, once the originals have one.
|
|
42
|
+
def record_types
|
|
43
|
+
ActiveStorage::Attachment.distinct.pluck(:record_type).
|
|
44
|
+
sort_by { |record_type| [record_type.start_with?('ActiveStorage::') ? 1 : 0, record_type] }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Correlated subqueries rather than UPDATE … FROM, which MySQL and SQLite
|
|
48
|
+
# spell differently. Several attachments of one file in different stores
|
|
49
|
+
# pick one; {#conflicting_blob_ids} reports them.
|
|
50
|
+
def assign_from_store_column(record_type, owner_class)
|
|
51
|
+
attachments_table = ActiveStorage::Attachment.table_name
|
|
52
|
+
owners_table = owner_class.table_name
|
|
53
|
+
owner_store = ActiveStorage::Attachment.
|
|
54
|
+
joins("INNER JOIN #{owners_table} ON #{owners_table}.id = #{attachments_table}.record_id").
|
|
55
|
+
where(record_type: record_type).
|
|
56
|
+
where("#{attachments_table}.blob_id = #{ActiveStorage::Blob.table_name}.id").
|
|
57
|
+
where.not(owners_table => { store_id: nil })
|
|
58
|
+
|
|
59
|
+
ActiveStorage::Blob.where(store_id: nil).
|
|
60
|
+
where(id: ActiveStorage::Attachment.where(record_type: record_type).select(:blob_id)).
|
|
61
|
+
where(owner_store.arel.exists).
|
|
62
|
+
update_all(["store_id = (#{owner_store.select("#{owners_table}.store_id").limit(1).to_sql})"])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# One update per store and batch rather than per file.
|
|
66
|
+
def assign_record_by_record(record_type)
|
|
67
|
+
unassigned = ActiveStorage::Attachment.joins(:blob).
|
|
68
|
+
where(record_type: record_type, ActiveStorage::Blob.table_name => { store_id: nil })
|
|
69
|
+
|
|
70
|
+
unassigned.preload(:record).in_batches(of: BATCH_SIZE).sum do |batch|
|
|
71
|
+
batch.select(&:record).group_by { |attachment| Spree::Uploads.store_id_for(attachment.record) }.sum do |store_id, owned|
|
|
72
|
+
next 0 if store_id.nil?
|
|
73
|
+
|
|
74
|
+
ActiveStorage::Blob.where(id: owned.map(&:blob_id), store_id: nil).update_all(store_id: store_id)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def conflicting_blob_ids
|
|
80
|
+
resolvable_types = record_types.select(&:safe_constantize)
|
|
81
|
+
shared_blob_ids = ActiveStorage::Attachment.where(record_type: resolvable_types).group(:blob_id).having('COUNT(*) > 1').pluck(:blob_id)
|
|
82
|
+
|
|
83
|
+
shared_blob_ids.each_slice(BATCH_SIZE).flat_map do |blob_ids|
|
|
84
|
+
ActiveStorage::Attachment.where(blob_id: blob_ids, record_type: resolvable_types).includes(:record).group_by(&:blob_id).filter_map do |blob_id, blob_attachments|
|
|
85
|
+
store_ids = blob_attachments.filter_map { |attachment| attachment.record && Spree::Uploads.store_id_for(attachment.record) }
|
|
86
|
+
blob_id if store_ids.map(&:to_s).uniq.size > 1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -44,6 +44,16 @@ module Spree
|
|
|
44
44
|
# identified outright and never reaches the tie-break at all.
|
|
45
45
|
CONTAINER_CONTENT_TYPES = %w[application/zip application/x-ole-storage].freeze
|
|
46
46
|
|
|
47
|
+
# Plain-text formats carry no signature at all, so the bytes can only prove
|
|
48
|
+
# what they are not. Marcel names anything it recognises — HTML, XML, SVG,
|
|
49
|
+
# scripts with a shebang, executables — and those never reach this
|
|
50
|
+
# tie-break. What is left is accepted under a text extension only when it
|
|
51
|
+
# is free of the control bytes every binary format carries. No particular
|
|
52
|
+
# encoding is required, since spreadsheet tools still export in legacy ones.
|
|
53
|
+
TEXT_CONTENT_TYPES = %w[text/csv].freeze
|
|
54
|
+
UNIDENTIFIED_CONTENT_TYPES = %w[application/octet-stream text/plain].freeze
|
|
55
|
+
BINARY_BYTES = /[\x00-\x08\x0B\x0E-\x1F\x7F]/n
|
|
56
|
+
|
|
47
57
|
# A misspelled or forgotten option would otherwise leave this validator
|
|
48
58
|
# silently accepting everything, on the one path whose whole purpose is
|
|
49
59
|
# refusing hostile files. An allowlist that resolves to empty at runtime is
|
|
@@ -116,9 +126,16 @@ module Spree
|
|
|
116
126
|
# read, which would hand the decision back to whoever uploaded the file.
|
|
117
127
|
detected = Marcel::MimeType.for(StringIO.new(head))
|
|
118
128
|
return true if permitted.include?(detected)
|
|
129
|
+
return permitted_plain_text?(blob, head, permitted) if UNIDENTIFIED_CONTENT_TYPES.include?(detected)
|
|
119
130
|
return false unless CONTAINER_CONTENT_TYPES.include?(detected)
|
|
120
131
|
|
|
121
|
-
permitted.include?(
|
|
132
|
+
permitted.include?(type_from_extension(blob))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def permitted_plain_text?(blob, head, permitted)
|
|
136
|
+
text_type = type_from_extension(blob)
|
|
137
|
+
|
|
138
|
+
TEXT_CONTENT_TYPES.include?(text_type) && permitted.include?(text_type) && !head.b.match?(BINARY_BYTES)
|
|
122
139
|
end
|
|
123
140
|
|
|
124
141
|
# Asks the extension alone, rather than re-reading the bytes with a filename
|
|
@@ -129,7 +146,7 @@ module Spree
|
|
|
129
146
|
# bytes we read.
|
|
130
147
|
#
|
|
131
148
|
# @return [String, nil]
|
|
132
|
-
def
|
|
149
|
+
def type_from_extension(blob)
|
|
133
150
|
filename = blob.filename.to_s
|
|
134
151
|
return if File.extname(filename).blank?
|
|
135
152
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Rejects locale codes `Spree::Locales::ALL` does not list, in a single code
|
|
3
|
+
# or a comma-separated list, naming the ones it refuses. Reads the stored
|
|
4
|
+
# column, since a store answers `default_locale` from its default market,
|
|
5
|
+
# and checks only an attribute that is changing, so a record saved before
|
|
6
|
+
# the list was enforced can still be edited.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# validates_with Spree::LocaleCodesValidator, attributes: %i[default_locale supported_locales]
|
|
10
|
+
class LocaleCodesValidator < ActiveModel::Validator
|
|
11
|
+
def validate(record)
|
|
12
|
+
Array(options[:attributes]).each do |attribute|
|
|
13
|
+
next unless record.will_save_change_to_attribute?(attribute)
|
|
14
|
+
|
|
15
|
+
unlisted = Spree::Locales.unlisted(record.read_attribute(attribute))
|
|
16
|
+
next if unlisted.empty?
|
|
17
|
+
|
|
18
|
+
record.errors.add(attribute, I18n.t('spree.locales_not_listed', codes: unlisted.join(', ')))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -320,8 +320,8 @@ module Spree
|
|
|
320
320
|
end
|
|
321
321
|
|
|
322
322
|
# Strict decimal parsing (same semantics as Shipment#cost=, which the
|
|
323
|
-
# update_columns freeze path bypasses) —
|
|
324
|
-
#
|
|
323
|
+
# update_columns freeze path bypasses) — a lenient parse would turn
|
|
324
|
+
# garbage into 0, and 0 is a legal cost here.
|
|
325
325
|
#
|
|
326
326
|
# @return [BigDecimal, Numeric, nil] nil when no cost was given (blank
|
|
327
327
|
# counts as omitted); a failure Result for malformed or negative input
|
|
@@ -14,11 +14,13 @@ module Spree
|
|
|
14
14
|
hooks :validate, :before_capture, :after_capture
|
|
15
15
|
|
|
16
16
|
# @param payment [Spree::Payment] a completed payment is a no-op
|
|
17
|
-
# @param amount [
|
|
18
|
-
# a smaller amount splits the remainder into a new
|
|
17
|
+
# @param amount [BigDecimal, nil] in the currency's own units; nil
|
|
18
|
+
# captures in full, a smaller amount splits the remainder into a new
|
|
19
|
+
# pending payment. An Integer is read as hundredths of the currency,
|
|
20
|
+
# the pre-6.0 contract (deprecated, removed in 6.1).
|
|
19
21
|
def perform(payment:, amount: nil)
|
|
20
22
|
super
|
|
21
|
-
@amount = amount
|
|
23
|
+
@amount = capture_amount(amount)
|
|
22
24
|
|
|
23
25
|
step :ensure_capturable
|
|
24
26
|
# Veto point — fraud holds, manual review. Before any money moves.
|
|
@@ -44,6 +46,17 @@ module Spree
|
|
|
44
46
|
|
|
45
47
|
attr_reader :response, :remainder
|
|
46
48
|
|
|
49
|
+
def capture_amount(amount)
|
|
50
|
+
return payment.amount if amount.nil?
|
|
51
|
+
return Spree::Money::Rounding.to_currency(amount, payment.currency) unless amount.is_a?(Integer)
|
|
52
|
+
|
|
53
|
+
Spree::Deprecation.warn(
|
|
54
|
+
'Passing an Integer amount to the payment capture workflow is deprecated and is read as hundredths of ' \
|
|
55
|
+
'the currency. Pass the amount in the currency\'s own units as a BigDecimal; removed in Spree 6.1.'
|
|
56
|
+
)
|
|
57
|
+
Spree::Money::Rounding.from_hundredths(amount)
|
|
58
|
+
end
|
|
59
|
+
|
|
47
60
|
# An already-captured payment returns success rather than failing:
|
|
48
61
|
# capture is naturally idempotent and double-submitted admin actions
|
|
49
62
|
# must not surface as errors. A failed payment is capturable — the
|
|
@@ -65,7 +78,9 @@ module Spree
|
|
|
65
78
|
def capture_at_gateway
|
|
66
79
|
@response = payment.protect_from_connection_error do
|
|
67
80
|
instrument_gateway_call(:capture, payment.payment_method) do
|
|
68
|
-
payment.payment_method.capture(
|
|
81
|
+
payment.payment_method.capture(
|
|
82
|
+
payment.payment_method.gateway_amount(@amount, payment.currency), payment.response_code, payment.gateway_options
|
|
83
|
+
)
|
|
69
84
|
end
|
|
70
85
|
end
|
|
71
86
|
|
|
@@ -86,7 +101,7 @@ module Spree
|
|
|
86
101
|
if Spree::Payment.where(id: payment.id, status: 'completed').exists?
|
|
87
102
|
already_captured = true
|
|
88
103
|
else
|
|
89
|
-
payment.capture_events.create!(amount:
|
|
104
|
+
payment.capture_events.create!(amount: @amount)
|
|
90
105
|
# Split before completing, so payment.completed publishes with
|
|
91
106
|
# the captured amount and the order recomputes from correct rows.
|
|
92
107
|
@remainder = payment.split_uncaptured_amount
|
|
@@ -57,7 +57,9 @@ module Spree
|
|
|
57
57
|
|
|
58
58
|
response = payment.protect_from_connection_error do
|
|
59
59
|
instrument_gateway_call(verb, payment.payment_method) do
|
|
60
|
-
payment.payment_method.public_send(
|
|
60
|
+
payment.payment_method.public_send(
|
|
61
|
+
verb, payment.payment_method.gateway_amount(payment.amount, payment.currency), payment.source, payment.gateway_options
|
|
62
|
+
)
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
@@ -61,7 +61,9 @@ module Spree
|
|
|
61
61
|
unless result.success?
|
|
62
62
|
refusal = result.error&.value
|
|
63
63
|
refusal = {} unless refusal.is_a?(Hash)
|
|
64
|
-
if refusal[:
|
|
64
|
+
if refusal[:malformed_amounts].present?
|
|
65
|
+
price_list.errors.add(:base, :malformed_price)
|
|
66
|
+
elsif refusal[:invalid_amounts].present?
|
|
65
67
|
price_list.errors.add(:base, :negative_price)
|
|
66
68
|
elsif refusal[:invalid_quantities].present?
|
|
67
69
|
price_list.errors.add(:base, :invalid_quantity)
|
|
@@ -76,12 +78,10 @@ module Spree
|
|
|
76
78
|
end
|
|
77
79
|
return failure(price_list)
|
|
78
80
|
end
|
|
79
|
-
|
|
80
|
-
touch_variants(rows.map { |row| row[:variant_id] }.uniq)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def clear_prices
|
|
84
|
-
variant_ids = price_list.prices.distinct.pluck(:variant_id)
|
|
84
|
+
variant_ids = price_list.prices.where.not(amount: nil).distinct.pluck(:variant_id)
|
|
85
85
|
price_list.prices.update_all(amount: nil, compare_at_amount: nil, updated_at: Time.current)
|
|
86
86
|
touch_variants(variant_ids)
|
|
87
87
|
end
|