spree_core 6.0.0.rc3 → 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/mailers/spree/email_template_mailer.rb +5 -2
- data/app/models/concerns/spree/has_external_references.rb +7 -6
- data/app/models/concerns/spree/preference_schema/json_schema.rb +8 -1
- data/app/models/concerns/spree/purchase/addresses.rb +95 -32
- data/app/models/concerns/spree/purchase/taxation.rb +44 -3
- data/app/models/concerns/spree/translatable_resource.rb +49 -0
- 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 +4 -6
- data/app/models/spree/company.rb +1 -1
- data/app/models/spree/data_request.rb +34 -1
- data/app/models/spree/delivery_rate.rb +12 -4
- data/app/models/spree/delivery_rate_provider/base.rb +1 -1
- data/app/models/spree/delivery_rate_provider/estimate.rb +5 -4
- data/app/models/spree/fulfillment.rb +1 -0
- data/app/models/spree/gateway/bogus.rb +7 -0
- data/app/models/spree/line_item.rb +3 -8
- data/app/models/spree/oauth_access_grant.rb +11 -0
- data/app/models/spree/oauth_access_token.rb +16 -0
- data/app/models/spree/oauth_application.rb +97 -0
- data/app/models/spree/oauth_consent_request.rb +23 -0
- data/app/models/spree/oauth_protected_resource.rb +21 -0
- data/app/models/spree/oauth_redirect.rb +12 -0
- data/app/models/spree/option_type.rb +5 -0
- data/app/models/spree/option_value.rb +5 -0
- data/app/models/spree/order.rb +6 -2
- data/app/models/spree/payment.rb +14 -2
- data/app/models/spree/payment_capture_event.rb +2 -0
- data/app/models/spree/policy.rb +6 -0
- data/app/models/spree/price.rb +16 -33
- data/app/models/spree/price_rule.rb +3 -16
- data/app/models/spree/price_rules/market_rule.rb +0 -6
- data/app/models/spree/pricing_provider/base.rb +1 -1
- data/app/models/spree/product.rb +1 -1
- data/app/models/spree/purchase_order_item.rb +1 -1
- data/app/models/spree/return.rb +1 -0
- data/app/models/spree/sales_context.rb +162 -0
- data/app/models/spree/search_provider/database.rb +1 -1
- data/app/models/spree/stock/estimator.rb +4 -20
- data/app/models/spree/store.rb +8 -0
- data/app/models/spree/tax_exemption/item_override.rb +14 -0
- data/app/models/spree/tax_exemption.rb +26 -0
- data/app/models/spree/tax_exemption_certificate.rb +13 -2
- data/app/models/spree/tax_exemption_reason.rb +62 -0
- data/app/models/spree/tax_line.rb +5 -4
- data/app/models/spree/tax_provider/base.rb +24 -4
- data/app/models/spree/tax_provider/internal.rb +36 -29
- data/app/models/spree/tax_rate.rb +9 -35
- data/app/models/spree/translations/batch.rb +14 -4
- data/app/services/spree/carts/destroy.rb +7 -2
- data/app/services/spree/carts/price_items.rb +2 -8
- data/app/services/spree/carts/split_by_seller.rb +6 -1
- data/app/services/spree/carts/update.rb +35 -11
- data/app/services/spree/data_requests/confirm.rb +37 -0
- data/app/services/spree/data_requests/create.rb +61 -13
- data/app/services/spree/email_templates/preview.rb +24 -3
- data/app/services/spree/seeds/store_resources.rb +1 -0
- data/app/services/spree/seeds/tax_exemption_reasons.rb +28 -0
- data/app/services/spree/tax/resolve_exemptions.rb +11 -2
- data/app/services/spree/update_check.rb +8 -1
- data/app/tools/spree/agent_tools/create_export.rb +116 -0
- data/app/tools/spree/agent_tools/create_import.rb +117 -0
- data/app/tools/spree/agent_tools/create_media.rb +81 -0
- data/app/tools/spree/agent_tools/create_resource.rb +36 -0
- data/app/tools/spree/agent_tools/curate_products.rb +90 -0
- data/app/tools/spree/agent_tools/delete_resource.rb +28 -0
- data/app/tools/spree/agent_tools/describe_import_mapping.rb +73 -0
- data/app/tools/spree/agent_tools/describe_reporting.rb +39 -0
- data/app/tools/spree/agent_tools/describe_resource.rb +81 -0
- data/app/tools/spree/agent_tools/get_import_status.rb +71 -0
- data/app/tools/spree/agent_tools/get_resource.rb +97 -0
- data/app/tools/spree/agent_tools/import_tool.rb +80 -0
- data/app/tools/spree/agent_tools/propose_import_mapping.rb +138 -0
- data/app/tools/spree/agent_tools/query_report.rb +124 -0
- data/app/tools/spree/agent_tools/resource_write.rb +99 -0
- data/app/tools/spree/agent_tools/search_resources.rb +155 -0
- data/app/tools/spree/agent_tools/set_stock.rb +52 -0
- data/app/tools/spree/agent_tools/update_resource.rb +42 -0
- data/app/tools/spree/agent_tools/upload_file.rb +104 -0
- data/app/workflows/spree/carts/complete.rb +115 -2
- data/app/workflows/spree/catalogs/set_price_list.rb +2 -2
- data/app/workflows/spree/customers/anonymize.rb +38 -17
- data/app/workflows/spree/data_requests/fulfill.rb +20 -6
- data/app/workflows/spree/orders/cancel.rb +8 -0
- data/app/workflows/spree/orders/complete.rb +8 -0
- data/app/{services → workflows}/spree/orders/create.rb +50 -26
- data/app/workflows/spree/purchase_orders/create.rb +3 -0
- data/app/workflows/spree/returns/refund.rb +4 -0
- data/app/{services → workflows}/spree/stock_levels/bulk_upsert.rb +49 -7
- data/app/workflows/spree/variants/create.rb +7 -4
- data/config/locales/en.yml +18 -0
- data/db/migrate/20250902143122_fix_policies_store_association.rb +6 -4
- data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +11 -2
- data/db/migrate/20260904100001_add_applied_tax_exemptions_to_spree_orders.rb +16 -0
- data/db/migrate/20260914230000_add_store_to_spree_option_types.rb +12 -2
- data/db/migrate/20261005120000_create_spree_oauth_tables.rb +67 -0
- data/db/migrate/20261016000001_remove_default_from_spree_tax_rates_included_in_price.rb +9 -0
- data/db/migrate/20261016100001_create_spree_tax_exemption_reasons.rb +21 -0
- data/db/migrate/20261017100001_add_tax_exemption_reason_to_spree_tax_exemption_certificates.rb +9 -0
- data/db/sample_data/orders.rb +1 -1
- data/lib/spree/agent_tool.rb +188 -0
- data/lib/spree/agent_tools/api_dispatch.rb +204 -0
- data/lib/spree/agent_tools/context.rb +191 -0
- data/lib/spree/agent_tools/default_catalog.rb +180 -0
- data/lib/spree/agent_tools/membership_map.rb +61 -0
- data/lib/spree/agent_tools/record_summary.rb +219 -0
- data/lib/spree/agent_tools/registry.rb +131 -0
- data/lib/spree/agent_tools/resource_map.rb +352 -0
- data/lib/spree/agent_tools/workflow_endpoints.rb +88 -0
- data/lib/spree/agent_tools/workflow_schema.rb +279 -0
- data/lib/spree/agent_tools/workflow_tool.rb +654 -0
- data/lib/spree/agent_tools.rb +24 -0
- data/lib/spree/core/configuration.rb +1 -1
- data/lib/spree/core/controller_helpers/store.rb +3 -13
- data/lib/spree/core/dependencies.rb +9 -0
- data/lib/spree/core/engine.rb +37 -1
- data/lib/spree/core/model_extensions.rb +111 -0
- data/lib/spree/core/permissions/default_catalog.rb +6 -1
- data/lib/spree/core/pricing/price_resolution.rb +4 -4
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +27 -2
- data/lib/spree/service_module.rb +11 -0
- data/lib/spree/testing_support/factories/data_request_factory.rb +4 -0
- data/lib/spree/testing_support/factories/tax_exemption_certificate_factory.rb +13 -1
- data/lib/spree/testing_support/factories/tax_exemption_reason_factory.rb +8 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +38 -0
- data/lib/tasks/markets.rake +47 -0
- data/lib/tasks/tax_exemption_reasons.rake +48 -0
- metadata +69 -7
- data/app/models/concerns/spree/vat_price_calculation.rb +0 -99
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
|
|
@@ -11,11 +11,14 @@ module Spree
|
|
|
11
11
|
# @param record_id [String, nil] the record to build sample data from
|
|
12
12
|
# @param email_key [String, nil] for the layout or a partial, the email to show it in
|
|
13
13
|
# @param branding [Hash] unsaved branding values
|
|
14
|
-
|
|
14
|
+
# @param mailer_logo_signed_id [String, nil] the signed id of an unsaved, uploaded email logo
|
|
15
|
+
# @param mailer_logo_cleared [Boolean] whether the email logo is sent as removed
|
|
16
|
+
def test_email(store, recipient, key, locale: nil, subject: nil, body: nil, record_id: nil, email_key: nil, branding: {},
|
|
17
|
+
mailer_logo_signed_id: nil, mailer_logo_cleared: false)
|
|
15
18
|
@current_store = store
|
|
16
19
|
email = Spree::EmailTemplates::Preview.new(
|
|
17
20
|
store: store, key: key, locale: locale, subject: subject, body: body, record_id: record_id, email_key: email_key,
|
|
18
|
-
branding: branding
|
|
21
|
+
branding: branding, mailer_logo_signed_id: mailer_logo_signed_id, mailer_logo_cleared: mailer_logo_cleared
|
|
19
22
|
).call
|
|
20
23
|
|
|
21
24
|
mail(to: recipient, subject: I18n.t('spree.email_templates.test_email_subject', subject: email.subject)) do |format|
|
|
@@ -11,13 +11,14 @@ module Spree
|
|
|
11
11
|
# Scope-friendly so controllers can chain it onto an already store-scoped
|
|
12
12
|
# relation: `current_store.products.with_external_id('erp', 'MAT-100')`.
|
|
13
13
|
# Scoping the lookup to the store is the caller's job, exactly as with any
|
|
14
|
-
# other id coming off a request.
|
|
14
|
+
# other id coming off a request. A subquery rather than a join, so a
|
|
15
|
+
# relation that also preloads `external_references` still loads every
|
|
16
|
+
# key, not only the matched one.
|
|
15
17
|
scope :with_external_id, lambda { |system, external_id|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
18
|
+
where(
|
|
19
|
+
id: Spree::ExternalReference.for_system(system).
|
|
20
|
+
where(resource_type: polymorphic_name, external_id: external_id.to_s.strip).
|
|
21
|
+
select(:resource_id)
|
|
21
22
|
)
|
|
22
23
|
}
|
|
23
24
|
end
|
|
@@ -52,7 +52,14 @@ module Spree
|
|
|
52
52
|
schema = value_schema(definition)
|
|
53
53
|
choices = definition[:choices]
|
|
54
54
|
choices = choices.call if choices.respond_to?(:call)
|
|
55
|
-
|
|
55
|
+
if choices.present?
|
|
56
|
+
# `{ value => label }` names values that cannot label themselves — an
|
|
57
|
+
# endpoint URL is no i18n key, let alone something to show a merchant.
|
|
58
|
+
labels = choices.to_h { |value, label| [value.to_s, label.to_s] } if choices.is_a?(Hash)
|
|
59
|
+
values = choices.is_a?(Hash) ? choices.keys : Array(choices)
|
|
60
|
+
schema['enum'] = values.map { |choice| choice.is_a?(Symbol) ? choice.to_s : choice }
|
|
61
|
+
schema['x-spree-choice-labels'] = labels if labels
|
|
62
|
+
end
|
|
56
63
|
default = definition[:dynamic_default] ? nil : wire_default(default, definition)
|
|
57
64
|
unset_reads_null = default.nil? && !definition[:dynamic_default]
|
|
58
65
|
|
|
@@ -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.
|
|
@@ -28,6 +28,15 @@ module Spree
|
|
|
28
28
|
market&.tax_provider_instance || Spree.default_tax_provider.new
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Whether this sale's prices include tax, which its market decides. With
|
|
32
|
+
# no market yet (an empty cart before currency resolution) the store's
|
|
33
|
+
# default market answers.
|
|
34
|
+
#
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def tax_inclusive?
|
|
37
|
+
(market || store&.default_market)&.tax_inclusive? || false
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
# The typed inputs every +estimate+ call carries. Assembled here rather
|
|
32
41
|
# than at each call site so a single-item re-estimate reaches the provider
|
|
33
42
|
# with the same buyer registration and exemption evidence as a full
|
|
@@ -39,7 +48,8 @@ module Spree
|
|
|
39
48
|
{
|
|
40
49
|
tax_date: Time.current,
|
|
41
50
|
tax_identifier: resolved_tax_identifier,
|
|
42
|
-
exemptions: usable_exemptions
|
|
51
|
+
exemptions: usable_exemptions,
|
|
52
|
+
tax_inclusive: tax_inclusive?
|
|
43
53
|
}
|
|
44
54
|
end
|
|
45
55
|
|
|
@@ -100,15 +110,30 @@ module Spree
|
|
|
100
110
|
pre_tax_item_amount + fulfillments.sum(:pre_tax_amount)
|
|
101
111
|
end
|
|
102
112
|
|
|
103
|
-
private
|
|
104
|
-
|
|
105
113
|
# The resolver is swappable, so what it returns is host code. An entry
|
|
106
114
|
# whose per-line overrides cannot name their line is not a narrower claim
|
|
107
115
|
# but a wider one — the provider would read it as the whole order being
|
|
108
116
|
# exempt — so it is dropped rather than trusted. Charging tax is the safe
|
|
109
117
|
# direction: under-exempting surfaces as a customer query, over-exempting
|
|
110
118
|
# is tax the merchant owes and never collected.
|
|
119
|
+
#
|
|
120
|
+
# Public because a provider filing the sale needs the same evidence core
|
|
121
|
+
# hands it as an +estimate+ argument: an external engine commits its
|
|
122
|
+
# document after placement, and resolving again there could file an
|
|
123
|
+
# exemption the estimate never applied.
|
|
124
|
+
#
|
|
125
|
+
# A placed order answers from its frozen snapshot for that reason, the way
|
|
126
|
+
# +resolved_tax_identifier+ does. Resolving live months later is not the
|
|
127
|
+
# same evidence: a certificate lapses on its own date with nothing written
|
|
128
|
+
# to it, can be revoked, and goes with its company when that is deleted —
|
|
129
|
+
# so a credit for an exempt sale would be filed as a consumer refund and
|
|
130
|
+
# declare tax the sale never collected. Orders placed before the snapshot
|
|
131
|
+
# existed have none, and still resolve live.
|
|
132
|
+
#
|
|
133
|
+
# @return [Array]
|
|
111
134
|
def usable_exemptions
|
|
135
|
+
return frozen_tax_exemptions if frozen_tax_exemptions?
|
|
136
|
+
|
|
112
137
|
resolved = Array(Spree.tax_resolve_exemptions_service.new.call(order: self).value)
|
|
113
138
|
|
|
114
139
|
resolved.select do |exemption|
|
|
@@ -127,6 +152,22 @@ module Spree
|
|
|
127
152
|
end
|
|
128
153
|
end
|
|
129
154
|
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
# Carts always resolve live — a buyer's certificates legitimately change
|
|
158
|
+
# while they shop, and the cart is priced again each time.
|
|
159
|
+
#
|
|
160
|
+
# Only nil is an absent snapshot. An empty one records that the sale found
|
|
161
|
+
# no claim, and reading that as absent would let a certificate added later
|
|
162
|
+
# explain a sale it was never part of.
|
|
163
|
+
def frozen_tax_exemptions?
|
|
164
|
+
is_a?(Spree::Order) && completed? && !applied_tax_exemptions.nil?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def frozen_tax_exemptions
|
|
168
|
+
Array(applied_tax_exemptions).map { |snapshot| Spree::TaxExemption.from_snapshot(snapshot) }
|
|
169
|
+
end
|
|
170
|
+
|
|
130
171
|
# Read through the legal entity, never the node — a division holds no
|
|
131
172
|
# registrations of its own.
|
|
132
173
|
def company_tax_identifier
|
|
@@ -12,6 +12,21 @@ module Spree
|
|
|
12
12
|
const_get(:TRANSLATABLE_FIELDS)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# The permission a caller needs in order to write this resource's
|
|
16
|
+
# translations.
|
|
17
|
+
#
|
|
18
|
+
# Declared rather than derived from the class name, because several
|
|
19
|
+
# resources are guarded by a permission that is not named after them:
|
|
20
|
+
# option types and values ride `write_products`, a policy rides
|
|
21
|
+
# `write_settings`. Deriving it produced names outside the permission
|
|
22
|
+
# catalog, which a credential could never hold and which the API
|
|
23
|
+
# therefore could not check at all.
|
|
24
|
+
#
|
|
25
|
+
# @return [String]
|
|
26
|
+
def translation_write_permission
|
|
27
|
+
"write_#{Spree::Base.polymorphic_api_type(name).pluralize}"
|
|
28
|
+
end
|
|
29
|
+
|
|
15
30
|
# Maps a public API field name to the internal Mobility field, for a
|
|
16
31
|
# model whose translatable column is named differently from the field the
|
|
17
32
|
# API exposes. The public name is what the serializer and the translation
|
|
@@ -70,6 +85,40 @@ module Spree
|
|
|
70
85
|
def arel_table_alias
|
|
71
86
|
Arel::Table.new(translation_table_alias)
|
|
72
87
|
end
|
|
88
|
+
|
|
89
|
+
# Sorts translated fields by the translation, or by the default-language
|
|
90
|
+
# value in the base column when the translation is missing or empty.
|
|
91
|
+
# Ransack sorts them by the translation alone, which leaves untranslated
|
|
92
|
+
# records unordered. On a DISTINCT relation the value is selected as
|
|
93
|
+
# well, because PostgreSQL rejects an ORDER BY the select list lacks.
|
|
94
|
+
#
|
|
95
|
+
# @return [ActiveRecord::Relation]
|
|
96
|
+
def with_translation_fallback_order
|
|
97
|
+
relation = all
|
|
98
|
+
return relation unless relation.order_values.any? { |node| translated_ordering?(node) }
|
|
99
|
+
|
|
100
|
+
relation = relation.select(arel_table[Arel.star]) if relation.distinct_value && relation.select_values.empty?
|
|
101
|
+
orders = relation.order_values.map do |node|
|
|
102
|
+
next node unless translated_ordering?(node)
|
|
103
|
+
|
|
104
|
+
value = Arel::Nodes::NamedFunction.new('COALESCE', [
|
|
105
|
+
Arel::Nodes::NamedFunction.new('NULLIF', [node.expr, Arel::Nodes.build_quoted('')]),
|
|
106
|
+
arel_table[node.expr.name]
|
|
107
|
+
])
|
|
108
|
+
relation = relation.select(Arel::Nodes::As.new(value, Arel.sql('translated_sort'))) if relation.distinct_value
|
|
109
|
+
node.class.new(value)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
relation.reorder(orders)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def translated_ordering?(node)
|
|
118
|
+
node.is_a?(Arel::Nodes::Ordering) &&
|
|
119
|
+
node.expr.is_a?(Mobility::Plugins::Arel::Attribute) &&
|
|
120
|
+
self <= node.expr.backend_class.model_class
|
|
121
|
+
end
|
|
73
122
|
end
|
|
74
123
|
|
|
75
124
|
def get_field_with_locale(locale, field_name, fallback: false)
|
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
|
@@ -179,6 +179,7 @@ module Spree
|
|
|
179
179
|
|
|
180
180
|
fulfillment_ids = fulfillments.map(&:id)
|
|
181
181
|
DeliveryRate.where(fulfillment_id: fulfillment_ids).delete_all
|
|
182
|
+
Spree::ExternalReference.where(resource_type: Spree::Fulfillment.name, resource_id: fulfillment_ids).delete_all if fulfillment_ids.any?
|
|
182
183
|
fulfillments.delete_all
|
|
183
184
|
fulfillment_items.reset
|
|
184
185
|
return fulfillments.reload unless can_propose_deliveries?
|
|
@@ -283,12 +284,9 @@ module Spree
|
|
|
283
284
|
#
|
|
284
285
|
# @return [ActiveRecord::Relation<Spree::Variant>]
|
|
285
286
|
def orderable_variants
|
|
286
|
-
|
|
287
|
-
store: store, channel: channel, customer: customer, company: resolved_company,
|
|
288
|
-
|
|
289
|
-
).value
|
|
290
|
-
|
|
291
|
-
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
|
|
292
290
|
end
|
|
293
291
|
|
|
294
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
|
|
@@ -41,7 +41,7 @@ module Spree
|
|
|
41
41
|
return price if tax_rate.nil? || tax_amount.zero? || !tax_rate.show_rate_in_label
|
|
42
42
|
|
|
43
43
|
I18n.t(
|
|
44
|
-
"spree.shipping_rates.display_price.#{
|
|
44
|
+
"spree.shipping_rates.display_price.#{tax_inclusive? ? :including_tax : :excluding_tax}",
|
|
45
45
|
price: price,
|
|
46
46
|
tax_amount: display_tax_amount,
|
|
47
47
|
tax_rate_name: tax_rate.name
|
|
@@ -79,7 +79,7 @@ module Spree
|
|
|
79
79
|
def tax_amount
|
|
80
80
|
@tax_amount ||= if tax_rate.nil?
|
|
81
81
|
BigDecimal(0)
|
|
82
|
-
elsif
|
|
82
|
+
elsif tax_inclusive?
|
|
83
83
|
Spree::Money::Rounding.to_currency(cost / (1 + tax_rate.amount) * tax_rate.amount, currency)
|
|
84
84
|
else
|
|
85
85
|
Spree::Money::Rounding.to_currency(cost * tax_rate.amount, currency)
|
|
@@ -90,14 +90,22 @@ module Spree
|
|
|
90
90
|
#
|
|
91
91
|
# @return [BigDecimal]
|
|
92
92
|
def additional_tax_total
|
|
93
|
-
tax_rate
|
|
93
|
+
tax_rate && tax_inclusive? ? BigDecimal(0) : tax_amount
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
# Returns the included tax total for the shipping rate
|
|
97
97
|
#
|
|
98
98
|
# @return [BigDecimal]
|
|
99
99
|
def included_tax_total
|
|
100
|
-
tax_rate
|
|
100
|
+
tax_rate && tax_inclusive? ? tax_amount : BigDecimal(0)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Whether the cost already includes its tax, which the sale's market
|
|
104
|
+
# decides (Spree::Purchase::Taxation#tax_inclusive?).
|
|
105
|
+
#
|
|
106
|
+
# @return [Boolean]
|
|
107
|
+
def tax_inclusive?
|
|
108
|
+
fulfillment&.owner&.tax_inclusive? || false
|
|
101
109
|
end
|
|
102
110
|
|
|
103
111
|
alias tax_total tax_amount
|
|
@@ -6,7 +6,7 @@ module Spree
|
|
|
6
6
|
# Stored as a string class name on DeliveryMethod and constantized at call
|
|
7
7
|
# time; registered via Spree.delivery_rate_providers. The Estimator
|
|
8
8
|
# dispatches here in place of calling the calculator directly, so
|
|
9
|
-
# filtering,
|
|
9
|
+
# filtering, markup, tax resolution and sorting stay in one place
|
|
10
10
|
# for every provider.
|
|
11
11
|
#
|
|
12
12
|
# External providers resolve credentials through the delivery method's
|
|
@@ -2,10 +2,11 @@ module Spree
|
|
|
2
2
|
module DeliveryRateProvider
|
|
3
3
|
# A single quote returned by a delivery rate provider.
|
|
4
4
|
#
|
|
5
|
-
# +cost+ is
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# onto the persisted
|
|
5
|
+
# +cost+ is the quoted price: the Estimator adds markup and resolves the
|
|
6
|
+
# tax rate afterwards, exactly as it does for calculator output, and the
|
|
7
|
+
# market decides whether that tax is inside the cost or added on top.
|
|
8
|
+
# Everything else is carrier metadata that flows onto the persisted
|
|
9
|
+
# DeliveryRate.
|
|
9
10
|
class Estimate
|
|
10
11
|
include ActiveModel::Model
|
|
11
12
|
include ActiveModel::Attributes
|
|
@@ -70,6 +70,13 @@ module Spree
|
|
|
70
70
|
Spree::PaymentResponse.new(true, 'Bogus Gateway: Forced success', {}, test: true, authorization: generate_authorization)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# A made-up address, so the dashboard link can be tried without a provider.
|
|
74
|
+
def gateway_dashboard_payment_url(payment)
|
|
75
|
+
return if payment.transaction_id.blank?
|
|
76
|
+
|
|
77
|
+
"https://dashboard.bogus.example/payments/#{payment.transaction_id}"
|
|
78
|
+
end
|
|
79
|
+
|
|
73
80
|
def test?
|
|
74
81
|
# Test mode is not really relevant with bogus gateway (no such thing as live server)
|
|
75
82
|
true
|
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
has_prefix_id :li # Spree-specific: line item
|
|
4
4
|
|
|
5
5
|
include Spree::HasCustomFields
|
|
6
|
+
include Spree::HasExternalReferences
|
|
6
7
|
include Spree::Metadata
|
|
7
8
|
|
|
8
9
|
publishes_lifecycle_events
|
|
@@ -157,12 +158,6 @@ module Spree
|
|
|
157
158
|
|
|
158
159
|
# Assigns the current price without saving — the caller is expected to.
|
|
159
160
|
# {#recalculate_price} persists instead.
|
|
160
|
-
#
|
|
161
|
-
# Restatement for a tax-inclusive market happens inside
|
|
162
|
-
# {Spree::Carts::PriceItems}: an address-less cart still has a taxing
|
|
163
|
-
# country (its market's own), and without it a cross-border cart is taxed
|
|
164
|
-
# for the destination at a price quoted for home, leaving the merchant to
|
|
165
|
-
# absorb the difference.
|
|
166
161
|
# persist: false — runs mid-build and from callers that save the record
|
|
167
162
|
# themselves.
|
|
168
163
|
def update_price
|
|
@@ -180,8 +175,8 @@ module Spree
|
|
|
180
175
|
price_record = Spree::PricingProvider::Internal.new.price_for(context)
|
|
181
176
|
return if price_record.blank?
|
|
182
177
|
|
|
183
|
-
# Through the same writer every resolved price goes through, so
|
|
184
|
-
#
|
|
178
|
+
# Through the same writer every resolved price goes through, so a line
|
|
179
|
+
# built here is priced exactly like a resolved one.
|
|
185
180
|
Spree::Carts::PriceItems.apply([[self, price_record]], persist: false)
|
|
186
181
|
end
|
|
187
182
|
|