mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-26 05:55:15 +00:00
Compare commits
132 Commits
v5.4.6
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d116d0027 | ||
|
|
1e6de5e251 | ||
|
|
af2299c666 | ||
|
|
b37111f007 | ||
|
|
043a8a84f3 | ||
|
|
a2fad2cab3 | ||
|
|
5ab1ce751b | ||
|
|
1a2b5ffc3a | ||
|
|
080c4f7cb5 | ||
|
|
9d389e22d3 | ||
|
|
6c4ae1d2c1 | ||
|
|
eff1ed4a5e | ||
|
|
7ea2b126f2 | ||
|
|
bfca6248ae | ||
|
|
8250029eb7 | ||
|
|
5e92fa9a17 | ||
|
|
d23ad9c8ad | ||
|
|
d80249da2d | ||
|
|
d6c69fdc2c | ||
|
|
06d6db5a07 | ||
|
|
3f81883bc7 | ||
|
|
27be0f6fd1 | ||
|
|
8880f83d09 | ||
|
|
23a4ca5933 | ||
|
|
4dc44c6156 | ||
|
|
8defb2f4c8 | ||
|
|
067349f742 | ||
|
|
74fd019863 | ||
|
|
1207bb5f8f | ||
|
|
d25eea660e | ||
|
|
fdfb155682 | ||
|
|
66d6627c89 | ||
|
|
c5d38d684b | ||
|
|
c2907b839a | ||
|
|
ee653bb825 | ||
|
|
75616e69e7 | ||
|
|
b4b3e21cf6 | ||
|
|
ce90ec0f5b | ||
|
|
2998432744 | ||
|
|
c5aaecf76a | ||
|
|
2e64f54740 | ||
|
|
c7de67a14f | ||
|
|
545e69835d | ||
|
|
e3a757bd2d | ||
|
|
8223d1ce52 | ||
|
|
032953e7d6 | ||
|
|
167846138f | ||
|
|
1878a39188 | ||
|
|
243e70427d | ||
|
|
0ae3c8d668 | ||
|
|
c8f46e52d3 | ||
|
|
131a916d99 | ||
|
|
9dd9e08694 | ||
|
|
e7180e956f | ||
|
|
7f312caa25 | ||
|
|
8f0d4d23a7 | ||
|
|
d56b4b4109 | ||
|
|
7a01409f5c | ||
|
|
5c634c269b | ||
|
|
bf22484add | ||
|
|
1696dd2de6 | ||
|
|
63988fff4f | ||
|
|
8e6f1c4e99 | ||
|
|
2004934399 | ||
|
|
827ba1990d | ||
|
|
d56790b71e | ||
|
|
c2f725b20c | ||
|
|
b939d41bf5 | ||
|
|
91c4ba03cd | ||
|
|
3f29cdab3c | ||
|
|
60a4d36408 | ||
|
|
9513c07c2f | ||
|
|
4b5fd2495f | ||
|
|
939ae20081 | ||
|
|
78e9524767 | ||
|
|
3f5b7aaea9 | ||
|
|
00ab4379c9 | ||
|
|
9dec68032c | ||
|
|
e449ae95d5 | ||
|
|
d0af6ddcc1 | ||
|
|
a17d3f34d9 | ||
|
|
6c5d49ae33 | ||
|
|
7d4389de4a | ||
|
|
93c9181c3f | ||
|
|
7473a2f0bd | ||
|
|
af8544a4fa | ||
|
|
be156e5621 | ||
|
|
ec8fcecbd6 | ||
|
|
35ae3a424a | ||
|
|
c2c83898a2 | ||
|
|
2879f77aa1 | ||
|
|
68032657c3 | ||
|
|
be4037d05a | ||
|
|
9661e8a53e | ||
|
|
5e93c27277 | ||
|
|
fcb3b67efb | ||
|
|
0629153362 | ||
|
|
5e871fc71e | ||
|
|
35e03ea3c7 | ||
|
|
ee7ed56f44 | ||
|
|
4140209820 | ||
|
|
1bb4fdf294 | ||
|
|
9ca1a9e33f | ||
|
|
62af416696 | ||
|
|
bcf39acebc | ||
|
|
82186118a7 | ||
|
|
18fb1cfa74 | ||
|
|
e9ce2df5a9 | ||
|
|
c165ade4ba | ||
|
|
7e8b3694be | ||
|
|
6ee715419a | ||
|
|
7da6adfe4f | ||
|
|
05c31db46a | ||
|
|
666e872ac8 | ||
|
|
de6eb9e281 | ||
|
|
299ada1220 | ||
|
|
5757f086ec | ||
|
|
8955ffe126 | ||
|
|
b26152cf0e | ||
|
|
78db179ff3 | ||
|
|
5fc6d25a69 | ||
|
|
b877540f5f | ||
|
|
04c0adf960 | ||
|
|
1c89e9979e | ||
|
|
eba2fbcc30 | ||
|
|
940aa57daf | ||
|
|
766bedb773 | ||
|
|
6fe2357ca0 | ||
|
|
bd01b5f113 | ||
|
|
e565243ce4 | ||
|
|
0f3b299544 | ||
|
|
1332051a6e |
@@ -13,6 +13,9 @@ CAPYBARA_MAX_WAIT_TIME="10"
|
||||
# successful fallback to `en`.
|
||||
LOCALE="en_TST"
|
||||
|
||||
# For multilingual - ENV doesn't have array so pass it as string with commas
|
||||
AVAILABLE_LOCALES="en_TST,es,pt"
|
||||
|
||||
OFN_REDIS_JOBS_URL="redis://localhost:6379/2"
|
||||
|
||||
SECRET_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/release.md
vendored
3
.github/ISSUE_TEMPLATE/release.md
vendored
@@ -32,7 +32,8 @@ assignees: ''
|
||||
- [ ] Stripe with Authentication required card: `4000002760003184` as shopper and as Admin. As admin, check authorization through customer account `/account#/transactions` and email.
|
||||
- [ ] Pay with Paypal.
|
||||
- [ ] Order on mobile.
|
||||
- [ ] Notify a deployer to deploy it.
|
||||
- [ ] Explore a bit the platform to see if there is nothing unusual (15 min. max)
|
||||
- [ ] Comment on the issue: if it's all good, move to ready to go. If there are issues, don't forget to mention your Desktop/Browser version and number and ping @ reviewers so someone available can have a look
|
||||
|
||||
## 3. Deployment at beginning of week
|
||||
|
||||
|
||||
2
.github/workflows/linters.yml
vendored
2
.github/workflows/linters.yml
vendored
@@ -5,7 +5,7 @@ permissions:
|
||||
jobs:
|
||||
lint:
|
||||
name: reviewdog
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04 # same as build.yml so it can share bundler cache
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
||||
3
Gemfile
3
Gemfile
@@ -63,7 +63,6 @@ gem "taler"
|
||||
gem 'devise'
|
||||
gem 'devise-encryptable'
|
||||
gem 'devise-i18n'
|
||||
gem 'devise-token_authenticatable'
|
||||
gem 'jwt', '~> 2.3'
|
||||
gem 'oauth2', '~> 1.4.7' # Used for Stripe Connect
|
||||
|
||||
@@ -113,7 +112,7 @@ gem "turbo-rails"
|
||||
|
||||
gem 'combine_pdf'
|
||||
gem 'wicked_pdf', github: "openfoodfoundation/wicked_pdf", branch: "master"
|
||||
gem 'wkhtmltopdf-binary'
|
||||
gem 'wkhtmltopdf-binary', source: 'https://rubygems.org' # due to https://github.com/gem-coop/gem.coop/issues/36
|
||||
|
||||
gem 'immigrant'
|
||||
gem 'roo' # read spreadsheets
|
||||
|
||||
66
Gemfile.lock
66
Gemfile.lock
@@ -167,7 +167,7 @@ GEM
|
||||
zeitwerk (>= 2.4, < 3.0)
|
||||
acts_as_list (1.0.4)
|
||||
activerecord (>= 4.2)
|
||||
addressable (2.8.8)
|
||||
addressable (2.8.9)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
afm (1.0.0)
|
||||
@@ -185,7 +185,7 @@ GEM
|
||||
ast (2.4.3)
|
||||
attr_required (1.0.2)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1223.0)
|
||||
aws-partitions (1.1227.0)
|
||||
aws-sdk-core (3.243.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
@@ -197,22 +197,22 @@ GEM
|
||||
aws-sdk-kms (1.122.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.213.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-s3 (1.215.0)
|
||||
aws-sdk-core (~> 3, >= 3.243.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.3.0)
|
||||
bcp47_spec (0.2.1)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt (3.1.22)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (3.3.1)
|
||||
bindata (2.5.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.22.0)
|
||||
msgpack (~> 1.2)
|
||||
bugsnag (6.28.0)
|
||||
bugsnag (6.29.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
builder (3.3.0)
|
||||
bullet (8.1.0)
|
||||
@@ -279,13 +279,13 @@ GEM
|
||||
datafoodconsortium-connector (1.2.0)
|
||||
virtual_assembly-semantizer (~> 1.0, >= 1.0.5)
|
||||
date (3.5.1)
|
||||
debug (1.11.0)
|
||||
debug (1.11.1)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
devise (4.9.4)
|
||||
devise (5.0.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
railties (>= 7.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-encryptable (0.2.0)
|
||||
@@ -293,8 +293,6 @@ GEM
|
||||
devise-i18n (1.15.0)
|
||||
devise (>= 4.9.0)
|
||||
rails-i18n
|
||||
devise-token_authenticatable (1.1.0)
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
diff-lcs (1.6.2)
|
||||
digest (3.2.1)
|
||||
docile (1.4.1)
|
||||
@@ -387,11 +385,11 @@ GEM
|
||||
good_migrations (0.3.1)
|
||||
activerecord (>= 3.1)
|
||||
railties (>= 3.1)
|
||||
haml (6.3.0)
|
||||
haml (7.2.0)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
haml_lint (0.68.0)
|
||||
haml_lint (0.72.0)
|
||||
haml (>= 5.0)
|
||||
parallel (~> 1.10)
|
||||
rainbow
|
||||
@@ -404,18 +402,19 @@ GEM
|
||||
highline (3.1.2)
|
||||
reline
|
||||
htmlentities (4.4.2)
|
||||
http_parser.rb (0.8.0)
|
||||
http_parser.rb (0.8.1)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.9.2)
|
||||
i18n (>= 0.6.6)
|
||||
i18n-tasks (1.0.15)
|
||||
i18n-tasks (1.1.2)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
erubi
|
||||
highline (>= 2.0.0)
|
||||
highline (>= 3.0.0)
|
||||
i18n
|
||||
parser (>= 3.2.2.1)
|
||||
prism
|
||||
rails-i18n
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.8, >= 1.8.1)
|
||||
@@ -443,7 +442,7 @@ GEM
|
||||
thor (>= 0.14, < 2.0)
|
||||
jquery-ui-rails (4.2.1)
|
||||
railties (>= 3.2.16)
|
||||
json (2.18.1)
|
||||
json (2.19.2)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.17.0)
|
||||
activesupport (>= 4.2)
|
||||
@@ -469,7 +468,8 @@ GEM
|
||||
activesupport (>= 4.2)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
knapsack_pro (9.2.2)
|
||||
knapsack_pro (9.2.3)
|
||||
logger
|
||||
rake
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.0.0)
|
||||
@@ -484,7 +484,7 @@ GEM
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
logger (1.7.0)
|
||||
loofah (2.25.0)
|
||||
loofah (2.25.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.9.0)
|
||||
@@ -506,7 +506,7 @@ GEM
|
||||
mini_magick (5.3.1)
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.6)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (6.0.2)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
@@ -515,7 +515,7 @@ GEM
|
||||
money (6.16.0)
|
||||
i18n (>= 0.6.4, <= 2)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.17.0)
|
||||
multi_json (1.19.1)
|
||||
multi_xml (0.6.0)
|
||||
mutex_m (0.3.0)
|
||||
net-http (0.9.1)
|
||||
@@ -531,7 +531,7 @@ GEM
|
||||
net-protocol
|
||||
newrelic_rpm (9.24.0)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.1)
|
||||
nokogiri (1.19.2)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri-html5-inference (0.3.0)
|
||||
@@ -596,7 +596,7 @@ GEM
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.9.0)
|
||||
private_address_check (0.5.0)
|
||||
private_address_check (0.6.0)
|
||||
pry (0.16.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
@@ -604,16 +604,17 @@ GEM
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (7.0.2)
|
||||
puffing-billy (4.0.2)
|
||||
public_suffix (7.0.5)
|
||||
puffing-billy (4.0.4)
|
||||
addressable (~> 2.5)
|
||||
cgi
|
||||
em-http-request (~> 1.1, >= 1.1.0)
|
||||
em-synchrony
|
||||
eventmachine (~> 1.2)
|
||||
eventmachine_httpserver
|
||||
http_parser.rb (~> 0.8.0)
|
||||
multi_json
|
||||
puma (6.5.0)
|
||||
puma (7.2.0)
|
||||
nio4r (~> 2.0)
|
||||
query_count (1.1.1)
|
||||
activerecord (>= 4.2)
|
||||
@@ -897,7 +898,7 @@ GEM
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
sysexits (1.2.0)
|
||||
taler (0.2.0)
|
||||
taler (0.3.0)
|
||||
temple (0.10.4)
|
||||
terminal-table (4.0.0)
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
@@ -915,7 +916,7 @@ GEM
|
||||
turbo-rails (>= 1.3.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
undercover (0.8.3)
|
||||
undercover (0.8.4)
|
||||
base64
|
||||
bigdecimal
|
||||
imagen (>= 0.2.0)
|
||||
@@ -969,12 +970,16 @@ GEM
|
||||
websocket-extensions (0.1.5)
|
||||
whenever (1.1.0)
|
||||
chronic (>= 0.6.3)
|
||||
wkhtmltopdf-binary (0.12.6.10)
|
||||
xml-simple (1.1.8)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.5)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
wkhtmltopdf-binary (0.12.6.10)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
@@ -1013,7 +1018,6 @@ DEPENDENCIES
|
||||
devise
|
||||
devise-encryptable
|
||||
devise-i18n
|
||||
devise-token_authenticatable
|
||||
dfc_provider!
|
||||
digest
|
||||
dotenv
|
||||
@@ -1125,7 +1129,7 @@ DEPENDENCIES
|
||||
webmock
|
||||
whenever
|
||||
wicked_pdf!
|
||||
wkhtmltopdf-binary
|
||||
wkhtmltopdf-binary!
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.8p72
|
||||
|
||||
@@ -3,7 +3,15 @@ angular.module('admin.orderCycles').controller 'AdminOrderCycleIncomingCtrl', ($
|
||||
|
||||
$scope.view = 'incoming'
|
||||
# NB: weirdly at this next line $scope.order_cycle.id comes out undefined so we use $scope.order_cycle_id instead
|
||||
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true)
|
||||
$scope.enterprise_fees = null
|
||||
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true) unless EnterpriseFee.loading
|
||||
|
||||
# We want to make sure to load the filtered EnterpriseFee when any previous request is finished
|
||||
# otherwise the enterprise_fees migh get overriden by non filtered ones.
|
||||
$scope.$watch(( -> EnterpriseFee.loading), (isLoading) =>
|
||||
$scope.enterprise_fees ||= EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true) unless isLoading
|
||||
)
|
||||
|
||||
$scope.exchangeTotalVariants = (exchange) ->
|
||||
return unless $scope.enterprises? && $scope.enterprises[exchange.enterprise_id]?
|
||||
|
||||
|
||||
@@ -14,10 +14,14 @@ angular.module('admin.orderCycles').factory('EnterpriseFee', ($resource) ->
|
||||
EnterpriseFee: EnterpriseFee
|
||||
enterprise_fees: {}
|
||||
loaded: false
|
||||
loading: false
|
||||
|
||||
index: (params={}) ->
|
||||
return if @loading == true
|
||||
@loading = true
|
||||
EnterpriseFee.index params, (data) =>
|
||||
@enterprise_fees = data
|
||||
@loading = false
|
||||
@loaded = true
|
||||
|
||||
forEnterprise: (enterprise_id) ->
|
||||
|
||||
@@ -6,6 +6,7 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris
|
||||
'manage_products'
|
||||
'edit_profile'
|
||||
'create_variant_overrides'
|
||||
'create_linked_variants'
|
||||
]
|
||||
|
||||
constructor: ->
|
||||
@@ -30,3 +31,4 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris
|
||||
when "manage_products" then t('js.services.manage_products')
|
||||
when "edit_profile" then t('js.services.edit_profile')
|
||||
when "create_variant_overrides" then t('js.services.add_products_to_inventory')
|
||||
when "create_linked_variants" then t('js.services.create_linked_variants')
|
||||
|
||||
@@ -107,6 +107,33 @@ module Admin
|
||||
end
|
||||
end
|
||||
|
||||
# Clone a variant, retaining a link to the "source"
|
||||
def create_linked_variant
|
||||
linked_variant = Spree::Variant.find(params[:variant_id])
|
||||
product_index = params[:product_index]
|
||||
authorize! :create_linked_variant, linked_variant
|
||||
status = :ok
|
||||
|
||||
begin
|
||||
variant = linked_variant.create_linked_variant(spree_current_user)
|
||||
|
||||
flash.now[:success] = t('.success')
|
||||
variant_index = "-#{variant.id}"
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
flash.now[:error] = variant.errors.full_messages.to_sentence
|
||||
status = :unprocessable_entity
|
||||
variant_index = "-1" # Create a unique-enough index
|
||||
end
|
||||
|
||||
respond_with do |format|
|
||||
format.turbo_stream {
|
||||
locals = { linked_variant:, variant:, product_index:, variant_index:,
|
||||
producer_options:, category_options: categories, tax_category_options: }
|
||||
render :create_linked_variant, status:, locals:
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def index_url(params)
|
||||
"/admin/products?#{params.to_query}" # todo: fix routing so this can be automaticly generated
|
||||
end
|
||||
|
||||
@@ -2,15 +2,36 @@
|
||||
|
||||
module PaymentGateways
|
||||
class TalerController < BaseController
|
||||
include OrderStockCheck
|
||||
include OrderCompletion
|
||||
|
||||
class StockError < StandardError
|
||||
end
|
||||
|
||||
# The Taler merchant backend has taken the payment.
|
||||
# Now we just need to confirm that and update our local database
|
||||
# before finalising the order.
|
||||
def confirm
|
||||
payment = Spree::Payment.find(params[:payment_id])
|
||||
|
||||
# Process payment early because it's probably paid already.
|
||||
# We want to capture that before any validations raise errors.
|
||||
unless payment.process!
|
||||
return redirect_to order_failed_route(step: "payment")
|
||||
end
|
||||
|
||||
@order = payment.order
|
||||
process_payment_completion!
|
||||
OrderLocker.lock_order_and_variants(@order) do
|
||||
raise StockError unless sufficient_stock?
|
||||
|
||||
process_payment_completion!
|
||||
end
|
||||
rescue Spree::Core::GatewayError => e
|
||||
flash[:notice] = e.message
|
||||
redirect_to order_failed_route(step: "payment")
|
||||
rescue StockError
|
||||
flash[:notice] = t("checkout.payment_cancelled_due_to_stock")
|
||||
redirect_to main_app.checkout_step_path(step: "details")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,7 +102,7 @@ module Spree
|
||||
def sign_in_if_change_own_password
|
||||
return unless spree_current_user == @user && @user.password.present?
|
||||
|
||||
sign_in(@user, event: :authentication, bypass: true)
|
||||
bypass_sign_in(@user)
|
||||
end
|
||||
|
||||
def new_email_unconfirmed?
|
||||
|
||||
@@ -47,5 +47,10 @@ module Admin
|
||||
def variant_tag_enabled?(user)
|
||||
feature?(:variant_tag, user) || feature?(:variant_tag, *user.enterprises)
|
||||
end
|
||||
|
||||
def allowed_source_producers
|
||||
@allowed_source_producers ||= OpenFoodNetwork::Permissions.new(spree_current_user)
|
||||
.enterprises_granting_linked_variants
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -202,7 +202,7 @@ module Spree
|
||||
def add_product_management_abilities(user)
|
||||
# Enterprise User can only access products that they are a supplier for
|
||||
can [:create], Spree::Product
|
||||
# An enterperprise user can change a product if they are supplier of at least
|
||||
# An enterprise user can change a product if they are supplier of at least
|
||||
# one of the product's associated variants
|
||||
can [:admin, :read, :index, :update,
|
||||
:seo, :group_buy_options,
|
||||
@@ -214,7 +214,16 @@ module Spree
|
||||
)
|
||||
end
|
||||
|
||||
can [:admin, :index, :bulk_update, :destroy, :destroy_variant, :clone], :products_v3
|
||||
# An enterprise user can clone if they have been granted permission to the source variant.
|
||||
# Technically I'd call this permission clone_linked_variant, but it would be less confusing to
|
||||
# use the same name as everywhere else.
|
||||
can [:create_linked_variant], Spree::Variant do |variant|
|
||||
OpenFoodNetwork::Permissions.new(user).
|
||||
enterprises_granting_linked_variants.include? variant.supplier
|
||||
end
|
||||
|
||||
can [:admin, :index, :bulk_update, :destroy, :destroy_variant, :clone,
|
||||
:create_linked_variant], :products_v3
|
||||
|
||||
can [:create], Spree::Variant
|
||||
can [:admin, :index, :read, :edit,
|
||||
|
||||
@@ -674,8 +674,6 @@ module Spree
|
||||
end
|
||||
|
||||
def process_each_payment
|
||||
raise Core::GatewayError, Spree.t(:no_pending_payments) if pending_payments.empty?
|
||||
|
||||
pending_payments.each do |payment|
|
||||
if payment.amount.zero? && zero_priced_order?
|
||||
payment.update_columns(state: "completed", captured_at: Time.zone.now)
|
||||
|
||||
@@ -5,10 +5,11 @@ module Spree
|
||||
include SetUnusedAddressFields
|
||||
|
||||
self.belongs_to_required_by_default = false
|
||||
self.ignored_columns += [:authentication_token]
|
||||
|
||||
searchable_attributes :email
|
||||
|
||||
devise :database_authenticatable, :token_authenticatable, :registerable, :recoverable,
|
||||
devise :database_authenticatable, :registerable, :recoverable,
|
||||
:rememberable, :trackable, :validatable, :omniauthable,
|
||||
:encryptable, :confirmable,
|
||||
encryptor: 'authlogic_sha512', reconfirmable: true,
|
||||
|
||||
@@ -40,6 +40,7 @@ module Spree
|
||||
belongs_to :shipping_category, class_name: 'Spree::ShippingCategory', optional: false
|
||||
belongs_to :primary_taxon, class_name: 'Spree::Taxon', touch: true, optional: false
|
||||
belongs_to :supplier, class_name: 'Enterprise', optional: false, touch: true
|
||||
belongs_to :hub, class_name: 'Enterprise', optional: true
|
||||
|
||||
delegate :name, :name=, :description, :description=, :meta_keywords, to: :product
|
||||
|
||||
@@ -72,6 +73,15 @@ module Spree
|
||||
has_many :semantic_links, as: :subject, dependent: :delete_all
|
||||
has_many :supplier_properties, through: :supplier, source: :properties
|
||||
|
||||
# Linked variants: I may have one or many sources.
|
||||
has_many :variant_links_as_target, class_name: 'VariantLink', foreign_key: :target_variant_id,
|
||||
dependent: :delete_all, inverse_of: :target_variant
|
||||
has_many :source_variants, through: :variant_links_as_target, source: :source_variant
|
||||
# I may also have one more many targets.
|
||||
has_many :variant_links_as_source, class_name: 'VariantLink', foreign_key: :source_variant_id,
|
||||
dependent: :delete_all, inverse_of: :source_variant
|
||||
has_many :target_variants, through: :variant_links_as_source, source: :target_variant
|
||||
|
||||
localize_number :price, :weight
|
||||
|
||||
validates_lengths_from_database
|
||||
@@ -263,6 +273,24 @@ module Spree
|
||||
@on_hand_desired = ActiveModel::Type::Integer.new.cast(val)
|
||||
end
|
||||
|
||||
# Clone this variant, retaining a 'source' link to it
|
||||
def create_linked_variant(user)
|
||||
# Hub owner is my enterprise which has permission to create variant sourced from that supplier
|
||||
hub_id = EnterpriseRelationship.permitted_by(supplier).permitting(user.enterprises)
|
||||
.with_permission(:create_linked_variants)
|
||||
.pick(:child_id)
|
||||
|
||||
dup.tap do |variant|
|
||||
variant.price = price
|
||||
variant.source_variants = [self]
|
||||
variant.stock_items << Spree::StockItem.new(variant:)
|
||||
variant.hub_id = hub_id
|
||||
variant.on_demand = on_demand
|
||||
variant.on_hand = on_hand
|
||||
variant.save!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_currency
|
||||
|
||||
6
app/models/variant_link.rb
Normal file
6
app/models/variant_link.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class VariantLink < ApplicationRecord
|
||||
belongs_to :source_variant, class_name: 'Spree::Variant'
|
||||
belongs_to :target_variant, class_name: 'Spree::Variant'
|
||||
end
|
||||
@@ -11,9 +11,10 @@
|
||||
|
||||
-# Filter out variant a user has not permission to update, but keep variant with no supplier
|
||||
- next if variant.supplier.present? && !allowed_producers.include?(variant.supplier)
|
||||
|
||||
= form.fields_for("products][#{product_index}][variants_attributes", variant, index: variant_index) do |variant_form|
|
||||
%tr.condensed{ id: dom_id(variant), 'data-controller': "variant", 'class': "nested-form-wrapper", 'data-new-record': variant.new_record? ? "true" : false }
|
||||
= render partial: 'variant_row', locals: { variant:, f: variant_form, category_options:, tax_category_options:, producer_options: }
|
||||
= render partial: 'variant_row', locals: { variant:, f: variant_form, product_index:, category_options:, tax_category_options:, producer_options: }
|
||||
|
||||
= form.fields_for("products][#{product_index}][variants_attributes][NEW_RECORD", prepare_new_variant(product, producer_options)) do |new_variant_form|
|
||||
%template{ 'data-nested-form-target': "template" }
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
-# locals: (variant:, f:, category_options:, tax_category_options:, producer_options:)
|
||||
-# haml-lint:disable ViewLength (This file is big, but doesn't make sense to split up at this point)
|
||||
-# locals: (variant:, f:, product_index: nil, category_options:, tax_category_options:, producer_options:)
|
||||
- method_on_demand, method_on_hand = variant.new_record? ? [:on_demand_desired, :on_hand_desired ]: [:on_demand, :on_hand]
|
||||
%td.col-image
|
||||
-# empty
|
||||
- variant.source_variants.each do |source_variant|
|
||||
= content_tag(:span, "🔗", title: t('admin.products_page.variant_row.sourced_from', source_name: source_variant.display_name, source_id: source_variant.id, hub_name: variant.hub&.name))
|
||||
%td.col-name.field.naked_inputs
|
||||
= f.hidden_field :id
|
||||
= f.text_field :display_name, 'aria-label': t('admin.products_page.columns.name'), placeholder: variant.product.name
|
||||
@@ -88,6 +91,10 @@
|
||||
= render(VerticalEllipsisMenuComponent.new) do
|
||||
- if variant.persisted?
|
||||
= link_to t('admin.products_page.actions.edit'), edit_admin_product_variant_path(variant.product, variant)
|
||||
|
||||
- if allowed_source_producers.include?(variant.supplier)
|
||||
= link_to t('admin.products_page.actions.create_linked_variant'), admin_create_linked_variant_path(variant_id: variant.id, product_index:), 'data-turbo-method': :post
|
||||
|
||||
- if variant.product.variants.size > 1
|
||||
%a{ "data-controller": "modal-link", "data-action": "click->modal-link#setModalDataSetOnConfirm click->modal-link#open",
|
||||
"data-modal-link-target-value": "variant-delete-modal", "class": "delete",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-# locals: (variant:, linked_variant:, product_index:, variant_index:, producer_options:, category_options:, tax_category_options:)
|
||||
-# Pre-render the form, because you can't do it inside turbo stream block
|
||||
- variant_row = nil
|
||||
- fields_for("products][#{product_index}][variants_attributes", variant, index: variant_index) do |f|
|
||||
- variant_row = render(partial: 'variant_row', formats: :html,
|
||||
locals: { f:,
|
||||
variant:,
|
||||
producer_options:,
|
||||
category_options:,
|
||||
tax_category_options:})
|
||||
= turbo_stream.after dom_id(linked_variant) do
|
||||
%tr.condensed{ id: dom_id(variant), 'data-controller': "variant", 'class': "nested-form-wrapper slide-in",'data-variant-bulk-form-outlet': "#products-form"}
|
||||
= variant_row
|
||||
|
||||
= turbo_stream.append "flashes" do
|
||||
= render(partial: 'admin/shared/flashes', locals: { flashes: flash })
|
||||
@@ -6,7 +6,7 @@
|
||||
%p= t ".description"
|
||||
|
||||
%fieldset.no-border-top.no-border-bottom
|
||||
.row
|
||||
.row.field
|
||||
= f.label :email, t(:email)
|
||||
= f.email_field :email, placeholder: t('.eg_email_address'), data: { controller: "select-user" }, inputmode: "email", autocomplete: "off"
|
||||
= f.error_message_on :email
|
||||
|
||||
@@ -44,12 +44,17 @@ export default class BulkFormController extends Controller {
|
||||
}
|
||||
|
||||
// Register any new elements (may be called by another controller after dynamically adding fields)
|
||||
registerElements() {
|
||||
const registeredElements = Object.values(this.recordElements).flat();
|
||||
// Select only elements that haven't been registered yet
|
||||
const newElements = Array.from(this.form.elements).filter(
|
||||
(n) => !registeredElements.includes(n),
|
||||
);
|
||||
// May be called with array of elements to register, otherwise finds all un-registered elements.
|
||||
registerElements(eventOrElements = null) {
|
||||
let newElements;
|
||||
|
||||
if (Array.isArray(eventOrElements)) {
|
||||
newElements = eventOrElements;
|
||||
} else {
|
||||
const registeredElements = Object.values(this.recordElements).flat();
|
||||
// Select only elements that haven't been registered yet
|
||||
newElements = Array.from(this.form.elements).filter((n) => !registeredElements.includes(n));
|
||||
}
|
||||
|
||||
this.#registerElements(newElements);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import OptionValueNamer from "js/services/option_value_namer";
|
||||
// Dynamically update related variant fields
|
||||
//
|
||||
export default class VariantController extends Controller {
|
||||
static outlets = ["bulk-form"];
|
||||
|
||||
connect() {
|
||||
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
|
||||
// It could automatically find (and cache a ref to) each dom element and get/set the values.
|
||||
@@ -40,6 +42,12 @@ export default class VariantController extends Controller {
|
||||
// on display_as changed; update unit_to_display
|
||||
// TODO: optimise to avoid unnecessary OptionValueNamer calc
|
||||
this.displayAs.addEventListener("input", this.#updateUnitDisplay.bind(this), { passive: true });
|
||||
|
||||
// Register with bulk products form to listen for changes. Used when dynamically appending variants.
|
||||
if (this.hasBulkFormOutlet) {
|
||||
const formElements = this.element.querySelectorAll("input, select, textarea, button");
|
||||
this.bulkFormOutlet.registerElements(formElements);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
.button, button {
|
||||
@include border-radius(0.5em);
|
||||
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
|
||||
&.x-small {
|
||||
@@ -65,7 +66,6 @@
|
||||
}
|
||||
|
||||
.button.primary, button.primary {
|
||||
font-family: $body-font;
|
||||
background: $orange-450;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -158,10 +158,10 @@ module Openfoodnetwork
|
||||
# Activate observers that should always be running.
|
||||
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
|
||||
|
||||
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
||||
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
||||
# The default locale is set in the environment.
|
||||
config.i18n.default_locale = OpenFoodNetwork::I18nConfig.default_locale
|
||||
config.i18n.available_locales = OpenFoodNetwork::I18nConfig.available_locales
|
||||
config.i18n.fallbacks = OpenFoodNetwork::I18nConfig.fallbacks
|
||||
I18n.locale = config.i18n.locale = config.i18n.default_locale
|
||||
|
||||
# Calculate digests for locale files so we can know when they change
|
||||
|
||||
@@ -111,8 +111,6 @@ Rails.application.configure do
|
||||
# Raises error for missing translations.
|
||||
# config.i18n.raise_on_missing_translations = true
|
||||
|
||||
config.i18n.fallbacks = [:en]
|
||||
|
||||
# Annotate rendered view with file names.
|
||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||
|
||||
|
||||
@@ -91,10 +91,6 @@ Rails.application.configure do
|
||||
# Use https in email links
|
||||
config.action_mailer.default_url_options = { protocol: 'https' }
|
||||
|
||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||
# the I18n.default_locale when a translation cannot be found).
|
||||
config.i18n.fallbacks = [:en]
|
||||
|
||||
# Send deprecation notices to registered listeners
|
||||
config.active_support.deprecation = :notify
|
||||
|
||||
|
||||
@@ -60,10 +60,6 @@ Openfoodnetwork::Application.configure do
|
||||
# Enable threaded mode
|
||||
# config.threadsafe!
|
||||
|
||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||
# the I18n.default_locale when a translation can not be found)
|
||||
config.i18n.fallbacks = [:en]
|
||||
|
||||
# Send deprecation notices to registered listeners
|
||||
config.active_support.deprecation = :notify
|
||||
end
|
||||
|
||||
@@ -89,12 +89,6 @@ Rails.application.configure do
|
||||
# Raises error for missing translations.
|
||||
config.i18n.raise_on_missing_translations = true
|
||||
|
||||
# Tests assume English text on the site.
|
||||
config.i18n.default_locale = "en"
|
||||
config.i18n.available_locales = ['en', 'es', 'pt']
|
||||
config.i18n.fallbacks = [:en]
|
||||
I18n.locale = config.i18n.locale = config.i18n.default_locale
|
||||
|
||||
# Annotate rendered view with file names.
|
||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||
|
||||
|
||||
@@ -138,11 +138,6 @@ Devise.setup do |config|
|
||||
config.case_insensitive_keys = [:email]
|
||||
end
|
||||
|
||||
Devise::TokenAuthenticatable.setup do |config|
|
||||
# Defines name of the authentication token params key
|
||||
config.token_authentication_key = :auth_token
|
||||
end
|
||||
|
||||
if ENV["OPENID_APP_ID"].present? && ENV["OPENID_APP_SECRET"].present?
|
||||
Devise.setup do |config|
|
||||
site = if Rails.env.development?
|
||||
|
||||
@@ -946,6 +946,12 @@ cy:
|
||||
clone:
|
||||
success: Llwyddwyd i glonio’r cynnyrch
|
||||
error: Methwyd â chlonio’r cynnyrch
|
||||
tag_rules:
|
||||
rules_per_tag:
|
||||
one: "%{tag}has 1 rule"
|
||||
two: "%{tag}has %{count} rules"
|
||||
many: "%{tag}has %{count} rules"
|
||||
other: "%{tag} has %{count} rules"
|
||||
product_import:
|
||||
title: Mewnforio Cynnyrch
|
||||
file_not_found: Ni ddaethpwyd o hyd i ffeil neu ni ellid ei hagor
|
||||
|
||||
@@ -717,8 +717,11 @@ en:
|
||||
delete: Delete
|
||||
remove: Remove
|
||||
preview: Preview
|
||||
create_linked_variant: Create linked variant
|
||||
image:
|
||||
edit: Edit
|
||||
variant_row:
|
||||
sourced_from: "Sourced from: %{source_name} (%{source_id}); Hub: %{hub_name}"
|
||||
product_preview:
|
||||
product_preview: Product preview
|
||||
shop_tab: Shop
|
||||
@@ -1098,6 +1101,8 @@ en:
|
||||
clone:
|
||||
success: Successfully cloned the product
|
||||
error: Unable to clone the product
|
||||
create_linked_variant:
|
||||
success: "Successfully created linked variant"
|
||||
tag_rules:
|
||||
rules_per_tag:
|
||||
one: "%{tag} has 1 rule"
|
||||
@@ -3901,6 +3906,7 @@ en:
|
||||
manage_products: "manage products"
|
||||
edit_profile: "edit profile"
|
||||
add_products_to_inventory: "add products to inventory"
|
||||
create_linked_variants: "create linked variants [BETA]"
|
||||
resources:
|
||||
could_not_delete_customer: 'Could not delete customer'
|
||||
product_import:
|
||||
|
||||
@@ -272,6 +272,10 @@ en_GB:
|
||||
no_default_card: "^No default card available for this customer"
|
||||
shipping_method:
|
||||
not_available_to_shop: "is not available to %{shop}"
|
||||
user_invitation:
|
||||
attributes:
|
||||
email:
|
||||
is_already_manager: is already a manager
|
||||
card_details: "Card details"
|
||||
card_type: "Card type"
|
||||
card_type_is: "Card type is"
|
||||
@@ -421,6 +425,11 @@ en_GB:
|
||||
authorization_required:
|
||||
subject: "A payment requires authorisation from the customer"
|
||||
message: "A payment for order %{order_number} requires additional authorisation from the customer. The customer has been notified via email and the payment will appear as pending until it is authorised."
|
||||
refund_available:
|
||||
subject: "Refund from %{shop}"
|
||||
message: |
|
||||
Your payment of %{amount} to %{shop} is being refunded
|
||||
Accept your refund following the link below.
|
||||
producer_mailer:
|
||||
order_cycle:
|
||||
subject: "Order cycle report for %{producer}"
|
||||
@@ -802,6 +811,7 @@ en_GB:
|
||||
bill_address: "Billing Address"
|
||||
ship_address: "Shipping Address"
|
||||
balance: "Balance"
|
||||
credit: "Available Credit"
|
||||
update_address_success: "Address updated successfully."
|
||||
update_address_error: "Sorry! Please input all of the required fields!"
|
||||
edit_bill_address: "Edit Billing Address"
|
||||
@@ -816,12 +826,16 @@ en_GB:
|
||||
guest_label: "Guest checkout"
|
||||
credit_owed: "Credit Owed"
|
||||
balance_due: "Balance Due"
|
||||
id: Id
|
||||
destroy:
|
||||
has_associated_subscriptions: "Delete failed: This customer has active subscriptions. Cancel them first."
|
||||
customer_account_transaction:
|
||||
index:
|
||||
available_credit: "Available credit: %{available_credit}"
|
||||
transaction_date: Transaction Date
|
||||
description: Description
|
||||
amount: Amount
|
||||
created_by: Created by
|
||||
running_balance: Running balance
|
||||
column_preferences:
|
||||
bulk_update:
|
||||
@@ -1414,10 +1428,12 @@ en_GB:
|
||||
show_hide_payment: 'Show or Hide payment methods at checkout'
|
||||
show_hide_order_cycles: 'Show or Hide order cycles in my shopfront'
|
||||
users:
|
||||
description: The users with permission to manage this enterprise.
|
||||
legend: "Users"
|
||||
email_confirmation_notice_html: "Email confirmation is pending. We've sent a confirmation email to %{email}."
|
||||
resend: Resend
|
||||
contact: "Contact"
|
||||
manager: "Manager"
|
||||
owner: 'Owner'
|
||||
contact_tip: "The manager who will receive enterprise emails for orders and notifications. Must have a confirmed email adress."
|
||||
owner_tip: The primary user responsible for this enterprise.
|
||||
@@ -1428,6 +1444,8 @@ en_GB:
|
||||
invite_manager: "Invite Manager"
|
||||
email_confirmed: "Email confirmed"
|
||||
email_not_confirmed: "Email not confirmed"
|
||||
set_as_contact: "Set %{email} as contact"
|
||||
set_as_owner: "Set %{email} as owner"
|
||||
vouchers:
|
||||
legend: Vouchers
|
||||
voucher_code: Voucher Code
|
||||
@@ -1776,6 +1794,11 @@ en_GB:
|
||||
images: "Images"
|
||||
contact: "Contact"
|
||||
web: "Web Resources"
|
||||
stimulus_pagination:
|
||||
navigation: Pagination
|
||||
page: "Page %{number}"
|
||||
previous: Previous page
|
||||
next: Next page
|
||||
enterprise_issues:
|
||||
create_new: Create New
|
||||
resend_email: Resend Email
|
||||
@@ -2005,7 +2028,10 @@ en_GB:
|
||||
user_invitations:
|
||||
new:
|
||||
back: Back
|
||||
description: "Invite a user to sign up and become a manager of this enterprise."
|
||||
eg_email_address: e.g. email address of a new or existing user
|
||||
email: Email
|
||||
invite_new_user: Invite a new user
|
||||
invite: Invite
|
||||
vouchers:
|
||||
new:
|
||||
@@ -2405,7 +2431,7 @@ en_GB:
|
||||
system_step3_text: "Set up payment methods and add your collection and delivery options."
|
||||
cta_headline: "The sustainable food network of our future."
|
||||
cta_label: "Start shopping"
|
||||
stats_headline: "Join over 18k shoppers and producers in creating a new food system"
|
||||
stats_headline: "Join over 21k shoppers and producers in creating a new food system"
|
||||
stats_producers: "food producers"
|
||||
stats_shops: "food shops"
|
||||
stats_shoppers: "food shoppers"
|
||||
@@ -2439,6 +2465,7 @@ en_GB:
|
||||
order_total: Total order
|
||||
order_payment: "Paying via:"
|
||||
no_payment_required: "No payment required"
|
||||
credit_used: "Credit used: %{amount}"
|
||||
customer_credit: Credit
|
||||
order_billing_address: Billing address
|
||||
order_delivery_on: Delivery on
|
||||
@@ -3423,6 +3450,7 @@ en_GB:
|
||||
no_orders_found: "No Orders Found"
|
||||
order_information: "Order Information"
|
||||
new_payment: "New Payment"
|
||||
credit_customer: Credit customer
|
||||
create_or_update_invoice: "Create or Update Invoice"
|
||||
date_completed: "Date Completed"
|
||||
amount: "Amount"
|
||||
@@ -4020,6 +4048,7 @@ en_GB:
|
||||
items_cannot_be_shipped: "Items cannot be shipped"
|
||||
gateway_config_unavailable: "Gateway config unavailable"
|
||||
gateway_error: "Payment failed"
|
||||
internal_payment_not_voidable: Payment not voidable
|
||||
more: "More"
|
||||
new_adjustment: "New adjustment"
|
||||
new_tax_category: "New Tax Category"
|
||||
@@ -4514,6 +4543,7 @@ en_GB:
|
||||
paypalexpress: "PayPal Express"
|
||||
stripesca: "Stripe SCA "
|
||||
taler: "Taler"
|
||||
customercredit: "Customer Credit"
|
||||
payments:
|
||||
source_forms:
|
||||
stripe:
|
||||
@@ -4521,6 +4551,7 @@ en_GB:
|
||||
submitting_payment: Submitting payment...
|
||||
paypal:
|
||||
no_payment_via_admin_backend: Paypal payments cannot be captured in the Backoffice
|
||||
customer_credit_successful: Customer has been successfully credited!
|
||||
products:
|
||||
image_upload_error: "Please upload the image in JPG, PNG, GIF, SVG or WEBP format."
|
||||
image_not_processable: "Image attachment is not a valid image."
|
||||
@@ -4836,6 +4867,7 @@ en_GB:
|
||||
orders: Orders
|
||||
cards: Credit Cards
|
||||
transactions: Transactions
|
||||
customer_account_transactions: Customer Transactions
|
||||
settings: Account Settings
|
||||
unconfirmed_email: "Pending email confirmation for: %{unconfirmed_email}. Your email address will be updated once the new email is confirmed."
|
||||
orders:
|
||||
@@ -4846,6 +4878,9 @@ en_GB:
|
||||
authorisation_required: Authorisation Required
|
||||
authorise: Authorise
|
||||
customer_account_transactions:
|
||||
title: Customer Transactions
|
||||
credit_available: "Credit available: %{credit}"
|
||||
transaction_date: Transaction Date
|
||||
description: Description
|
||||
amount: Amount
|
||||
running_balance: Running balance
|
||||
@@ -4988,3 +5023,22 @@ en_GB:
|
||||
invisible_captcha:
|
||||
sentence_for_humans: "Please leave empty"
|
||||
timestamp_error_message: "Please try again after 5 seconds."
|
||||
api_customer_credit: "API credit: %{description}"
|
||||
credit_payment_method:
|
||||
name: Customer credit
|
||||
description: Allow customer to pay with credit
|
||||
success: Payment with credit was sucessful
|
||||
void_success: Credit void was sucessful
|
||||
order_payment_description: "Customer credit: Payment for order: %{order_number}"
|
||||
order_void_description: "Customer credit: Refund for order: %{order_number}"
|
||||
errors:
|
||||
customer_not_found: Customer not found
|
||||
missing_payment: Missing payment
|
||||
credit_payment_method_missing: Credit payment method is missing
|
||||
no_credit_available: No credit available
|
||||
not_enough_credit_available: Not enough credit available
|
||||
orders:
|
||||
customer_credit_service:
|
||||
no_credit_owed: No credit owed
|
||||
credit_payment_method_missing: Customer credit payment method is missing, please check configuration
|
||||
refund_sucessful: Refund successful!
|
||||
|
||||
@@ -549,7 +549,7 @@ hu:
|
||||
on_hand: "Készlet"
|
||||
on_demand: "Igény szerint"
|
||||
tax_category: "Adókategória"
|
||||
inherits_properties: "Örökölje a tulajdonságokat?"
|
||||
inherits_properties: "Tulajdonságok öröklése?"
|
||||
import_date: "Importálás dátuma"
|
||||
actions: Műveletek
|
||||
tags: Címkék
|
||||
@@ -562,7 +562,7 @@ hu:
|
||||
on_hand: "Készlet"
|
||||
on_demand: "Igény szerint"
|
||||
tax_category: "Adókategória"
|
||||
inherits_properties: "Örökölje a tulajdonságokat?"
|
||||
inherits_properties: "Tulajdonságok öröklése?"
|
||||
import_date: "Importálás dátuma"
|
||||
actions:
|
||||
edit: Szerkesztés
|
||||
@@ -798,7 +798,7 @@ hu:
|
||||
display_as: Megjelenítés mint
|
||||
category: Kategória
|
||||
tax_category: Adókategória
|
||||
inherits_properties?: Örökölje a tulajdonságokat?
|
||||
inherits_properties?: Tulajdonságok öröklése?
|
||||
av_on: "Av. On"
|
||||
import_date: Importált
|
||||
upload_an_image: Tölts fel egy képet
|
||||
@@ -1877,7 +1877,9 @@ hu:
|
||||
user_invitations:
|
||||
new:
|
||||
back: Vissza
|
||||
description: "Hívj meg egy felhasználót, hogy regisztráljon és a vállalkozás menedzsere legyen."
|
||||
email: Email
|
||||
invite_new_user: Új felhasználó meghívása
|
||||
invite: Meghívás
|
||||
vouchers:
|
||||
new:
|
||||
@@ -4371,7 +4373,7 @@ hu:
|
||||
display_as: Megjelenítés mint
|
||||
category: Kategória
|
||||
tax_category: Adókategória
|
||||
inherits_properties?: Örökölje a tulajdonságokat?
|
||||
inherits_properties?: Tulajdonságok öröklése?
|
||||
av_on: "Av. On"
|
||||
import_date: "Importálás dátuma"
|
||||
products_variant:
|
||||
|
||||
@@ -82,6 +82,7 @@ Openfoodnetwork::Application.routes.draw do
|
||||
delete 'products_v3/:id', to: 'products_v3#destroy', as: 'product_destroy'
|
||||
delete 'products_v3/destroy_variant/:id', to: 'products_v3#destroy_variant', as: 'destroy_variant'
|
||||
post 'clone/:id', to: 'products_v3#clone', as: 'clone_product'
|
||||
post 'products/create_linked_variant', to: 'products_v3#create_linked_variant', as: 'create_linked_variant'
|
||||
resources :product_preview, only: [:show]
|
||||
|
||||
resources :variant_overrides do
|
||||
|
||||
@@ -95,8 +95,5 @@ Openfoodnetwork::Application.routes.draw do
|
||||
|
||||
resources :customer_account_transaction, only: [:create]
|
||||
end
|
||||
|
||||
match '*path', to: redirect(path: "/api/v0/%{path}"), via: :all,
|
||||
constraints: { path: /(?!v[0-9]).+/ }
|
||||
end
|
||||
end
|
||||
|
||||
19
db/migrate/20260211055758_create_variant_links.rb
Normal file
19
db/migrate/20260211055758_create_variant_links.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateVariantLinks < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
# Create a join table to join two variants. One is the source of the other.
|
||||
# Primary key index ensures uniqueness and assists querying. target_variant_id is the most
|
||||
# likely subject and so is first in the index.
|
||||
# An additional index for source_variant is also included because it may be helpful
|
||||
# (https://stackoverflow.com/questions/10790518/best-sql-indexes-for-join-table).
|
||||
create_table :variant_links, primary_key: [:target_variant_id, :source_variant_id] do |t|
|
||||
t.integer :source_variant_id, null: false, index: true
|
||||
t.integer :target_variant_id, null: false
|
||||
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
add_foreign_key :variant_links, :spree_variants, column: :source_variant_id
|
||||
add_foreign_key :variant_links, :spree_variants, column: :target_variant_id
|
||||
end
|
||||
end
|
||||
7
db/migrate/20260225022934_add_hub_to_spree_variants.rb
Normal file
7
db/migrate/20260225022934_add_hub_to_spree_variants.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddHubToSpreeVariants < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_reference :spree_variants, :hub, foreign_key: { to_table: :enterprises }
|
||||
end
|
||||
end
|
||||
12
db/schema.rb
12
db/schema.rb
@@ -1009,6 +1009,8 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
|
||||
t.bigint "supplier_id"
|
||||
t.float "variant_unit_scale"
|
||||
t.string "variant_unit_name", limit: 255
|
||||
t.bigint "hub_id"
|
||||
t.index ["hub_id"], name: "index_spree_variants_on_hub_id"
|
||||
t.index ["primary_taxon_id"], name: "index_spree_variants_on_primary_taxon_id"
|
||||
t.index ["product_id"], name: "index_variants_on_product_id"
|
||||
t.index ["shipping_category_id"], name: "index_spree_variants_on_shipping_category_id"
|
||||
@@ -1113,6 +1115,13 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
|
||||
t.datetime "updated_at", precision: nil, null: false
|
||||
end
|
||||
|
||||
create_table "variant_links", primary_key: ["target_variant_id", "source_variant_id"], force: :cascade do |t|
|
||||
t.integer "source_variant_id", null: false
|
||||
t.integer "target_variant_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["source_variant_id"], name: "index_variant_links_on_source_variant_id"
|
||||
end
|
||||
|
||||
create_table "variant_overrides", id: :serial, force: :cascade do |t|
|
||||
t.integer "variant_id", null: false
|
||||
t.integer "hub_id", null: false
|
||||
@@ -1262,6 +1271,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
|
||||
add_foreign_key "spree_tax_rates", "spree_zones", column: "zone_id", name: "spree_tax_rates_zone_id_fk"
|
||||
add_foreign_key "spree_users", "spree_addresses", column: "bill_address_id", name: "spree_users_bill_address_id_fk"
|
||||
add_foreign_key "spree_users", "spree_addresses", column: "ship_address_id", name: "spree_users_ship_address_id_fk"
|
||||
add_foreign_key "spree_variants", "enterprises", column: "hub_id"
|
||||
add_foreign_key "spree_variants", "enterprises", column: "supplier_id"
|
||||
add_foreign_key "spree_variants", "spree_products", column: "product_id", name: "spree_variants_product_id_fk"
|
||||
add_foreign_key "spree_variants", "spree_shipping_categories", column: "shipping_category_id"
|
||||
@@ -1278,6 +1288,8 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
|
||||
add_foreign_key "subscriptions", "spree_payment_methods", column: "payment_method_id", name: "subscriptions_payment_method_id_fk"
|
||||
add_foreign_key "subscriptions", "spree_shipping_methods", column: "shipping_method_id", name: "subscriptions_shipping_method_id_fk"
|
||||
add_foreign_key "tag_rules", "enterprises"
|
||||
add_foreign_key "variant_links", "spree_variants", column: "source_variant_id"
|
||||
add_foreign_key "variant_links", "spree_variants", column: "target_variant_id"
|
||||
add_foreign_key "variant_overrides", "enterprises", column: "hub_id", name: "variant_overrides_hub_id_fk"
|
||||
add_foreign_key "variant_overrides", "spree_variants", column: "variant_id", name: "variant_overrides_variant_id_fk"
|
||||
add_foreign_key "vouchers", "enterprises"
|
||||
|
||||
@@ -20,6 +20,10 @@ module OpenFoodNetwork
|
||||
(selectable_locales + [default_locale, source_locale]).uniq
|
||||
end
|
||||
|
||||
def self.fallbacks
|
||||
[default_locale, source_locale].uniq
|
||||
end
|
||||
|
||||
# The default locale that is used when the user doesn't have a preference.
|
||||
def self.default_locale
|
||||
ENV["LOCALE"] || ENV["I18N_LOCALE"] || source_locale
|
||||
|
||||
@@ -86,6 +86,10 @@ module OpenFoodNetwork
|
||||
managed_and_related_enterprises_granting :manage_products
|
||||
end
|
||||
|
||||
def enterprises_granting_linked_variants
|
||||
related_enterprises_granting :create_linked_variants
|
||||
end
|
||||
|
||||
def manages_one_enterprise?
|
||||
@user.enterprises.length == 1
|
||||
end
|
||||
|
||||
@@ -6,8 +6,6 @@ module Spree
|
||||
class Money
|
||||
attr_reader :money
|
||||
|
||||
delegate :cents, to: :money
|
||||
|
||||
def initialize(amount, options = {})
|
||||
@money = ::Monetize.parse([amount, options[:currency] || CurrentConfig.get(:currency)].join)
|
||||
|
||||
@@ -32,10 +30,6 @@ module Spree
|
||||
.html_safe # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
|
||||
def format(options = {})
|
||||
@money.format(@options.merge!(options))
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
@money == other.money
|
||||
end
|
||||
|
||||
@@ -1,54 +1,5 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/orders
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"order":{"amount":"KUDOS:10.0","summary":"Open Food Network order","fulfillment_url":"http://test.host/payment_gateways/taler/61"},"create_token":false}'
|
||||
headers:
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Taler Ruby
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx/1.26.3
|
||||
Date:
|
||||
- Thu, 22 Jan 2026 04:43:32 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '42'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Expose-Headers:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
Via:
|
||||
- 1.1 Caddy
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |-
|
||||
{
|
||||
"order_id": "2026.022-0284X4GE8WKMJ"
|
||||
}
|
||||
recorded_at: Thu, 22 Jan 2026 04:43:33 GMT
|
||||
- request:
|
||||
method: get
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/orders/2026.022-0284X4GE8WKMJ
|
||||
@@ -103,4 +54,108 @@ http_interactions:
|
||||
}
|
||||
}
|
||||
recorded_at: Thu, 22 Jan 2026 04:43:34 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
- request:
|
||||
method: post
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"scope":"write"}'
|
||||
headers:
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Taler Ruby
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx/1.26.3
|
||||
Date:
|
||||
- Fri, 20 Mar 2026 04:31:47 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '258'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Expose-Headers:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
Via:
|
||||
- 1.1 Caddy
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: |-
|
||||
{
|
||||
"access_token": "secret-token:J38S28NEJ6T07H1WP60F3T6PPWQYNKMR251TEZEX3CXP3SH54210",
|
||||
"token": "secret-token:J38S28NEJ6T07H1WP60F3T6PPWQYNKMR251TEZEX3CXP3SH54210",
|
||||
"scope": "write",
|
||||
"refreshable": false,
|
||||
"expiration": {
|
||||
"t_s": 1774067507
|
||||
}
|
||||
}
|
||||
recorded_at: Fri, 20 Mar 2026 04:31:48 GMT
|
||||
- request:
|
||||
method: post
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/orders
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"order":{"amount":"KUDOS:10.0","summary":"Open Food Network order","fulfillment_url":"http://test.host/payment_gateways/taler/198"},"create_token":false}'
|
||||
headers:
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Taler Ruby
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx/1.26.3
|
||||
Date:
|
||||
- Fri, 20 Mar 2026 04:31:48 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '42'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Expose-Headers:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
Via:
|
||||
- 1.1 Caddy
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |-
|
||||
{
|
||||
"order_id": "2026.079-0189PJNWMX6JA"
|
||||
}
|
||||
recorded_at: Fri, 20 Mar 2026 04:31:48 GMT
|
||||
recorded_with: VCR 6.4.0
|
||||
|
||||
@@ -47,6 +47,61 @@ http_interactions:
|
||||
"detail": "taler-order-id:12345"
|
||||
}
|
||||
recorded_at: Sat, 24 Jan 2026 00:51:31 GMT
|
||||
- request:
|
||||
method: post
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"scope":"write"}'
|
||||
headers:
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Taler Ruby
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx/1.26.3
|
||||
Date:
|
||||
- Fri, 20 Mar 2026 04:52:23 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '258'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Expose-Headers:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
Via:
|
||||
- 1.1 Caddy
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: |-
|
||||
{
|
||||
"access_token": "secret-token:176N5XTVVSR98FE6V4QR2Y35HKS61ZW5CK1BC7YEZYHX9M41N5GG",
|
||||
"token": "secret-token:176N5XTVVSR98FE6V4QR2Y35HKS61ZW5CK1BC7YEZYHX9M41N5GG",
|
||||
"scope": "write",
|
||||
"refreshable": false,
|
||||
"expiration": {
|
||||
"t_s": 1774068743
|
||||
}
|
||||
}
|
||||
recorded_at: Fri, 20 Mar 2026 04:52:23 GMT
|
||||
- request:
|
||||
method: get
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/orders/2026.020-03R3ETNZZ0DVA
|
||||
@@ -70,7 +125,7 @@ http_interactions:
|
||||
Server:
|
||||
- nginx/1.26.3
|
||||
Date:
|
||||
- Sat, 24 Jan 2026 00:55:33 GMT
|
||||
- Fri, 20 Mar 2026 04:52:24 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
@@ -205,5 +260,5 @@ http_interactions:
|
||||
"refund_details": [],
|
||||
"order_status_url": "https://backend.demo.taler.net/instances/sandbox/orders/2026.020-03R3ETNZZ0DVA"
|
||||
}
|
||||
recorded_at: Sat, 24 Jan 2026 00:55:32 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
recorded_at: Fri, 20 Mar 2026 04:52:24 GMT
|
||||
recorded_with: VCR 6.4.0
|
||||
|
||||
@@ -15,7 +15,7 @@ RSpec.describe I18nHelper do
|
||||
|
||||
it "sets the default locale" do
|
||||
helper.set_locale
|
||||
expect(I18n.locale).to eq :en
|
||||
expect(I18n.locale).to eq :en_TST
|
||||
end
|
||||
|
||||
it "sets the chosen locale" do
|
||||
@@ -36,11 +36,11 @@ RSpec.describe I18nHelper do
|
||||
it "ignores unavailable locales" do
|
||||
allow(helper).to receive(:params) { { locale: "xx" } }
|
||||
helper.set_locale
|
||||
expect(I18n.locale).to eq :en
|
||||
expect(I18n.locale).to eq :en_TST
|
||||
end
|
||||
|
||||
it "remembers the last chosen locale" do
|
||||
allow(helper).to receive(:params) { { locale: "en" } }
|
||||
allow(helper).to receive(:params) { { locale: "en_TST" } }
|
||||
helper.set_locale
|
||||
|
||||
allow(helper).to receive(:params) { { locale: "es" } }
|
||||
@@ -71,7 +71,7 @@ RSpec.describe I18nHelper do
|
||||
|
||||
allow(helper).to receive(:params) { {} }
|
||||
helper.set_locale
|
||||
expect(I18n.locale).to eq :en
|
||||
expect(I18n.locale).to eq :en_TST
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,7 +82,7 @@ RSpec.describe I18nHelper do
|
||||
|
||||
it "sets the default locale" do
|
||||
helper.set_locale
|
||||
expect(I18n.locale).to eq :en
|
||||
expect(I18n.locale).to eq :en_TST
|
||||
end
|
||||
|
||||
it "sets the chosen locale" do
|
||||
@@ -102,7 +102,7 @@ RSpec.describe I18nHelper do
|
||||
end
|
||||
|
||||
it "remembers the last chosen locale" do
|
||||
allow(helper).to receive(:params) { { locale: "en" } }
|
||||
allow(helper).to receive(:params) { { locale: "en_TST" } }
|
||||
helper.set_locale
|
||||
|
||||
allow(helper).to receive(:params) { { locale: "es" } }
|
||||
|
||||
@@ -16,3 +16,4 @@ describe "enterprise relationships", ->
|
||||
expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "manage products"
|
||||
expect(EnterpriseRelationships.permission_presentation("edit_profile")).toEqual "edit profile"
|
||||
expect(EnterpriseRelationships.permission_presentation("create_variant_overrides")).toEqual "add products to inventory"
|
||||
expect(EnterpriseRelationships.permission_presentation("create_linked_variants")).toEqual "create linked variants [BETA]"
|
||||
|
||||
@@ -17,7 +17,7 @@ RSpec.describe Spree::Money do
|
||||
|
||||
it "can get cents" do
|
||||
money = Spree::Money.new(10)
|
||||
expect(money.cents).to eq(1000)
|
||||
expect(money.money.cents).to eq(1000)
|
||||
end
|
||||
|
||||
context "with currency" do
|
||||
|
||||
@@ -364,6 +364,19 @@ RSpec.describe Spree::Ability do
|
||||
for: p2.variants.first)
|
||||
end
|
||||
|
||||
describe "create_linked_variant" do
|
||||
it "should not be able to create linked variant without permission" do
|
||||
is_expected.not_to have_ability([:create_linked_variant], for: p_related.variants.first)
|
||||
end
|
||||
|
||||
it "should be able to create linked variant when granted permission" do
|
||||
create(:enterprise_relationship, parent: s_related, child: s1,
|
||||
permissions_list: [:create_linked_variants])
|
||||
|
||||
is_expected.to have_ability([:create_linked_variant], for: p_related.variants.first)
|
||||
end
|
||||
end
|
||||
|
||||
it "should not be able to access admin actions on orders" do
|
||||
is_expected.not_to have_ability([:admin], for: Spree::Order)
|
||||
end
|
||||
@@ -729,6 +742,19 @@ RSpec.describe Spree::Ability do
|
||||
it "can request permitted enterprise fees for an order cycle" do
|
||||
is_expected.to have_ability([:for_order_cycle], for: EnterpriseFee)
|
||||
end
|
||||
|
||||
describe "create_linked_variant" do
|
||||
it "should not be able to create linked variant without permission" do
|
||||
is_expected.not_to have_ability([:create_linked_variant], for: p_related.variants.first)
|
||||
end
|
||||
|
||||
it "should be able to create linked variant when granted permission" do
|
||||
create(:enterprise_relationship, parent: s_related, child: d1,
|
||||
permissions_list: [:create_linked_variants])
|
||||
|
||||
is_expected.to have_ability([:create_linked_variant], for: p_related.variants.first)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Order Cycle co-ordinator, distributor enterprise manager' do
|
||||
@@ -804,6 +830,19 @@ RSpec.describe Spree::Ability do
|
||||
it "has the ability to manage vouchers" do
|
||||
is_expected.to have_ability([:admin, :create], for: Voucher)
|
||||
end
|
||||
|
||||
describe "create_linked_variant for own enterprise" do
|
||||
it "should not be able to create own sourced variant without permission" do
|
||||
is_expected.not_to have_ability([:create_linked_variant], for: p1.variants.first)
|
||||
end
|
||||
|
||||
it "should be able to create own sourced variant when granted self permission" do
|
||||
create(:enterprise_relationship, parent: s1, child: s1,
|
||||
permissions_list: [:create_linked_variants])
|
||||
|
||||
is_expected.to have_ability([:create_linked_variant], for: p1.variants.first)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'enterprise owner' do
|
||||
|
||||
@@ -282,11 +282,6 @@ RSpec.describe Spree::Order do
|
||||
let(:payment) { build(:payment) }
|
||||
before { allow(order).to receive_messages pending_payments: [payment], total: 10 }
|
||||
|
||||
it "returns false if no pending_payments available" do
|
||||
allow(order).to receive_messages pending_payments: []
|
||||
expect(order.process_payments!).to be_falsy
|
||||
end
|
||||
|
||||
context "when the processing is sucessful" do
|
||||
it "processes the payments" do
|
||||
expect(payment).to receive(:process!)
|
||||
|
||||
@@ -10,16 +10,18 @@ RSpec.describe Spree::PaymentMethod::Taler do
|
||||
)
|
||||
}
|
||||
let(:backend_url) { "https://backend.demo.taler.net/instances/sandbox" }
|
||||
let(:token_url) { "#{backend_url}/private/token" }
|
||||
|
||||
describe "#external_payment_url", vcr: true do
|
||||
it "creates an order reference and retrieves a URL to pay at" do
|
||||
order = create(:order_ready_for_confirmation, payment_method: taler)
|
||||
|
||||
url = subject.external_payment_url(order:)
|
||||
expect(url).to eq "#{backend_url}/orders/2026.022-0284X4GE8WKMJ"
|
||||
expect(url).to start_with "#{backend_url}/orders/"
|
||||
expect(url).to match "orders/20...[0-9A-Z-]{17}$"
|
||||
|
||||
payment = order.payments.last.reload
|
||||
expect(payment.response_code).to match "2026.022-0284X4GE8WKMJ"
|
||||
expect(payment.response_code).to match "20...[0-9A-Z-]{17}$"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,6 +31,10 @@ RSpec.describe Spree::PaymentMethod::Taler do
|
||||
let(:payment) { build(:payment, response_code: "taler-order-7") }
|
||||
let(:order_url) { "#{backend_url}/private/orders/taler-order-7" }
|
||||
|
||||
before do
|
||||
stub_request(:post, token_url).to_return(body: { token: "12345" }.to_json)
|
||||
end
|
||||
|
||||
it "returns an ActiveMerchant response" do
|
||||
order_status = "paid"
|
||||
stub_request(:get, order_url).to_return(body: { order_status: }.to_json)
|
||||
@@ -57,6 +63,10 @@ RSpec.describe Spree::PaymentMethod::Taler do
|
||||
"taler://refund/backend.demo.taler.net/instances/sandbox/taler-order-8/"
|
||||
}
|
||||
|
||||
before do
|
||||
stub_request(:post, token_url).to_return(body: { token: "12345" }.to_json)
|
||||
end
|
||||
|
||||
it "starts the refund process" do
|
||||
order_status = {
|
||||
order_status: "paid",
|
||||
|
||||
@@ -8,6 +8,7 @@ RSpec.describe Spree::Variant do
|
||||
it { is_expected.to have_many :semantic_links }
|
||||
it { is_expected.to belong_to(:product).required }
|
||||
it { is_expected.to belong_to(:supplier).required }
|
||||
it { is_expected.to belong_to(:hub).optional }
|
||||
it { is_expected.to have_many(:inventory_units) }
|
||||
it { is_expected.to have_many(:line_items) }
|
||||
it { is_expected.to have_many(:stock_items) }
|
||||
@@ -20,6 +21,9 @@ RSpec.describe Spree::Variant do
|
||||
it { is_expected.to have_many(:inventory_items) }
|
||||
it { is_expected.to have_many(:supplier_properties).through(:supplier) }
|
||||
|
||||
it { is_expected.to have_many(:source_variants).through(:variant_links_as_target) }
|
||||
it { is_expected.to have_many(:target_variants).through(:variant_links_as_source) }
|
||||
|
||||
describe "shipping category" do
|
||||
it "sets a shipping category if none provided" do
|
||||
variant = build(:variant, shipping_category: nil)
|
||||
@@ -1001,4 +1005,30 @@ RSpec.describe Spree::Variant do
|
||||
expect(variant.unit_presentation).to eq "My display"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create_linked_variant" do
|
||||
let(:user) { create(:user, enterprises: [enterprise]) }
|
||||
let(:supplier) { variant.supplier }
|
||||
let(:enterprise) { create(:enterprise) }
|
||||
|
||||
context "with create_linked_variants permissions on supplier" do
|
||||
let!(:enterprise_relationship) {
|
||||
create(:enterprise_relationship,
|
||||
parent: supplier,
|
||||
child: enterprise,
|
||||
permissions_list: [:create_linked_variants])
|
||||
}
|
||||
let(:variant) { create(:variant, price: 10.95, on_demand: false, on_hand: 5) }
|
||||
|
||||
it "clones the variant, retaining a link to the source" do
|
||||
linked_variant = variant.create_linked_variant(user)
|
||||
|
||||
expect(linked_variant.source_variants).to eq [variant]
|
||||
expect(linked_variant.hub).to eq enterprise
|
||||
expect(linked_variant.price).to eq 10.95
|
||||
expect(linked_variant.on_demand).to eq false
|
||||
expect(linked_variant.on_hand).to eq 5
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -59,4 +59,71 @@ RSpec.describe "Admin::ProductsV3" do
|
||||
expect(response).to redirect_to('/unauthorized')
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /admin/products/create_linked_variant" do
|
||||
let(:enterprise) { create(:supplier_enterprise) }
|
||||
let(:user) { create(:user, enterprises: [enterprise]) }
|
||||
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:variant) { create(:variant, display_name: "Original variant", supplier: supplier) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it "checks for permission" do
|
||||
params = { variant_id: variant.id, product_index: 1 }
|
||||
|
||||
expect {
|
||||
post(admin_create_linked_variant_path, as: :turbo_stream, params:)
|
||||
expect(response).to redirect_to('/unauthorized')
|
||||
}.not_to change { variant.product.variants.count }
|
||||
end
|
||||
|
||||
context "With create_linked_variants permissions on supplier" do
|
||||
let!(:enterprise_relationship) {
|
||||
create(:enterprise_relationship,
|
||||
parent: supplier,
|
||||
child: enterprise,
|
||||
permissions_list: [:create_linked_variants])
|
||||
}
|
||||
|
||||
it "clones the variant, retaining link as source" do
|
||||
params = { variant_id: variant.id, product_index: 1 }
|
||||
|
||||
expect {
|
||||
post(admin_create_linked_variant_path, as: :turbo_stream, params:)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to match "Original variant" # cloned variant name
|
||||
}.to change { variant.product.variants.count }.by(1)
|
||||
|
||||
new_variant = variant.product.variants.order(:id).last
|
||||
# The new variant is a target of the original. It is a "sourced" variant.
|
||||
expect(variant.target_variants.first).to eq new_variant
|
||||
# The new variant's source is the original
|
||||
expect(new_variant.source_variants.first).to eq variant
|
||||
end
|
||||
|
||||
context "and I'm also owner of another enterprise" do
|
||||
let!(:enterprise2) { create(:enterprise) }
|
||||
let(:user) { create(:user, enterprises: [enterprise, enterprise2]) }
|
||||
|
||||
it "clones the variant, owned by my enterprise that has permission" do
|
||||
enterprise2.owner = user
|
||||
params = { variant_id: variant.id, product_index: 1 }
|
||||
|
||||
expect {
|
||||
post(admin_create_linked_variant_path, as: :turbo_stream, params:)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
}.to change { variant.product.variants.count }.by(1)
|
||||
|
||||
# The new variant is owned by my enterprise that has permission, not the other one
|
||||
new_variant = variant.product.variants.order(:id).last
|
||||
expect(new_variant.hub).to eq enterprise
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,17 +6,6 @@ RSpec.describe 'Orders Cycles endpoint' do
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:order_cycle) { create(:order_cycle, distributors: [distributor]) }
|
||||
|
||||
context "requesting the latest version" do
|
||||
let(:path) { "/api/order_cycles/#{order_cycle.id}/products?distributor=#{distributor.id}" }
|
||||
|
||||
it "redirects to v0, preserving URL params" do
|
||||
get path
|
||||
expect(response).to redirect_to(
|
||||
"/api/v0/order_cycles/#{order_cycle.id}/products?distributor=#{distributor.id}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "requesting a specific API version" do
|
||||
let(:path) { "/api/v0/order_cycles/#{order_cycle.id}/products?distributor=#{distributor.id}" }
|
||||
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe "/payment_gateways/taler/:id" do
|
||||
it "completes the order", :vcr do
|
||||
shop = create(:distributor_enterprise)
|
||||
taler = Spree::PaymentMethod::Taler.create!(
|
||||
let(:shop) { create(:distributor_enterprise) }
|
||||
let(:taler) {
|
||||
Spree::PaymentMethod::Taler.create!(
|
||||
name: "Taler",
|
||||
environment: "test",
|
||||
distributors: [shop],
|
||||
preferred_backend_url: "https://backend.demo.taler.net/instances/sandbox",
|
||||
preferred_api_key: "sandbox",
|
||||
)
|
||||
order = create(:order_ready_for_confirmation, payment_method: taler)
|
||||
}
|
||||
let!(:order) { create(:order_ready_for_confirmation, payment_method: taler) }
|
||||
|
||||
it "completes the order", :vcr do
|
||||
payment = Spree::Payment.last
|
||||
payment.update!(
|
||||
source: taler,
|
||||
@@ -30,4 +33,65 @@ RSpec.describe "/payment_gateways/taler/:id" do
|
||||
payment.reload
|
||||
expect(payment.state).to eq "completed"
|
||||
end
|
||||
|
||||
it "redirects when payment invalid" do
|
||||
payment = Spree::Payment.last
|
||||
payment.update!(
|
||||
source: taler,
|
||||
payment_method: taler,
|
||||
state: "processing", # invalid state to start processing again
|
||||
)
|
||||
|
||||
get payment_gateways_confirm_taler_path(payment_id: payment.id)
|
||||
expect(response).to redirect_to "/checkout/payment"
|
||||
|
||||
payment.reload
|
||||
expect(payment.state).to eq "processing"
|
||||
|
||||
order.reload
|
||||
expect(order.state).to eq "confirmation"
|
||||
end
|
||||
|
||||
it "redirects when payment failed" do
|
||||
payment = Spree::Payment.last
|
||||
payment.update!(
|
||||
source: taler,
|
||||
payment_method: taler,
|
||||
response_code: "2026.020-03R3ETNZZ0DVA",
|
||||
)
|
||||
|
||||
allow_any_instance_of(Taler::Order)
|
||||
.to receive(:fetch).with("order_status").and_return("claimed")
|
||||
|
||||
get payment_gateways_confirm_taler_path(payment_id: payment.id)
|
||||
expect(response).to redirect_to "/checkout/payment"
|
||||
|
||||
payment.reload
|
||||
expect(payment.state).to eq "failed"
|
||||
|
||||
order.reload
|
||||
expect(order.state).to eq "confirmation"
|
||||
end
|
||||
|
||||
it "handles all variants going out of stock" do
|
||||
payment = Spree::Payment.last
|
||||
payment.update!(
|
||||
source: taler,
|
||||
payment_method: taler,
|
||||
response_code: "2026.020-03R3ETNZZ0DVA",
|
||||
)
|
||||
|
||||
allow_any_instance_of(Taler::Order)
|
||||
.to receive(:fetch).with("order_status").and_return("paid")
|
||||
order.line_items[0].variant.on_hand = 0
|
||||
|
||||
get payment_gateways_confirm_taler_path(payment_id: payment.id)
|
||||
expect(response).to redirect_to "/checkout/details"
|
||||
|
||||
payment.reload
|
||||
expect(payment.state).to eq "completed"
|
||||
|
||||
order.reload
|
||||
expect(order.state).to eq "confirmation"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -152,7 +152,7 @@ RSpec.configure do |config|
|
||||
|
||||
# Reset locale for all specs.
|
||||
config.around(:each) do |example|
|
||||
locale = ENV.fetch('LOCALE', 'en_TST')
|
||||
locale = OpenFoodNetwork::I18nConfig.default_locale
|
||||
I18n.with_locale(locale) { example.run }
|
||||
end
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ module StripeStubs
|
||||
|
||||
def stub_capture_request(order, response_mock)
|
||||
stub_request(:post, "https://api.stripe.com/v1/payment_intents/pi_123/capture")
|
||||
.with(body: { amount_to_capture: Spree::Money.new(order.total).cents },
|
||||
.with(body: { amount_to_capture: Spree::Money.new(order.total).money.cents },
|
||||
headers: { 'Stripe-Account' => 'abc123' })
|
||||
.to_return(response_mock)
|
||||
end
|
||||
|
||||
@@ -27,10 +27,6 @@ module WebHelper
|
||||
yield
|
||||
end
|
||||
|
||||
def set_i18n_locale(locale = 'en')
|
||||
page.execute_script("I18n.locale = '#{locale}'")
|
||||
end
|
||||
|
||||
def pick_i18n_locale
|
||||
page.evaluate_script("I18n.locale;")
|
||||
end
|
||||
|
||||
@@ -47,18 +47,23 @@ create(:enterprise)
|
||||
uncheck 'to manage products'
|
||||
check 'to edit profile'
|
||||
check 'to add products to inventory'
|
||||
check 'to create linked variants'
|
||||
select2_select 'Two', from: 'enterprise_relationship_child_id'
|
||||
click_button 'Create'
|
||||
|
||||
# Wait for row to appear since have_relationship doesn't wait
|
||||
expect(page).to have_selector 'tr', count: 2
|
||||
# Permissions appear.. in a different order for some reason.
|
||||
expect_relationship_with_permissions e1, e2,
|
||||
['to add to order cycle',
|
||||
'to add products to inventory', 'to edit profile']
|
||||
'to create linked variants [BETA]',
|
||||
'to add products to inventory',
|
||||
'to edit profile']
|
||||
er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first
|
||||
expect(er).to be_present
|
||||
expect(er.permissions.map(&:name)).to match_array ['add_to_order_cycle', 'edit_profile',
|
||||
'create_variant_overrides']
|
||||
'create_variant_overrides',
|
||||
'create_linked_variants']
|
||||
end
|
||||
|
||||
it "attempting to create a relationship with invalid data" do
|
||||
|
||||
@@ -664,16 +664,16 @@ RSpec.describe '
|
||||
end
|
||||
|
||||
it "finally, can invite unregistered users" do
|
||||
within ".reveal-modal" do
|
||||
tomselect_fill_in "user_invitation[email]", with: "email@email.com"
|
||||
expect do
|
||||
within ".reveal-modal" do
|
||||
tomselect_fill_in "user_invitation[email]", with: "email@email.com"
|
||||
|
||||
expect do
|
||||
click_button "Invite"
|
||||
end.to enqueue_job(ActionMailer::MailDeliveryJob).exactly(:twice)
|
||||
end
|
||||
end
|
||||
|
||||
expect(page)
|
||||
.to have_content "email@email.com has been invited to manage this enterprise"
|
||||
expect(page)
|
||||
.to have_content "email@email.com has been invited to manage this enterprise"
|
||||
end.to enqueue_job(ActionMailer::MailDeliveryJob).exactly(:twice)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,17 +10,12 @@ RSpec.describe 'Multilingual' do
|
||||
|
||||
before do
|
||||
login_as admin_user
|
||||
visit spree.admin_dashboard_path
|
||||
end
|
||||
|
||||
it 'has three locales available' do
|
||||
expect(Rails.application.config.i18n[:default_locale]).to eq 'en'
|
||||
expect(Rails.application.config.i18n[:locale]).to eq 'en'
|
||||
expect(Rails.application.config.i18n[:available_locales]).to eq ['en', 'es', 'pt']
|
||||
end
|
||||
|
||||
it 'can switch language by params' do
|
||||
expect(pick_i18n_locale).to eq 'en'
|
||||
visit spree.admin_dashboard_path
|
||||
|
||||
expect(pick_i18n_locale).to eq 'en_TST'
|
||||
expect(get_i18n_translation('spree_admin_overview_enterprises_header')).to eq 'My Enterprises'
|
||||
expect(page).to have_content 'My Enterprises'
|
||||
expect(admin_user.locale).to be_nil
|
||||
@@ -36,9 +31,9 @@ RSpec.describe 'Multilingual' do
|
||||
|
||||
it 'fallbacks to default_locale' do
|
||||
visit spree.admin_dashboard_path(locale: 'it')
|
||||
expect(pick_i18n_locale).to eq 'en'
|
||||
expect(pick_i18n_locale).to eq 'en_TST'
|
||||
expect(get_i18n_translation('spree_admin_overview_enterprises_header')).to eq 'My Enterprises'
|
||||
expect(page).to have_content 'My Enterprises'
|
||||
expect(admin_user.locale).to be_nil
|
||||
expect(admin_user.reload.locale).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,8 +32,10 @@ RSpec.describe "Admin -> Order -> Payments" do
|
||||
amount: "KUDOS:2",
|
||||
}
|
||||
}
|
||||
token_endpoint = "https://taler.example.com/private/token"
|
||||
order_endpoint = "https://taler.example.com/private/orders/taler-id-1"
|
||||
refund_endpoint = "https://taler.example.com/private/orders/taler-id-1/refund"
|
||||
stub_request(:post, token_endpoint).to_return(body: { token: "abc" }.to_json)
|
||||
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
|
||||
stub_request(:post, refund_endpoint).to_return(body: "{}")
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
require "system_helper"
|
||||
|
||||
RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
RSpec.describe 'As an enterprise user, I can perform actions on the products screen' do
|
||||
include AdminHelper
|
||||
include WebHelper
|
||||
include AuthenticationHelper
|
||||
@@ -19,18 +19,6 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
let(:categories_search_selector) { 'input[placeholder="Select category"]' }
|
||||
let(:tax_categories_search_selector) { 'input[placeholder="Search for tax categories"]' }
|
||||
|
||||
describe "with no products" do
|
||||
before { visit admin_products_url }
|
||||
it "can see the new product page" do
|
||||
expect(page).to have_content "Bulk Edit Products"
|
||||
expect(page).to have_text "No products found"
|
||||
# displays buttons to add products with the correct links
|
||||
expect(page).to have_link(class: "button", text: "New Product", href: "/admin/products/new")
|
||||
expect(page).to have_link(class: "button", text: "Import multiple products",
|
||||
href: admin_product_import_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe "column selector" do
|
||||
let!(:product) { create(:simple_product) }
|
||||
|
||||
@@ -105,8 +93,6 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
end
|
||||
end
|
||||
|
||||
describe "columns"
|
||||
|
||||
describe "Changing producers, category and tax category" do
|
||||
let!(:variant_a1) {
|
||||
product_a.variants.first.tap{ |v|
|
||||
@@ -260,24 +246,24 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
|
||||
describe "Cloning product" do
|
||||
it "shows the cloned product on page when clicked on the cloned option" do
|
||||
# TODO, variant supplier missing, needs to be copied from variant and not product
|
||||
within "table.products" do
|
||||
# Gather input values, because page.content doesn't include them.
|
||||
input_content = page.find_all('input[type=text]').map(&:value).join
|
||||
|
||||
# Products does not include the cloned product.
|
||||
expect(input_content).not_to match /COPY OF Apples/
|
||||
end
|
||||
|
||||
click_product_clone "Apples"
|
||||
|
||||
expect(page).to have_content "Successfully cloned the product"
|
||||
within "table.products" do
|
||||
# Gather input values, because page.content doesn't include them.
|
||||
input_content = page.find_all('input[type=text]').map(&:value).join
|
||||
# Product list includes the cloned product.
|
||||
expect(all_input_values).to match /COPY OF Apples/
|
||||
|
||||
# Products include the cloned product.
|
||||
expect(input_content).to match /COPY OF Apples/
|
||||
# And I can perform actions on the new product
|
||||
within row_containing_name "COPY OF Apples" do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
expect(page).to have_link "Edit"
|
||||
expect(page).to have_link "Clone"
|
||||
# expect(page).to have_link "Delete" # it's not a proper link :/
|
||||
|
||||
fill_in "Name", with: "My copy of Apples"
|
||||
end
|
||||
|
||||
click_button "Save changes"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -298,6 +284,88 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
end
|
||||
end
|
||||
|
||||
describe "Create linked variant" do
|
||||
let!(:variant) {
|
||||
create(:variant, display_name: "My box", supplier: producer)
|
||||
}
|
||||
let!(:other_producer) { create(:supplier_enterprise) }
|
||||
let!(:other_variant) {
|
||||
create(:variant, display_name: "My friends box", supplier: other_producer)
|
||||
}
|
||||
let!(:enterprise_relationship) {
|
||||
# Other producer grants me access to manage their variant
|
||||
create(:enterprise_relationship, parent: other_producer, child: producer,
|
||||
permissions_list: [:manage_products])
|
||||
}
|
||||
|
||||
context "with create_linked_variants permission for my, and other's variants" do
|
||||
it "creates a linked variant" do
|
||||
create(:enterprise_relationship, parent: producer, child: producer,
|
||||
permissions_list: [:create_linked_variants])
|
||||
enterprise_relationship.permissions.create! name: :create_linked_variants
|
||||
|
||||
visit admin_products_url
|
||||
|
||||
# Check my own variant
|
||||
within row_containing_name("My box") do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
|
||||
expect(page).to have_link "Create linked variant"
|
||||
end
|
||||
|
||||
# Create linked variant sourced from my friend
|
||||
within row_containing_name("My friends box") do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
|
||||
click_link "Create linked variant"
|
||||
end
|
||||
|
||||
expect(page).to have_content "Successfully created linked variant"
|
||||
|
||||
within "table.products" do
|
||||
# There are now two copies
|
||||
expect(all_input_values).to match /My friends box.*My friends box/
|
||||
# One of them is designated as a linked variant
|
||||
expect(page).to have_content "🔗"
|
||||
|
||||
last_box = page.all(row_containing_name("My friends box")).last
|
||||
# Close action menu (shouldn't need this, it should close itself)
|
||||
last_box.click
|
||||
|
||||
# And I can perform actions on the new product
|
||||
within last_box do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
expect(page).to have_link "Edit"
|
||||
# expect(page).to have_link "Clone" # tofix: menu is partially obscured
|
||||
# expect(page).to have_link "Delete" # it's not a proper link
|
||||
|
||||
fill_in "Name", with: "My copy of Apples"
|
||||
end
|
||||
click_button "Save changes"
|
||||
|
||||
# initially obscured by the previous message, then disappears before capybara sees it.
|
||||
# expect(page).to have_content "Changes saved"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without create_linked_variants permission" do
|
||||
it "does not show the option in the menu" do
|
||||
visit admin_products_url
|
||||
|
||||
within row_containing_name("My box") do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
expect(page).not_to have_link "Create linked variant"
|
||||
end
|
||||
|
||||
within row_containing_name("My friends box") do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
expect(page).not_to have_link "Create linked variant"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete" do
|
||||
let!(:product_a) { create(:simple_product, name: "Apples", sku: "APL-00") }
|
||||
let(:delete_option_selector) { "a[data-controller='modal-link'].delete" }
|
||||
@@ -527,90 +595,6 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise manager" do
|
||||
let(:supplier_managed1) { create(:supplier_enterprise, name: 'Supplier Managed 1') }
|
||||
let(:supplier_managed2) { create(:supplier_enterprise, name: 'Supplier Managed 2') }
|
||||
let(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Supplier Unmanaged') }
|
||||
let(:supplier_permitted) { create(:supplier_enterprise, name: 'Supplier Permitted') }
|
||||
let(:distributor_managed) { create(:distributor_enterprise, name: 'Distributor Managed') }
|
||||
let(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Distributor Unmanaged') }
|
||||
let!(:product_supplied) { create(:product, supplier_id: supplier_managed1.id, price: 10.0) }
|
||||
let!(:product_not_supplied) { create(:product, supplier_id: supplier_unmanaged.id) }
|
||||
let!(:product_supplied_permitted) {
|
||||
create(:product, name: 'Product Permitted', supplier_id: supplier_permitted.id, price: 10.0)
|
||||
}
|
||||
let(:product_supplied_inactive) {
|
||||
create(:product, supplier_id: supplier_managed1.id, price: 10.0)
|
||||
}
|
||||
|
||||
let!(:supplier_permitted_relationship) do
|
||||
create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed1,
|
||||
permissions_list: [:manage_products])
|
||||
end
|
||||
|
||||
before do
|
||||
enterprise_user = create(:user)
|
||||
enterprise_user.enterprise_roles.build(enterprise: supplier_managed1).save
|
||||
enterprise_user.enterprise_roles.build(enterprise: supplier_managed2).save
|
||||
enterprise_user.enterprise_roles.build(enterprise: distributor_managed).save
|
||||
|
||||
login_as enterprise_user
|
||||
end
|
||||
|
||||
it "shows only products that I supply" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
# displays permitted product list only
|
||||
expect(page).to have_selector row_containing_name(product_supplied.name)
|
||||
expect(page).to have_selector row_containing_name(product_supplied_permitted.name)
|
||||
expect(page).not_to have_selector row_containing_name(product_not_supplied.name)
|
||||
end
|
||||
|
||||
it "shows only suppliers that I manage or have permission to" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
within row_containing_placeholder(product_supplied.name) do
|
||||
expect(page).to have_select(
|
||||
'_products_0_variants_attributes_0_supplier_id',
|
||||
options: [
|
||||
'Select producer',
|
||||
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
|
||||
], selected: supplier_managed1.name
|
||||
)
|
||||
end
|
||||
|
||||
within row_containing_placeholder(product_supplied_permitted.name) do
|
||||
expect(page).to have_select(
|
||||
'_products_1_variants_attributes_0_supplier_id',
|
||||
options: [
|
||||
'Select producer',
|
||||
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
|
||||
], selected: supplier_permitted.name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "shows inactive products that I supply" do
|
||||
product_supplied_inactive
|
||||
|
||||
visit spree.admin_products_path
|
||||
|
||||
expect(page).to have_selector row_containing_name(product_supplied_inactive.name)
|
||||
end
|
||||
|
||||
it "allows me to update a product" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
within row_containing_name(product_supplied.name) do
|
||||
fill_in "Name", with: "Pommes"
|
||||
end
|
||||
click_button "Save changes"
|
||||
|
||||
expect(page).to have_content "Changes saved"
|
||||
expect(page).to have_selector row_containing_name("Pommes")
|
||||
end
|
||||
end
|
||||
|
||||
def open_action_menu
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
end
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
require "system_helper"
|
||||
|
||||
RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
RSpec.describe 'As an enterprise user, I can browse my products' do
|
||||
include AdminHelper
|
||||
include WebHelper
|
||||
include AuthenticationHelper
|
||||
include FileHelper
|
||||
|
||||
let(:producer) { create(:supplier_enterprise) }
|
||||
let(:producer) { create(:supplier_enterprise, name: "My Enterprise") }
|
||||
let(:user) { create(:user, enterprises: [producer]) }
|
||||
|
||||
before do
|
||||
@@ -19,15 +19,25 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
let(:categories_search_selector) { 'input[placeholder="Search for categories"]' }
|
||||
let(:tax_categories_search_selector) { 'input[placeholder="Search for tax categories"]' }
|
||||
|
||||
describe "with no products" do
|
||||
before { visit admin_products_url }
|
||||
it "can see the new product page" do
|
||||
expect(page).to have_content "Bulk Edit Products"
|
||||
expect(page).to have_text "No products found"
|
||||
# displays buttons to add products with the correct links
|
||||
expect(page).to have_link(class: "button", text: "New Product", href: "/admin/products/new")
|
||||
expect(page).to have_link(class: "button", text: "Import multiple products",
|
||||
href: admin_product_import_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe "listing" do
|
||||
let!(:p1) { create(:product, name: "Product1") }
|
||||
let!(:p2) { create(:product, name: "Product2") }
|
||||
|
||||
before do
|
||||
visit admin_products_url
|
||||
end
|
||||
|
||||
it "displays a list of products" do
|
||||
visit admin_products_path
|
||||
|
||||
within ".products" do
|
||||
# displays table header
|
||||
expect(page).to have_selector "th", text: "Name"
|
||||
@@ -129,6 +139,34 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
expect(page).to have_select "variant_unit_with_scale", selected: "Items"
|
||||
expect(page).to have_field "variant_unit_name", with: "packet"
|
||||
end
|
||||
|
||||
context "with sourced variant" do
|
||||
let(:source_producer) { create(:supplier_enterprise) }
|
||||
let(:p3) { create(:product, name: "Product3", supplier_id: source_producer.id) }
|
||||
|
||||
let!(:v3_source) { p3.variants.first }
|
||||
let!(:v3_sourced) {
|
||||
create(:variant, display_name: "Variant3-sourced", product: p3, supplier: source_producer,
|
||||
hub: producer)
|
||||
}
|
||||
let!(:enterprise_relationship) {
|
||||
# Other producer grants me access to manage their variant
|
||||
create(:enterprise_relationship, parent: source_producer, child: producer,
|
||||
permissions_list: [:manage_products])
|
||||
}
|
||||
|
||||
before do
|
||||
v3_sourced.source_variants << v3_source
|
||||
visit admin_products_url
|
||||
end
|
||||
|
||||
it "shows sourced variant with indicator" do
|
||||
within row_containing_name("Variant3-sourced") do
|
||||
expect(page).to have_selector 'span[title*="Sourced from: "]'
|
||||
expect(page).to have_selector 'span[title*="Hub: My Enterprise"]'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "sorting" do
|
||||
@@ -463,4 +501,88 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise manager" do
|
||||
let(:supplier_managed1) { create(:supplier_enterprise, name: 'Supplier Managed 1') }
|
||||
let(:supplier_managed2) { create(:supplier_enterprise, name: 'Supplier Managed 2') }
|
||||
let(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Supplier Unmanaged') }
|
||||
let(:supplier_permitted) { create(:supplier_enterprise, name: 'Supplier Permitted') }
|
||||
let(:distributor_managed) { create(:distributor_enterprise, name: 'Distributor Managed') }
|
||||
let(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Distributor Unmanaged') }
|
||||
let!(:product_supplied) { create(:product, supplier_id: supplier_managed1.id, price: 10.0) }
|
||||
let!(:product_not_supplied) { create(:product, supplier_id: supplier_unmanaged.id) }
|
||||
let!(:product_supplied_permitted) {
|
||||
create(:product, name: 'Product Permitted', supplier_id: supplier_permitted.id, price: 10.0)
|
||||
}
|
||||
let(:product_supplied_inactive) {
|
||||
create(:product, supplier_id: supplier_managed1.id, price: 10.0)
|
||||
}
|
||||
|
||||
let!(:supplier_permitted_relationship) do
|
||||
create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed1,
|
||||
permissions_list: [:manage_products])
|
||||
end
|
||||
|
||||
before do
|
||||
enterprise_user = create(:user)
|
||||
enterprise_user.enterprise_roles.build(enterprise: supplier_managed1).save
|
||||
enterprise_user.enterprise_roles.build(enterprise: supplier_managed2).save
|
||||
enterprise_user.enterprise_roles.build(enterprise: distributor_managed).save
|
||||
|
||||
login_as enterprise_user
|
||||
end
|
||||
|
||||
it "shows only products that I supply" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
# displays permitted product list only
|
||||
expect(page).to have_selector row_containing_name(product_supplied.name)
|
||||
expect(page).to have_selector row_containing_name(product_supplied_permitted.name)
|
||||
expect(page).not_to have_selector row_containing_name(product_not_supplied.name)
|
||||
end
|
||||
|
||||
it "shows only suppliers that I manage or have permission to" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
within row_containing_placeholder(product_supplied.name) do
|
||||
expect(page).to have_select(
|
||||
'_products_0_variants_attributes_0_supplier_id',
|
||||
options: [
|
||||
'Select producer',
|
||||
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
|
||||
], selected: supplier_managed1.name
|
||||
)
|
||||
end
|
||||
|
||||
within row_containing_placeholder(product_supplied_permitted.name) do
|
||||
expect(page).to have_select(
|
||||
'_products_1_variants_attributes_0_supplier_id',
|
||||
options: [
|
||||
'Select producer',
|
||||
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
|
||||
], selected: supplier_permitted.name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "shows inactive products that I supply" do
|
||||
product_supplied_inactive
|
||||
|
||||
visit spree.admin_products_path
|
||||
|
||||
expect(page).to have_selector row_containing_name(product_supplied_inactive.name)
|
||||
end
|
||||
|
||||
it "allows me to update a product" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
within row_containing_name(product_supplied.name) do
|
||||
fill_in "Name", with: "Pommes"
|
||||
end
|
||||
click_button "Save changes"
|
||||
|
||||
expect(page).to have_content "Changes saved"
|
||||
expect(page).to have_selector row_containing_name("Pommes")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,201 +5,192 @@ require "system_helper"
|
||||
RSpec.describe "Managing users" do
|
||||
include AuthenticationHelper
|
||||
|
||||
context "as super-admin" do
|
||||
before do
|
||||
login_as_admin
|
||||
end
|
||||
let(:admin_user) { create(:admin_user) }
|
||||
|
||||
context "from the index page" do
|
||||
before do
|
||||
create(:user, email: "a@example.com")
|
||||
create(:user, email: "b@example.com")
|
||||
before do
|
||||
login_as admin_user
|
||||
end
|
||||
|
||||
context "from the index page" do
|
||||
let!(:user_a) { create(:user, email: "a@example.com") }
|
||||
let!(:user_b) { create(:user, email: "b@example.com") }
|
||||
|
||||
context "searching users" do
|
||||
it "should display the correct results for a user search" do
|
||||
visit spree.admin_dashboard_path
|
||||
click_link "Users"
|
||||
end
|
||||
|
||||
context "users index page with sorting" do
|
||||
before(:each) do
|
||||
click_link "users_email_title"
|
||||
end
|
||||
|
||||
it "should list users with order email asc" do
|
||||
expect(page).to have_css('table#listing_users')
|
||||
within("table#listing_users") do
|
||||
expect(page).to have_content("a@example.com")
|
||||
expect(page).to have_content("b@example.com")
|
||||
end
|
||||
end
|
||||
|
||||
it "should list users with order email desc" do
|
||||
click_link "users_email_title"
|
||||
within("table#listing_users") do
|
||||
expect(page).to have_content("a@example.com")
|
||||
expect(page).to have_content("b@example.com")
|
||||
end
|
||||
fill_in "q_email_cont", with: "a@example"
|
||||
click_button "Search"
|
||||
within("table#listing_users") do
|
||||
expect(page).to have_content("a@example")
|
||||
expect(page).not_to have_content("b@example")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "searching users" do
|
||||
it "should display the correct results for a user search" do
|
||||
fill_in "q_email_cont", with: "a@example"
|
||||
click_button "Search"
|
||||
within("table#listing_users") do
|
||||
expect(page).to have_content("a@example")
|
||||
expect(page).not_to have_content("b@example")
|
||||
end
|
||||
end
|
||||
context "editing users" do
|
||||
it "should allow editing the user password" do
|
||||
visit spree.admin_users_path
|
||||
click_link("a@example.com")
|
||||
fill_in "user_password", with: "welcome"
|
||||
fill_in "user_password_confirmation", with: "welcome"
|
||||
click_button "Update"
|
||||
|
||||
expect(page).to have_content("Account updated")
|
||||
expect(current_path).to eq spree.edit_admin_user_path(user_a)
|
||||
end
|
||||
|
||||
context "editing users" do
|
||||
before(:each) do
|
||||
click_link("a@example.com")
|
||||
end
|
||||
it "allows to change your own password without logging you out" do
|
||||
visit spree.edit_admin_user_path(admin_user)
|
||||
|
||||
it "should allow editing the user password" do
|
||||
fill_in "user_password", with: "welcome"
|
||||
fill_in "user_password_confirmation", with: "welcome"
|
||||
click_button "Update"
|
||||
fill_in "user_password", with: "welcome"
|
||||
fill_in "user_password_confirmation", with: "welcome"
|
||||
click_button "Update"
|
||||
|
||||
expect(page).to have_content("Account updated")
|
||||
end
|
||||
expect(page).to have_content("Account updated")
|
||||
expect(current_path).to eq spree.edit_admin_user_path(admin_user)
|
||||
end
|
||||
|
||||
it "should let me edit the user email" do
|
||||
fill_in "Email", with: "newemail@example.org"
|
||||
click_button "Update"
|
||||
it "should let me edit the user email" do
|
||||
visit spree.edit_admin_user_path(user_a)
|
||||
|
||||
expect(page).to have_content("The account will be updated once " \
|
||||
"the new email is confirmed.")
|
||||
end
|
||||
fill_in "Email", with: "newemail@example.org"
|
||||
click_button "Update"
|
||||
|
||||
it "should allow to generate, regenarate and clear the user api key" do
|
||||
user = Spree::User.find_by(email: "a@example.com")
|
||||
expect(page).to have_content "NO KEY"
|
||||
expect(page).to have_content("The account will be updated once " \
|
||||
"the new email is confirmed.")
|
||||
end
|
||||
|
||||
click_button "Generate API key"
|
||||
first_user_api_key = user.reload.spree_api_key
|
||||
expect(page).to have_content first_user_api_key
|
||||
it "should allow to generate, regenarate and clear the user api key" do
|
||||
visit spree.edit_admin_user_path(user_a)
|
||||
|
||||
click_button "Regenerate Key"
|
||||
second_user_api_key = user.reload.spree_api_key
|
||||
expect(page).to have_content second_user_api_key
|
||||
expect(second_user_api_key).not_to eq first_user_api_key
|
||||
expect(page).to have_content "NO KEY"
|
||||
|
||||
click_button "Clear key"
|
||||
expect(page).to have_content "NO KEY"
|
||||
end
|
||||
click_button "Generate API key"
|
||||
first_user_api_key = user_a.reload.spree_api_key
|
||||
expect(page).to have_content first_user_api_key
|
||||
|
||||
it "should allow to disable the user and to enable it" do
|
||||
expect(page).to have_unchecked_field "Disabled"
|
||||
check "Disabled"
|
||||
click_button "Update"
|
||||
click_button "Regenerate Key"
|
||||
second_user_api_key = user_a.reload.spree_api_key
|
||||
expect(page).to have_content second_user_api_key
|
||||
expect(second_user_api_key).not_to eq first_user_api_key
|
||||
|
||||
expect(page).to have_content("Account updated")
|
||||
expect(page).to have_checked_field "Disabled"
|
||||
uncheck "Disabled"
|
||||
click_button "Update"
|
||||
click_button "Clear key"
|
||||
expect(page).to have_content "NO KEY"
|
||||
end
|
||||
|
||||
expect(page).to have_content("Account updated")
|
||||
expect(page).to have_unchecked_field "Disabled"
|
||||
end
|
||||
it "should allow to disable the user and to enable it" do
|
||||
visit spree.edit_admin_user_path(user_a)
|
||||
|
||||
it "should toggle the api key generation view" do
|
||||
user = Spree::User.find_by(email: "a@example.com")
|
||||
expect(page).to have_unchecked_field "Disabled"
|
||||
check "Disabled"
|
||||
click_button "Update"
|
||||
|
||||
expect(page).to have_content "NO KEY"
|
||||
expect {
|
||||
click_button("Generate API key")
|
||||
expect(page).to have_content("Key generated")
|
||||
}.to change { user.reload.spree_api_key }.from(nil)
|
||||
expect(page).to have_content("Account updated")
|
||||
expect(page).to have_checked_field "Disabled"
|
||||
uncheck "Disabled"
|
||||
click_button "Update"
|
||||
|
||||
expect(page).to have_content("Account updated")
|
||||
expect(page).to have_unchecked_field "Disabled"
|
||||
end
|
||||
|
||||
it "should toggle the api key generation view" do
|
||||
visit spree.edit_admin_user_path(user_a)
|
||||
|
||||
expect(page).to have_content "NO KEY"
|
||||
expect {
|
||||
click_button("Generate API key")
|
||||
expect(page).to have_content("Key generated")
|
||||
}.to change { user_a.reload.spree_api_key }.from(nil)
|
||||
|
||||
expect(page).to have_unchecked_field "Show API key view for user"
|
||||
|
||||
expect {
|
||||
check "Show API key view for user"
|
||||
expect(page).to have_content("Show API key view has been changed!")
|
||||
expect(page).to have_checked_field "Show API key view for user"
|
||||
}.to change { user_a.reload.show_api_key_view }.from(false).to(true)
|
||||
|
||||
expect {
|
||||
uncheck "Show API key view for user"
|
||||
expect(page).to have_content("Show API key view has been changed!")
|
||||
expect(page).to have_unchecked_field "Show API key view for user"
|
||||
|
||||
expect {
|
||||
check "Show API key view for user"
|
||||
expect(page).to have_content("Show API key view has been changed!")
|
||||
expect(page).to have_checked_field "Show API key view for user"
|
||||
}.to change { user.reload.show_api_key_view }.from(false).to(true)
|
||||
|
||||
expect {
|
||||
uncheck "Show API key view for user"
|
||||
expect(page).to have_content("Show API key view has been changed!")
|
||||
expect(page).to have_unchecked_field "Show API key view for user"
|
||||
}.to change { user.reload.show_api_key_view }.to(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "pagination" do
|
||||
before do
|
||||
# creates 8 more users
|
||||
8.times { create(:user) }
|
||||
expect(Spree::User.count).to eq 11
|
||||
visit spree.admin_users_path
|
||||
end
|
||||
it "displays pagination" do
|
||||
# table displays 10 entries
|
||||
within('tbody') do
|
||||
expect(page).to have_css('tr', count: 10)
|
||||
end
|
||||
within ".pagination" do
|
||||
expect(page).not_to have_content "Previous"
|
||||
expect(page).to have_content "Next"
|
||||
click_on "2"
|
||||
end
|
||||
# table displays 1 entry
|
||||
within('tbody') do
|
||||
expect(page).to have_css('tr', count: 1)
|
||||
end
|
||||
within ".pagination" do
|
||||
expect(page).to have_content "Previous"
|
||||
expect(page).not_to have_content "Next"
|
||||
end
|
||||
end
|
||||
}.to change { user_a.reload.show_api_key_view }.to(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "creating a user" do
|
||||
it "confirms successful creation" do
|
||||
visit spree.new_admin_user_path
|
||||
|
||||
# shows no confirmation message to start with
|
||||
expect(page).not_to have_text "Email confirmation is pending"
|
||||
|
||||
fill_in "Email", with: "user1@example.org"
|
||||
fill_in "Password", with: "user1Secret"
|
||||
fill_in "Confirm Password", with: "user1Secret"
|
||||
|
||||
expect(page).to have_select "Language", selected: "English"
|
||||
select "Español", from: "Language"
|
||||
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
click_button "Create"
|
||||
end.to change { Spree::User.count }.by 1
|
||||
expect(page).to have_text "Created Successfully"
|
||||
expect(page).to have_text "Email confirmation is pending"
|
||||
|
||||
expect(Spree::User.last.locale).to eq "es"
|
||||
|
||||
expect(ActionMailer::Base.deliveries.first.subject).to match(
|
||||
"Por favor, confirma tu cuenta de OFN"
|
||||
)
|
||||
end
|
||||
context "pagination" do
|
||||
before do
|
||||
# creates 8 more users
|
||||
8.times { create(:user) }
|
||||
expect(Spree::User.count).to eq 11
|
||||
visit spree.admin_users_path
|
||||
end
|
||||
end
|
||||
|
||||
describe "resending confirmation email" do
|
||||
let(:user) { create :user, confirmed_at: nil }
|
||||
|
||||
it "displays success" do
|
||||
visit spree.edit_admin_user_path user
|
||||
|
||||
expect do
|
||||
# The `a` element doesn't have an href, so we can't use click_link.
|
||||
find("a", text: "Resend").click
|
||||
expect(page).to have_text "Resend done"
|
||||
end.to enqueue_job ActionMailer::MailDeliveryJob
|
||||
it "displays pagination" do
|
||||
# table displays 10 entries
|
||||
within('tbody') do
|
||||
expect(page).to have_css('tr', count: 10)
|
||||
end
|
||||
within ".pagination" do
|
||||
expect(page).not_to have_content "Previous"
|
||||
expect(page).to have_content "Next"
|
||||
click_on "2"
|
||||
end
|
||||
# table displays 1 entry
|
||||
within('tbody') do
|
||||
expect(page).to have_css('tr', count: 1)
|
||||
end
|
||||
within ".pagination" do
|
||||
expect(page).to have_content "Previous"
|
||||
expect(page).not_to have_content "Next"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "creating a user" do
|
||||
it "confirms successful creation" do
|
||||
visit spree.new_admin_user_path
|
||||
|
||||
# shows no confirmation message to start with
|
||||
expect(page).not_to have_text "Email confirmation is pending"
|
||||
|
||||
fill_in "Email", with: "user1@example.org"
|
||||
fill_in "Password", with: "user1Secret"
|
||||
fill_in "Confirm Password", with: "user1Secret"
|
||||
|
||||
expect(page).to have_select "Language", selected: "English"
|
||||
select "Español", from: "Language"
|
||||
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
click_button "Create"
|
||||
end.to change { Spree::User.count }.by 1
|
||||
expect(page).to have_text "Created Successfully"
|
||||
expect(page).to have_text "Email confirmation is pending"
|
||||
|
||||
expect(Spree::User.last.locale).to eq "es"
|
||||
|
||||
expect(ActionMailer::Base.deliveries.first.subject).to match(
|
||||
"Por favor, confirma tu cuenta de OFN"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "resending confirmation email" do
|
||||
let(:user) { create :user, confirmed_at: nil }
|
||||
|
||||
it "displays success" do
|
||||
visit spree.edit_admin_user_path user
|
||||
|
||||
expect do
|
||||
# The `a` element doesn't have an href, so we can't use click_link.
|
||||
find("a", text: "Resend").click
|
||||
expect(page).to have_text "Resend done"
|
||||
end.to enqueue_job ActionMailer::MailDeliveryJob
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -273,7 +273,7 @@ RSpec.describe "Authentication" do
|
||||
expect_logged_in
|
||||
|
||||
expect(page).to have_content 'SHOP NOW'
|
||||
expect(user.reload.locale).to eq "en"
|
||||
expect(user.reload.locale).to eq "en_TST"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
|
||||
before do
|
||||
login_as(user)
|
||||
visit checkout_path
|
||||
end
|
||||
|
||||
context "payment step" do
|
||||
@@ -67,6 +66,7 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
|
||||
context "with a transaction fee" do
|
||||
before do
|
||||
visit checkout_path
|
||||
click_button "Next - Order summary"
|
||||
end
|
||||
|
||||
@@ -84,6 +84,7 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
|
||||
context "after completing the order" do
|
||||
before do
|
||||
visit checkout_path
|
||||
click_on "Complete order"
|
||||
end
|
||||
it_behaves_like "displays the transaction fee", "order confirmation"
|
||||
@@ -277,7 +278,7 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
|
||||
describe "choosing" do
|
||||
shared_examples "different payment methods" do |pay_method|
|
||||
context "checking out with #{pay_method}", if: pay_method.eql?("Stripe SCA") == false do
|
||||
context "checking out with #{pay_method}" do
|
||||
before do
|
||||
visit checkout_step_path(:payment)
|
||||
end
|
||||
@@ -291,46 +292,9 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
expect(order.reload.state).to eq "complete"
|
||||
end
|
||||
end
|
||||
|
||||
context "for Stripe SCA", if: pay_method.eql?("Stripe SCA") do
|
||||
around do |example|
|
||||
with_stripe_setup { example.run }
|
||||
end
|
||||
|
||||
before do
|
||||
stripe_enable
|
||||
visit checkout_step_path(:payment)
|
||||
end
|
||||
|
||||
it "selects Stripe SCA and proceeds to the summary step" do
|
||||
choose pay_method.to_s
|
||||
fill_out_card_details
|
||||
click_on "Next - Order summary"
|
||||
proceed_to_summary
|
||||
end
|
||||
|
||||
context "when saving card" do
|
||||
it "selects Stripe SCA and proceeds to the summary step" do
|
||||
stub_customers_post_request(email: order.user.email)
|
||||
stub_payment_method_attach_request
|
||||
|
||||
choose pay_method.to_s
|
||||
fill_out_card_details
|
||||
check "Save card for future use"
|
||||
|
||||
click_on "Next - Order summary"
|
||||
proceed_to_summary
|
||||
|
||||
# Verify card has been saved with correct stripe IDs
|
||||
user_credit_card = order.reload.user.credit_cards.first
|
||||
expect(user_credit_card.gateway_payment_profile_id).to eq "pm_123"
|
||||
expect(user_credit_card.gateway_customer_profile_id).to eq "cus_A123"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "shared examples" do
|
||||
describe "payment method" do
|
||||
let!(:cash) { create(:payment_method, distributors: [distributor], name: "Cash") }
|
||||
|
||||
context "Cash" do
|
||||
@@ -365,7 +329,40 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
create(:stripe_sca_payment_method, distributors: [distributor], name: "Stripe SCA")
|
||||
}
|
||||
|
||||
it_behaves_like "different payment methods", "Stripe SCA"
|
||||
around do |example|
|
||||
with_stripe_setup { example.run }
|
||||
end
|
||||
|
||||
before do
|
||||
stripe_enable
|
||||
visit checkout_step_path(:payment)
|
||||
end
|
||||
|
||||
it "selects Stripe SCA and proceeds to the summary step" do
|
||||
choose "Stripe SCA"
|
||||
fill_out_card_details
|
||||
click_on "Next - Order summary"
|
||||
proceed_to_summary
|
||||
end
|
||||
|
||||
context "when saving card" do
|
||||
it "selects Stripe SCA and proceeds to the summary step" do
|
||||
stub_customers_post_request(email: order.user.email)
|
||||
stub_payment_method_attach_request
|
||||
|
||||
choose "Stripe SCA"
|
||||
fill_out_card_details
|
||||
check "Save card for future use"
|
||||
|
||||
click_on "Next - Order summary"
|
||||
proceed_to_summary
|
||||
|
||||
# Verify card has been saved with correct stripe IDs
|
||||
user_credit_card = order.reload.user.credit_cards.first
|
||||
expect(user_credit_card.gateway_payment_profile_id).to eq "pm_123"
|
||||
expect(user_credit_card.gateway_customer_profile_id).to eq "cus_A123"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "Taler" do
|
||||
|
||||
@@ -5,189 +5,72 @@ require 'system_helper'
|
||||
RSpec.describe 'Multilingual' do
|
||||
include AuthenticationHelper
|
||||
include WebHelper
|
||||
include ShopWorkflow
|
||||
include UIComponentHelper
|
||||
include CookieHelper
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'has three locales available' do
|
||||
expect(Rails.application.config.i18n[:default_locale]).to eq 'en'
|
||||
expect(Rails.application.config.i18n[:locale]).to eq 'en'
|
||||
expect(Rails.application.config.i18n[:available_locales]).to eq ['en', 'es', 'pt']
|
||||
expect(Rails.application.config.i18n[:default_locale]).to eq 'en_TST'
|
||||
expect(Rails.application.config.i18n[:locale]).to eq 'en_TST'
|
||||
expect(Rails.application.config.i18n[:available_locales]).to eq ['en_TST', 'es', 'pt', 'en']
|
||||
end
|
||||
|
||||
it '18n-js fallsback to default language' do
|
||||
# in backend it doesn't until we change enforce_available_locales to `true`
|
||||
it 'can switch language by params' do
|
||||
visit root_path
|
||||
set_i18n_locale('it')
|
||||
expect(pick_i18n_locale).to eq 'en_TST'
|
||||
expect(get_i18n_translation('label_shops')).to eq 'Shops'
|
||||
expect(cookies_name).not_to include('locale')
|
||||
expect(page).to have_content 'SHOPS'
|
||||
|
||||
visit root_path(locale: 'es')
|
||||
expect(pick_i18n_locale).to eq 'es'
|
||||
expect(get_i18n_translation('label_shops')).to eq 'Tiendas'
|
||||
expect_menu_and_cookie_in_es
|
||||
|
||||
# it is not in the list of available of available_locales
|
||||
visit root_path(locale: 'it')
|
||||
expect(pick_i18n_locale).to eq 'es'
|
||||
expect(get_i18n_translation('label_shops')).to eq 'Tiendas'
|
||||
expect_menu_and_cookie_in_es
|
||||
end
|
||||
|
||||
context 'can switch language by params' do
|
||||
it 'in root path' do
|
||||
visit root_path
|
||||
expect(pick_i18n_locale).to eq 'en'
|
||||
expect(get_i18n_translation('label_shops')).to eq 'Shops'
|
||||
expect(cookies_name).not_to include('locale')
|
||||
expect(page).to have_content 'SHOPS'
|
||||
it 'updates user locale from cookie if it is empty' do
|
||||
visit root_path(locale: 'es')
|
||||
|
||||
visit root_path(locale: 'es')
|
||||
expect(pick_i18n_locale).to eq 'es'
|
||||
expect(get_i18n_translation('label_shops')).to eq 'Tiendas'
|
||||
expect_menu_and_cookie_in_es
|
||||
expect_menu_and_cookie_in_es
|
||||
expect(user.locale).to be_nil
|
||||
login_as user
|
||||
visit root_path
|
||||
|
||||
# it is not in the list of available of available_locales
|
||||
visit root_path(locale: 'it')
|
||||
expect(pick_i18n_locale).to eq 'es'
|
||||
expect(get_i18n_translation('label_shops')).to eq 'Tiendas'
|
||||
expect_menu_and_cookie_in_es
|
||||
end
|
||||
expect_menu_and_cookie_in_es
|
||||
|
||||
context 'with a product in the cart' do
|
||||
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) }
|
||||
let!(:order_cycle) {
|
||||
create(:simple_order_cycle, distributors: [distributor], variants: [product.variants.first])
|
||||
}
|
||||
let(:product) { create(:simple_product) }
|
||||
let(:order) { create(:order, order_cycle:, distributor:) }
|
||||
# The user's locale is not changed if the language was chosen before
|
||||
# login. Is it a bug or a feature? Probably not important...
|
||||
expect(user.reload.locale).to eq nil
|
||||
|
||||
before do
|
||||
pick_order order
|
||||
add_product_to_cart order, product, quantity: 1
|
||||
end
|
||||
visit root_path(locale: 'es')
|
||||
expect(user.reload.locale).to eq 'es'
|
||||
|
||||
it "in the cart page" do
|
||||
visit main_app.cart_path(locale: 'es')
|
||||
logout
|
||||
|
||||
expect_menu_and_cookie_in_es
|
||||
expect(page).to have_content 'Precio'
|
||||
end
|
||||
|
||||
it "visiting checkout as a guest user" do
|
||||
visit checkout_path(locale: 'es')
|
||||
|
||||
expect_menu_and_cookie_in_es
|
||||
expect(page).to have_content 'Iniciar sesión'
|
||||
end
|
||||
end
|
||||
expect_menu_and_cookie_in_es
|
||||
expect(page).to have_content '¿Estás interesada en entrar en Open Food Network?'
|
||||
end
|
||||
|
||||
context 'with user' do
|
||||
let(:user) { create(:user) }
|
||||
it "allows switching language via the main navigation" do
|
||||
visit root_path
|
||||
|
||||
it 'updates user locale from cookie if it is empty' do
|
||||
visit root_path(locale: 'es')
|
||||
expect(page).to have_content 'SHOPS'
|
||||
|
||||
expect_menu_and_cookie_in_es
|
||||
expect(user.locale).to be_nil
|
||||
login_as user
|
||||
visit root_path
|
||||
find('.language-switcher').click
|
||||
within '.language-switcher .dropdown' do
|
||||
expect(page).not_to have_link 'English'
|
||||
expect(page).to have_link 'Español'
|
||||
|
||||
expect_menu_and_cookie_in_es
|
||||
click_link 'Español'
|
||||
end
|
||||
|
||||
it 'updates user locale and stays in cookie after logout' do
|
||||
login_as user
|
||||
|
||||
visit root_path(locale: 'es')
|
||||
user.reload
|
||||
|
||||
expect(user.locale).to eq 'es'
|
||||
|
||||
logout
|
||||
|
||||
expect_menu_and_cookie_in_es
|
||||
expect(page).to have_content '¿Estás interesada en entrar en Open Food Network?'
|
||||
end
|
||||
|
||||
context "visiting checkout as logged user" do
|
||||
let!(:zone) { create(:zone_with_member) }
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:distributor) { create(:distributor_enterprise, charges_sales_tax: true) }
|
||||
let(:product) {
|
||||
create(:taxed_product, supplier_id: supplier.id, price: 10, zone:)
|
||||
}
|
||||
let(:variant) { product.variants.first }
|
||||
let!(:order_cycle) {
|
||||
create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor],
|
||||
coordinator: create(:distributor_enterprise),
|
||||
variants: [variant])
|
||||
}
|
||||
|
||||
let(:free_shipping) {
|
||||
create(:shipping_method, require_ship_address: false)
|
||||
}
|
||||
let!(:payment) {
|
||||
create(:payment_method, distributors: [distributor],
|
||||
name: "Payment")
|
||||
}
|
||||
let(:order) {
|
||||
create(:order_ready_for_confirmation, distributor:)
|
||||
}
|
||||
before do
|
||||
pick_order order
|
||||
login_as user
|
||||
end
|
||||
|
||||
it "on the details step" do
|
||||
visit checkout_step_path(:details, locale: 'es')
|
||||
|
||||
expect_menu_and_cookie_in_es
|
||||
expect(page).to have_content "Sus detalles"
|
||||
end
|
||||
|
||||
it "on the payment step" do
|
||||
visit checkout_step_path(:payment, locale: 'es')
|
||||
|
||||
expect_menu_and_cookie_in_es
|
||||
expect(page).to have_content "Puede revisar y confirmar su pedido"
|
||||
end
|
||||
|
||||
it "on the summary step" do
|
||||
visit checkout_step_path(:summary, locale: 'es')
|
||||
|
||||
expect_menu_and_cookie_in_es
|
||||
expect(page).to have_content "Detalles de entrega"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "using the language switcher UI" do
|
||||
context "when there is only one language available" do
|
||||
before do
|
||||
allow(ENV).to receive(:[]).and_call_original
|
||||
allow(ENV).to receive(:[]).with("LOCALE").and_return("en")
|
||||
allow(ENV).to receive(:[]).with("AVAILABLE_LOCALES").and_return("en")
|
||||
end
|
||||
|
||||
it "hides the dropdown language menu" do
|
||||
visit root_path
|
||||
expect(page).not_to have_css 'ul.right li.language-switcher.has-dropdown'
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are multiple languages available" do
|
||||
before do
|
||||
allow(ENV).to receive(:[]).and_call_original
|
||||
allow(ENV).to receive(:[]).with("LOCALE").and_return("en")
|
||||
allow(ENV).to receive(:[]).with("AVAILABLE_LOCALES").and_return("en,es")
|
||||
end
|
||||
|
||||
it "allows switching language via the main navigation" do
|
||||
visit root_path
|
||||
|
||||
expect(page).to have_content 'SHOPS'
|
||||
|
||||
find('.language-switcher').click
|
||||
within '.language-switcher .dropdown' do
|
||||
expect(page).not_to have_link 'English', href: '/locales/en'
|
||||
expect(page).to have_link 'Español', href: '/locales/es'
|
||||
|
||||
find('li a[href="/locales/es"]').click
|
||||
end
|
||||
|
||||
expect_menu_and_cookie_in_es
|
||||
end
|
||||
end
|
||||
expect_menu_and_cookie_in_es
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -3,221 +3,34 @@
|
||||
require 'system_helper'
|
||||
|
||||
RSpec.describe "Check out with Stripe" do
|
||||
include AuthenticationHelper
|
||||
include ShopWorkflow
|
||||
include CheckoutRequestsHelper
|
||||
include StripeHelper
|
||||
include StripeStubs
|
||||
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let!(:order_cycle) {
|
||||
create(:simple_order_cycle, distributors: [distributor], variants: [variant])
|
||||
}
|
||||
let(:product) { create(:product, price: 10) }
|
||||
let(:variant) { product.variants.first }
|
||||
let(:order) {
|
||||
create(:order, order_cycle:, distributor:, bill_address_id: nil,
|
||||
ship_address_id: nil)
|
||||
}
|
||||
|
||||
let(:shipping_with_fee) {
|
||||
create(:shipping_method, require_ship_address: false, name: "Donkeys",
|
||||
calculator: Calculator::FlatRate.new(preferred_amount: 4.56))
|
||||
}
|
||||
let(:free_shipping) { create(:shipping_method) }
|
||||
let!(:check_with_fee) {
|
||||
create(:payment_method, distributors: [distributor],
|
||||
calculator: Calculator::FlatRate.new(preferred_amount: 5.67))
|
||||
}
|
||||
|
||||
around do |example|
|
||||
with_stripe_setup { example.run }
|
||||
end
|
||||
|
||||
before do
|
||||
stripe_enable
|
||||
pick_order order
|
||||
add_product_to_cart order, product
|
||||
distributor.shipping_methods << [shipping_with_fee, free_shipping]
|
||||
end
|
||||
|
||||
pending "using Stripe SCA" do
|
||||
let!(:stripe_account) { create(:stripe_account, enterprise: distributor) }
|
||||
let!(:stripe_sca_payment_method) {
|
||||
create(:stripe_sca_payment_method, distributors: [distributor])
|
||||
}
|
||||
let!(:shipping_method) { create(:shipping_method) }
|
||||
let(:error_message) { "Card was declined: insufficient funds." }
|
||||
|
||||
before do
|
||||
stub_payment_intent_get_request
|
||||
stub_payment_methods_post_request
|
||||
end
|
||||
|
||||
describe "using Stripe SCA" do
|
||||
context "with guest checkout" do
|
||||
before do
|
||||
stub_retrieve_payment_method_request("pm_123")
|
||||
stub_list_customers_request(email: order.user.email, response: {})
|
||||
stub_get_customer_payment_methods_request(customer: "cus_A456", response: {})
|
||||
end
|
||||
|
||||
context "when the card is accepted" do
|
||||
before do
|
||||
stub_payment_intents_post_request(order:)
|
||||
stub_successful_capture_request order:
|
||||
end
|
||||
|
||||
it "completes checkout successfully" do
|
||||
checkout_with_stripe
|
||||
|
||||
expect(page).to have_content "Confirmed"
|
||||
expect(page.find("#amount-paid").text).to have_content "$19.99"
|
||||
|
||||
expect(order.reload.completed?).to eq true
|
||||
expect(order.payments.first.state).to eq "completed"
|
||||
end
|
||||
it "completes checkout successfully"
|
||||
end
|
||||
|
||||
context "when the card is rejected" do
|
||||
before do
|
||||
stub_payment_intents_post_request(order:)
|
||||
stub_failed_capture_request order:, response: { message: error_message }
|
||||
end
|
||||
|
||||
it "shows an error message from the Stripe response" do
|
||||
checkout_with_stripe
|
||||
|
||||
expect(page).to have_content error_message
|
||||
expect(order.reload.state).to eq "cart"
|
||||
expect(order.payments.first.state).to eq "failed"
|
||||
end
|
||||
it "shows an error message from the Stripe response"
|
||||
end
|
||||
|
||||
context "when the card needs extra SCA authorization" do
|
||||
before do
|
||||
stripe_redirect_url = checkout_path(payment_intent: "pi_123")
|
||||
stub_payment_intents_post_request_with_redirect order:,
|
||||
redirect_url: stripe_redirect_url
|
||||
end
|
||||
|
||||
describe "and the authorization succeeds" do
|
||||
before do
|
||||
stub_successful_capture_request order:
|
||||
end
|
||||
|
||||
it "completes checkout successfully" do
|
||||
checkout_with_stripe
|
||||
|
||||
# We make stripe return stripe_redirect_url (which is already sending the user back
|
||||
# to the checkout) as if the authorization was done. We can then control the actual
|
||||
# authorization or failure of the payment through the mock
|
||||
# stub_successful_capture_request
|
||||
|
||||
expect(page).to have_content "Confirmed"
|
||||
expect(order.reload.completed?).to eq true
|
||||
expect(order.payments.first.state).to eq "completed"
|
||||
end
|
||||
it "completes checkout successfully"
|
||||
end
|
||||
|
||||
describe "and the authorization fails" do
|
||||
before do
|
||||
stub_failed_capture_request order:, response: { message: error_message }
|
||||
end
|
||||
|
||||
it "shows an error message from the Stripe response" do
|
||||
checkout_with_stripe
|
||||
|
||||
# We make stripe return stripe_redirect_url (which is already sending the user back to
|
||||
# the checkout) as if the authorization was done. We can then control the actual
|
||||
# authorization or failure of the payment through the mock stub_failed_capture_request
|
||||
expect(page).to have_content error_message
|
||||
expect(order.reload.state).to eq "cart"
|
||||
expect(order.payments.first.state).to eq "failed"
|
||||
end
|
||||
it "shows an error message from the Stripe response"
|
||||
end
|
||||
end
|
||||
|
||||
context "with multiple payment attempts; one failed and one succeeded" do
|
||||
before do
|
||||
stub_payment_intents_post_request order:
|
||||
end
|
||||
|
||||
it "records failed payment attempt and allows order completion" do
|
||||
# First payment attempt is rejected
|
||||
stub_failed_capture_request(order:, response: { message: error_message })
|
||||
checkout_with_stripe
|
||||
expect(page).to have_content error_message
|
||||
|
||||
expect(order.reload.payments.count).to eq 1
|
||||
expect(order.state).to eq "cart"
|
||||
expect(order.payments.first.state).to eq "failed"
|
||||
|
||||
# Second payment attempt is accepted
|
||||
stub_successful_capture_request(order:)
|
||||
place_order
|
||||
expect(page).to have_content "Confirmed"
|
||||
|
||||
expect(order.reload.payments.count).to eq 2
|
||||
expect(order.state).to eq "complete"
|
||||
expect(order.payments.last.state).to eq "completed"
|
||||
end
|
||||
it "records failed payment attempt and allows order completion"
|
||||
end
|
||||
end
|
||||
|
||||
context "with a logged in user" do
|
||||
let(:user) { order.user }
|
||||
|
||||
before do
|
||||
login_as user
|
||||
end
|
||||
|
||||
context "saving a card and re-using it" do
|
||||
before do
|
||||
stub_retrieve_payment_method_request("pm_123")
|
||||
stub_list_customers_request(email: order.user.email, response: {})
|
||||
stub_get_customer_payment_methods_request(customer: "cus_A456", response: {})
|
||||
stub_get_customer_payment_methods_request(customer: "cus_A123", response: {})
|
||||
stub_payment_methods_post_request(
|
||||
request: { payment_method: "pm_123", customer: "cus_A123" },
|
||||
response: { pm_id: "pm_123" }
|
||||
)
|
||||
stub_add_metadata_request(payment_method: "pm_123", response: {})
|
||||
stub_payment_intents_post_request(order:)
|
||||
stub_successful_capture_request(order:)
|
||||
stub_customers_post_request email: "test@test.com" # First checkout with default details
|
||||
stub_customers_post_request email: user.email # Second checkout with saved user details
|
||||
stub_payment_method_attach_request
|
||||
end
|
||||
|
||||
it "allows saving a card and re-using it" do
|
||||
checkout_with_stripe guest_checkout: false, remember_card: true
|
||||
|
||||
expect(page).to have_content "Confirmed"
|
||||
expect(order.reload.completed?).to eq true
|
||||
expect(order.payments.first.state).to eq "completed"
|
||||
|
||||
# Verify card has been saved with correct stripe IDs
|
||||
user_credit_card = order.reload.user.credit_cards.first
|
||||
expect(user_credit_card.gateway_payment_profile_id).to eq "pm_123"
|
||||
expect(user_credit_card.gateway_customer_profile_id).to eq "cus_A123"
|
||||
|
||||
# Prepare a second order
|
||||
new_order = create(:order, user:, order_cycle:,
|
||||
distributor:, bill_address_id: nil,
|
||||
ship_address_id: nil)
|
||||
pick_order(new_order)
|
||||
add_product_to_cart(new_order, product, quantity: 10)
|
||||
stub_payment_intents_post_request order: new_order
|
||||
stub_successful_capture_request order: new_order
|
||||
|
||||
# Checkout with saved card
|
||||
visit checkout_path
|
||||
choose free_shipping.name
|
||||
choose stripe_sca_payment_method.name
|
||||
expect(page).to have_content "Use a saved card"
|
||||
expect(page).to have_select 'selected_card', selected: "Visa x-4242 Exp:10/2050"
|
||||
place_order
|
||||
end
|
||||
it "allows saving a card and re-using it"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,13 +6,13 @@ RSpec.describe "registration/steps/_details.html.haml" do
|
||||
it "uses Google Maps when it is enabled" do
|
||||
allow(view).to receive_messages(using_google_maps?: true)
|
||||
|
||||
is_expected.to match /<ui-gmap-google-map center='map.center' zoom='map.zoom'>/
|
||||
is_expected.to match /<ui-gmap-google-map center="map.center" zoom="map.zoom">/
|
||||
end
|
||||
|
||||
it "uses OpenStreetMap when it is enabled" do
|
||||
ContentConfig.open_street_map_enabled = true
|
||||
allow(view).to receive_messages(using_google_maps?: false)
|
||||
|
||||
is_expected.to match /<div class='map-container--registration' id='open-street-map'>/
|
||||
is_expected.to match /<div class="map-container--registration" id="open-street-map">/
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user