spree_core 6.0.0.beta2 → 6.0.0.beta3
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/payments/handle_webhook_job.rb +1 -0
- data/app/jobs/spree/sample_data/load_job.rb +7 -2
- data/app/jobs/spree/seller_transfers/execute_pending_job.rb +1 -0
- data/app/mailers/spree/admin_user_mailer.rb +2 -2
- data/app/models/concerns/spree/customer_methods.rb +17 -1
- data/app/models/concerns/spree/has_number.rb +3 -2
- data/app/models/concerns/spree/prefixed_id.rb +4 -1
- data/app/models/concerns/spree/purchase/totals.rb +3 -1
- data/app/models/concerns/spree/ransackable_attributes.rb +38 -6
- data/app/models/concerns/spree/translatable_resource.rb +6 -7
- data/app/models/concerns/spree/webhook_payload_redaction.rb +132 -21
- data/app/models/spree/adjusters/promotion.rb +1 -1
- data/app/models/spree/authentication/lockout.rb +34 -0
- data/app/models/spree/authentication/strategies/email_password_strategy.rb +4 -12
- data/app/models/spree/authentication/strategies/oidc_strategy.rb +1 -1
- data/app/models/spree/claim.rb +1 -0
- data/app/models/spree/commission_line.rb +4 -2
- data/app/models/spree/commission_rule.rb +1 -1
- data/app/models/spree/coupon_code.rb +14 -2
- data/app/models/spree/current.rb +15 -0
- data/app/models/spree/exchange.rb +1 -0
- data/app/models/spree/export.rb +8 -5
- data/app/models/spree/exports/customers.rb +2 -2
- data/app/models/spree/fulfillment_group.rb +127 -0
- data/app/models/spree/gift_card.rb +9 -0
- data/app/models/spree/invitation.rb +18 -5
- data/app/models/spree/number_generators/sequential.rb +2 -2
- data/app/models/spree/option_value.rb +7 -0
- data/app/models/spree/order.rb +35 -12
- data/app/models/spree/order_group.rb +78 -6
- data/app/models/spree/payment_source.rb +1 -1
- data/app/models/spree/payment_split.rb +24 -0
- data/app/models/spree/price.rb +74 -0
- data/app/models/spree/product.rb +4 -0
- data/app/models/spree/product_option_type.rb +11 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +92 -30
- data/app/models/spree/promotion/rules/category.rb +14 -9
- data/app/models/spree/promotion/rules/product.rb +17 -1
- data/app/models/spree/promotion.rb +1 -1
- data/app/models/spree/promotion_action.rb +12 -1
- data/app/models/spree/promotion_handler/coupon.rb +50 -5
- data/app/models/spree/promotion_rule.rb +14 -0
- data/app/models/spree/return.rb +16 -6
- data/app/models/spree/return_line_item.rb +19 -0
- data/app/models/spree/search_provider/database.rb +10 -2
- data/app/models/spree/seller_payout.rb +3 -1
- data/app/models/spree/seller_transfer.rb +3 -1
- data/app/models/spree/store_credit.rb +4 -0
- data/app/models/spree/translations/batch.rb +4 -2
- data/app/models/spree/user_identity.rb +10 -6
- data/app/models/spree/variant.rb +8 -3
- data/app/models/spree/webhook_delivery.rb +28 -0
- data/app/models/spree/webhook_endpoint.rb +14 -0
- data/app/presenters/spree/csv/order_line_item_presenter.rb +2 -2
- data/app/services/spree/carts/split_by_seller.rb +22 -12
- data/app/services/spree/companies/add_member.rb +8 -2
- data/app/services/spree/imports/row_processors/price_list_price.rb +6 -0
- data/app/services/spree/imports/row_processors/product_translation.rb +1 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +17 -11
- data/app/services/spree/order_groups/allocate_payments.rb +3 -0
- data/app/services/spree/orders/build_fulfillments.rb +6 -0
- data/app/services/spree/orders/update_statuses.rb +8 -2
- data/app/services/spree/prices/bulk_upsert.rb +186 -47
- data/app/services/spree/sample_data/import_builder.rb +1 -1
- data/app/services/spree/sample_data/loader.rb +54 -40
- data/app/services/spree/seeds/all.rb +1 -26
- data/app/services/spree/seeds/allowed_origins.rb +5 -4
- data/app/services/spree/seeds/api_keys.rb +10 -9
- data/app/services/spree/seeds/channels.rb +9 -8
- data/app/services/spree/seeds/commission_rates.rb +21 -20
- data/app/services/spree/seeds/customer_groups.rb +5 -4
- data/app/services/spree/seeds/digital_delivery.rb +7 -8
- data/app/services/spree/seeds/payment_methods.rb +12 -11
- data/app/services/spree/seeds/product_types.rb +7 -8
- data/app/services/spree/seeds/returns_environment.rb +12 -11
- data/app/services/spree/seeds/roles.rb +8 -7
- data/app/services/spree/seeds/saved_reports.rb +19 -20
- data/app/services/spree/seeds/seller_requirements.rb +5 -4
- data/app/services/spree/seeds/store_resources.rb +41 -0
- data/app/services/spree/seeds/store_scoped.rb +15 -0
- data/app/services/spree/seeds/tax_categories.rb +10 -9
- data/app/services/spree/stock_locations/stock_items/create.rb +1 -1
- data/app/services/spree/stock_locations/stock_levels/create.rb +19 -3
- data/app/subscribers/spree/order_status_subscriber.rb +15 -7
- data/app/workflows/spree/carts/add_item.rb +5 -7
- data/app/workflows/spree/carts/complete.rb +20 -64
- data/app/workflows/spree/carts/recalculate_totals.rb +3 -1
- data/app/workflows/spree/claims/create.rb +31 -1
- data/app/workflows/spree/claims/resolve.rb +7 -11
- data/app/workflows/spree/exchanges/create.rb +11 -0
- data/app/workflows/spree/exchanges/fulfill.rb +21 -13
- data/app/workflows/spree/orders/complete.rb +99 -2
- data/app/workflows/spree/price_lists/update.rb +17 -0
- data/app/workflows/spree/refunds/order_payments.rb +55 -1
- data/app/workflows/spree/returns/create.rb +9 -15
- data/app/workflows/spree/returns/refund.rb +15 -24
- data/app/workflows/spree/returns/returnable_quantity.rb +53 -0
- data/app/workflows/spree/sellers/invite.rb +1 -1
- data/config/locales/en.yml +7 -119
- data/db/migrate/20260916000001_add_email_delivery_flags_to_spree_order_groups.rb +14 -0
- data/db/migrate/20260922000001_add_refunded_order_to_spree_store_credits.rb +5 -0
- data/db/migrate/20260923000001_change_spree_user_identities_tokens_to_text.rb +10 -0
- data/db/sample_data/channels.rb +1 -1
- data/db/sample_data/collections.rb +1 -1
- data/db/sample_data/custom_field_definitions.rb +1 -1
- data/db/sample_data/digital_products.rb +1 -1
- data/db/sample_data/fulfillment.rb +1 -1
- data/db/sample_data/options.rb +4 -2
- data/db/sample_data/orders.rb +9 -10
- data/db/sample_data/payment_methods.rb +18 -18
- data/db/sample_data/product_type_categories.rb +1 -1
- data/db/sample_data/product_types.rb +2 -2
- data/db/sample_data/promotions.rb +2 -4
- data/db/sample_data/wholesale.rb +1 -1
- data/lib/generators/spree/api_resource/api_resource_generator.rb +58 -1
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +6 -5
- data/lib/generators/spree/api_resource/templates/permissions.en.yml.tt +6 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +2 -2
- data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
- data/lib/generators/spree/subscriber/subscriber_generator.rb +3 -3
- data/lib/spree/core/engine.rb +8 -8
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/iso_data.rb +23 -4
- data/lib/spree/store_scope_guard.rb +16 -1
- data/lib/spree/testing_support/factories/invitation_factory.rb +1 -0
- data/lib/spree/testing_support/factories/order_group_factory.rb +49 -0
- data/lib/spree/testing_support/factories/payment_factory.rb +7 -2
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
- data/lib/tasks/sample_data.rake +8 -2
- data/lib/tasks/upgrade.rake +6 -21
- metadata +13 -4
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Spree
|
|
4
4
|
module Refunds
|
|
5
|
-
# Gives a post-sale workflow
|
|
5
|
+
# Gives a post-sale workflow its two ways to put money back on an order:
|
|
6
|
+
# back to what paid for it, or as store credit.
|
|
6
7
|
#
|
|
7
8
|
# Returns, claims and exchanges all owe the customer money for the same
|
|
8
9
|
# reason — goods that are going back, or were never right — and all three
|
|
@@ -22,6 +23,57 @@ module Spree
|
|
|
22
23
|
|
|
23
24
|
private
|
|
24
25
|
|
|
26
|
+
# Puts `amount` back as store credit.
|
|
27
|
+
#
|
|
28
|
+
# Credit is its own ledger and writes no {Spree::Refund} row, so the
|
|
29
|
+
# credit names the order it settles — without that the order could not
|
|
30
|
+
# tell it had given anything back, and its payment status stayed `paid`
|
|
31
|
+
# on money the customer had already been made whole for.
|
|
32
|
+
#
|
|
33
|
+
# @param order [Spree::Order] the order being put right
|
|
34
|
+
# @param amount [BigDecimal] how much to give back
|
|
35
|
+
# @param record [Spree::Return, Spree::Claim, Spree::Exchange] what asked
|
|
36
|
+
# for it; it originates the credit
|
|
37
|
+
# @param memo [String] what the customer reads on the credit
|
|
38
|
+
# @param refunder [Object, nil] whoever is issuing it
|
|
39
|
+
# @return [Array<Spree::StoreCredit>] the one credit written, shaped like
|
|
40
|
+
# {#refund_order_payments} so both branches answer the same way
|
|
41
|
+
def issue_refund_store_credit(order:, amount:, record:, memo:, refunder: nil)
|
|
42
|
+
failure(record, :refund_exceeds_paid) if amount.to_d > refundable_balance(order)
|
|
43
|
+
|
|
44
|
+
[
|
|
45
|
+
Spree::StoreCredit.create!(
|
|
46
|
+
store: record.store,
|
|
47
|
+
customer: order.customer,
|
|
48
|
+
refunded_order: order,
|
|
49
|
+
amount: amount,
|
|
50
|
+
currency: record.currency,
|
|
51
|
+
created_by: refunder,
|
|
52
|
+
originator: record,
|
|
53
|
+
memo: memo
|
|
54
|
+
)
|
|
55
|
+
]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# What the order can still give back, as store credit or to its
|
|
59
|
+
# payments: what its payments can still refund, less credit already
|
|
60
|
+
# issued against it. Credit writes no refund row, so without the second
|
|
61
|
+
# half every return, claim and exchange on one order could each hand back
|
|
62
|
+
# the full amount paid, and a refund to the card could follow a credit.
|
|
63
|
+
#
|
|
64
|
+
# The order row is locked first, so two settlements racing on one order
|
|
65
|
+
# cannot both read the same headroom.
|
|
66
|
+
#
|
|
67
|
+
# @param order [Spree::Order]
|
|
68
|
+
# @param shares [Hash{Spree::Payment => BigDecimal}, nil] already read
|
|
69
|
+
# @return [BigDecimal]
|
|
70
|
+
def refundable_balance(order, shares = nil)
|
|
71
|
+
Spree::Order.lock.find(order.id)
|
|
72
|
+
|
|
73
|
+
(shares || refundable_shares(order)).values.sum(0.to_d) -
|
|
74
|
+
Spree::StoreCredit.where(refunded_order: order).sum(:amount).to_d
|
|
75
|
+
end
|
|
76
|
+
|
|
25
77
|
# Puts `amount` back on `order`, oldest payment first, until it is
|
|
26
78
|
# covered.
|
|
27
79
|
#
|
|
@@ -44,6 +96,8 @@ module Spree
|
|
|
44
96
|
remaining = amount.to_d
|
|
45
97
|
refunds = []
|
|
46
98
|
shares = refundable_shares(order)
|
|
99
|
+
# With no payment to draw on the caller reports that instead.
|
|
100
|
+
failure(record, :refund_exceeds_paid) if shares.any? && remaining > refundable_balance(order, shares)
|
|
47
101
|
# Resolved once rather than per payment — but only once there is
|
|
48
102
|
# something to refund, since it is a find_or_create_by and a run that
|
|
49
103
|
# refunds nothing should leave no reason row behind.
|
|
@@ -7,6 +7,8 @@ module Spree
|
|
|
7
7
|
# regional consumer rights. That policy is the most-customized rule in
|
|
8
8
|
# commerce and deliberately does not live in core.
|
|
9
9
|
class Create < Spree::Workflow
|
|
10
|
+
include Spree::Returns::ReturnableQuantity
|
|
11
|
+
|
|
10
12
|
hooks :validate, :after_create
|
|
11
13
|
|
|
12
14
|
# The created return — hook handlers read it (nil while :validate runs).
|
|
@@ -48,7 +50,7 @@ module Spree
|
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
# Quantities are validated against what is actually still returnable —
|
|
51
|
-
# units already returned
|
|
53
|
+
# units already returned or exchanged must not come back twice.
|
|
52
54
|
def normalize_items
|
|
53
55
|
@normalized_items = items.map do |item|
|
|
54
56
|
fulfillment_item = item[:fulfillment_item]
|
|
@@ -57,26 +59,18 @@ module Spree
|
|
|
57
59
|
failure(order, :invalid_quantity) unless quantity.positive?
|
|
58
60
|
failure(order, :item_not_on_order) unless fulfillment_item&.order_id == order.id
|
|
59
61
|
|
|
60
|
-
available = returnable_quantity_for(fulfillment_item)
|
|
61
|
-
if quantity > available
|
|
62
|
-
failure(order, "Only #{available} of #{fulfillment_item.variant.name} can be returned")
|
|
63
|
-
end
|
|
64
|
-
|
|
65
62
|
{ fulfillment_item: fulfillment_item, quantity: quantity }
|
|
66
63
|
end
|
|
67
|
-
end
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
already_requested = Spree::ReturnLineItem.
|
|
71
|
-
joins(:return).
|
|
72
|
-
where(fulfillment_item_id: fulfillment_item.id).
|
|
73
|
-
where.not(spree_returns: { status: 'canceled' }).
|
|
74
|
-
sum(:quantity)
|
|
75
|
-
|
|
76
|
-
fulfillment_item.quantity.to_i - already_requested
|
|
65
|
+
ensure_returnable_quantities(@normalized_items, action: 'returned')
|
|
77
66
|
end
|
|
78
67
|
|
|
79
68
|
def build_return
|
|
69
|
+
# Again under the order's row lock, so two requests racing for the same
|
|
70
|
+
# units cannot both pass the check above.
|
|
71
|
+
Spree::Order.lock.find(order.id)
|
|
72
|
+
ensure_returnable_quantities(@normalized_items, action: 'returned')
|
|
73
|
+
|
|
80
74
|
@return_record = order.returns.new(
|
|
81
75
|
store: order.store,
|
|
82
76
|
stock_location: stock_location || default_stock_location,
|
|
@@ -52,15 +52,14 @@ module Spree
|
|
|
52
52
|
# original supply date rather than today's — the rate that applied then is
|
|
53
53
|
# the rate to credit back.
|
|
54
54
|
#
|
|
55
|
-
# Only lines that actually arrived are credited
|
|
56
|
-
#
|
|
57
|
-
# two must not have tax credited on the third.
|
|
55
|
+
# Only lines that actually arrived are credited: a customer who announced
|
|
56
|
+
# three items and sent two must not have tax credited on the third.
|
|
58
57
|
#
|
|
59
58
|
# The refunded amount goes with them, because it can be less than those
|
|
60
59
|
# lines are worth — a restocking fee, or an agreed part-refund. Without it
|
|
61
60
|
# a provider would credit the tax on goods whose value the merchant kept.
|
|
62
61
|
def refund_tax
|
|
63
|
-
received = return_record.return_line_items.select { |line| line.
|
|
62
|
+
received = return_record.return_line_items.select { |line| line.refund_amount.positive? }
|
|
64
63
|
return if received.empty?
|
|
65
64
|
|
|
66
65
|
order = return_record.order
|
|
@@ -83,34 +82,26 @@ module Spree
|
|
|
83
82
|
failure(return_record, :not_received) unless return_record.received?
|
|
84
83
|
end
|
|
85
84
|
|
|
86
|
-
# Only what actually came back is refundable — a customer who sent two
|
|
87
|
-
#
|
|
85
|
+
# Only what actually came back is refundable — a customer who sent two of
|
|
86
|
+
# three items gets two items' worth. One figure serves as both the
|
|
87
|
+
# default and the ceiling, so a caller naming an amount cannot ask for
|
|
88
|
+
# more than a caller who names none would get.
|
|
88
89
|
def resolve_amount
|
|
89
|
-
|
|
90
|
+
refundable = return_record.refundable_total.to_d
|
|
91
|
+
@amount_to_refund = amount ? amount.to_d : refundable
|
|
90
92
|
|
|
91
93
|
failure(return_record, :nothing_to_refund) unless @amount_to_refund.positive?
|
|
92
|
-
failure(return_record, :refund_exceeds_balance) if @amount_to_refund >
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def received_total
|
|
96
|
-
return_record.return_line_items.sum do |line|
|
|
97
|
-
next 0 if line.quantity.to_i.zero?
|
|
98
|
-
|
|
99
|
-
(line.pre_tax_amount / line.quantity) * line.received_quantity.to_i
|
|
100
|
-
end
|
|
94
|
+
failure(return_record, :refund_exceeds_balance) if @amount_to_refund > refundable
|
|
101
95
|
end
|
|
102
96
|
|
|
103
97
|
def issue_store_credit
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
customer: return_record.order.customer,
|
|
98
|
+
@refunds = issue_refund_store_credit(
|
|
99
|
+
order: return_record.order,
|
|
107
100
|
amount: @amount_to_refund,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
memo: "Return #{return_record.number}"
|
|
101
|
+
record: return_record,
|
|
102
|
+
memo: "Return #{return_record.number}",
|
|
103
|
+
refunder: refunder
|
|
112
104
|
)
|
|
113
|
-
@refunds = [credit]
|
|
114
105
|
end
|
|
115
106
|
|
|
116
107
|
# Each refund row commits, then Refund#perform! credits it at the
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Returns
|
|
3
|
+
# How many units of a shipped item can still go back.
|
|
4
|
+
#
|
|
5
|
+
# A return and an exchange both take the same units off the customer, so
|
|
6
|
+
# each counts the other: without that, the units a customer returned could
|
|
7
|
+
# be exchanged again, and the credit for the exchange paid out a second
|
|
8
|
+
# time. Units named twice in one request count twice too.
|
|
9
|
+
module ReturnableQuantity
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# @param fulfillment_item [Spree::FulfillmentItem]
|
|
15
|
+
# @param requested [Hash{Integer => Integer}] units already asked for in
|
|
16
|
+
# this request, keyed by fulfillment item id
|
|
17
|
+
# @return [Integer]
|
|
18
|
+
def returnable_quantity_for(fulfillment_item, requested: {})
|
|
19
|
+
returned = Spree::ReturnLineItem.
|
|
20
|
+
joins(:return).
|
|
21
|
+
where(fulfillment_item_id: fulfillment_item.id).
|
|
22
|
+
where.not(Spree::Return.table_name => { status: 'canceled' }).
|
|
23
|
+
sum(:quantity)
|
|
24
|
+
exchanged = Spree::ExchangeLineItem.
|
|
25
|
+
joins(:exchange).
|
|
26
|
+
where(fulfillment_item_id: fulfillment_item.id).
|
|
27
|
+
where.not(Spree::Exchange.table_name => { status: 'canceled' }).
|
|
28
|
+
sum(:quantity)
|
|
29
|
+
|
|
30
|
+
fulfillment_item.quantity.to_i - returned - exchanged - requested.fetch(fulfillment_item.id, 0)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Checks each item's quantity against what is still returnable and
|
|
34
|
+
# fails with the item's name when it is not.
|
|
35
|
+
#
|
|
36
|
+
# @param items [Array<Hash>] `[{ fulfillment_item:, quantity: }]`
|
|
37
|
+
# @param action [String] 'returned' or 'exchanged', for the message
|
|
38
|
+
def ensure_returnable_quantities(items, action:)
|
|
39
|
+
requested = Hash.new(0)
|
|
40
|
+
|
|
41
|
+
items.each do |item|
|
|
42
|
+
fulfillment_item = item[:fulfillment_item]
|
|
43
|
+
available = returnable_quantity_for(fulfillment_item, requested: requested)
|
|
44
|
+
if item[:quantity] > available
|
|
45
|
+
failure(order, "Only #{[available, 0].max} of #{fulfillment_item.variant.name} can be #{action}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
requested[fulfillment_item.id] += item[:quantity]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -48,7 +48,7 @@ module Spree
|
|
|
48
48
|
# role naming somewhere else comes back as a 422 rather than access
|
|
49
49
|
# granted elsewhere.
|
|
50
50
|
def send_invitation
|
|
51
|
-
@invitation = seller.invitations.new(email: email, role: role, inviter: inviter)
|
|
51
|
+
@invitation = seller.invitations.new(email: email, role: role || seller.default_user_role, inviter: inviter)
|
|
52
52
|
|
|
53
53
|
failure(@invitation, @invitation.errors) unless @invitation.save
|
|
54
54
|
end
|
data/config/locales/en.yml
CHANGED
|
@@ -375,6 +375,8 @@ en:
|
|
|
375
375
|
greater_than_max_amount: is greater than the allowed maximum amount of %{max_amount}
|
|
376
376
|
spree/price:
|
|
377
377
|
attributes:
|
|
378
|
+
amount:
|
|
379
|
+
leaves_a_rising_ladder: would leave the price from %{quantity} units at %{amount}, above the %{floor} a buyer pays below that quantity
|
|
378
380
|
min_quantity:
|
|
379
381
|
requires_price_list: quantity breaks can only be set on a price list
|
|
380
382
|
too_many_breaks: a variant can carry at most %{count} quantity breaks on one price list
|
|
@@ -385,9 +387,12 @@ en:
|
|
|
385
387
|
spree/price_list:
|
|
386
388
|
attributes:
|
|
387
389
|
base:
|
|
390
|
+
duplicate_quantity: each quantity break must have its own quantity
|
|
388
391
|
invalid_quantity: each quantity break must start at a whole number of units
|
|
389
392
|
negative_price: a price cannot be below zero
|
|
390
393
|
nothing_to_apply_to: has no rules, no catalog and no products, so it would apply to nothing. Add one before activating.
|
|
394
|
+
price_rises_with_quantity: a quantity break cannot cost more than the price a buyer pays below it
|
|
395
|
+
quantity_on_base_price: a shop price applies at every quantity, so it cannot start at one. Remove the quantity, or pick a price list to put the break on.
|
|
391
396
|
ends_at:
|
|
392
397
|
must_be_after_starts_at: must be after starts at
|
|
393
398
|
price_adjustment_percentage:
|
|
@@ -844,15 +849,6 @@ en:
|
|
|
844
849
|
company_invitations:
|
|
845
850
|
email_mismatch: This invitation was sent to a different email address.
|
|
846
851
|
not_pending: This invitation is no longer pending and cannot be revoked.
|
|
847
|
-
company_mailer:
|
|
848
|
-
invitation_email:
|
|
849
|
-
action: Accept invitation
|
|
850
|
-
expiry_notice: This invitation link will expire in 30 days.
|
|
851
|
-
greeting: Hi,
|
|
852
|
-
ignore_notice: If you were not expecting this invitation, you can safely ignore this email.
|
|
853
|
-
instructions: You have been invited to join %{company} on %{store}. Accept the invitation to start purchasing on the company's behalf.
|
|
854
|
-
subject: invitation to join %{company}
|
|
855
|
-
title: Join %{company}
|
|
856
852
|
compare_at_amount: Compare at amount
|
|
857
853
|
compare_at_price: Compare at price
|
|
858
854
|
complete: complete
|
|
@@ -912,21 +908,6 @@ en:
|
|
|
912
908
|
anonymize_failed: The erasure could not be completed and nothing was changed
|
|
913
909
|
customer_group: Customer Group
|
|
914
910
|
customer_groups: Customer Groups
|
|
915
|
-
customer_mailer:
|
|
916
|
-
data_export_email:
|
|
917
|
-
action: Download Your Data
|
|
918
|
-
expiry_notice: This download link expires on %{date}.
|
|
919
|
-
greeting: Hello,
|
|
920
|
-
ignore_notice: If you did not request a copy of your data, please contact us.
|
|
921
|
-
instructions: Your copy of the personal data we hold about you is ready. Use the button below to download it.
|
|
922
|
-
subject: Your Data Is Ready to Download
|
|
923
|
-
password_reset_email:
|
|
924
|
-
action: Reset Password
|
|
925
|
-
expiry_notice: This password reset link will expire shortly. If you did not request this, no action is needed.
|
|
926
|
-
greeting: Hi %{name},
|
|
927
|
-
ignore_notice: If you did not request a password reset, you can safely ignore this email. Your password will not be changed.
|
|
928
|
-
instructions: We received a request to reset your password. Click the button below to choose a new password.
|
|
929
|
-
subject: Password Reset Instructions
|
|
930
911
|
customer_support_email: Customer Support Email
|
|
931
912
|
customers: Customers
|
|
932
913
|
cut: Cut
|
|
@@ -985,14 +966,6 @@ en:
|
|
|
985
966
|
developers: Developers
|
|
986
967
|
digital:
|
|
987
968
|
digital_delivery: Digital Delivery
|
|
988
|
-
digital_asset_mailer:
|
|
989
|
-
files_ready_email:
|
|
990
|
-
available_until: Available until %{date}
|
|
991
|
-
dear_customer: Dear %{name},
|
|
992
|
-
instructions: The files from your order %{number} are ready to download.
|
|
993
|
-
store_team: "%{store_name} Team"
|
|
994
|
-
subject: Your files are ready
|
|
995
|
-
thanks: Thank you for your business.
|
|
996
969
|
digital_assets: Digital assets
|
|
997
970
|
digital_assets_attachment_must_be_private: Upload the file to private storage before attaching it
|
|
998
971
|
discontinue_on: Discontinue On
|
|
@@ -1145,17 +1118,6 @@ en:
|
|
|
1145
1118
|
free_shipping: Free Shipping
|
|
1146
1119
|
from: From
|
|
1147
1120
|
front_end: Front End
|
|
1148
|
-
fulfillment_mailer:
|
|
1149
|
-
fulfilled_email:
|
|
1150
|
-
dear_customer: Dear %{name},
|
|
1151
|
-
delivery_method: 'Delivery method: %{delivery_method}'
|
|
1152
|
-
fulfillment_summary: Fulfillment summary for order
|
|
1153
|
-
instructions: Your order %{number} has been fulfilled
|
|
1154
|
-
store_team: "%{store_name} Team"
|
|
1155
|
-
subject: Fulfillment Notification
|
|
1156
|
-
thanks: Thank you for your business.
|
|
1157
|
-
track_information: 'Tracking Information: %{tracking}'
|
|
1158
|
-
track_link: 'Tracking Link: %{url}'
|
|
1159
1121
|
fulfillments:
|
|
1160
1122
|
errors:
|
|
1161
1123
|
backordered_units: This fulfillment holds backordered units and cannot be handed over yet
|
|
@@ -1331,16 +1293,6 @@ en:
|
|
|
1331
1293
|
new_state: New State
|
|
1332
1294
|
new_user: New User
|
|
1333
1295
|
new_variant: New Variant
|
|
1334
|
-
newsletter_mailer:
|
|
1335
|
-
email_confirmation:
|
|
1336
|
-
dear_customer: Hey,
|
|
1337
|
-
ignore_disclaimer: If you didn't request this, please ignore this email.
|
|
1338
|
-
instructions: Please click the button below to verify your email address.
|
|
1339
|
-
message_copy_link: 'If the button doesn''t work please copy and paste the following link to your browser:'
|
|
1340
|
-
store_team: "%{store_name} Team"
|
|
1341
|
-
subject: Verify your email address
|
|
1342
|
-
thanks: Thank you
|
|
1343
|
-
verify_email_address_link: Verify email address
|
|
1344
1296
|
newsletter_subscribers: Newsletter Subscribers
|
|
1345
1297
|
next: Next
|
|
1346
1298
|
no_cc_type: N/A
|
|
@@ -1372,34 +1324,6 @@ en:
|
|
|
1372
1324
|
order: Order
|
|
1373
1325
|
order_canceled: Order canceled
|
|
1374
1326
|
order_cannot_be_deleted: Completed orders and orders with settled payments cannot be deleted
|
|
1375
|
-
order_mailer:
|
|
1376
|
-
cancel_email:
|
|
1377
|
-
dear_customer: Hey %{name},
|
|
1378
|
-
instructions: Your order has been CANCELED. Please retain this cancellation information for your records.
|
|
1379
|
-
order_summary_canceled: Order %{number} Summary [CANCELED]
|
|
1380
|
-
subject: Cancellation of Order
|
|
1381
|
-
thanks: Thank you for
|
|
1382
|
-
confirm_email:
|
|
1383
|
-
dear_customer: Hey %{name},
|
|
1384
|
-
instructions: Please review and retain the following order information for your records.
|
|
1385
|
-
order_summary: Order %{number} Summary
|
|
1386
|
-
subject: Order Confirmation
|
|
1387
|
-
thanks: Thank you
|
|
1388
|
-
payment_link_email:
|
|
1389
|
-
message: Please click the button below to pay for your order.
|
|
1390
|
-
message_copy_link: 'If the button doesn''t work please copy and paste the following link to your browser:'
|
|
1391
|
-
pay_for_order: Pay for order
|
|
1392
|
-
subject: 'Payment link for order #%{number}'
|
|
1393
|
-
po_number: 'PO Number: %{number}'
|
|
1394
|
-
store_owner_notification_email:
|
|
1395
|
-
heading: New Order Received
|
|
1396
|
-
instructions: You have received a new order.
|
|
1397
|
-
order_summary: Order %{number} Summary
|
|
1398
|
-
subject: "%{store_name} received a new order"
|
|
1399
|
-
thanks: Thank you
|
|
1400
|
-
store_team: "%{store_name} Team"
|
|
1401
|
-
subtotal: 'Subtotal:'
|
|
1402
|
-
total: 'Order Total:'
|
|
1403
1327
|
order_not_found: We couldn't find your order. Please try that action again.
|
|
1404
1328
|
order_number: Order %{number}
|
|
1405
1329
|
order_resumed: Order resumed
|
|
@@ -1537,6 +1461,7 @@ en:
|
|
|
1537
1461
|
price_list_import_invalid_amount: "%{column} must be a number like 16.50, got %{value}."
|
|
1538
1462
|
price_list_import_invalid_quantity: min_quantity must be a whole number of units, got %{value}.
|
|
1539
1463
|
price_list_import_no_price_list: This import is not attached to a price list.
|
|
1464
|
+
price_list_import_price_rises_with_quantity: The price from %{quantity} units must not be above %{floor}, what a buyer pays below that quantity. A file is applied a row at a time, so list the higher quantities first when lowering a whole ladder.
|
|
1540
1465
|
price_list_import_sku_required: SKU is required.
|
|
1541
1466
|
price_list_import_too_many_breaks: A variant can carry at most %{count} quantity breaks on one price list.
|
|
1542
1467
|
price_list_import_unknown_sku: No product in this store has the SKU %{sku}.
|
|
@@ -1571,7 +1496,7 @@ en:
|
|
|
1571
1496
|
description: Creates a promotion credit adjustment on a line item
|
|
1572
1497
|
name: Create per-line-item adjustment
|
|
1573
1498
|
create_line_items:
|
|
1574
|
-
description:
|
|
1499
|
+
description: Adds the specified quantity of a variant to the cart free of charge
|
|
1575
1500
|
name: Create line items
|
|
1576
1501
|
free_shipping:
|
|
1577
1502
|
description: Makes all shipments for the order free
|
|
@@ -2002,16 +1927,6 @@ en:
|
|
|
2002
1927
|
return: return
|
|
2003
1928
|
return_errors:
|
|
2004
1929
|
outside_window: This order is outside the %{days}-day return window
|
|
2005
|
-
return_mailer:
|
|
2006
|
-
refunded_email:
|
|
2007
|
-
dear_customer: Dear %{name},
|
|
2008
|
-
instructions: Your return for order %{number} has been refunded.
|
|
2009
|
-
refund_summary: Refund Summary
|
|
2010
|
-
returned_items: Returned Items
|
|
2011
|
-
store_team: "%{store_name} Team"
|
|
2012
|
-
subject: Return Refunded
|
|
2013
|
-
thanks: Thank you for your business.
|
|
2014
|
-
total_refunded: 'Total refunded: %{total}'
|
|
2015
1930
|
return_quantity: Return Quantity
|
|
2016
1931
|
returned: Returned
|
|
2017
1932
|
returns: Returns
|
|
@@ -2052,22 +1967,6 @@ en:
|
|
|
2052
1967
|
selected_quantity_not_available: selected of %{item} is not available.
|
|
2053
1968
|
seller: Seller
|
|
2054
1969
|
seller_access_denied: You do not have access to this seller.
|
|
2055
|
-
seller_mailer:
|
|
2056
|
-
approved_email:
|
|
2057
|
-
action: Go to your dashboard
|
|
2058
|
-
heading: You're approved
|
|
2059
|
-
message: Your application to sell on %{store_name} has been approved. You can now list products and start taking orders.
|
|
2060
|
-
subject: You're approved to sell on %{store_name}
|
|
2061
|
-
get_in_touch: If you think this is a mistake, reply to this email or contact us at %{store_email}.
|
|
2062
|
-
greeting: Hi %{seller_name},
|
|
2063
|
-
rejected_email:
|
|
2064
|
-
heading: About your application
|
|
2065
|
-
message: Your application to sell on %{store_name} has not been accepted.
|
|
2066
|
-
subject: Your application to sell on %{store_name}
|
|
2067
|
-
suspended_email:
|
|
2068
|
-
heading: Your account has been suspended
|
|
2069
|
-
message: Selling on %{store_name} has been paused for your account. Your products and order history are unaffected.
|
|
2070
|
-
subject: Your %{store_name} account has been suspended
|
|
2071
1970
|
seller_membership_required: This account does not belong to any seller.
|
|
2072
1971
|
seller_payout_nothing_to_settle: This seller has nothing to settle right now.
|
|
2073
1972
|
seller_requirement_types:
|
|
@@ -2132,17 +2031,6 @@ en:
|
|
|
2132
2031
|
ship_total: Ship Total
|
|
2133
2032
|
shipment: Shipment
|
|
2134
2033
|
shipment_adjustments: Shipment adjustments
|
|
2135
|
-
shipment_mailer:
|
|
2136
|
-
shipped_email:
|
|
2137
|
-
dear_customer: Dear %{name},
|
|
2138
|
-
instructions: Your order %{number} has been shipped
|
|
2139
|
-
shipment_summary: Shipment Summary for order
|
|
2140
|
-
shipping_method: 'Shipping method: %{shipping_method}'
|
|
2141
|
-
store_team: "%{store_name} Team"
|
|
2142
|
-
subject: Shipment Notification
|
|
2143
|
-
thanks: Thank you for your business.
|
|
2144
|
-
track_information: 'Tracking Information: %{tracking}'
|
|
2145
|
-
track_link: 'Tracking Link: %{url}'
|
|
2146
2034
|
shipment_state: Shipment State
|
|
2147
2035
|
shipments: Shipments
|
|
2148
2036
|
shipped: Shipped
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class AddEmailDeliveryFlagsToSpreeOrderGroups < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
# The same pair spree_orders carries, for the same reason: a confirmation
|
|
4
|
+
# is sent once, and completion is replayable — a resumed finalize
|
|
5
|
+
# re-publishes order_group.completed, so without a record of what was sent
|
|
6
|
+
# a customer gets the same email twice. These say what happened, not what
|
|
7
|
+
# the purchase is, so unlike the group's statuses they cannot be derived
|
|
8
|
+
# from the children.
|
|
9
|
+
add_column :spree_order_groups, :confirmation_delivered, :boolean, default: false
|
|
10
|
+
add_column :spree_order_groups, :store_owner_notification_delivered, :boolean, default: false
|
|
11
|
+
|
|
12
|
+
add_index :spree_order_groups, :confirmation_delivered
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class ChangeSpreeUserIdentitiesTokensToText < ActiveRecord::Migration[8.1]
|
|
2
|
+
def up
|
|
3
|
+
change_column :spree_user_identities, :access_token, :text
|
|
4
|
+
change_column :spree_user_identities, :refresh_token, :text
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def down
|
|
8
|
+
raise ActiveRecord::IrreversibleMigration, 'Encrypted tokens may not fit back into string columns'
|
|
9
|
+
end
|
|
10
|
+
end
|
data/db/sample_data/channels.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Adds two extra channels alongside the seeded 'Online Store' so sample data
|
|
2
2
|
# exercises the channel-aware code paths (publishing, channel filters,
|
|
3
3
|
# channel-scoped order attribution).
|
|
4
|
-
store = Spree::
|
|
4
|
+
store = Spree::Current.store
|
|
5
5
|
|
|
6
6
|
store.channels.find_or_create_by!(code: 'pos') do |channel|
|
|
7
7
|
channel.name = 'Point of Sale'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Three collections that between them exercise both membership models: two
|
|
2
2
|
# rule-based (they regenerate themselves) and one hand-curated with a dynamic
|
|
3
3
|
# sort. Runs after the product import so there is a catalog to draw from.
|
|
4
|
-
store = Spree::
|
|
4
|
+
store = Spree::Current.store
|
|
5
5
|
|
|
6
6
|
# Each automatic collection is defined by the single rule that materializes it.
|
|
7
7
|
automatic_collections = [
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# type stamps it at creation. The one thing a CSV row cannot carry is the file
|
|
4
4
|
# buyers download, so that is all this step does: attach a PDF to the imported
|
|
5
5
|
# product's default variant.
|
|
6
|
-
store = Spree::
|
|
6
|
+
store = Spree::Current.store
|
|
7
7
|
|
|
8
8
|
product = Spree::Product.find_by(store: store, slug: 'the-spree-commerce-handbook')
|
|
9
9
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Shopify-style delivery methods on the Domestic and International zones.
|
|
3
3
|
# The free international method showcases delivery-method rules — free
|
|
4
4
|
# above a spend threshold, hidden below it.
|
|
5
|
-
store = Spree::
|
|
5
|
+
store = Spree::Current.store
|
|
6
6
|
|
|
7
7
|
# The zones are created from the store's country at first-run setup, so an
|
|
8
8
|
# install that has not been set up yet has none. Provision them here from
|
data/db/sample_data/options.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
store = Spree::Current.store
|
|
2
|
+
|
|
3
|
+
color = Spree::OptionType.for_store(store).find_or_initialize_by(name: 'color')
|
|
2
4
|
color.label = 'Color'
|
|
3
5
|
color.kind = 'color_swatch'
|
|
4
6
|
color.save!
|
|
@@ -15,7 +17,7 @@ color.save!
|
|
|
15
17
|
# sells only new goods should not find this on a fresh install. A marketplace
|
|
16
18
|
# that wants its sellers to describe condition the same way loads it, or
|
|
17
19
|
# creates its own.
|
|
18
|
-
condition = Spree::OptionType.find_or_initialize_by(name: 'condition')
|
|
20
|
+
condition = Spree::OptionType.for_store(store).find_or_initialize_by(name: 'condition')
|
|
19
21
|
condition.label = 'Condition'
|
|
20
22
|
condition.kind = 'buttons'
|
|
21
23
|
condition.filterable = true
|
data/db/sample_data/orders.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
store = Spree::
|
|
1
|
+
store = Spree::Current.store
|
|
2
2
|
|
|
3
3
|
# Find products created by the product import
|
|
4
|
-
product_1 =
|
|
5
|
-
product_2 =
|
|
4
|
+
product_1 = store.products.find_by(name: 'Automatic Espresso Machine')
|
|
5
|
+
product_2 = store.products.find_by(name: 'Drip Coffee Maker 1.5L')
|
|
6
6
|
|
|
7
7
|
unless product_1 && product_2
|
|
8
8
|
puts ' Skipping orders: required products not found'
|
|
@@ -84,7 +84,7 @@ orders.each(&:rebuild_fulfillments!)
|
|
|
84
84
|
Spree::Order.where(id: orders.map(&:id)).update_all(status: 'placed', completed_at: Time.current - 1.day)
|
|
85
85
|
|
|
86
86
|
# Tax lines (zero-amount sample rows against the California rate)
|
|
87
|
-
tax_rate =
|
|
87
|
+
tax_rate = store.tax_rates.find_by(name: 'California')
|
|
88
88
|
|
|
89
89
|
if tax_rate
|
|
90
90
|
orders.each do |order|
|
|
@@ -105,7 +105,7 @@ if tax_rate
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
# Payments
|
|
108
|
-
method =
|
|
108
|
+
method = store.payment_methods.where(name: 'Credit Card', active: true).first
|
|
109
109
|
|
|
110
110
|
if method
|
|
111
111
|
Spree::Gateway.class_eval do
|
|
@@ -114,8 +114,7 @@ if method
|
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
credit_card = Spree::CreditCard.find_or_initialize_by(gateway_customer_profile_id: 'BGS-1234')
|
|
118
|
-
credit_card.payment_method = method
|
|
117
|
+
credit_card = Spree::CreditCard.find_or_initialize_by(gateway_customer_profile_id: 'BGS-1234', payment_method: method)
|
|
119
118
|
credit_card.cc_type = 'visa'
|
|
120
119
|
credit_card.month = 12
|
|
121
120
|
credit_card.year = 2.years.from_now.year
|
|
@@ -140,15 +139,15 @@ orders.each { |order| order.reload.update_statuses! }
|
|
|
140
139
|
|
|
141
140
|
# A return in progress, so the admin has something to look at. Built through
|
|
142
141
|
# the workflows rather than by direct writes — they own every transition.
|
|
143
|
-
first_complete_order =
|
|
142
|
+
first_complete_order = store.orders.complete.first
|
|
144
143
|
if first_complete_order && first_complete_order.fulfillment_items.any?
|
|
145
144
|
fulfillment_item = first_complete_order.fulfillment_items.first
|
|
146
145
|
|
|
147
146
|
Spree::Returns::Create.call(
|
|
148
147
|
order: first_complete_order,
|
|
149
148
|
items: [{ fulfillment_item: fulfillment_item, quantity: 1 }],
|
|
150
|
-
stock_location: fulfillment_item.fulfillment&.stock_location ||
|
|
151
|
-
reason:
|
|
149
|
+
stock_location: fulfillment_item.fulfillment&.stock_location || store.stock_locations.first,
|
|
150
|
+
reason: store.return_reasons.first,
|
|
152
151
|
memo: 'Sample return'
|
|
153
152
|
)
|
|
154
153
|
end
|