spree_core 6.0.0.rc4 → 6.0.0.rc5
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/models/concerns/spree/purchase/addresses.rb +95 -32
- data/app/models/concerns/spree/webhook_payload_redaction.rb +1 -0
- data/app/models/spree/ability.rb +8 -0
- data/app/models/spree/address.rb +6 -0
- data/app/models/spree/cart.rb +3 -6
- data/app/models/spree/company.rb +1 -1
- data/app/models/spree/data_request.rb +34 -1
- data/app/models/spree/order.rb +1 -1
- data/app/models/spree/sales_context.rb +162 -0
- data/app/models/spree/tax_line.rb +5 -4
- data/app/services/spree/carts/destroy.rb +7 -2
- data/app/services/spree/carts/update.rb +1 -4
- data/app/services/spree/data_requests/confirm.rb +37 -0
- data/app/services/spree/data_requests/create.rb +61 -13
- data/app/workflows/spree/customers/anonymize.rb +38 -17
- data/app/workflows/spree/data_requests/fulfill.rb +20 -6
- data/app/workflows/spree/orders/create.rb +41 -17
- data/config/locales/en.yml +4 -0
- data/lib/spree/core/engine.rb +2 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/testing_support/factories/data_request_factory.rb +4 -0
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e7960e9257c5de15c7d9e69d8916c669c5ab082fccd02e17c5ee9c9d9d378c8
|
|
4
|
+
data.tar.gz: 3c160cd58beb2d3fc3c99bead42bc0d3f365952ae7c2b2c103d92004303f7d13
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b396fb4c3d553e372dab904f57cb794015b4c2670028fea33c775fc343ac844db1d0d2733a9c47cfb2450898408ad4696ba312114719f7b3d1ad55716149f674
|
|
7
|
+
data.tar.gz: 89e9eb2e33742ec34f5c89e5e732544a868e4241ba5886cfa357e0ca43929107d895024ff6e936952c3648cf51ac379e7839ac2d211dc2515697d2ff52d8e7a7
|
|
@@ -3,8 +3,8 @@ module Spree
|
|
|
3
3
|
# The full address surface shared by Spree::Cart and Spree::Order:
|
|
4
4
|
# the bill/ship associations and their public aliases, default filling
|
|
5
5
|
# from the customer's saved addresses, address-row deduplication on
|
|
6
|
-
# write, ownership-guarded address_id assignment, promotion
|
|
7
|
-
# addresses to the customer's defaults, and the ship→bill cloning
|
|
6
|
+
# write, ownership-guarded address_id assignment and deletion, promotion
|
|
7
|
+
# of checkout addresses to the customer's defaults, and the ship→bill cloning
|
|
8
8
|
# predicate. The shipping address is canonical: use_shipping copies it
|
|
9
9
|
# onto the billing address; the reverse use_billing direction is a
|
|
10
10
|
# deprecated 6.1-removal bridge. Clone callbacks are wired per model.
|
|
@@ -12,8 +12,13 @@ module Spree
|
|
|
12
12
|
extend ActiveSupport::Concern
|
|
13
13
|
|
|
14
14
|
included do
|
|
15
|
-
belongs_to :bill_address, class_name: 'Spree::Address', optional: true
|
|
16
|
-
belongs_to :ship_address, class_name: 'Spree::Address', optional: true
|
|
15
|
+
belongs_to :bill_address, class_name: 'Spree::Address', optional: true
|
|
16
|
+
belongs_to :ship_address, class_name: 'Spree::Address', optional: true
|
|
17
|
+
|
|
18
|
+
# Stands in for `dependent: :destroy`, which would take any attached
|
|
19
|
+
# row down with the purchase. See {#owns_address?} for why only some
|
|
20
|
+
# of them may go.
|
|
21
|
+
after_destroy :destroy_own_addresses
|
|
17
22
|
|
|
18
23
|
# The writers these generate are shadowed by the hand-written ones
|
|
19
24
|
# below, which deduplicate address rows. The declarations still earn
|
|
@@ -40,9 +45,12 @@ module Spree
|
|
|
40
45
|
before_validation :clone_shipping_address, if: :use_shipping?
|
|
41
46
|
end
|
|
42
47
|
|
|
48
|
+
# Whether both addresses are present and match. With neither present
|
|
49
|
+
# there is nothing to call the same.
|
|
50
|
+
#
|
|
43
51
|
# @return [Boolean]
|
|
44
52
|
def shipping_eq_billing_address?
|
|
45
|
-
bill_address == ship_address
|
|
53
|
+
ship_address.present? && bill_address == ship_address
|
|
46
54
|
end
|
|
47
55
|
|
|
48
56
|
# Whether checkout must collect a customer shipping address. Selected
|
|
@@ -90,6 +98,68 @@ module Spree
|
|
|
90
98
|
use_shipping.in?([true, 'true', '1'])
|
|
91
99
|
end
|
|
92
100
|
|
|
101
|
+
# The address behind an id a client may select, or nil when the id names
|
|
102
|
+
# one this purchase has no business shipping to. Takes a prefixed id or a
|
|
103
|
+
# raw primary key, so the services that resolve a client-supplied id and
|
|
104
|
+
# the writers below share one rule rather than each inventing their own.
|
|
105
|
+
#
|
|
106
|
+
# Two books qualify. The buyer's own is the ordinary case. The company's
|
|
107
|
+
# is the B2B one: a purchase for a node ships to that node's sites, and
|
|
108
|
+
# to its ancestors' — the same self-and-ancestors chain the node's
|
|
109
|
+
# default address is prefilled from, so a division can pick the
|
|
110
|
+
# headquarters address it already inherits.
|
|
111
|
+
#
|
|
112
|
+
# @param id [Integer, String, nil]
|
|
113
|
+
# @return [Spree::Address, nil]
|
|
114
|
+
def selectable_address(id)
|
|
115
|
+
address = ::Spree::Address.not_deleted.find_by_param(id) || ::Spree::Address.not_deleted.find_by(id: id)
|
|
116
|
+
return if address.nil?
|
|
117
|
+
|
|
118
|
+
return address if customer_id.present? && address.customer_owned? && address.owner_id == customer_id
|
|
119
|
+
|
|
120
|
+
company = readable_company
|
|
121
|
+
return if company.nil? || address.owner_type != 'Spree::Company'
|
|
122
|
+
|
|
123
|
+
company.self_and_ancestors.any? { |node| node.id == address.owner_id } ? address : nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Everything a checkout address picker may offer for this purchase: the
|
|
127
|
+
# buyer's own book plus the book of the company it names, that node's
|
|
128
|
+
# own entries and the ones it inherits, limited to the countries the
|
|
129
|
+
# purchase's market sells to. A subset of what {#selectable_address}
|
|
130
|
+
# accepts, so a picker built from it never offers an address the
|
|
131
|
+
# checkout cannot deliver to.
|
|
132
|
+
#
|
|
133
|
+
# @return [ActiveRecord::Relation<Spree::Address>]
|
|
134
|
+
def selectable_addresses
|
|
135
|
+
books = []
|
|
136
|
+
books << ::Spree::Address.not_deleted.where(owner_type: ::Spree.customer_class.to_s, owner_id: customer_id) if customer_id.present?
|
|
137
|
+
books << readable_company&.address_book
|
|
138
|
+
|
|
139
|
+
addresses = books.compact.reduce { |left, right| left.or(right) } || ::Spree::Address.none
|
|
140
|
+
return addresses if market.nil?
|
|
141
|
+
|
|
142
|
+
addresses.where(country_code: market.market_countries.select(:country_code))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Whether an attached address row is this purchase's own to delete.
|
|
146
|
+
#
|
|
147
|
+
# Only the ownerless rows are: the one a guest typed in, and the
|
|
148
|
+
# snapshot completion copies onto the order. Everything else is an entry
|
|
149
|
+
# in somebody's address book — the buyer's, a company node's, a seller's
|
|
150
|
+
# — which the purchase is merely pointing at. A cart may name a company
|
|
151
|
+
# site every other member ships from, so taking an attached row down
|
|
152
|
+
# with the cart would let one member empty the shared book.
|
|
153
|
+
#
|
|
154
|
+
# A business snapshot keeps its +owner_type+ to stay addressed to a
|
|
155
|
+
# company rather than to a person, so ownership is the id, not the type.
|
|
156
|
+
#
|
|
157
|
+
# @param address [Spree::Address, nil]
|
|
158
|
+
# @return [Boolean]
|
|
159
|
+
def owns_address?(address)
|
|
160
|
+
address.present? && address.owner_id.blank?
|
|
161
|
+
end
|
|
162
|
+
|
|
93
163
|
# Fills any blank bill/ship address from the customer's valid saved
|
|
94
164
|
# defaults. The ship address is skipped when no physical delivery is
|
|
95
165
|
# required, so shipping-address validations never fire on digital-only
|
|
@@ -174,6 +244,26 @@ module Spree
|
|
|
174
244
|
|
|
175
245
|
private
|
|
176
246
|
|
|
247
|
+
# The company whose book this purchase may read: only the one the
|
|
248
|
+
# client named, never one inferred from a sole membership. A cart asks
|
|
249
|
+
# again rather than trusting the column, which nothing clears when a
|
|
250
|
+
# membership is revoked; an order's authority is staff or a completed
|
|
251
|
+
# cart, so it keeps what it was given.
|
|
252
|
+
#
|
|
253
|
+
# @return [Spree::Company, nil]
|
|
254
|
+
def readable_company
|
|
255
|
+
return company if company.nil? || !is_a?(Spree::Cart)
|
|
256
|
+
|
|
257
|
+
company if customer&.standing_for?(company)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Both slots often hold the same row, so destroy it once.
|
|
261
|
+
def destroy_own_addresses
|
|
262
|
+
[bill_address, ship_address].compact.uniq.each do |address|
|
|
263
|
+
address.destroy if owns_address?(address)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
177
267
|
def items_require_shipping_address?
|
|
178
268
|
# Answered by the profile kind, resolved once per profile.
|
|
179
269
|
profile_requires_address = Hash.new do |cache, profile|
|
|
@@ -187,33 +277,6 @@ module Spree
|
|
|
187
277
|
end
|
|
188
278
|
end
|
|
189
279
|
|
|
190
|
-
# The address behind an id a client may select, or nil when the id names
|
|
191
|
-
# one this purchase has no business shipping to.
|
|
192
|
-
#
|
|
193
|
-
# Two books qualify. The buyer's own is the ordinary case. The company's
|
|
194
|
-
# is the B2B one: a purchase for a node ships to that node's sites, and
|
|
195
|
-
# to its ancestors' — the same self-and-ancestors chain the node's
|
|
196
|
-
# default address is prefilled from, so a division can pick the
|
|
197
|
-
# headquarters address it already inherits.
|
|
198
|
-
#
|
|
199
|
-
# Standing is not re-checked here: a cart cannot name a company its
|
|
200
|
-
# customer lacks standing over, so reaching this point already means the
|
|
201
|
-
# buyer may act for the node.
|
|
202
|
-
#
|
|
203
|
-
# @param id [Integer, String, nil]
|
|
204
|
-
# @return [Spree::Address, nil]
|
|
205
|
-
def selectable_address(id)
|
|
206
|
-
address = ::Spree::Address.find_by(id: id)
|
|
207
|
-
return nil if address.nil?
|
|
208
|
-
|
|
209
|
-
return address if customer_id.present? && address.customer_owned? && address.owner_id == customer_id
|
|
210
|
-
|
|
211
|
-
company = resolved_company
|
|
212
|
-
return nil if company.nil? || address.owner_type != 'Spree::Company'
|
|
213
|
-
|
|
214
|
-
company.self_and_ancestors.any? { |node| node.id == address.owner_id } ? address : nil
|
|
215
|
-
end
|
|
216
|
-
|
|
217
280
|
# Reuse never reaches past what the buyer already owns: a signed-in buyer
|
|
218
281
|
# may name any entry of their own address book, and a guest — who keeps
|
|
219
282
|
# no book — may only edit the row already in the slot being written.
|
data/app/models/spree/ability.rb
CHANGED
|
@@ -167,6 +167,9 @@ module Spree
|
|
|
167
167
|
|
|
168
168
|
# Reference data every staff member can read regardless of keys — address
|
|
169
169
|
# forms need countries/states, and the dashboard shell reads the store.
|
|
170
|
+
# The back office also reads the store's markets and channels: its
|
|
171
|
+
# currencies and languages come from the markets, and list filters offer
|
|
172
|
+
# both.
|
|
170
173
|
#
|
|
171
174
|
# The store grant is unrestricted only for the store's own back office.
|
|
172
175
|
# Another panel reads the one store it operates under and no other, so a
|
|
@@ -180,6 +183,11 @@ module Spree
|
|
|
180
183
|
else
|
|
181
184
|
can :read, Spree::Store, id: @store.id
|
|
182
185
|
end
|
|
186
|
+
|
|
187
|
+
return unless store_resource?
|
|
188
|
+
|
|
189
|
+
can :read, Spree::Market, store_id: @resource.id
|
|
190
|
+
can :read, Spree::Channel, store_id: @resource.id
|
|
183
191
|
end
|
|
184
192
|
|
|
185
193
|
end
|
data/app/models/spree/address.rb
CHANGED
|
@@ -343,6 +343,12 @@ module Spree
|
|
|
343
343
|
def snapshot
|
|
344
344
|
dup.tap do |copy|
|
|
345
345
|
copy.owner = nil
|
|
346
|
+
|
|
347
|
+
if business_owned?
|
|
348
|
+
copy.company = company.presence || owner&.name
|
|
349
|
+
copy.owner_type = owner_type
|
|
350
|
+
end
|
|
351
|
+
|
|
346
352
|
copy.label = nil
|
|
347
353
|
copy.deleted_at = nil
|
|
348
354
|
end
|
data/app/models/spree/cart.rb
CHANGED
|
@@ -284,12 +284,9 @@ module Spree
|
|
|
284
284
|
#
|
|
285
285
|
# @return [ActiveRecord::Relation<Spree::Variant>]
|
|
286
286
|
def orderable_variants
|
|
287
|
-
|
|
288
|
-
store: store, channel: channel, customer: customer, company: resolved_company,
|
|
289
|
-
|
|
290
|
-
).value
|
|
291
|
-
|
|
292
|
-
Spree::Variant.for_products(products)
|
|
287
|
+
Spree::SalesContext.new(
|
|
288
|
+
store: store, channel: channel, customer: customer, company: resolved_company, resolve_company: false
|
|
289
|
+
).orderable_variants
|
|
293
290
|
end
|
|
294
291
|
|
|
295
292
|
# Removes out-of-stock/discontinued items and populates warnings
|
data/app/models/spree/company.rb
CHANGED
|
@@ -245,7 +245,7 @@ module Spree
|
|
|
245
245
|
#
|
|
246
246
|
# @return [ActiveRecord::Relation]
|
|
247
247
|
def address_book
|
|
248
|
-
Spree::Address.where(owner_type: 'Spree::Company', owner_id: self_and_ancestors.map(&:id))
|
|
248
|
+
Spree::Address.not_deleted.where(owner_type: 'Spree::Company', owner_id: self_and_ancestors.map(&:id))
|
|
249
249
|
end
|
|
250
250
|
|
|
251
251
|
private
|
|
@@ -20,9 +20,12 @@ module Spree
|
|
|
20
20
|
|
|
21
21
|
publishes_lifecycle_events
|
|
22
22
|
publishes_event :completed
|
|
23
|
+
publishes_event :confirmation_requested,
|
|
24
|
+
serializer: 'Spree::Api::V3::DataRequestConfirmationRequestedEventSerializer',
|
|
25
|
+
credential: 'write_customers'
|
|
23
26
|
|
|
24
27
|
include Spree::HasStatus
|
|
25
|
-
has_status :pending, :processing, :completed, :failed, default: :pending
|
|
28
|
+
has_status :awaiting_confirmation, :pending, :processing, :completed, :failed, :lapsed, default: :pending
|
|
26
29
|
|
|
27
30
|
ACCESS = 'access'.freeze
|
|
28
31
|
ERASURE = 'erasure'.freeze
|
|
@@ -33,11 +36,23 @@ module Spree
|
|
|
33
36
|
# about them is not left lying around.
|
|
34
37
|
DEFAULT_EXPIRY = 7.days
|
|
35
38
|
|
|
39
|
+
# How long a request a customer raised waits for confirmation from its
|
|
40
|
+
# email address before it lapses.
|
|
41
|
+
CONFIRMATION_WINDOW = 24.hours
|
|
42
|
+
|
|
36
43
|
# Addresses the finished export. Plaintext like the digital-link token:
|
|
37
44
|
# the URL is the credential, it is only ever sent to the address that
|
|
38
45
|
# asked, and it stops working when the request expires.
|
|
39
46
|
has_secure_token :download_token
|
|
40
47
|
|
|
48
|
+
# Proves that whoever confirms reads mail at the address the request
|
|
49
|
+
# names. Bound to
|
|
50
|
+
# the download token, so an erasure, which clears it, voids every link
|
|
51
|
+
# already sent.
|
|
52
|
+
generates_token_for :confirmation do
|
|
53
|
+
download_token
|
|
54
|
+
end
|
|
55
|
+
|
|
41
56
|
belongs_to :customer, class_name: Spree.customer_class.to_s
|
|
42
57
|
# Null when the subject asked for it themselves; set when staff acted on a
|
|
43
58
|
# request that arrived by email.
|
|
@@ -52,6 +67,8 @@ module Spree
|
|
|
52
67
|
scope :access, -> { where(kind: ACCESS) }
|
|
53
68
|
scope :erasure, -> { where(kind: ERASURE) }
|
|
54
69
|
scope :in_progress, -> { with_status(:pending, :processing) }
|
|
70
|
+
scope :confirmable, -> { awaiting_confirmation.where(requested_at: CONFIRMATION_WINDOW.ago..) }
|
|
71
|
+
scope :past_confirmation_window, -> { awaiting_confirmation.where(requested_at: ...CONFIRMATION_WINDOW.ago) }
|
|
55
72
|
scope :recent_first, -> { order(requested_at: :desc) }
|
|
56
73
|
|
|
57
74
|
self.whitelisted_ransackable_attributes = %w[number kind status email requested_at completed_at]
|
|
@@ -89,6 +106,22 @@ module Spree
|
|
|
89
106
|
expires_at.present? && expires_at.past?
|
|
90
107
|
end
|
|
91
108
|
|
|
109
|
+
# Whether the request is still waiting for confirmation, within the window
|
|
110
|
+
# to receive it, and still names the account's address.
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def confirmable?
|
|
113
|
+
awaiting_confirmation? && requested_at > CONFIRMATION_WINDOW.ago && addressed_to_customer?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Whether the request names the address the account has now. The work
|
|
117
|
+
# matches guest rows by the account's address when it runs, so a request
|
|
118
|
+
# for an address the account has since left would reach rows it never
|
|
119
|
+
# named.
|
|
120
|
+
# @return [Boolean]
|
|
121
|
+
def addressed_to_customer?
|
|
122
|
+
customer.present? && email.to_s.casecmp?(customer.email.to_s)
|
|
123
|
+
end
|
|
124
|
+
|
|
92
125
|
private
|
|
93
126
|
|
|
94
127
|
def set_defaults
|
data/app/models/spree/order.rb
CHANGED
|
@@ -152,7 +152,7 @@ module Spree
|
|
|
152
152
|
self.whitelisted_ransackable_attributes = %w[
|
|
153
153
|
completed_at email number status payment_status payment_state fulfillment_status shipment_state delivery_total
|
|
154
154
|
total item_total total_quantity considered_risky channel_id currency coupon_code customer_id seller_id
|
|
155
|
-
order_group_id po_number
|
|
155
|
+
order_group_id po_number market_id locale
|
|
156
156
|
]
|
|
157
157
|
self.whitelisted_ransackable_scopes = %w[complete incomplete refunded partially_refunded search]
|
|
158
158
|
self.ransackable_scope_types = %w[complete incomplete refunded partially_refunded].index_with('boolean')
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Who buys, on which channel, in which market: what a buyer can be sold, and
|
|
3
|
+
# at what price, depends on all of these together. A market fixes the
|
|
4
|
+
# currency and the languages a sale may use.
|
|
5
|
+
class SalesContext
|
|
6
|
+
include ActiveModel::Model
|
|
7
|
+
include ActiveModel::Attributes
|
|
8
|
+
|
|
9
|
+
attribute :store
|
|
10
|
+
attribute :market
|
|
11
|
+
attribute :channel
|
|
12
|
+
attribute :customer
|
|
13
|
+
attribute :company
|
|
14
|
+
attribute :country
|
|
15
|
+
attribute :currency, :string
|
|
16
|
+
attribute :locale, :string
|
|
17
|
+
# A placed sale keeps the company it was stamped with, even none, so only
|
|
18
|
+
# a sale still being put together looks up the customer's company.
|
|
19
|
+
attribute :resolve_company, :boolean, default: true
|
|
20
|
+
|
|
21
|
+
validate :currency_sold_in_a_market
|
|
22
|
+
validate :currency_matches_market
|
|
23
|
+
validate :locale_supported_by_market
|
|
24
|
+
|
|
25
|
+
# @param order [Spree::Order]
|
|
26
|
+
# @return [Spree::SalesContext]
|
|
27
|
+
def self.from_order(order)
|
|
28
|
+
new(
|
|
29
|
+
store: order.store, market: order.market, channel: order.channel, customer: order.customer,
|
|
30
|
+
company: order.resolved_company, resolve_company: false, country: order.tax_country,
|
|
31
|
+
currency: order.currency, locale: order.locale
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The market chosen for the sale, else one selling the requested currency,
|
|
36
|
+
# else the store's default.
|
|
37
|
+
#
|
|
38
|
+
# @return [Spree::Market, nil]
|
|
39
|
+
def market
|
|
40
|
+
chosen = super
|
|
41
|
+
return chosen if chosen
|
|
42
|
+
return @resolved_market if defined?(@resolved_market)
|
|
43
|
+
|
|
44
|
+
@resolved_market = resolve_market
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def channel
|
|
48
|
+
super || store&.default_channel
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [String] the requested currency, else the market's, else the store's
|
|
52
|
+
def currency
|
|
53
|
+
requested_currency || market&.currency || store&.default_currency
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [String, nil] the requested language, else the market's default
|
|
57
|
+
def locale
|
|
58
|
+
super.presence || market&.default_locale
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The company chosen for the sale, else the one the customer stands in
|
|
62
|
+
# alone — the same answer pricing and catalog visibility reach.
|
|
63
|
+
#
|
|
64
|
+
# @return [Spree::Company, nil]
|
|
65
|
+
def company
|
|
66
|
+
chosen = super
|
|
67
|
+
return chosen if chosen || !resolve_company
|
|
68
|
+
return @standing_company if defined?(@standing_company)
|
|
69
|
+
|
|
70
|
+
@standing_company =
|
|
71
|
+
if customer.is_a?(Spree.customer_class) && store
|
|
72
|
+
Spree::Company.sole_standing_for(store: store, customer: customer)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Variants the buyer may order on the channel: published there and inside
|
|
77
|
+
# their catalogs.
|
|
78
|
+
#
|
|
79
|
+
# @return [ActiveRecord::Relation<Spree::Variant>]
|
|
80
|
+
def orderable_variants
|
|
81
|
+
@orderable_variants ||= Spree::Variant.for_products(
|
|
82
|
+
Spree.products_for_context_service.call(
|
|
83
|
+
store: store, channel: channel, customer: customer, company: company,
|
|
84
|
+
base: store.products.not_discontinued_on(channel)
|
|
85
|
+
).value
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Orderable variants the buyer can be quoted in the sale's currency: a base
|
|
90
|
+
# price, or a price from a list that applies to them, so a product sold to
|
|
91
|
+
# a buyer only under their agreement is offered to that buyer alone.
|
|
92
|
+
#
|
|
93
|
+
# @return [ActiveRecord::Relation<Spree::Variant>]
|
|
94
|
+
def sellable_variants
|
|
95
|
+
prices = Spree::Price.where(variant_id: orderable_variants.select(:id), currency: currency)
|
|
96
|
+
.where.not(amount: nil)
|
|
97
|
+
.where(price_list_id: [nil, *applicable_price_list_ids])
|
|
98
|
+
|
|
99
|
+
orderable_variants.where(id: prices.select(:variant_id))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Spree::Pricing::Context] the context prices are resolved in
|
|
103
|
+
def pricing_context
|
|
104
|
+
@pricing_context ||= Spree::Pricing::Context.new(
|
|
105
|
+
currency: currency, store: store, country: country, market: market,
|
|
106
|
+
channel: channel, user: customer, company: company
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def requested_currency
|
|
113
|
+
attributes['currency'].presence&.upcase
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def resolve_market
|
|
117
|
+
return unless store
|
|
118
|
+
|
|
119
|
+
candidates = [(Spree::Current.market if Spree::Current.store == store), store.default_market].compact
|
|
120
|
+
return candidates.first unless requested_currency
|
|
121
|
+
|
|
122
|
+
candidates.find { |candidate| candidate.currency == requested_currency } ||
|
|
123
|
+
store.markets.find_by(currency: requested_currency)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Catalog price lists the buyer's catalogs carry, and standalone lists
|
|
127
|
+
# whose rules match the sale, judged the way the internal pricing provider
|
|
128
|
+
# judges them.
|
|
129
|
+
def applicable_price_list_ids
|
|
130
|
+
@applicable_price_list_ids ||= begin
|
|
131
|
+
catalogs = Spree::Catalog.groups_for_buyer(store: store, customer: customer, company: company, channel: channel)
|
|
132
|
+
catalog_lists = catalogs.flatten.filter_map(&:price_list).select do |list|
|
|
133
|
+
list.currently_active? && list.contextual_rules_applicable?(pricing_context)
|
|
134
|
+
end
|
|
135
|
+
standalone_lists = Spree::PriceList.for_context(pricing_context).select { |list| list.applicable?(pricing_context) }
|
|
136
|
+
|
|
137
|
+
(catalog_lists + standalone_lists).map(&:id).uniq
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def currency_sold_in_a_market
|
|
142
|
+
return if requested_currency.nil? || market || store.nil? || !store.markets.exists?
|
|
143
|
+
|
|
144
|
+
errors.add(:currency, :currency_not_supported_by_store, message: I18n.t('spree.currency_not_supported_by_store'))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def currency_matches_market
|
|
148
|
+
return if market.nil? || currency == market.currency
|
|
149
|
+
|
|
150
|
+
errors.add(
|
|
151
|
+
:currency, :currency_does_not_match_market,
|
|
152
|
+
message: I18n.t('spree.currency_does_not_match_market', currency: market.currency)
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def locale_supported_by_market
|
|
157
|
+
return if market.nil? || locale.blank? || market.supported_locales_list.include?(locale)
|
|
158
|
+
|
|
159
|
+
errors.add(:locale, :locale_not_supported_by_market, message: I18n.t('spree.locale_not_supported_by_market'))
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -22,10 +22,11 @@ module Spree
|
|
|
22
22
|
# core doesn't know about registers its reason here. The bar for a new
|
|
23
23
|
# value: it distinguishes a treatment that reporting or invoicing has to
|
|
24
24
|
# tell apart, not merely a different amount.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
BUILT_IN_TAXABILITY_REASONS = %w[standard_rated reduced_rated zero_rated reverse_charge
|
|
26
|
+
intra_community_supply export customer_exempt product_exempt
|
|
27
|
+
not_collecting not_subject_to_tax].freeze
|
|
28
|
+
|
|
29
|
+
class_attribute :taxability_reasons, default: BUILT_IN_TAXABILITY_REASONS.dup
|
|
29
30
|
|
|
30
31
|
# Provider payloads (jurisdiction breakdowns, external ids) read nil-safe.
|
|
31
32
|
attribute :data, default: -> { {} }
|
|
@@ -52,9 +52,14 @@ module Spree
|
|
|
52
52
|
success(cart: cart)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Lets go of everything the cart was only borrowing, so the cascade that
|
|
56
|
+
# follows reaches nothing but the cart's own rows. An entry in somebody's
|
|
57
|
+
# address book stays in it, and a row a fulfillment or a completed order
|
|
58
|
+
# still points at is detached rather than soft-deleted out from under
|
|
59
|
+
# them.
|
|
55
60
|
def clear_addresses(cart:)
|
|
56
|
-
cart.ship_address = nil unless cart.ship_address
|
|
57
|
-
cart.bill_address = nil unless cart.bill_address
|
|
61
|
+
cart.ship_address = nil unless cart.owns_address?(cart.ship_address) && cart.ship_address.can_be_deleted?
|
|
62
|
+
cart.bill_address = nil unless cart.owns_address?(cart.bill_address) && cart.bill_address.can_be_deleted?
|
|
58
63
|
|
|
59
64
|
success(cart: cart)
|
|
60
65
|
end
|
|
@@ -206,10 +206,7 @@ module Spree
|
|
|
206
206
|
end
|
|
207
207
|
|
|
208
208
|
def resolve_address_id(prefixed_id)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
decoded = Spree::Address.decode_prefixed_id(prefixed_id)
|
|
212
|
-
decoded ? cart.customer.addresses.find_by(id: decoded)&.id : nil
|
|
209
|
+
cart.selectable_address(prefixed_id)&.id
|
|
213
210
|
end
|
|
214
211
|
|
|
215
212
|
def assign_market
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module DataRequests
|
|
3
|
+
# Confirms a request the customer raised, from the link emailed to the
|
|
4
|
+
# address it names, and queues the work.
|
|
5
|
+
#
|
|
6
|
+
# Confirming again is answered with the request as it stands, so a second
|
|
7
|
+
# click on the same link changes nothing.
|
|
8
|
+
class Confirm
|
|
9
|
+
prepend Spree::ServiceModule::Base
|
|
10
|
+
|
|
11
|
+
# @param data_request [Spree::DataRequest]
|
|
12
|
+
# @return [Spree::ServiceModule::Result] the confirmed request
|
|
13
|
+
def call(data_request:)
|
|
14
|
+
newly_confirmed = false
|
|
15
|
+
|
|
16
|
+
data_request.with_lock do
|
|
17
|
+
newly_confirmed = data_request.confirmable?
|
|
18
|
+
|
|
19
|
+
if newly_confirmed
|
|
20
|
+
data_request.update!(status: 'pending')
|
|
21
|
+
elsif data_request.awaiting_confirmation?
|
|
22
|
+
data_request.update!(status: 'lapsed')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if data_request.lapsed?
|
|
27
|
+
data_request.errors.add(:base, I18n.t('spree.data_request_errors.confirmation_invalid'))
|
|
28
|
+
return failure(data_request)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Spree::DataRequests::ProcessJob.perform_later(data_request.prefixed_id) if newly_confirmed
|
|
32
|
+
|
|
33
|
+
success(data_request)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -7,9 +7,15 @@ module Spree
|
|
|
7
7
|
# whole history, and a customer holding the button down should not be able
|
|
8
8
|
# to queue a hundred of them. It is also the honest answer — the request
|
|
9
9
|
# they already made is still being worked on.
|
|
10
|
+
#
|
|
11
|
+
# A request the customer raised themselves waits for confirmation from its
|
|
12
|
+
# email address instead of being queued, because it reaches guest orders
|
|
13
|
+
# placed at that address. Asking again while it waits sends the email again.
|
|
10
14
|
class Create
|
|
11
15
|
prepend Spree::ServiceModule::Base
|
|
12
16
|
|
|
17
|
+
CONFIRMATION_EVENT = 'data_request.confirmation_requested'.freeze
|
|
18
|
+
|
|
13
19
|
# @param store [Spree::Store]
|
|
14
20
|
# @param customer [Spree::Customer]
|
|
15
21
|
# @param kind [String] Spree::DataRequest::ACCESS or ERASURE
|
|
@@ -19,8 +25,12 @@ module Spree
|
|
|
19
25
|
# caller fulfils the request itself — the admin export renders the file
|
|
20
26
|
# inline for a waiting staffer, and queueing would additionally email
|
|
21
27
|
# the customer a copy of something they did not ask for
|
|
28
|
+
# @param confirm_by_email [Boolean] hold the request until it is
|
|
29
|
+
# confirmed from its email address; true for the customer's own requests
|
|
30
|
+
# @param redirect_url [String, nil] the storefront page the confirmation
|
|
31
|
+
# link opens, already checked against the store's allowed origins
|
|
22
32
|
# @return [Spree::ServiceModule::Result]
|
|
23
|
-
def call(store:, customer:, kind:, requested_by: nil, process: true)
|
|
33
|
+
def call(store:, customer:, kind:, requested_by: nil, process: true, confirm_by_email: false, redirect_url: nil)
|
|
24
34
|
# Nothing useful is left to answer with: an export would return the
|
|
25
35
|
# tombstone and email a link to an address that no longer receives,
|
|
26
36
|
# and a second erasure has nothing to erase.
|
|
@@ -38,7 +48,9 @@ module Spree
|
|
|
38
48
|
# of the same person's whole history; taking the lock closes it without
|
|
39
49
|
# a partial unique index, which MySQL does not support.
|
|
40
50
|
customer.with_lock do
|
|
41
|
-
|
|
51
|
+
lapse_unconfirmed_requests(store: store, customer: customer, kind: kind, staff_request: !confirm_by_email)
|
|
52
|
+
existing = in_flight_request(store: store, customer: customer, kind: kind,
|
|
53
|
+
include_unconfirmed: confirm_by_email)
|
|
42
54
|
|
|
43
55
|
unless existing
|
|
44
56
|
data_request = Spree::DataRequest.new(
|
|
@@ -46,20 +58,27 @@ module Spree
|
|
|
46
58
|
customer: customer,
|
|
47
59
|
kind: kind.to_s,
|
|
48
60
|
email: customer.email,
|
|
49
|
-
requested_by: requested_by
|
|
61
|
+
requested_by: requested_by,
|
|
62
|
+
status: confirm_by_email ? 'awaiting_confirmation' : 'pending'
|
|
50
63
|
)
|
|
51
64
|
data_request.save
|
|
52
65
|
end
|
|
53
66
|
end
|
|
54
67
|
|
|
55
|
-
return
|
|
56
|
-
return failure(data_request) unless data_request.persisted?
|
|
68
|
+
return failure(data_request) if data_request && !data_request.persisted?
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
# and it should not contend for the row this flow still holds.
|
|
60
|
-
Spree::DataRequests::ProcessJob.perform_later(data_request.prefixed_id) if process
|
|
70
|
+
request = existing || data_request
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
# Both happen after the lock is released: the job may start at once and
|
|
73
|
+
# should not contend for the row this flow holds, and the email must
|
|
74
|
+
# not go out before the row is committed.
|
|
75
|
+
if request.awaiting_confirmation?
|
|
76
|
+
request_confirmation(request, redirect_url: redirect_url)
|
|
77
|
+
elsif data_request && process
|
|
78
|
+
Spree::DataRequests::ProcessJob.perform_later(data_request.prefixed_id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
success(request)
|
|
63
82
|
end
|
|
64
83
|
|
|
65
84
|
private
|
|
@@ -67,10 +86,39 @@ module Spree
|
|
|
67
86
|
# Only the person's own requests. A staff member exporting from the admin
|
|
68
87
|
# opens their own row and answers it inline, and handing that back here
|
|
69
88
|
# would close the customer's request with a file they can never download.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
# A request still waiting for confirmation is returned only to the
|
|
90
|
+
# customer: staff have checked who they are dealing with, so theirs
|
|
91
|
+
# proceeds on its own row (see lapse_unconfirmed_requests).
|
|
92
|
+
def in_flight_request(store:, customer:, kind:, include_unconfirmed:)
|
|
93
|
+
requests = Spree::DataRequest.where(store_id: store.id, customer_id: customer.id, kind: kind.to_s,
|
|
94
|
+
requested_by_id: nil)
|
|
95
|
+
open_requests = include_unconfirmed ? requests.in_progress.or(requests.confirmable) : requests.in_progress
|
|
96
|
+
|
|
97
|
+
open_requests.recent_first.first
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Lapses what can no longer be confirmed: requests past the window, and
|
|
101
|
+
# requests naming an address the account has since moved away from, so
|
|
102
|
+
# asking again emails the current address. A staff request of the same
|
|
103
|
+
# kind replaces the customer's unconfirmed one, which would otherwise
|
|
104
|
+
# run the same work a second time once confirmed.
|
|
105
|
+
def lapse_unconfirmed_requests(store:, customer:, kind:, staff_request:)
|
|
106
|
+
unconfirmed = Spree::DataRequest.where(store_id: store.id, customer_id: customer.id).awaiting_confirmation
|
|
107
|
+
stale = unconfirmed.past_confirmation_window.or(unconfirmed.where.not(email: customer.email))
|
|
108
|
+
stale = stale.or(unconfirmed.where(kind: kind.to_s)) if staff_request
|
|
109
|
+
|
|
110
|
+
stale.find_each { |data_request| data_request.update!(status: 'lapsed') }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The token travels in a credential event, which reaches only webhook
|
|
114
|
+
# endpoints that name it and keeps the token out of the delivery log.
|
|
115
|
+
def request_confirmation(data_request, redirect_url:)
|
|
116
|
+
payload = data_request.event_payload_for(
|
|
117
|
+
CONFIRMATION_EVENT,
|
|
118
|
+
confirmation_token: data_request.generate_token_for(:confirmation),
|
|
119
|
+
redirect_url: redirect_url.presence
|
|
120
|
+
)
|
|
121
|
+
data_request.publish_event(CONFIRMATION_EVENT, payload)
|
|
74
122
|
end
|
|
75
123
|
end
|
|
76
124
|
end
|
|
@@ -212,9 +212,15 @@ module Spree
|
|
|
212
212
|
# redacting that would overwrite the shared address for every other
|
|
213
213
|
# member and every other order. Only rows this person owns, or rows
|
|
214
214
|
# nobody owns, are this erasure's to rewrite.
|
|
215
|
+
#
|
|
216
|
+
# Ownership is the id alone, never the type. A snapshot taken from a
|
|
217
|
+
# company site keeps its owner_type so the copy stays addressed to a
|
|
218
|
+
# business rather than to a person, so matching on the type too would
|
|
219
|
+
# skip exactly those copies — and an erasure that quietly covers less
|
|
220
|
+
# than it did raises nothing.
|
|
215
221
|
snapshots = Spree::Address.where(id: address_ids)
|
|
216
222
|
mine = snapshots.where(owner_type: customer.class.base_class.to_s, owner_id: customer.id)
|
|
217
|
-
unowned = snapshots.where(
|
|
223
|
+
unowned = snapshots.where(owner_id: nil)
|
|
218
224
|
|
|
219
225
|
mine.or(unowned).find_each do |address|
|
|
220
226
|
redact_address(address, deleted: false)
|
|
@@ -407,7 +413,9 @@ module Spree
|
|
|
407
413
|
end
|
|
408
414
|
|
|
409
415
|
# The token goes too, so a link already emailed stops resolving rather
|
|
410
|
-
# than waiting out its expiry.
|
|
416
|
+
# than waiting out its expiry. Confirmation links are bound to it, so
|
|
417
|
+
# a request still waiting for one can never be confirmed.
|
|
418
|
+
requests.awaiting_confirmation.update_all(status: 'lapsed', updated_at: Time.current)
|
|
411
419
|
requests.update_all(
|
|
412
420
|
email: customer.email,
|
|
413
421
|
download_token: nil,
|
|
@@ -458,22 +466,35 @@ module Spree
|
|
|
458
466
|
|
|
459
467
|
# @param address [Spree::Address]
|
|
460
468
|
# @param deleted [Boolean] whether to soft-delete it as well
|
|
469
|
+
# What the person left on an address, rewritten; the address itself kept
|
|
470
|
+
# readable enough to answer a tax enquiry.
|
|
471
|
+
#
|
|
472
|
+
# A snapshot copied from a company site is the narrower case. The dock
|
|
473
|
+
# and the trading name on it are the business's, not this person's, and
|
|
474
|
+
# erasing them would take the merchant's record of a lawful sale with
|
|
475
|
+
# them — so only the contact details go. A business address is allowed
|
|
476
|
+
# to name nobody, which is why those fields are emptied rather than
|
|
477
|
+
# stamped: writing "Redacted" where a company never named anyone would
|
|
478
|
+
# invent a person who was not there.
|
|
461
479
|
def redact_address(address, deleted:)
|
|
462
|
-
attributes =
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
480
|
+
attributes =
|
|
481
|
+
if address.business_owned?
|
|
482
|
+
{ first_name: nil, last_name: nil }
|
|
483
|
+
else
|
|
484
|
+
{
|
|
485
|
+
first_name: REDACTED_NAME,
|
|
486
|
+
last_name: REDACTED_NAME,
|
|
487
|
+
address1: REDACTED_NAME,
|
|
488
|
+
address2: nil,
|
|
489
|
+
company: nil,
|
|
490
|
+
label: nil,
|
|
491
|
+
latitude: nil,
|
|
492
|
+
longitude: nil,
|
|
493
|
+
postal_code: truncated_postal_code(address.postal_code)
|
|
494
|
+
}
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
attributes.merge!(phone: nil, alternative_phone: nil, metadata: {}, updated_at: Time.current)
|
|
477
498
|
attributes[:deleted_at] = Time.current if deleted && address.deleted_at.nil?
|
|
478
499
|
|
|
479
500
|
address.update_columns(attributes)
|
|
@@ -36,6 +36,7 @@ module Spree
|
|
|
36
36
|
super
|
|
37
37
|
|
|
38
38
|
step :ensure_pending
|
|
39
|
+
step :ensure_addressed_to_customer
|
|
39
40
|
step :mark_processing
|
|
40
41
|
|
|
41
42
|
run_hooks :before_fulfill
|
|
@@ -56,6 +57,15 @@ module Spree
|
|
|
56
57
|
failure(data_request, I18n.t('spree.data_request_errors.not_pending'))
|
|
57
58
|
end
|
|
58
59
|
|
|
60
|
+
# An account already erased has a replaced address by design, and
|
|
61
|
+
# erase_customer counts that as done.
|
|
62
|
+
def ensure_addressed_to_customer
|
|
63
|
+
customer = data_request.customer
|
|
64
|
+
return if customer.nil? || customer.anonymized? || data_request.addressed_to_customer?
|
|
65
|
+
|
|
66
|
+
failure(data_request, I18n.t('spree.data_request_errors.address_changed'))
|
|
67
|
+
end
|
|
68
|
+
|
|
59
69
|
def mark_processing
|
|
60
70
|
data_request.update!(status: 'processing')
|
|
61
71
|
end
|
|
@@ -78,21 +88,25 @@ module Spree
|
|
|
78
88
|
# anonymizer starts after the check and finishes its attachment scan
|
|
79
89
|
# before this attaches, stranding a full personal-data file it will
|
|
80
90
|
# never see.
|
|
91
|
+
#
|
|
92
|
+
# The address is checked again under the same lock, so it cannot change
|
|
93
|
+
# between the check and the export reading it.
|
|
81
94
|
return attach_export unless customer
|
|
82
95
|
|
|
83
|
-
|
|
96
|
+
refusal = nil
|
|
84
97
|
|
|
85
98
|
customer.with_lock do
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
customer.reload
|
|
100
|
+
if customer.anonymized?
|
|
101
|
+
refusal = 'spree.data_request_errors.already_anonymized'
|
|
102
|
+
elsif !data_request.addressed_to_customer?
|
|
103
|
+
refusal = 'spree.data_request_errors.address_changed'
|
|
88
104
|
else
|
|
89
105
|
attach_export
|
|
90
106
|
end
|
|
91
107
|
end
|
|
92
108
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
failure(data_request, I18n.t('spree.data_request_errors.already_anonymized'))
|
|
109
|
+
failure(data_request, I18n.t(refusal)) if refusal
|
|
96
110
|
end
|
|
97
111
|
|
|
98
112
|
def attach_export
|
|
@@ -28,6 +28,14 @@ module Spree
|
|
|
28
28
|
|
|
29
29
|
return failure(:store_is_required) if store.nil?
|
|
30
30
|
|
|
31
|
+
@sales_context = Spree::SalesContext.new(
|
|
32
|
+
store: store,
|
|
33
|
+
market: (resolve_market if @params[:market_id].present?),
|
|
34
|
+
currency: @params[:currency],
|
|
35
|
+
locale: @params[:locale]
|
|
36
|
+
)
|
|
37
|
+
return refuse_sales_context unless @sales_context.valid?
|
|
38
|
+
|
|
31
39
|
order = nil
|
|
32
40
|
ApplicationRecord.transaction do
|
|
33
41
|
order = build_order
|
|
@@ -60,8 +68,9 @@ module Spree
|
|
|
60
68
|
customer: @customer,
|
|
61
69
|
created_by: @created_by,
|
|
62
70
|
email: @params[:email] || @customer&.email,
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
market: @sales_context.market,
|
|
72
|
+
currency: @sales_context.currency,
|
|
73
|
+
locale: @sales_context.locale || Spree::Current.locale,
|
|
65
74
|
customer_note: @params[:customer_note],
|
|
66
75
|
po_number: @params[:po_number],
|
|
67
76
|
internal_note: @params[:internal_note],
|
|
@@ -74,7 +83,6 @@ module Spree
|
|
|
74
83
|
# signed id, so it assigns like any other attribute.
|
|
75
84
|
attrs[:po_document_signed_id] = @params[:po_document_signed_id] if @params[:po_document_signed_id].present?
|
|
76
85
|
|
|
77
|
-
attrs[:market] = resolve_market if @params[:market_id].present?
|
|
78
86
|
attrs[:channel] = resolve_channel if @params[:channel_id].present?
|
|
79
87
|
attrs[:company] = resolve_company if @params[:company_id].present?
|
|
80
88
|
attrs[:preferred_stock_location] = resolve_preferred_stock_location if @params[:preferred_stock_location_id].present?
|
|
@@ -83,6 +91,12 @@ module Spree
|
|
|
83
91
|
@store.orders.new(attrs)
|
|
84
92
|
end
|
|
85
93
|
|
|
94
|
+
def refuse_sales_context
|
|
95
|
+
order = @store.orders.new
|
|
96
|
+
order.errors.merge!(@sales_context.errors)
|
|
97
|
+
failure(order, order.errors.full_messages.to_sentence)
|
|
98
|
+
end
|
|
99
|
+
|
|
86
100
|
def resolve_market
|
|
87
101
|
@store.markets.find_by_param!(@params[:market_id])
|
|
88
102
|
end
|
|
@@ -100,34 +114,44 @@ module Spree
|
|
|
100
114
|
end
|
|
101
115
|
|
|
102
116
|
def assign_addresses(order)
|
|
103
|
-
if @params[:use_customer_default_address]
|
|
104
|
-
@customer.association(:bill_address).load_target
|
|
105
|
-
@customer.association(:ship_address).load_target
|
|
106
|
-
order.bill_address = @customer.bill_address&.snapshot
|
|
107
|
-
order.ship_address = @customer.ship_address&.snapshot
|
|
108
|
-
end
|
|
117
|
+
assign_default_addresses(order) if @params[:use_customer_default_address]
|
|
109
118
|
|
|
110
119
|
assign_address(order, :ship_address, @params[:shipping_address_id], @params[:shipping_address])
|
|
111
120
|
assign_address(order, :bill_address, @params[:billing_address_id], @params[:billing_address])
|
|
121
|
+
order.use_shipping = @params[:use_shipping]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def assign_default_addresses(order)
|
|
125
|
+
company = order.company
|
|
126
|
+
|
|
127
|
+
if company.present?
|
|
128
|
+
order.bill_address = company.default_billing_address&.snapshot
|
|
129
|
+
order.ship_address = company.default_shipping_address&.snapshot
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
return if @customer.nil?
|
|
134
|
+
|
|
135
|
+
@customer.association(:bill_address).load_target
|
|
136
|
+
@customer.association(:ship_address).load_target
|
|
137
|
+
order.bill_address = @customer.bill_address&.snapshot
|
|
138
|
+
order.ship_address = @customer.ship_address&.snapshot
|
|
112
139
|
end
|
|
113
140
|
|
|
114
141
|
def assign_address(order, association, address_id, address_attrs)
|
|
115
142
|
if address_id.present?
|
|
116
|
-
address =
|
|
143
|
+
address = order.selectable_address(address_id)
|
|
117
144
|
order.public_send(:"#{association}_id=", address.id) if address
|
|
118
145
|
elsif address_attrs.present?
|
|
119
146
|
order.public_send(:"#{association}_attributes=", address_attrs)
|
|
120
147
|
end
|
|
121
148
|
end
|
|
122
149
|
|
|
123
|
-
def resolve_user_address(address_id)
|
|
124
|
-
return unless @customer
|
|
125
|
-
|
|
126
|
-
@customer.addresses.find_by_param(address_id)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
150
|
def add_items(order)
|
|
130
|
-
result = Spree::Orders::UpsertItems.call(
|
|
151
|
+
result = Spree::Orders::UpsertItems.call(
|
|
152
|
+
order: order, items: @params[:items],
|
|
153
|
+
orderable_variants: Spree::SalesContext.from_order(order).orderable_variants
|
|
154
|
+
)
|
|
131
155
|
return if result.success?
|
|
132
156
|
|
|
133
157
|
propagate_step_failure!(order, result, fallback: 'Failed to add items to order')
|
data/config/locales/en.yml
CHANGED
|
@@ -765,6 +765,7 @@ en:
|
|
|
765
765
|
created_at: Created at
|
|
766
766
|
credit_card: Credit Card
|
|
767
767
|
currency: Currency
|
|
768
|
+
currency_does_not_match_market: must be %{currency}, the currency of the chosen market
|
|
768
769
|
currency_not_supported_by_store: is not supported by this store
|
|
769
770
|
custom_field_definitions: Custom Field Definitions
|
|
770
771
|
custom_fields: Custom Fields
|
|
@@ -773,7 +774,9 @@ en:
|
|
|
773
774
|
anonymize_failed: The erasure could not be completed and nothing was changed
|
|
774
775
|
customers: Customers
|
|
775
776
|
data_request_errors:
|
|
777
|
+
address_changed: The account's email address changed after this request was made
|
|
776
778
|
already_anonymized: This customer's personal data has already been erased
|
|
779
|
+
confirmation_invalid: This confirmation link has expired or is no longer valid
|
|
777
780
|
download_unavailable: This download link has expired or is no longer valid
|
|
778
781
|
not_pending: This request has already been processed
|
|
779
782
|
default: Default
|
|
@@ -1016,6 +1019,7 @@ en:
|
|
|
1016
1019
|
line_items: Line items
|
|
1017
1020
|
link: Link
|
|
1018
1021
|
locale: Locale
|
|
1022
|
+
locale_not_supported_by_market: is not offered by the chosen market
|
|
1019
1023
|
locale_not_supported_by_store: is not supported by this store
|
|
1020
1024
|
locales_not_listed: 'includes locales Spree does not support: %{codes}'
|
|
1021
1025
|
manual: Manual
|
data/lib/spree/core/engine.rb
CHANGED
|
@@ -506,6 +506,7 @@ module Spree
|
|
|
506
506
|
Spree::Address,
|
|
507
507
|
Spree::Claim,
|
|
508
508
|
Spree::Collection,
|
|
509
|
+
Spree::Company,
|
|
509
510
|
Spree::CreditCard,
|
|
510
511
|
Spree::Exchange,
|
|
511
512
|
Spree::GiftCard,
|
|
@@ -521,6 +522,7 @@ module Spree
|
|
|
521
522
|
Spree::Product,
|
|
522
523
|
Spree::ProductType,
|
|
523
524
|
Spree::Promotion,
|
|
525
|
+
Spree::PurchaseOrder,
|
|
524
526
|
Spree::Refund,
|
|
525
527
|
Spree::Return,
|
|
526
528
|
Spree::Fulfillment,
|
data/lib/spree/core/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.0.0.
|
|
4
|
+
version: 6.0.0.rc5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sean Schofield
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date: 2026-10-
|
|
13
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: i18n-tasks
|
|
@@ -1193,6 +1193,7 @@ files:
|
|
|
1193
1193
|
- app/models/spree/rich_text_sanitizer.rb
|
|
1194
1194
|
- app/models/spree/role.rb
|
|
1195
1195
|
- app/models/spree/role_user.rb
|
|
1196
|
+
- app/models/spree/sales_context.rb
|
|
1196
1197
|
- app/models/spree/saved_report.rb
|
|
1197
1198
|
- app/models/spree/search_provider.rb
|
|
1198
1199
|
- app/models/spree/search_provider/base.rb
|
|
@@ -1358,6 +1359,7 @@ files:
|
|
|
1358
1359
|
- app/services/spree/coupon_codes/bulk_generate.rb
|
|
1359
1360
|
- app/services/spree/coupon_codes/coupon_codes_handler.rb
|
|
1360
1361
|
- app/services/spree/customers/data_export.rb
|
|
1362
|
+
- app/services/spree/data_requests/confirm.rb
|
|
1361
1363
|
- app/services/spree/data_requests/create.rb
|
|
1362
1364
|
- app/services/spree/deliveries/create.rb
|
|
1363
1365
|
- app/services/spree/deliveries/destroy.rb
|
|
@@ -2369,9 +2371,9 @@ licenses:
|
|
|
2369
2371
|
- BSD-3-Clause
|
|
2370
2372
|
metadata:
|
|
2371
2373
|
bug_tracker_uri: https://github.com/spree/spree/issues
|
|
2372
|
-
changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.0.
|
|
2374
|
+
changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.0.rc5
|
|
2373
2375
|
documentation_uri: https://docs.spreecommerce.org/
|
|
2374
|
-
source_code_uri: https://github.com/spree/spree/tree/v6.0.0.
|
|
2376
|
+
source_code_uri: https://github.com/spree/spree/tree/v6.0.0.rc5
|
|
2375
2377
|
post_install_message:
|
|
2376
2378
|
rdoc_options: []
|
|
2377
2379
|
require_paths:
|