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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/app/jobs/spree/payments/handle_webhook_job.rb +1 -0
  3. data/app/jobs/spree/sample_data/load_job.rb +7 -2
  4. data/app/jobs/spree/seller_transfers/execute_pending_job.rb +1 -0
  5. data/app/mailers/spree/admin_user_mailer.rb +2 -2
  6. data/app/models/concerns/spree/customer_methods.rb +17 -1
  7. data/app/models/concerns/spree/has_number.rb +3 -2
  8. data/app/models/concerns/spree/prefixed_id.rb +4 -1
  9. data/app/models/concerns/spree/purchase/totals.rb +3 -1
  10. data/app/models/concerns/spree/ransackable_attributes.rb +38 -6
  11. data/app/models/concerns/spree/translatable_resource.rb +6 -7
  12. data/app/models/concerns/spree/webhook_payload_redaction.rb +132 -21
  13. data/app/models/spree/adjusters/promotion.rb +1 -1
  14. data/app/models/spree/authentication/lockout.rb +34 -0
  15. data/app/models/spree/authentication/strategies/email_password_strategy.rb +4 -12
  16. data/app/models/spree/authentication/strategies/oidc_strategy.rb +1 -1
  17. data/app/models/spree/claim.rb +1 -0
  18. data/app/models/spree/commission_line.rb +4 -2
  19. data/app/models/spree/commission_rule.rb +1 -1
  20. data/app/models/spree/coupon_code.rb +14 -2
  21. data/app/models/spree/current.rb +15 -0
  22. data/app/models/spree/exchange.rb +1 -0
  23. data/app/models/spree/export.rb +8 -5
  24. data/app/models/spree/exports/customers.rb +2 -2
  25. data/app/models/spree/fulfillment_group.rb +127 -0
  26. data/app/models/spree/gift_card.rb +9 -0
  27. data/app/models/spree/invitation.rb +18 -5
  28. data/app/models/spree/number_generators/sequential.rb +2 -2
  29. data/app/models/spree/option_value.rb +7 -0
  30. data/app/models/spree/order.rb +35 -12
  31. data/app/models/spree/order_group.rb +78 -6
  32. data/app/models/spree/payment_source.rb +1 -1
  33. data/app/models/spree/payment_split.rb +24 -0
  34. data/app/models/spree/price.rb +74 -0
  35. data/app/models/spree/product.rb +4 -0
  36. data/app/models/spree/product_option_type.rb +11 -0
  37. data/app/models/spree/promotion/actions/create_line_items.rb +92 -30
  38. data/app/models/spree/promotion/rules/category.rb +14 -9
  39. data/app/models/spree/promotion/rules/product.rb +17 -1
  40. data/app/models/spree/promotion.rb +1 -1
  41. data/app/models/spree/promotion_action.rb +12 -1
  42. data/app/models/spree/promotion_handler/coupon.rb +50 -5
  43. data/app/models/spree/promotion_rule.rb +14 -0
  44. data/app/models/spree/return.rb +16 -6
  45. data/app/models/spree/return_line_item.rb +19 -0
  46. data/app/models/spree/search_provider/database.rb +10 -2
  47. data/app/models/spree/seller_payout.rb +3 -1
  48. data/app/models/spree/seller_transfer.rb +3 -1
  49. data/app/models/spree/store_credit.rb +4 -0
  50. data/app/models/spree/translations/batch.rb +4 -2
  51. data/app/models/spree/user_identity.rb +10 -6
  52. data/app/models/spree/variant.rb +8 -3
  53. data/app/models/spree/webhook_delivery.rb +28 -0
  54. data/app/models/spree/webhook_endpoint.rb +14 -0
  55. data/app/presenters/spree/csv/order_line_item_presenter.rb +2 -2
  56. data/app/services/spree/carts/split_by_seller.rb +22 -12
  57. data/app/services/spree/companies/add_member.rb +8 -2
  58. data/app/services/spree/imports/row_processors/price_list_price.rb +6 -0
  59. data/app/services/spree/imports/row_processors/product_translation.rb +1 -1
  60. data/app/services/spree/imports/row_processors/product_variant.rb +17 -11
  61. data/app/services/spree/order_groups/allocate_payments.rb +3 -0
  62. data/app/services/spree/orders/build_fulfillments.rb +6 -0
  63. data/app/services/spree/orders/update_statuses.rb +8 -2
  64. data/app/services/spree/prices/bulk_upsert.rb +186 -47
  65. data/app/services/spree/sample_data/import_builder.rb +1 -1
  66. data/app/services/spree/sample_data/loader.rb +54 -40
  67. data/app/services/spree/seeds/all.rb +1 -26
  68. data/app/services/spree/seeds/allowed_origins.rb +5 -4
  69. data/app/services/spree/seeds/api_keys.rb +10 -9
  70. data/app/services/spree/seeds/channels.rb +9 -8
  71. data/app/services/spree/seeds/commission_rates.rb +21 -20
  72. data/app/services/spree/seeds/customer_groups.rb +5 -4
  73. data/app/services/spree/seeds/digital_delivery.rb +7 -8
  74. data/app/services/spree/seeds/payment_methods.rb +12 -11
  75. data/app/services/spree/seeds/product_types.rb +7 -8
  76. data/app/services/spree/seeds/returns_environment.rb +12 -11
  77. data/app/services/spree/seeds/roles.rb +8 -7
  78. data/app/services/spree/seeds/saved_reports.rb +19 -20
  79. data/app/services/spree/seeds/seller_requirements.rb +5 -4
  80. data/app/services/spree/seeds/store_resources.rb +41 -0
  81. data/app/services/spree/seeds/store_scoped.rb +15 -0
  82. data/app/services/spree/seeds/tax_categories.rb +10 -9
  83. data/app/services/spree/stock_locations/stock_items/create.rb +1 -1
  84. data/app/services/spree/stock_locations/stock_levels/create.rb +19 -3
  85. data/app/subscribers/spree/order_status_subscriber.rb +15 -7
  86. data/app/workflows/spree/carts/add_item.rb +5 -7
  87. data/app/workflows/spree/carts/complete.rb +20 -64
  88. data/app/workflows/spree/carts/recalculate_totals.rb +3 -1
  89. data/app/workflows/spree/claims/create.rb +31 -1
  90. data/app/workflows/spree/claims/resolve.rb +7 -11
  91. data/app/workflows/spree/exchanges/create.rb +11 -0
  92. data/app/workflows/spree/exchanges/fulfill.rb +21 -13
  93. data/app/workflows/spree/orders/complete.rb +99 -2
  94. data/app/workflows/spree/price_lists/update.rb +17 -0
  95. data/app/workflows/spree/refunds/order_payments.rb +55 -1
  96. data/app/workflows/spree/returns/create.rb +9 -15
  97. data/app/workflows/spree/returns/refund.rb +15 -24
  98. data/app/workflows/spree/returns/returnable_quantity.rb +53 -0
  99. data/app/workflows/spree/sellers/invite.rb +1 -1
  100. data/config/locales/en.yml +7 -119
  101. data/db/migrate/20260916000001_add_email_delivery_flags_to_spree_order_groups.rb +14 -0
  102. data/db/migrate/20260922000001_add_refunded_order_to_spree_store_credits.rb +5 -0
  103. data/db/migrate/20260923000001_change_spree_user_identities_tokens_to_text.rb +10 -0
  104. data/db/sample_data/channels.rb +1 -1
  105. data/db/sample_data/collections.rb +1 -1
  106. data/db/sample_data/custom_field_definitions.rb +1 -1
  107. data/db/sample_data/digital_products.rb +1 -1
  108. data/db/sample_data/fulfillment.rb +1 -1
  109. data/db/sample_data/options.rb +4 -2
  110. data/db/sample_data/orders.rb +9 -10
  111. data/db/sample_data/payment_methods.rb +18 -18
  112. data/db/sample_data/product_type_categories.rb +1 -1
  113. data/db/sample_data/product_types.rb +2 -2
  114. data/db/sample_data/promotions.rb +2 -4
  115. data/db/sample_data/wholesale.rb +1 -1
  116. data/lib/generators/spree/api_resource/api_resource_generator.rb +58 -1
  117. data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +6 -5
  118. data/lib/generators/spree/api_resource/templates/permissions.en.yml.tt +6 -0
  119. data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +2 -2
  120. data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
  121. data/lib/generators/spree/subscriber/subscriber_generator.rb +3 -3
  122. data/lib/spree/core/engine.rb +8 -8
  123. data/lib/spree/core/version.rb +1 -1
  124. data/lib/spree/iso_data.rb +23 -4
  125. data/lib/spree/store_scope_guard.rb +16 -1
  126. data/lib/spree/testing_support/factories/invitation_factory.rb +1 -0
  127. data/lib/spree/testing_support/factories/order_group_factory.rb +49 -0
  128. data/lib/spree/testing_support/factories/payment_factory.rb +7 -2
  129. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
  130. data/lib/tasks/sample_data.rake +8 -2
  131. data/lib/tasks/upgrade.rake +6 -21
  132. metadata +13 -4
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Spree
4
4
  module Refunds
5
- # Gives a post-sale workflow one way to put money back on an order.
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 on an earlier request must not come back twice.
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
- def returnable_quantity_for(fulfillment_item)
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, matching what
56
- # +received_total+ refunds: a customer who announced three items and sent
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.received_quantity.to_i.positive? }
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
- # of three items gets two items' worth.
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
- @amount_to_refund = (amount || received_total).to_d
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 > return_record.refundable_total.to_d
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
- credit = Spree::StoreCredit.create!(
105
- store: return_record.store,
106
- customer: return_record.order.customer,
98
+ @refunds = issue_refund_store_credit(
99
+ order: return_record.order,
107
100
  amount: @amount_to_refund,
108
- currency: return_record.currency,
109
- created_by: refunder,
110
- originator: return_record,
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
@@ -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: Populates the cart with the specified quantity of variant
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,5 @@
1
+ class AddRefundedOrderToSpreeStoreCredits < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_reference :spree_store_credits, :refunded_order, index: true
4
+ end
5
+ 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
@@ -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::Store.default
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::Store.default
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 = [
@@ -1,4 +1,4 @@
1
- store = Spree::Store.default
1
+ store = Spree::Current.store
2
2
 
3
3
  %w[warranty capacity voltage wattage runtime room_coverage noise_level connectivity].each do |key|
4
4
  store.custom_field_definitions.find_or_create_by!(
@@ -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::Store.default
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::Store.default
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
@@ -1,4 +1,6 @@
1
- color = Spree::OptionType.find_or_initialize_by(name: 'color')
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
@@ -1,8 +1,8 @@
1
- store = Spree::Store.default
1
+ store = Spree::Current.store
2
2
 
3
3
  # Find products created by the product import
4
- product_1 = Spree::Product.find_by(name: 'Automatic Espresso Machine')
5
- product_2 = Spree::Product.find_by(name: 'Drip Coffee Maker 1.5L')
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 = Spree::TaxRate.find_by(name: 'California')
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 = Spree::PaymentMethod.where(name: 'Credit Card', active: true).first
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 = Spree::Order.complete.first
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 || Spree::StockLocation.first,
151
- reason: Spree::ReturnReason.first,
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