From 8daa01e2288c8db2a2c7029d628fbc69143773e8 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 2 May 2025 16:58:00 +1000 Subject: [PATCH 1/9] Process OC emails individually to avoid duplicates Errors or delays can cause multiple duplicate emails to the same suppliers. --- app/jobs/order_cycle_notification_job.rb | 2 +- spec/jobs/order_cycle_notification_job_spec.rb | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/jobs/order_cycle_notification_job.rb b/app/jobs/order_cycle_notification_job.rb index c713e12472..42862d832d 100644 --- a/app/jobs/order_cycle_notification_job.rb +++ b/app/jobs/order_cycle_notification_job.rb @@ -5,7 +5,7 @@ class OrderCycleNotificationJob < ApplicationJob def perform(order_cycle_id) order_cycle = OrderCycle.find(order_cycle_id) order_cycle.suppliers.each do |supplier| - ProducerMailer.order_cycle_report(supplier, order_cycle).deliver_now + ProducerMailer.order_cycle_report(supplier, order_cycle).deliver_later end order_cycle.update_columns mails_sent: true end diff --git a/spec/jobs/order_cycle_notification_job_spec.rb b/spec/jobs/order_cycle_notification_job_spec.rb index f8496fc86c..872b63d6ad 100644 --- a/spec/jobs/order_cycle_notification_job_spec.rb +++ b/spec/jobs/order_cycle_notification_job_spec.rb @@ -4,20 +4,18 @@ require 'spec_helper' RSpec.describe OrderCycleNotificationJob do let(:order_cycle) { create(:order_cycle) } - let(:mail) { double(:mail, deliver_now: true) } - - before do - allow(ProducerMailer).to receive(:order_cycle_report).twice.and_return(mail) - end it "sends a mail to each supplier" do - OrderCycleNotificationJob.perform_now order_cycle.id - expect(ProducerMailer).to have_received(:order_cycle_report).twice + expect { + OrderCycleNotificationJob.perform_now(order_cycle.id) + }.to enqueue_mail(ProducerMailer, :order_cycle_report).twice end it "records that mails have been sent for the order cycle" do - expect do + expect { OrderCycleNotificationJob.perform_now(order_cycle.id) - end.to change{ order_cycle.reload.mails_sent? }.from(false).to(true) + }.to change { + order_cycle.reload.mails_sent? + }.from(false).to(true) end end From fb7c74471e045d4a71bc286ad50fca97b9dc0b29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 09:57:54 +0000 Subject: [PATCH 2/9] Bump jasmine-core from 5.7.0 to 5.7.1 Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 5.7.0 to 5.7.1. - [Release notes](https://github.com/jasmine/jasmine/releases) - [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md) - [Commits](https://github.com/jasmine/jasmine/compare/v5.7.0...v5.7.1) --- updated-dependencies: - dependency-name: jasmine-core dependency-version: 5.7.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3b49d6d01e..0ed9527219 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "webpack": "~4" }, "devDependencies": { - "jasmine-core": "~5.7.0", + "jasmine-core": "~5.7.1", "jest": "^27.4.7", "karma": "~6.4.4", "karma-chrome-launcher": "~3.2.0", diff --git a/yarn.lock b/yarn.lock index b96386a416..dd85b54c23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5192,10 +5192,10 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jasmine-core@~5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-5.7.0.tgz#23476f408056a33674469005d87d57d873b4c525" - integrity sha512-EnUzZBHxS1Ofq+FPWs16rs2YC9o6Hb3buKJQDlkhJBDx+Bm5wNF+J1gUS06dWuW2ozaQ3oNIA1SESX9M5LopOQ== +jasmine-core@~5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-5.7.1.tgz#c347ede2dbf1b6b3b3c3362a5b6651414ada4745" + integrity sha512-QnurrtpKsPoixxG2R3d1xP0St/2kcX5oTZyDyQJMY+Vzi/HUlu1kGm+2V8Tz+9lV991leB1l0xcsyz40s9xOOw== jest-changed-files@^27.5.1: version "27.5.1" From ac7768df0575145641050892e03dbe62ee86ea29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 09:58:03 +0000 Subject: [PATCH 3/9] Bump @floating-ui/dom from 1.6.13 to 1.7.0 Bumps [@floating-ui/dom](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/dom) from 1.6.13 to 1.7.0. - [Release notes](https://github.com/floating-ui/floating-ui/releases) - [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/dom/CHANGELOG.md) - [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/dom@1.7.0/packages/dom) --- updated-dependencies: - dependency-name: "@floating-ui/dom" dependency-version: 1.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 3b49d6d01e..b1628b83ae 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "pretty-quick": "pretty-quick" }, "dependencies": { - "@floating-ui/dom": "^1.6.13", + "@floating-ui/dom": "^1.7.0", "@hotwired/stimulus": "^3.2", "@hotwired/turbo": "^8.0.13", "@rails/webpacker": "5.4.4", diff --git a/yarn.lock b/yarn.lock index b96386a416..25361721b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1127,22 +1127,22 @@ resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== -"@floating-ui/core@^1.6.0": - version "1.6.4" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.4.tgz#0140cf5091c8dee602bff9da5ab330840ff91df6" - integrity sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA== +"@floating-ui/core@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.0.tgz#1aff27a993ea1b254a586318c29c3b16ea0f4d0a" + integrity sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA== dependencies: - "@floating-ui/utils" "^0.2.4" - -"@floating-ui/dom@^1.6.13": - version "1.6.13" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34" - integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w== - dependencies: - "@floating-ui/core" "^1.6.0" "@floating-ui/utils" "^0.2.9" -"@floating-ui/utils@^0.2.4", "@floating-ui/utils@^0.2.9": +"@floating-ui/dom@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.0.tgz#f9f83ee4fee78ac23ad9e65b128fc11a27857532" + integrity sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg== + dependencies: + "@floating-ui/core" "^1.7.0" + "@floating-ui/utils" "^0.2.9" + +"@floating-ui/utils@^0.2.9": version "0.2.9" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429" integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg== From 0225db6840e47b6894bcfbb8cd8bb889f6628383 Mon Sep 17 00:00:00 2001 From: David Cook Date: Mon, 5 May 2025 12:35:32 +1000 Subject: [PATCH 4/9] Refactor without setter methods This class was originally built to flexibly accept paramters in any order. It also allowed you to specify multiple of the same type of parameter, with the later one overriding the earlier. This is too flexible and likely to cause mistakes. And besides, we don't use that feature! --- .rubocop_styleguide.yml | 4 --- .../concerns/checkout_callbacks.rb | 3 +- .../admin/subscription_customer_serializer.rb | 2 +- app/serializers/api/admin/user_serializer.rb | 4 +-- lib/open_food_network/address_finder.rb | 31 +++-------------- .../open_food_network/address_finder_spec.rb | 33 ++++--------------- 6 files changed, 15 insertions(+), 62 deletions(-) diff --git a/.rubocop_styleguide.yml b/.rubocop_styleguide.yml index 7f80e7d866..ead7f299be 100644 --- a/.rubocop_styleguide.yml +++ b/.rubocop_styleguide.yml @@ -86,10 +86,6 @@ Metrics/MethodLength: Enabled: true Max: 25 # default 10 -Naming/MemoizedInstanceVariableName: - Exclude: - - 'lib/open_food_network/address_finder.rb' - Metrics/ParameterLists: CountKeywordArgs: false diff --git a/app/controllers/concerns/checkout_callbacks.rb b/app/controllers/concerns/checkout_callbacks.rb index b6dbbfd6c1..bf6a23b860 100644 --- a/app/controllers/concerns/checkout_callbacks.rb +++ b/app/controllers/concerns/checkout_callbacks.rb @@ -48,7 +48,8 @@ module CheckoutCallbacks end def load_saved_addresses - finder = OpenFoodNetwork::AddressFinder.new(@order.email, @order.customer, spree_current_user) + finder = OpenFoodNetwork::AddressFinder.new(email: @order.email, customer: @order.customer, + user: spree_current_user) @order.bill_address ||= finder.bill_address @order.ship_address ||= finder.ship_address diff --git a/app/serializers/api/admin/subscription_customer_serializer.rb b/app/serializers/api/admin/subscription_customer_serializer.rb index 66e676aee4..16243067b2 100644 --- a/app/serializers/api/admin/subscription_customer_serializer.rb +++ b/app/serializers/api/admin/subscription_customer_serializer.rb @@ -10,7 +10,7 @@ module Api delegate :ship_address, to: :finder def finder - @finder ||= OpenFoodNetwork::AddressFinder.new(object, object.email) + @finder ||= OpenFoodNetwork::AddressFinder.new(customer: object, email: object.email) end end end diff --git a/app/serializers/api/admin/user_serializer.rb b/app/serializers/api/admin/user_serializer.rb index 462515d112..116397e245 100644 --- a/app/serializers/api/admin/user_serializer.rb +++ b/app/serializers/api/admin/user_serializer.rb @@ -11,11 +11,11 @@ module Api has_one :bill_address, serializer: Api::AddressSerializer def ship_address - OpenFoodNetwork::AddressFinder.new(object.email, object).ship_address + OpenFoodNetwork::AddressFinder.new(email: object.email, user: object).ship_address end def bill_address - OpenFoodNetwork::AddressFinder.new(object.email, object).bill_address + OpenFoodNetwork::AddressFinder.new(email: object.email, user: object).bill_address end def confirmed diff --git a/lib/open_food_network/address_finder.rb b/lib/open_food_network/address_finder.rb index 32975f9428..0e681bcb6f 100644 --- a/lib/open_food_network/address_finder.rb +++ b/lib/open_food_network/address_finder.rb @@ -10,13 +10,10 @@ module OpenFoodNetwork class AddressFinder attr_reader :email, :user, :customer - def initialize(*args) - args.each do |arg| - type = types[arg.class] - next unless type - - public_send("#{type}=", arg) - end + def initialize(email: nil, user: nil, customer: nil) + @email = email + @user = user + @customer = customer end def bill_address @@ -27,28 +24,8 @@ module OpenFoodNetwork customer_preferred_ship_address || user_preferred_ship_address || fallback_ship_address end - def email=(arg) - @email ||= arg - end - - def customer=(arg) - @customer ||= arg - end - - def user=(arg) - @user ||= arg - end - private - def types - { - String => "email", - Customer => "customer", - Spree::User => "user" - } - end - def customer_preferred_bill_address customer&.bill_address end diff --git a/spec/lib/open_food_network/address_finder_spec.rb b/spec/lib/open_food_network/address_finder_spec.rb index c7068eefe9..79462bb28b 100644 --- a/spec/lib/open_food_network/address_finder_spec.rb +++ b/spec/lib/open_food_network/address_finder_spec.rb @@ -12,39 +12,18 @@ module OpenFoodNetwork let(:customer) { create(:customer) } context "when passed any combination of instances of String, Customer or Spree::User" do - let(:finder1) { AddressFinder.new(email, customer, user) } - let(:finder2) { AddressFinder.new(customer, user, email) } + let(:finder1) { AddressFinder.new(email:, customer:, user:) } - it "stores arguments based on their class" do + it "stores arguments" do expect(finder1.email).to eq email - expect(finder2.email).to eq email expect(finder1.customer).to be customer - expect(finder2.customer).to be customer expect(finder1.user).to be user - expect(finder2.user).to be user - end - end - - context "when passed multiples instances of a class" do - let(:email2) { 'test2@example.com' } - let(:user2) { create(:user) } - let(:customer2) { create(:customer) } - let(:finder1) { AddressFinder.new(user2, email, email2, customer2, user, customer) } - let(:finder2) { AddressFinder.new(email2, customer, user, email, user2, customer2) } - - it "only stores the first encountered instance of a given class" do - expect(finder1.email).to eq email - expect(finder2.email).to eq email2 - expect(finder1.customer).to be customer2 - expect(finder2.customer).to be customer - expect(finder1.user).to be user2 - expect(finder2.user).to be user end end end describe "fallback_bill_address" do - let(:finder) { AddressFinder.new(email) } + let(:finder) { AddressFinder.new(email:) } let(:address) { double(:address, clone: 'address_clone') } context "when a last_used_bill_address is found" do @@ -65,7 +44,7 @@ module OpenFoodNetwork end describe "fallback_ship_address" do - let(:finder) { AddressFinder.new(email) } + let(:finder) { AddressFinder.new(email:) } let(:address) { double(:address, clone: 'address_clone') } context "when a last_used_ship_address is found" do @@ -92,7 +71,7 @@ module OpenFoodNetwork create(:completed_order_with_totals, user: nil, email:, distributor:, bill_address: nil) } - let(:finder) { AddressFinder.new(email) } + let(:finder) { AddressFinder.new(email:) } context "when searching by email is not allowed" do before do @@ -142,7 +121,7 @@ module OpenFoodNetwork describe "last_used_ship_address" do let(:address) { create(:address) } let(:distributor) { create(:distributor_enterprise) } - let(:finder) { AddressFinder.new(email) } + let(:finder) { AddressFinder.new(email:) } context "when searching by email is not allowed" do before do From f6df412355183f5f2e9d20726968aa83fb2e7612 Mon Sep 17 00:00:00 2001 From: cyrillefr Date: Mon, 5 May 2025 10:39:15 +0200 Subject: [PATCH 5/9] Fixes NamingMethodParameterName rubocop offense --- .rubocop_todo.yml | 7 ------- .../payment_gateways/stripe_controller.rb | 2 +- app/services/process_payment_intent.rb | 18 +++++++++--------- spec/services/process_payment_intent_spec.rb | 8 ++++---- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 56b0fbc890..e186ef06fa 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -221,13 +221,6 @@ Metrics/PerceivedComplexity: - 'app/models/spree/ability.rb' - 'app/models/spree/order/checkout.rb' -# Offense count: 1 -# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to -Naming/MethodParameterName: - Exclude: - - 'app/services/process_payment_intent.rb' - # Offense count: 26 # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer diff --git a/app/controllers/payment_gateways/stripe_controller.rb b/app/controllers/payment_gateways/stripe_controller.rb index 37167a636d..bdc4a8e6c3 100644 --- a/app/controllers/payment_gateways/stripe_controller.rb +++ b/app/controllers/payment_gateways/stripe_controller.rb @@ -24,7 +24,7 @@ module PaymentGateways result = ProcessPaymentIntent.new(params["payment_intent"], @order).call! - unless result.ok? + unless result.success? flash.now[:error] = "#{I18n.t('payment_could_not_process')}. #{result.error}" end diff --git a/app/services/process_payment_intent.rb b/app/services/process_payment_intent.rb index eb4429edbb..1a57d9dd27 100644 --- a/app/services/process_payment_intent.rb +++ b/app/services/process_payment_intent.rb @@ -13,13 +13,13 @@ class ProcessPaymentIntent class Result attr_reader :error - def initialize(ok:, error: "") - @ok = ok + def initialize(success:, error: "") + @success = success @error = error end - def ok? - @ok + def success? + @success end end @@ -30,8 +30,8 @@ class ProcessPaymentIntent end def call! - return Result.new(ok: false) unless payment.present? && ready_for_capture? - return Result.new(ok: true) if already_processed? + return Result.new(success: false) unless payment.present? && ready_for_capture? + return Result.new(success: true) if already_processed? process_payment @@ -39,16 +39,16 @@ class ProcessPaymentIntent payment.complete_authorization payment.clear_authorization_url - Result.new(ok: true) + Result.new(success: true) else payment.fail_authorization payment.clear_authorization_url - Result.new(ok: false, error: I18n.t("payment_could_not_complete")) + Result.new(success: false, error: I18n.t("payment_could_not_complete")) end rescue Stripe::StripeError => e payment.fail_authorization payment.clear_authorization_url - Result.new(ok: false, error: e.message) + Result.new(success: false, error: e.message) end private diff --git a/spec/services/process_payment_intent_spec.rb b/spec/services/process_payment_intent_spec.rb index 10af03a91e..98bc89f00e 100644 --- a/spec/services/process_payment_intent_spec.rb +++ b/spec/services/process_payment_intent_spec.rb @@ -39,7 +39,7 @@ RSpec.describe ProcessPaymentIntent do it "returns false" do result = service.call! - expect(result.ok?).to eq(false) + expect(result.success?).to eq(false) expect(result.error).to eq("") end @@ -58,7 +58,7 @@ RSpec.describe ProcessPaymentIntent do it "returns returns the error message" do result = service.call! - expect(result.ok?).to eq(false) + expect(result.success?).to eq(false) expect(result.error).to eq("error message") end @@ -150,7 +150,7 @@ RSpec.describe ProcessPaymentIntent do it "does not return any error message" do result = service.call! - expect(result.ok?).to eq(false) + expect(result.success?).to eq(false) expect(result.error).to eq("") end @@ -173,7 +173,7 @@ RSpec.describe ProcessPaymentIntent do it "returns a failed result" do result = service.call! - expect(result.ok?).to eq(false) + expect(result.success?).to eq(false) expect(result.error).to eq('The payment could not be completed') end From 81d35741a1bd7d7824ea95f078c5ecbba723d12b Mon Sep 17 00:00:00 2001 From: filipefurtad0 Date: Mon, 5 May 2025 19:50:32 +0100 Subject: [PATCH 6/9] Adds tests to toggle the css sections It's a lucky guess, but the idea is that pre-loading the CSS sections and its contents makes it slightly faster, for following assertions, which are the flaky offenders --- spec/system/admin/order_cycles/simple_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/system/admin/order_cycles/simple_spec.rb b/spec/system/admin/order_cycles/simple_spec.rb index 6fef56e540..36bb02a9a9 100644 --- a/spec/system/admin/order_cycles/simple_spec.rb +++ b/spec/system/admin/order_cycles/simple_spec.rb @@ -555,6 +555,14 @@ RSpec.describe ' expect(page).to have_selector "tr.distributor-#{distributor_permitted.id}" expect(page).to have_selector 'tr.distributor', count: 2 + # Toggling all products displays the css section of the distributed products + click_link "Expand all" + expect(page).to have_css ".exchange-distributed-products", count: 2 + + # Colapsing all products hides the css section of the distributed products + click_link "Collapse all" + expect(page).not_to have_css ".exchange-distributed-products" + # Open the products list for managed_supplier's incoming exchange within "tr.distributor-#{distributor_managed.id}" do page.find("td.products").click From e069cad0343c5aebaa4eef5fdb259e6558fe024c Mon Sep 17 00:00:00 2001 From: filipefurtad0 Date: Mon, 5 May 2025 20:16:56 +0100 Subject: [PATCH 7/9] Updates Knapsack Pro --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 01f026fc9c..3c063de220 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -406,7 +406,7 @@ GEM activesupport (>= 4.2) jwt (2.8.1) base64 - knapsack_pro (7.4.0) + knapsack_pro (8.1.2) rake language_server-protocol (3.17.0.3) launchy (3.0.0) From e7edc068ef6c4b828721f9ff0c7aac3825ac0abb Mon Sep 17 00:00:00 2001 From: filipefurtad0 Date: Tue, 6 May 2025 17:25:01 +0100 Subject: [PATCH 8/9] Splits order_spec --- spec/system/admin/fees_on_orders_spec.rb | 675 +++++++++++++++++++++++ spec/system/admin/order_spec.rb | 634 --------------------- 2 files changed, 675 insertions(+), 634 deletions(-) create mode 100644 spec/system/admin/fees_on_orders_spec.rb diff --git a/spec/system/admin/fees_on_orders_spec.rb b/spec/system/admin/fees_on_orders_spec.rb new file mode 100644 index 0000000000..94fbd1e671 --- /dev/null +++ b/spec/system/admin/fees_on_orders_spec.rb @@ -0,0 +1,675 @@ +# frozen_string_literal: true + +require "system_helper" + +RSpec.describe ' + As an administrator + I want to create and edit orders +' do + include WebHelper + include AuthenticationHelper + + let(:user) { create(:user) } + let(:product) { create(:simple_product) } + let(:distributor) { create(:distributor_enterprise, owner: user, charges_sales_tax: true) } + let(:order_cycle) do + create(:simple_order_cycle, name: 'One', distributors: [distributor], + variants: [product.variants.first]) + end + + let(:order) do + create(:order_with_totals_and_distribution, user:, distributor:, + order_cycle:, state: 'complete', + payment_state: 'balance_due') + end + let(:customer) { order.customer } + + before do + # ensure order has a payment to capture + order.finalize! + + create :check_payment, order:, amount: order.total + end + + def new_order_with_distribution(distributor, order_cycle) + visit spree.new_admin_order_path + expect(page).to have_selector('#s2id_order_distributor_id') + select2_select distributor.name, from: 'order_distributor_id' + select2_select order_cycle.name, from: 'order_order_cycle_id' + click_button 'Next' + end + + context "as an enterprise manager" do + let(:coordinator1) { create(:distributor_enterprise) } + let(:coordinator2) { create(:distributor_enterprise) } + let!(:order_cycle1) { create(:order_cycle, coordinator: coordinator1) } + let!(:order_cycle2) { create(:simple_order_cycle, coordinator: coordinator2) } + let!(:supplier1) { order_cycle1.suppliers.first } + let!(:supplier2) { order_cycle1.suppliers.last } + let!(:distributor1) { order_cycle1.distributors.first } + let!(:distributor2) do + order_cycle1.distributors.reject{ |d| d == distributor1 }.last # ensure d1 != d2 + end + let(:product) { order_cycle1.products.first } + + before(:each) do + enterprise_user = create(:user) + enterprise_user.enterprise_roles.build(enterprise: supplier1).save + enterprise_user.enterprise_roles.build(enterprise: coordinator1).save + enterprise_user.enterprise_roles.build(enterprise: distributor1).save + + login_as enterprise_user + end + + describe "viewing the edit page" do + let!(:shipping_method_for_distributor1) do + create(:shipping_method_with, :flat_rate, name: "Normal", amount: 12, + distributors: [distributor1]) + end + let!(:order) do + create( + :order_with_taxes, + distributor: distributor1, + order_cycle: order_cycle1, + ship_address: create(:address), + product_price: 110, + tax_rate_amount: 0.1, + included_in_price: true, + tax_rate_name: "Tax 1" + ).tap do |order| + # Add a values to the fees + first_calculator = supplier_enterprise_fee1.calculator + first_calculator.preferred_amount = 2.5 + first_calculator.save! + + last_calculator = supplier_enterprise_fee2.calculator + last_calculator.preferred_amount = 7.5 + last_calculator.save! + + # Add all variant to the order cycle for a more realistic scenario + order.variants.each do |v| + first_exchange.variants << v + order_cycle1.cached_outgoing_exchanges.first.variants << v + end + + variant1 = first_exchange.variants.first + variant2 = last_exchange.variants.first + + order.contents.add(variant1) + order.contents.add(variant2) + # make sure all the fees are applied to the order + order.recreate_all_fees! + + order.update_order! + end + end + + let(:first_exchange) { order_cycle1.cached_incoming_exchanges.first } + let(:last_exchange) { order_cycle1.cached_incoming_exchanges.last } + let(:coordinator_fee) { order_cycle1.coordinator_fees.first } + let(:distributor_fee) { order_cycle1.cached_outgoing_exchanges.first.enterprise_fees.first } + let(:supplier_enterprise_fee1) { first_exchange.enterprise_fees.first } + let(:supplier_enterprise_fee2) { last_exchange.enterprise_fees.first } + + before do + distributor1.update_attribute(:abn, '12345678') + + visit spree.edit_admin_order_path(order) + end + + it "verifying page contents" do + # shows a list of line_items + within('table.index tbody', match: :first) do + order.line_items.each do |item| + expect(page).to have_selector "td", match: :first, text: item.full_name + expect(page).to have_selector "td.item-price", text: item.single_display_amount + expect(page).to have_selector "input#quantity[value='#{item.quantity}']", visible: false + expect(page).to have_selector "td.item-total", text: item.display_amount + end + end + + # shows the order non-tax adjustments + within "#order_adjustments" do + # supplier fees only apply to specific product + first_exchange.variants.each do |variant| + expect(page).to have_content( + "#{variant.name} - #{supplier_enterprise_fee1.name} fee \ + by supplier #{supplier1.name}: $2.50".squish + ) + expect(page).not_to have_content( + "#{variant.name} - #{supplier_enterprise_fee2.name} fee \ + by supplier #{supplier2.name}: $7.50".squish + ) + end + + last_exchange.variants.each do |variant| + expect(page).to have_content( + "#{variant.name} - #{supplier_enterprise_fee2.name} fee \ + by supplier #{supplier2.name}: $7.50".squish + ) + expect(page).not_to have_content( + "#{variant.name} - #{supplier_enterprise_fee1.name} fee \ + by supplier #{supplier1.name}: $2.50".squish + ) + end + + ## Coordinator fee and Distributor fee apply to all product + order.variants.each do |variant| + expect(page).to have_content( + "#{variant.name} - #{coordinator_fee.name} fee \ + by coordinator #{coordinator1.name}: $0.00".squish + ) + expect(page).to have_content( + "#{variant.name} - #{distributor_fee.name} fee \ + by distributor #{distributor1.name}: $0.00".squish + ) + end + end + + # shows the order total + expect(page).to have_selector "fieldset#order-total", text: order.display_total + + # shows the order tax adjustments + within('fieldset', text: 'Line Item Adjustments') do + expect(page).to have_selector "td", match: :first, text: "Tax 1" + expect(page).to have_selector "td.total", text: Spree::Money.new(10) + end + + # shows the dropdown menu" do + find("#links-dropdown .ofn-drop-down").click + within "#links-dropdown" do + expect(page).to have_link "Resend Confirmation", + href: spree.resend_admin_order_path(order) + end + end + + context "Resending confirmation email" do + before do + visit spree.edit_admin_order_path(order) + find("#links-dropdown .ofn-drop-down").click + end + + it "shows the link" do + expect(page).to have_link "Resend Confirmation", + href: spree.resend_admin_order_path(order) + end + + it "resends the confirmation email" do + accept_alert "Are you sure you want to resend the order confirmation email?" do + click_link "Resend Confirmation" + end + expect(page).to have_content "Order email has been resent" + end + end + + context "Canceling an order" do + shared_examples "canceling an order" do + it "shows the link" do + expect(page).to have_link "Cancel Order", + href: spree.fire_admin_order_path(order, e: 'cancel') + end + it 'cancels the order' do + within ".ofn-drop-down .menu" do + expect(page).to have_selector("span", text: "Cancel Order") + page.find("span", text: "Cancel Order").click + end + within '.modal-content' do + expect { + find_button("OK").click + }.to change { order.reload.state }.from('complete').to('canceled') + end + end + end + + context "from order details page" do + before do + visit spree.edit_admin_order_path(order) + find("#links-dropdown .ofn-drop-down").click + end + it_behaves_like "canceling an order" + end + + context "from order's payments" do + before do + visit spree.admin_order_payments_path(order) + find("#links-dropdown .ofn-drop-down").click + end + it_behaves_like "canceling an order" + end + + context "from order's adjustments" do + before do + visit spree.admin_order_adjustments_path(order) + find("#links-dropdown .ofn-drop-down").click + end + it_behaves_like "canceling an order" + end + end + + context "Check send/print invoice links" do + shared_examples_for 'can send/print invoices' do + before do + visit spree.edit_admin_order_path(order) + find("#links-dropdown .ofn-drop-down").click + end + + it 'shows the right links' do + expect(page).to have_link "Send Invoice", href: spree.invoice_admin_order_path(order) + expect(page).to have_link "Print Invoice", href: spree.print_admin_order_path(order) + end + + it 'can send invoices' do + accept_alert "An invoice for this order will be sent to the customer. " \ + "Are you sure you want to continue?" do + click_link "Send Invoice" + end + expect(page).to have_content "Invoice email has been sent" + end + end + + context "when abn number is not mandatory to send/print invoices" do + before do + Spree::Config[:enterprise_number_required_on_invoices?] = false + distributor1.update_attribute(:abn, "") + end + + it_should_behave_like 'can send/print invoices' + end + + context "when abn number is mandatory to send/print invoices" do + before do + Spree::Config[:enterprise_number_required_on_invoices?] = true + end + + context "and a abn numer is set on the distributor" do + before do + distributor1.update_attribute(:abn, '12345678') + end + + it_should_behave_like 'can send/print invoices' + end + + context "and a abn number is not set on the distributor" do + before do + distributor1.update_attribute(:abn, "") + end + + it "should not display links but a js alert" do + visit spree.edit_admin_order_path(order) + + find("summary", text: "Actions").click + expect(page).to have_link "Send Invoice", href: "#" + expect(page).to have_link "Print Invoice", href: "#" + + message = accept_prompt do + click_link "Print Invoice" + end + expect(message) + .to eq "#{distributor1.name} must have a valid ABN before invoices can be used." + + find("summary", text: "Actions").click + message = accept_prompt do + click_link "Send Invoice" + end + expect(message) + .to eq "#{distributor1.name} must have a valid ABN before invoices can be used." + end + end + end + end + + context "with different shipping methods" do + let!(:different_shipping_method_for_distributor1) do + create(:shipping_method_with, :flat_rate, name: "Different", amount: 15, + distributors: [distributor1]) + end + let!(:shipping_method_for_distributor2) do + create(:shipping_method, name: "Other", distributors: [distributor2]) + end + + it "can edit shipping method" do + visit spree.edit_admin_order_path(order) + + expect(page).not_to have_content different_shipping_method_for_distributor1.name + + find('.edit-method').click + + expect(page).to have_select2('selected_shipping_rate_id', + with_options: [ + shipping_method_for_distributor1.name, + different_shipping_method_for_distributor1.name + ], without_options: [shipping_method_for_distributor2.name]) + + select2_select(different_shipping_method_for_distributor1.name, + from: 'selected_shipping_rate_id') + find('.save-method').click + + expect(page).to have_content( + "Shipping: #{different_shipping_method_for_distributor1.name}" + ) + + within "#order-total" do + expect(page).to have_content "$239.98" + end + end + + context "when the distributor unsupport a shipping method that's selected " \ + "in an existing order " do + before do + distributor1.shipping_methods = [shipping_method_for_distributor1, + different_shipping_method_for_distributor1] + order.shipments.each(&:refresh_rates) + order.shipment.adjustments.first.open + order.select_shipping_method(different_shipping_method_for_distributor1) + order.shipment.adjustments.first.close + distributor1.shipping_methods = [shipping_method_for_distributor1] + end + + context "shipment is shipped" do + before do + order.shipments.first.update_attribute(:state, 'shipped') + end + + it "should not change the shipping method" do + visit spree.edit_admin_order_path(order) + expect(page).to have_content( + "Shipping: #{different_shipping_method_for_distributor1.name} $15.00" + ) + + within "#order-total" do + expect(page).to have_content "$224.98" + end + end + + context "when shipping rate is updated" do + before do + different_shipping_method_for_distributor1.shipping_rates.first.update!(cost: 16) + end + + it "should not update the shipping cost" do + visit spree.edit_admin_order_path(order) + expect(page).to have_content( + "Shipping: #{different_shipping_method_for_distributor1.name} $15.00" + ) + + within "#order-total" do + expect(page).to have_content "$224.98" + end + end + end + end + context "shipment is pending" do + before do + order.shipments.first.ensure_correct_adjustment + expect(order.shipments.first.state).to eq('pending') + end + + it "should not replace the selected shipment method" do + visit spree.edit_admin_order_path(order) + expect(page).to have_content( + "Shipping: #{different_shipping_method_for_distributor1.name} $15.00" + ) + + within "#order-total" do + expect(page).to have_content "$224.98" + end + end + + context "when shipping rate is updated" do + before do + different_shipping_method_for_distributor1.shipping_rates.first.update!(cost: 16) + end + + it "should not update the shipping cost" do + # Since the order is completed, the price is not supposed to be updated + visit spree.edit_admin_order_path(order) + expect(page).to have_content( + "Shipping: #{different_shipping_method_for_distributor1.name} $15.00" + ) + + within "#order-total" do + expect(page).to have_content "$224.98" + end + end + end + end + end + end + + it "can edit and delete tracking number" do + test_tracking_number = "ABCCBA" + expect(page).not_to have_content test_tracking_number + + find('.edit-tracking').click + fill_in "tracking", with: test_tracking_number + find('.save-tracking').click + + expect(page).to have_content test_tracking_number + + find('.delete-tracking.icon-trash').click + # Cancel Deletion + # Check if the alert box shows and after clicking cancel + # the alert box vanishes and tracking num is still present + expect(page).to have_content 'Are you sure?' + find('.cancel').click + expect(page).not_to have_content 'Are you sure?' + expect(page).to have_content test_tracking_number + + find('.delete-tracking.icon-trash').click + expect(page).to have_content 'Are you sure?' + find('.confirm').click + expect(page).not_to have_content test_tracking_number + end + + it "can edit and delete note" do + test_note = "this is a note" + expect(page).not_to have_content test_note + + find('.edit-note.icon-edit').click + fill_in "note", with: test_note + find('.save-note').click + + expect(page).to have_content test_note + + find('.delete-note.icon-trash').click + # Cancel Deletion + # Check if the alert box shows and after clicking cancel + # the alert box vanishes and note is still present + expect(page).to have_content 'Are you sure?' + find('.cancel').click + expect(page).not_to have_content 'Are you sure?' + expect(page).to have_content test_note + + find('.delete-note.icon-trash').click + expect(page).to have_content 'Are you sure?' + find('.confirm').click + expect(page).not_to have_content test_note + end + + it "viewing shipping fees" do + shipping_fee = order.shipment_adjustments.first + + click_link "Adjustments" + + expect(page).to have_selector "tr#spree_adjustment_#{shipping_fee.id}" + expect(page).to have_selector 'td.amount', text: shipping_fee.amount.to_s + expect(page).to have_selector 'td.tax', text: shipping_fee.included_tax_total.to_s + end + + context "shipping orders" do + before do + order.finalize! # ensure order has a payment to capture + order.payments << create(:check_payment, order:, amount: order.total) + order.payments.first.capture! + visit spree.edit_admin_order_path(order) + end + + it "ships the order and shipment email is sent" do + expect(page).to have_content "ready" + expect(page).not_to have_content "shipped" + + click_button 'Ship' + + expect { + within ".reveal-modal" do + expect(page).to have_checked_field( + 'Send a shipment/pick up notification email to the customer.' + ) + click_button "Confirm" + end + expect(page).to have_content "shipped" + }.to enqueue_mail + .and change { order.reload.shipped? }.to true + end + + it "ships the order without sending email" do + expect(page).to have_content "ready" + expect(page).not_to have_content "shipped" + + click_button 'Ship' + + expect { + within ".reveal-modal" do + uncheck 'Send a shipment/pick up notification email to the customer.' + click_button "Confirm" + end + expect(page).to have_content "shipped" + }.to enqueue_mail.exactly(0).times + .and change { order.reload.shipped? }.to true + end + + shared_examples "ship order from dropdown" do |subpage| + context "in the #{subpage}", feature: :invoices do + it "ships the order and sends email" do + click_on subpage + expect(order.reload.shipped?).to be false + + find('.ofn-drop-down').click + click_link 'Ship Order' + + within ".reveal-modal" do + expect(page).to have_checked_field('Send a shipment/pick up ' \ + 'notification email to the customer.') + find_button("Confirm").click + end + + expect(page).to have_selector('.reveal-modal', visible: false) + expect(page).to have_content "SHIPPED" + click_link('Order Details') unless subpage == 'Order Details' + + expect(order.reload.shipped?).to be true + expect(ActionMailer::MailDeliveryJob).to have_been_enqueued + .exactly(:once) + .with("Spree::ShipmentMailer", "shipped_email", "deliver_now", + { args: [order.shipment.id, { delivery: true }] }) + end + + it "ships the order without sending email" do + click_on subpage + expect(order.reload.shipped?).to be false + + find('.ofn-drop-down').click + click_link 'Ship Order' + + within ".reveal-modal" do + uncheck 'Send a shipment/pick up notification email to the customer.' + find_button("Confirm").click + end + + expect(page).to have_selector('.reveal-modal', visible: false) + click_link('Order Details') unless subpage == 'Order Details' + + expect(page).to have_content "SHIPPED" + expect(order.reload.shipped?).to be true + expect(ActionMailer::MailDeliveryJob).not_to have_been_enqueued + .with(array_including("Spree::ShipmentMailer")) + end + end + end + + it_behaves_like "ship order from dropdown", "Order Details" + it_behaves_like "ship order from dropdown", "Customer Details" + it_behaves_like "ship order from dropdown", "Payments" + it_behaves_like "ship order from dropdown", "Adjustments" + it_behaves_like "ship order from dropdown", "Invoices" + it_behaves_like "ship order from dropdown", "Return Authorizations" + end + + context "when an included variant has been deleted" do + let!(:deleted_variant) do + order.line_items.first.variant.tap(&:delete) + end + + it "still lists the variant in the order page" do + within ".stock-contents" do + expect(page).to have_content deleted_variant.product_and_full_name + end + end + end + + context "and the order has been canceled" do + it "does not allow modifying line items" do + order.cancel! + visit spree.edit_admin_order_path(order) + within("tr.stock-item", text: order.products.first.name) do + expect(page).not_to have_selector("a.edit-item") + end + end + end + + context "when an incomplete order has some line items with insufficient stock" do + let(:incomplete_order) do + create(:order_with_line_items, user:, distributor:, + order_cycle:) + end + + it "displays the out of stock line items and they can be deleted from the order" do + incomplete_order.line_items.first.variant.update!(on_demand: false, on_hand: 0) + + visit spree.edit_admin_order_path(incomplete_order) + + expect(page).to have_content "Out of Stock" + + within ".insufficient-stock-items" do + expect(page).to have_content incomplete_order.products.first.name + accept_alert 'Are you sure?' do + find("a.delete-resource").click + end + expect(page).not_to have_content incomplete_order.products.first.name + end + + # updates the order and verifies the warning disappears + click_button "Update And Recalculate Fees" + expect(page).not_to have_content "Out of Stock" + end + end + end + + it "creating an order with distributor and order cycle" do + new_order_with_distribution(distributor1, order_cycle1) + expect(page).to have_selector 'h1', text: 'Customer Details' + click_link "Order Details" + + expect(page).to have_content 'Add Product' + select2_select product.name, from: 'add_variant_id', search: true + + find('button.add_variant').click + page.has_selector? "table.index tbody tr" + expect(page).to have_selector 'td', text: product.name + + expect(page).to have_select2 'order_distributor_id', with_options: [distributor1.name] + expect(page).not_to have_select2 'order_distributor_id', with_options: [distributor2.name] + + expect(page).to have_select2 'order_order_cycle_id', + with_options: ["#{order_cycle1.name} (open)"] + expect(page).not_to have_select2 'order_order_cycle_id', + with_options: ["#{order_cycle2.name} (open)"] + + click_button 'Update' + + o = Spree::Order.last + expect(o.distributor).to eq distributor1 + expect(o.order_cycle).to eq order_cycle1 + end + end +end diff --git a/spec/system/admin/order_spec.rb b/spec/system/admin/order_spec.rb index 55b422ec9a..db3ba09e8a 100644 --- a/spec/system/admin/order_spec.rb +++ b/spec/system/admin/order_spec.rb @@ -555,640 +555,6 @@ RSpec.describe ' end end - context "as an enterprise manager" do - let(:coordinator1) { create(:distributor_enterprise) } - let(:coordinator2) { create(:distributor_enterprise) } - let!(:order_cycle1) { create(:order_cycle, coordinator: coordinator1) } - let!(:order_cycle2) { create(:simple_order_cycle, coordinator: coordinator2) } - let!(:supplier1) { order_cycle1.suppliers.first } - let!(:supplier2) { order_cycle1.suppliers.last } - let!(:distributor1) { order_cycle1.distributors.first } - let!(:distributor2) do - order_cycle1.distributors.reject{ |d| d == distributor1 }.last # ensure d1 != d2 - end - let(:product) { order_cycle1.products.first } - - before(:each) do - enterprise_user = create(:user) - enterprise_user.enterprise_roles.build(enterprise: supplier1).save - enterprise_user.enterprise_roles.build(enterprise: coordinator1).save - enterprise_user.enterprise_roles.build(enterprise: distributor1).save - - login_as enterprise_user - end - - describe "viewing the edit page" do - let!(:shipping_method_for_distributor1) do - create(:shipping_method_with, :flat_rate, name: "Normal", amount: 12, - distributors: [distributor1]) - end - let!(:order) do - create( - :order_with_taxes, - distributor: distributor1, - order_cycle: order_cycle1, - ship_address: create(:address), - product_price: 110, - tax_rate_amount: 0.1, - included_in_price: true, - tax_rate_name: "Tax 1" - ).tap do |order| - # Add a values to the fees - first_calculator = supplier_enterprise_fee1.calculator - first_calculator.preferred_amount = 2.5 - first_calculator.save! - - last_calculator = supplier_enterprise_fee2.calculator - last_calculator.preferred_amount = 7.5 - last_calculator.save! - - # Add all variant to the order cycle for a more realistic scenario - order.variants.each do |v| - first_exchange.variants << v - order_cycle1.cached_outgoing_exchanges.first.variants << v - end - - variant1 = first_exchange.variants.first - variant2 = last_exchange.variants.first - - order.contents.add(variant1) - order.contents.add(variant2) - # make sure all the fees are applied to the order - order.recreate_all_fees! - - order.update_order! - end - end - - let(:first_exchange) { order_cycle1.cached_incoming_exchanges.first } - let(:last_exchange) { order_cycle1.cached_incoming_exchanges.last } - let(:coordinator_fee) { order_cycle1.coordinator_fees.first } - let(:distributor_fee) { order_cycle1.cached_outgoing_exchanges.first.enterprise_fees.first } - let(:supplier_enterprise_fee1) { first_exchange.enterprise_fees.first } - let(:supplier_enterprise_fee2) { last_exchange.enterprise_fees.first } - - before do - distributor1.update_attribute(:abn, '12345678') - - visit spree.edit_admin_order_path(order) - end - - it "verifying page contents" do - # shows a list of line_items - within('table.index tbody', match: :first) do - order.line_items.each do |item| - expect(page).to have_selector "td", match: :first, text: item.full_name - expect(page).to have_selector "td.item-price", text: item.single_display_amount - expect(page).to have_selector "input#quantity[value='#{item.quantity}']", visible: false - expect(page).to have_selector "td.item-total", text: item.display_amount - end - end - - # shows the order non-tax adjustments - within "#order_adjustments" do - # supplier fees only apply to specific product - first_exchange.variants.each do |variant| - expect(page).to have_content( - "#{variant.name} - #{supplier_enterprise_fee1.name} fee \ - by supplier #{supplier1.name}: $2.50".squish - ) - expect(page).not_to have_content( - "#{variant.name} - #{supplier_enterprise_fee2.name} fee \ - by supplier #{supplier2.name}: $7.50".squish - ) - end - - last_exchange.variants.each do |variant| - expect(page).to have_content( - "#{variant.name} - #{supplier_enterprise_fee2.name} fee \ - by supplier #{supplier2.name}: $7.50".squish - ) - expect(page).not_to have_content( - "#{variant.name} - #{supplier_enterprise_fee1.name} fee \ - by supplier #{supplier1.name}: $2.50".squish - ) - end - - ## Coordinator fee and Distributor fee apply to all product - order.variants.each do |variant| - expect(page).to have_content( - "#{variant.name} - #{coordinator_fee.name} fee \ - by coordinator #{coordinator1.name}: $0.00".squish - ) - expect(page).to have_content( - "#{variant.name} - #{distributor_fee.name} fee \ - by distributor #{distributor1.name}: $0.00".squish - ) - end - end - - # shows the order total - expect(page).to have_selector "fieldset#order-total", text: order.display_total - - # shows the order tax adjustments - within('fieldset', text: 'Line Item Adjustments') do - expect(page).to have_selector "td", match: :first, text: "Tax 1" - expect(page).to have_selector "td.total", text: Spree::Money.new(10) - end - - # shows the dropdown menu" do - find("#links-dropdown .ofn-drop-down").click - within "#links-dropdown" do - expect(page).to have_link "Resend Confirmation", - href: spree.resend_admin_order_path(order) - end - end - - context "Resending confirmation email" do - before do - visit spree.edit_admin_order_path(order) - find("#links-dropdown .ofn-drop-down").click - end - - it "shows the link" do - expect(page).to have_link "Resend Confirmation", - href: spree.resend_admin_order_path(order) - end - - it "resends the confirmation email" do - accept_alert "Are you sure you want to resend the order confirmation email?" do - click_link "Resend Confirmation" - end - expect(page).to have_content "Order email has been resent" - end - end - - context "Canceling an order" do - shared_examples "canceling an order" do - it "shows the link" do - expect(page).to have_link "Cancel Order", - href: spree.fire_admin_order_path(order, e: 'cancel') - end - it 'cancels the order' do - within ".ofn-drop-down .menu" do - expect(page).to have_selector("span", text: "Cancel Order") - page.find("span", text: "Cancel Order").click - end - within '.modal-content' do - expect { - find_button("OK").click - }.to change { order.reload.state }.from('complete').to('canceled') - end - end - end - - context "from order details page" do - before do - visit spree.edit_admin_order_path(order) - find("#links-dropdown .ofn-drop-down").click - end - it_behaves_like "canceling an order" - end - - context "from order's payments" do - before do - visit spree.admin_order_payments_path(order) - find("#links-dropdown .ofn-drop-down").click - end - it_behaves_like "canceling an order" - end - - context "from order's adjustments" do - before do - visit spree.admin_order_adjustments_path(order) - find("#links-dropdown .ofn-drop-down").click - end - it_behaves_like "canceling an order" - end - end - - context "Check send/print invoice links" do - shared_examples_for 'can send/print invoices' do - before do - visit spree.edit_admin_order_path(order) - find("#links-dropdown .ofn-drop-down").click - end - - it 'shows the right links' do - expect(page).to have_link "Send Invoice", href: spree.invoice_admin_order_path(order) - expect(page).to have_link "Print Invoice", href: spree.print_admin_order_path(order) - end - - it 'can send invoices' do - accept_alert "An invoice for this order will be sent to the customer. " \ - "Are you sure you want to continue?" do - click_link "Send Invoice" - end - expect(page).to have_content "Invoice email has been sent" - end - end - - context "when abn number is not mandatory to send/print invoices" do - before do - Spree::Config[:enterprise_number_required_on_invoices?] = false - distributor1.update_attribute(:abn, "") - end - - it_should_behave_like 'can send/print invoices' - end - - context "when abn number is mandatory to send/print invoices" do - before do - Spree::Config[:enterprise_number_required_on_invoices?] = true - end - - context "and a abn numer is set on the distributor" do - before do - distributor1.update_attribute(:abn, '12345678') - end - - it_should_behave_like 'can send/print invoices' - end - - context "and a abn number is not set on the distributor" do - before do - distributor1.update_attribute(:abn, "") - end - - it "should not display links but a js alert" do - visit spree.edit_admin_order_path(order) - - find("summary", text: "Actions").click - expect(page).to have_link "Send Invoice", href: "#" - expect(page).to have_link "Print Invoice", href: "#" - - message = accept_prompt do - click_link "Print Invoice" - end - expect(message) - .to eq "#{distributor1.name} must have a valid ABN before invoices can be used." - - find("summary", text: "Actions").click - message = accept_prompt do - click_link "Send Invoice" - end - expect(message) - .to eq "#{distributor1.name} must have a valid ABN before invoices can be used." - end - end - end - end - - context "with different shipping methods" do - let!(:different_shipping_method_for_distributor1) do - create(:shipping_method_with, :flat_rate, name: "Different", amount: 15, - distributors: [distributor1]) - end - let!(:shipping_method_for_distributor2) do - create(:shipping_method, name: "Other", distributors: [distributor2]) - end - - it "can edit shipping method" do - visit spree.edit_admin_order_path(order) - - expect(page).not_to have_content different_shipping_method_for_distributor1.name - - find('.edit-method').click - - expect(page).to have_select2('selected_shipping_rate_id', - with_options: [ - shipping_method_for_distributor1.name, - different_shipping_method_for_distributor1.name - ], without_options: [shipping_method_for_distributor2.name]) - - select2_select(different_shipping_method_for_distributor1.name, - from: 'selected_shipping_rate_id') - find('.save-method').click - - expect(page).to have_content( - "Shipping: #{different_shipping_method_for_distributor1.name}" - ) - - within "#order-total" do - expect(page).to have_content "$239.98" - end - end - - context "when the distributor unsupport a shipping method that's selected " \ - "in an existing order " do - before do - distributor1.shipping_methods = [shipping_method_for_distributor1, - different_shipping_method_for_distributor1] - order.shipments.each(&:refresh_rates) - order.shipment.adjustments.first.open - order.select_shipping_method(different_shipping_method_for_distributor1) - order.shipment.adjustments.first.close - distributor1.shipping_methods = [shipping_method_for_distributor1] - end - - context "shipment is shipped" do - before do - order.shipments.first.update_attribute(:state, 'shipped') - end - - it "should not change the shipping method" do - visit spree.edit_admin_order_path(order) - expect(page).to have_content( - "Shipping: #{different_shipping_method_for_distributor1.name} $15.00" - ) - - within "#order-total" do - expect(page).to have_content "$224.98" - end - end - - context "when shipping rate is updated" do - before do - different_shipping_method_for_distributor1.shipping_rates.first.update!(cost: 16) - end - - it "should not update the shipping cost" do - visit spree.edit_admin_order_path(order) - expect(page).to have_content( - "Shipping: #{different_shipping_method_for_distributor1.name} $15.00" - ) - - within "#order-total" do - expect(page).to have_content "$224.98" - end - end - end - end - context "shipment is pending" do - before do - order.shipments.first.ensure_correct_adjustment - expect(order.shipments.first.state).to eq('pending') - end - - it "should not replace the selected shipment method" do - visit spree.edit_admin_order_path(order) - expect(page).to have_content( - "Shipping: #{different_shipping_method_for_distributor1.name} $15.00" - ) - - within "#order-total" do - expect(page).to have_content "$224.98" - end - end - - context "when shipping rate is updated" do - before do - different_shipping_method_for_distributor1.shipping_rates.first.update!(cost: 16) - end - - it "should not update the shipping cost" do - # Since the order is completed, the price is not supposed to be updated - visit spree.edit_admin_order_path(order) - expect(page).to have_content( - "Shipping: #{different_shipping_method_for_distributor1.name} $15.00" - ) - - within "#order-total" do - expect(page).to have_content "$224.98" - end - end - end - end - end - end - - it "can edit and delete tracking number" do - test_tracking_number = "ABCCBA" - expect(page).not_to have_content test_tracking_number - - find('.edit-tracking').click - fill_in "tracking", with: test_tracking_number - find('.save-tracking').click - - expect(page).to have_content test_tracking_number - - find('.delete-tracking.icon-trash').click - # Cancel Deletion - # Check if the alert box shows and after clicking cancel - # the alert box vanishes and tracking num is still present - expect(page).to have_content 'Are you sure?' - find('.cancel').click - expect(page).not_to have_content 'Are you sure?' - expect(page).to have_content test_tracking_number - - find('.delete-tracking.icon-trash').click - expect(page).to have_content 'Are you sure?' - find('.confirm').click - expect(page).not_to have_content test_tracking_number - end - - it "can edit and delete note" do - test_note = "this is a note" - expect(page).not_to have_content test_note - - find('.edit-note.icon-edit').click - fill_in "note", with: test_note - find('.save-note').click - - expect(page).to have_content test_note - - find('.delete-note.icon-trash').click - # Cancel Deletion - # Check if the alert box shows and after clicking cancel - # the alert box vanishes and note is still present - expect(page).to have_content 'Are you sure?' - find('.cancel').click - expect(page).not_to have_content 'Are you sure?' - expect(page).to have_content test_note - - find('.delete-note.icon-trash').click - expect(page).to have_content 'Are you sure?' - find('.confirm').click - expect(page).not_to have_content test_note - end - - it "viewing shipping fees" do - shipping_fee = order.shipment_adjustments.first - - click_link "Adjustments" - - expect(page).to have_selector "tr#spree_adjustment_#{shipping_fee.id}" - expect(page).to have_selector 'td.amount', text: shipping_fee.amount.to_s - expect(page).to have_selector 'td.tax', text: shipping_fee.included_tax_total.to_s - end - - context "shipping orders" do - before do - order.finalize! # ensure order has a payment to capture - order.payments << create(:check_payment, order:, amount: order.total) - order.payments.first.capture! - visit spree.edit_admin_order_path(order) - end - - it "ships the order and shipment email is sent" do - expect(page).to have_content "ready" - expect(page).not_to have_content "shipped" - - click_button 'Ship' - - expect { - within ".reveal-modal" do - expect(page).to have_checked_field( - 'Send a shipment/pick up notification email to the customer.' - ) - click_button "Confirm" - end - expect(page).to have_content "shipped" - }.to enqueue_mail - .and change { order.reload.shipped? }.to true - end - - it "ships the order without sending email" do - expect(page).to have_content "ready" - expect(page).not_to have_content "shipped" - - click_button 'Ship' - - expect { - within ".reveal-modal" do - uncheck 'Send a shipment/pick up notification email to the customer.' - click_button "Confirm" - end - expect(page).to have_content "shipped" - }.to enqueue_mail.exactly(0).times - .and change { order.reload.shipped? }.to true - end - - shared_examples "ship order from dropdown" do |subpage| - context "in the #{subpage}", feature: :invoices do - it "ships the order and sends email" do - click_on subpage - expect(order.reload.shipped?).to be false - - find('.ofn-drop-down').click - click_link 'Ship Order' - - within ".reveal-modal" do - expect(page).to have_checked_field('Send a shipment/pick up ' \ - 'notification email to the customer.') - find_button("Confirm").click - end - - expect(page).to have_selector('.reveal-modal', visible: false) - expect(page).to have_content "SHIPPED" - click_link('Order Details') unless subpage == 'Order Details' - - expect(order.reload.shipped?).to be true - expect(ActionMailer::MailDeliveryJob).to have_been_enqueued - .exactly(:once) - .with("Spree::ShipmentMailer", "shipped_email", "deliver_now", - { args: [order.shipment.id, { delivery: true }] }) - end - - it "ships the order without sending email" do - click_on subpage - expect(order.reload.shipped?).to be false - - find('.ofn-drop-down').click - click_link 'Ship Order' - - within ".reveal-modal" do - uncheck 'Send a shipment/pick up notification email to the customer.' - find_button("Confirm").click - end - - expect(page).to have_selector('.reveal-modal', visible: false) - click_link('Order Details') unless subpage == 'Order Details' - - expect(page).to have_content "SHIPPED" - expect(order.reload.shipped?).to be true - expect(ActionMailer::MailDeliveryJob).not_to have_been_enqueued - .with(array_including("Spree::ShipmentMailer")) - end - end - end - - it_behaves_like "ship order from dropdown", "Order Details" - it_behaves_like "ship order from dropdown", "Customer Details" - it_behaves_like "ship order from dropdown", "Payments" - it_behaves_like "ship order from dropdown", "Adjustments" - it_behaves_like "ship order from dropdown", "Invoices" - it_behaves_like "ship order from dropdown", "Return Authorizations" - end - - context "when an included variant has been deleted" do - let!(:deleted_variant) do - order.line_items.first.variant.tap(&:delete) - end - - it "still lists the variant in the order page" do - within ".stock-contents" do - expect(page).to have_content deleted_variant.product_and_full_name - end - end - end - - context "and the order has been canceled" do - it "does not allow modifying line items" do - order.cancel! - visit spree.edit_admin_order_path(order) - within("tr.stock-item", text: order.products.first.name) do - expect(page).not_to have_selector("a.edit-item") - end - end - end - - context "when an incomplete order has some line items with insufficient stock" do - let(:incomplete_order) do - create(:order_with_line_items, user:, distributor:, - order_cycle:) - end - - it "displays the out of stock line items and they can be deleted from the order" do - incomplete_order.line_items.first.variant.update!(on_demand: false, on_hand: 0) - - visit spree.edit_admin_order_path(incomplete_order) - - expect(page).to have_content "Out of Stock" - - within ".insufficient-stock-items" do - expect(page).to have_content incomplete_order.products.first.name - accept_alert 'Are you sure?' do - find("a.delete-resource").click - end - expect(page).not_to have_content incomplete_order.products.first.name - end - - # updates the order and verifies the warning disappears - click_button "Update And Recalculate Fees" - expect(page).not_to have_content "Out of Stock" - end - end - end - - it "creating an order with distributor and order cycle" do - new_order_with_distribution(distributor1, order_cycle1) - expect(page).to have_selector 'h1', text: 'Customer Details' - click_link "Order Details" - - expect(page).to have_content 'Add Product' - select2_select product.name, from: 'add_variant_id', search: true - - find('button.add_variant').click - page.has_selector? "table.index tbody tr" - expect(page).to have_selector 'td', text: product.name - - expect(page).to have_select2 'order_distributor_id', with_options: [distributor1.name] - expect(page).not_to have_select2 'order_distributor_id', with_options: [distributor2.name] - - expect(page).to have_select2 'order_order_cycle_id', - with_options: ["#{order_cycle1.name} (open)"] - expect(page).not_to have_select2 'order_order_cycle_id', - with_options: ["#{order_cycle2.name} (open)"] - - click_button 'Update' - - o = Spree::Order.last - expect(o.distributor).to eq distributor1 - expect(o.order_cycle).to eq order_cycle1 - end - end - describe "searching customers" do def searching_for_customers # opens the customer dropdown From af8059c22fa72c20bc1afcefd7fcb900bc811d92 Mon Sep 17 00:00:00 2001 From: filipefurtad0 Date: Tue, 6 May 2025 18:08:51 +0100 Subject: [PATCH 9/9] Splits shopping_spec --- .../shopping/shopfront_order_cycles_spec.rb | 459 +++++++++++++ .../system/consumer/shopping/shopping_spec.rb | 610 +++--------------- 2 files changed, 551 insertions(+), 518 deletions(-) create mode 100644 spec/system/consumer/shopping/shopfront_order_cycles_spec.rb diff --git a/spec/system/consumer/shopping/shopfront_order_cycles_spec.rb b/spec/system/consumer/shopping/shopfront_order_cycles_spec.rb new file mode 100644 index 0000000000..a4952cb76b --- /dev/null +++ b/spec/system/consumer/shopping/shopfront_order_cycles_spec.rb @@ -0,0 +1,459 @@ +# frozen_string_literal: true + +require 'system_helper' + +RSpec.describe "As a consumer I want to shop with a distributor" do + include AuthenticationHelper + include FileHelper + include WebHelper + include ShopWorkflow + include UIComponentHelper + + describe "Viewing a distributor" do + let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } + let(:supplier) { create(:supplier_enterprise, name: 'The small mammals company') } + let(:oc1) { + create(:simple_order_cycle, distributors: [distributor], + coordinator: create(:distributor_enterprise), + orders_close_at: 2.days.from_now) + } + let(:oc2) { + create(:simple_order_cycle, distributors: [distributor], + coordinator: create(:distributor_enterprise), + orders_close_at: 3.days.from_now) + } + let(:product) { create(:simple_product, supplier_id: supplier.id, meta_keywords: "Domestic") } + let(:variant) { product.variants.first } + let(:order) { create(:order, distributor:) } + + before do + pick_order order + end + + context "when no order cycles are available" do + it "tells us orders are closed" do + visit shop_path + expect(page).to have_content "Orders are closed" + end + + it "shows the last order cycle" do + oc1 = create(:simple_order_cycle, distributors: [distributor], + orders_open_at: 17.days.ago, + orders_close_at: 10.days.ago) + visit shop_path + expect(page).to have_content "The last cycle closed 10 days ago" + end + + it "shows the next order cycle" do + oc1 = create(:simple_order_cycle, distributors: [distributor], + orders_open_at: 10.days.from_now, + orders_close_at: 17.days.from_now) + visit shop_path + expect(page).to have_content "The next cycle opens in 10 days" + end + end + + describe "selecting an order cycle" do + let(:exchange1) { oc1.exchanges.to_enterprises(distributor).outgoing.first } + + describe "with only one open order cycle" do + before { exchange1.update_attribute :pickup_time, "turtles" } + + it "selects an order cycle" do + visit shop_path + expect(page).to have_selector "p", text: 'turtles' + expect(page).not_to have_content "choose when you want your order" + expect(page).to have_content "Next order closing in 2 days" + end + + describe "when order cycle closes in more than 3 months" do + before { oc1.update orders_close_at: 5.months.from_now } + + it "shows alternative to 'closing in' message" do + visit shop_path + expect(page).to have_content "Orders are currently open" + end + end + end + + describe "with multiple order cycles" do + let(:exchange2) { oc2.exchanges.to_enterprises(distributor).outgoing.first } + + before do + exchange1.update_attribute :pickup_time, "frogs" + exchange2.update_attribute :pickup_time, "turtles" + distributor.update!(preferred_shopfront_message: "Hello!") + end + + it "shows a select with all order cycles, but doesn't show the products by default" do + visit shop_path + + expect(page).to have_selector "option", text: 'frogs' + expect(page).to have_selector "option", text: 'turtles' + expect(page).to have_content "Choose when you want your order:" + expect(page).not_to have_selector("input.button.right") + end + + it "shows products after selecting an order cycle" do + variant.update_attribute(:display_name, "kitten") + variant.update_attribute(:display_as, "rabbit") + add_variant_to_order_cycle(exchange1, variant) + visit shop_path + expect(page).not_to have_content product.name + expect(Spree::Order.last.order_cycle).to be_nil + + select "frogs", from: "order_cycle_id" + expect(page).to have_selector "products" + expect(page).to have_content "Next order closing in 2 days" + expect(Spree::Order.last.order_cycle).to eq(oc1) + expect(page).to have_content product.name + expect(page).to have_content variant.display_name + expect(page).to have_content variant.display_as + + open_product_modal product + modal_should_be_open_for product + end + + describe "changing order cycle" do + it "shows the correct fees after selecting and changing an order cycle" do + enterprise_fee = create(:enterprise_fee, amount: 1001) + exchange2.enterprise_fees << enterprise_fee + add_variant_to_order_cycle(exchange2, variant) + add_variant_to_order_cycle(exchange1, variant) + + # -- Selecting an order cycle + visit shop_path + select "turtles", from: "order_cycle_id" + expect(page).to have_content with_currency(1020.99) + + # -- Cart shows correct price + click_add_to_cart variant + expect(page).to have_in_cart with_currency(1020.99) + toggle_cart + + # -- Changing order cycle + accept_alert do + select "frogs", from: "order_cycle_id" + end + expect(page).to have_content with_currency(19.99) + + # -- Cart should be cleared + # ng-animate means that the old product row is likely to be present, so we ensure + # that we are not filling in the quantity on the outgoing row + expect(page).not_to have_selector "tr.product-cart" + within('product:not(.ng-leave)') { click_add_to_cart variant } + expect(page).to have_in_cart with_currency(19.99) + end + + describe "declining to clear the cart" do + before do + add_variant_to_order_cycle(exchange2, variant) + add_variant_to_order_cycle(exchange1, variant) + + visit shop_path + select "turtles", from: "order_cycle_id" + click_add_to_cart variant + end + + it "leaves the cart untouched when the user declines" do + handle_js_confirm(false) do + select "frogs", from: "order_cycle_id" + expect(page).to have_in_cart "1" + expect(page).to have_selector "tr.product-cart" + + # The order cycle choice should not have changed + expect(page).to have_select 'order_cycle_id', selected: 'turtles' + end + end + end + end + + describe "two order cycles" do + before do + visit shop_path + end + context "one having 20 products" do + before do + 20.times do + product = create(:simple_product, supplier_id: supplier.id) + add_variant_to_order_cycle(exchange1, product.variants.first) + end + end + it "displays 20 products, 10 per page" do + select "frogs", from: "order_cycle_id" + expect(page).to have_selector("product.animate-repeat", count: 10) + scroll_to(page.find(".product-listing"), align: :bottom) + expect(page).to have_selector("product.animate-repeat", count: 20) + end + end + + context "another having 5 products" do + before do + 5.times do + product = create(:simple_product, supplier_id: supplier.id) + add_variant_to_order_cycle(exchange2, product.variants.first) + end + end + + it "displays 5 products, on one page" do + select "turtles", from: "order_cycle_id" + expect(page).to have_selector("product.animate-repeat", count: 5) + end + end + end + end + end + + describe "after selecting an order cycle with products visible" do + let(:variant1) { create(:variant, product:, price: 20) } + let(:variant2) do + create(:variant, product:, price: 30, display_name: "Badgers", + display_as: 'displayedunderthename') + end + let(:product2) { + create(:simple_product, supplier_id: supplier.id, name: "Meercats", + meta_keywords: "Wild Fresh") + } + let(:variant3) { + create(:variant, product: product2, supplier:, price: 40, display_name: "Ferrets") + } + let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } + + before do + exchange.update_attribute :pickup_time, "frogs" + add_variant_to_order_cycle(exchange, variant) + add_variant_to_order_cycle(exchange, variant1) + add_variant_to_order_cycle(exchange, variant2) + add_variant_to_order_cycle(exchange, variant3) + order.order_cycle = oc1 + end + + context "adjusting the price" do + before do + enterprise_fee1 = create(:enterprise_fee, amount: 20) + enterprise_fee2 = create(:enterprise_fee, amount: 3) + exchange.enterprise_fees = [enterprise_fee1, enterprise_fee2] + exchange.save + visit shop_path + end + it "displays the correct price" do + # Page should not have product.price (with or without fee) + expect(page).not_to have_price with_currency(10.00) + expect(page).not_to have_price with_currency(33.00) + + # Page should have variant prices (with fee) + expect(page).to have_price with_currency(43.00) + expect(page).to have_price with_currency(53.00) + + # Product price should be listed as the lesser of these + expect(page).to have_price with_currency(43.00) + end + end + + context "filtering search results" do + it "returns results when successful" do + visit shop_path + # When we see the Add button, it means product are loaded on the page + expect(page).to have_content("Add", count: 4) + + fill_in "search", with: "74576345634XXXXXX" + expect(page).to have_content "Sorry, no results found" + expect(page).not_to have_content 'Meercats' + + click_on "Clear search" # clears search by clicking text + expect(page).to have_content("Add", count: 4) + + fill_in "search", with: "Meer" # For product named "Meercats" + expect(page).to have_content 'Meercats' + expect(page).not_to have_content product.name + + find("a.clear").click # clears search by clicking the X button + expect(page).to have_content("Add", count: 4) + end + + it "returns results by looking at different columns in DB" do + visit shop_path + # When we see the Add button, it means product are loaded on the page + expect(page).to have_content("Add", count: 4) + + # by keyword model: meta_keywords + fill_in "search", with: "Wild" # For product named "Meercats" + expect(page).to have_content 'Wild' + find("a.clear").click + + # by variant display name model: variant display_name + fill_in "search", with: "Ferrets" # For variants named "Ferrets" + within('div.pad-top') do + expect(page).to have_content 'Ferrets' + expect(page).not_to have_content 'Badgers' + end + + # model: variant display_as + fill_in "search", with: "displayedunder" # "Badgers" + within('div.pad-top') do + expect(page).not_to have_content 'Ferrets' + expect(page).to have_content 'Badgers' + end + + # model: Enterprise name + fill_in "search", with: "Enterp" # Enterprise 1 sells nothing + within('p.no-results') do + expect(page).to have_content "Sorry, no results found for Enterp" + end + end + end + + context "when supplier uses property" do + let(:product3) { + create(:simple_product, supplier_id: supplier.id, inherits_properties: false) + } + + before do + add_variant_to_order_cycle(exchange, product3.variants.first) + property = create(:property, presentation: 'certified') + supplier.update!(properties: [property]) + end + + it "filters product by properties" do + visit shop_path + + expect(page).to have_content product2.name + expect(page).to have_content product3.name + + expect(page).to have_selector( + ".sticky-shop-filters-container .property-selectors span", text: "certified" + ) + find(".sticky-shop-filters-container .property-selectors span", text: 'certified').click + expect(page).to have_content "Results for certified" + + expect(page).to have_content product2.name + expect(page).not_to have_content product3.name + end + end + + it "returns search results for products where the search term matches one of the product's " \ + "variant names" do + visit shop_path + fill_in "search", with: "Badg" # For variant with display_name "Badgers" + + within('div.pad-top') do + expect(page).not_to have_content product2.name + expect(page).not_to have_content variant3.display_name + expect(page).to have_content product.name + expect(page).to have_content variant2.display_name + end + end + + context "when the distributor has no available payment/shipping methods" do + before do + distributor.update shipping_methods: [], payment_methods: [] + end + + # Display only shops are a very useful hack that is described in the user guide + it "still renders a display only shop" do + visit shop_path + expect(page).to have_content product.name + + click_add_to_cart variant + expect(page).to have_in_cart product.name + + # Try to go to cart + visit main_app.cart_path + expect(page).to have_content "The hub you have selected is temporarily closed for " \ + "orders. Please try again later." + end + end + end + + context "when shopping requires a customer" do + let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } + let(:product) { create(:simple_product) } + let(:variant) { create(:variant, product:) } + let(:unregistered_customer) { create(:customer, user: nil, enterprise: distributor) } + + before do + add_variant_to_order_cycle(exchange, variant) + set_order_cycle(order, oc1) + distributor.require_login = true + distributor.save! + end + + context "when not logged in" do + it "tells us to login" do + visit shop_path + expect(page).to have_content "Only approved customers can access this shop." + expect(page).to have_content "login to proceed" + expect(page).not_to have_content product.name + expect(page).not_to have_selector "ordercycle" + end + end + + context "when logged in" do + let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } + let(:user) { create(:user, bill_address: address, ship_address: address) } + + before do + login_as user + end + + context "as non-customer" do + it "tells us to contact enterprise" do + visit shop_path + expect(page).to have_content "Only approved customers can access this shop." + expect(page).to have_content "please contact #{distributor.name}" + expect(page).not_to have_content product.name + expect(page).not_to have_selector "ordercycle" + end + end + + context "as customer" do + let!(:customer) { create(:customer, user:, enterprise: distributor) } + + it "shows just products" do + visit shop_path + shows_products_without_customer_warning + end + end + + context "as a manager" do + let!(:role) { create(:enterprise_role, user:, enterprise: distributor) } + + it "shows just products" do + visit shop_path + shows_products_without_customer_warning + end + end + + context "as the owner" do + before do + distributor.owner = user + distributor.save! + end + + it "shows just products" do + visit shop_path + shows_products_without_customer_warning + end + end + end + + context "when previously unregistered customer registers" do + let!(:returning_user) { create(:user, email: unregistered_customer.email) } + + before do + login_as returning_user + end + + it "shows the products without customer only message" do + visit shop_path + shows_products_without_customer_warning + end + end + end + end + + def shows_products_without_customer_warning + expect(page).not_to have_content "This shop is for customers only." + expect(page).to have_content product.name + end +end diff --git a/spec/system/consumer/shopping/shopping_spec.rb b/spec/system/consumer/shopping/shopping_spec.rb index def7ca7349..7d2cce80cf 100644 --- a/spec/system/consumer/shopping/shopping_spec.rb +++ b/spec/system/consumer/shopping/shopping_spec.rb @@ -30,426 +30,6 @@ RSpec.describe "As a consumer I want to shop with a distributor" do pick_order order end - it "shows a distributor with images" do - # Given the distributor has a logo - distributor.update!(logo: white_logo_file) - # Then we should see the distributor and its logo - visit shop_path - expect(page).to have_text distributor.name - within ".tab-buttons" do - click_link "About" - end - expect(first("distributor img")['src']).to include "logo-white.png" - end - - describe "shop tabs for a distributor" do - default_tabs = ["Shop", "About", "Producers", "Contact"].freeze - all_tabs = (default_tabs + ["Groups", "Home"]).freeze - - before do - visit shop_path - end - - shared_examples_for "reveal all right tabs" do |tabs, default| - tabs.each do |tab| - it "shows the #{tab} tab" do - within ".tab-buttons" do - expect(page).to have_content tab - end - end - end - - (all_tabs - tabs).each do |tab| - it "does not show the #{tab} tab" do - within ".tab-buttons" do - expect(page).not_to have_content tab - end - end - end - - it "shows the #{default} tab by default" do - within ".tab-buttons" do - expect(page).to have_selector ".selected", text: default - end - end - end - - context "default" do - it_behaves_like "reveal all right tabs", default_tabs, "Shop" - end - - context "when the distributor has a shopfront message" do - before do - distributor.update_attribute(:preferred_shopfront_message, "Hello") - visit shop_path - end - - it_behaves_like "reveal all right tabs", default_tabs + ["Home"], "Home" - end - - context "when the distributor has a custom tab" do - let(:custom_tab) { create(:custom_tab, title: "Custom") } - - before do - distributor.update(custom_tab:) - visit shop_path - end - - it_behaves_like "reveal all right tabs", default_tabs + ["Custom"], "Shop" - end - end - - describe "producers tab" do - before do - exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) - add_variant_to_order_cycle(exchange, variant) - visit shop_path - within ".tab-buttons" do - click_link "Producers" - end - end - - it "shows the producers for a distributor" do - expect(page).to have_content supplier.name - find("a", text: supplier.name).click - within ".reveal-modal" do - expect(page).to have_content supplier.name - end - end - - context "when the producer visibility is set to 'hidden'" do - before do - supplier.visible = "hidden" - supplier.save - visit shop_path - within ".tab-buttons" do - click_link "Producers" - end - end - - it "shows the producer name" do - expect(page).to have_content supplier.name - end - - it "does not show the producer modal" do - expect(page).not_to have_link supplier.name - expect(page).not_to have_selector ".reveal-modal" - end - end - end - - describe "selecting an order cycle" do - let(:exchange1) { oc1.exchanges.to_enterprises(distributor).outgoing.first } - - describe "with only one open order cycle" do - before { exchange1.update_attribute :pickup_time, "turtles" } - - it "selects an order cycle" do - visit shop_path - expect(page).to have_selector "p", text: 'turtles' - expect(page).not_to have_content "choose when you want your order" - expect(page).to have_content "Next order closing in 2 days" - end - - describe "when order cycle closes in more than 3 months" do - before { oc1.update orders_close_at: 5.months.from_now } - - it "shows alternative to 'closing in' message" do - visit shop_path - expect(page).to have_content "Orders are currently open" - end - end - end - - describe "with multiple order cycles" do - let(:exchange2) { oc2.exchanges.to_enterprises(distributor).outgoing.first } - - before do - exchange1.update_attribute :pickup_time, "frogs" - exchange2.update_attribute :pickup_time, "turtles" - distributor.update!(preferred_shopfront_message: "Hello!") - end - - it "shows a select with all order cycles, but doesn't show the products by default" do - visit shop_path - - expect(page).to have_selector "option", text: 'frogs' - expect(page).to have_selector "option", text: 'turtles' - expect(page).to have_content "Choose when you want your order:" - expect(page).not_to have_selector("input.button.right") - end - - it "shows products after selecting an order cycle" do - variant.update_attribute(:display_name, "kitten") - variant.update_attribute(:display_as, "rabbit") - add_variant_to_order_cycle(exchange1, variant) - visit shop_path - expect(page).not_to have_content product.name - expect(Spree::Order.last.order_cycle).to be_nil - - select "frogs", from: "order_cycle_id" - expect(page).to have_selector "products" - expect(page).to have_content "Next order closing in 2 days" - expect(Spree::Order.last.order_cycle).to eq(oc1) - expect(page).to have_content product.name - expect(page).to have_content variant.display_name - expect(page).to have_content variant.display_as - - open_product_modal product - modal_should_be_open_for product - end - - describe "changing order cycle" do - it "shows the correct fees after selecting and changing an order cycle" do - enterprise_fee = create(:enterprise_fee, amount: 1001) - exchange2.enterprise_fees << enterprise_fee - add_variant_to_order_cycle(exchange2, variant) - add_variant_to_order_cycle(exchange1, variant) - - # -- Selecting an order cycle - visit shop_path - select "turtles", from: "order_cycle_id" - expect(page).to have_content with_currency(1020.99) - - # -- Cart shows correct price - click_add_to_cart variant - expect(page).to have_in_cart with_currency(1020.99) - toggle_cart - - # -- Changing order cycle - accept_alert do - select "frogs", from: "order_cycle_id" - end - expect(page).to have_content with_currency(19.99) - - # -- Cart should be cleared - # ng-animate means that the old product row is likely to be present, so we ensure - # that we are not filling in the quantity on the outgoing row - expect(page).not_to have_selector "tr.product-cart" - within('product:not(.ng-leave)') { click_add_to_cart variant } - expect(page).to have_in_cart with_currency(19.99) - end - - describe "declining to clear the cart" do - before do - add_variant_to_order_cycle(exchange2, variant) - add_variant_to_order_cycle(exchange1, variant) - - visit shop_path - select "turtles", from: "order_cycle_id" - click_add_to_cart variant - end - - it "leaves the cart untouched when the user declines" do - handle_js_confirm(false) do - select "frogs", from: "order_cycle_id" - expect(page).to have_in_cart "1" - expect(page).to have_selector "tr.product-cart" - - # The order cycle choice should not have changed - expect(page).to have_select 'order_cycle_id', selected: 'turtles' - end - end - end - end - - describe "two order cycles" do - before do - visit shop_path - end - context "one having 20 products" do - before do - 20.times do - product = create(:simple_product, supplier_id: supplier.id) - add_variant_to_order_cycle(exchange1, product.variants.first) - end - end - it "displays 20 products, 10 per page" do - select "frogs", from: "order_cycle_id" - expect(page).to have_selector("product.animate-repeat", count: 10) - scroll_to(page.find(".product-listing"), align: :bottom) - expect(page).to have_selector("product.animate-repeat", count: 20) - end - end - - context "another having 5 products" do - before do - 5.times do - product = create(:simple_product, supplier_id: supplier.id) - add_variant_to_order_cycle(exchange2, product.variants.first) - end - end - - it "displays 5 products, on one page" do - select "turtles", from: "order_cycle_id" - expect(page).to have_selector("product.animate-repeat", count: 5) - end - end - end - end - end - - describe "after selecting an order cycle with products visible" do - let(:variant1) { create(:variant, product:, price: 20) } - let(:variant2) do - create(:variant, product:, price: 30, display_name: "Badgers", - display_as: 'displayedunderthename') - end - let(:product2) { - create(:simple_product, supplier_id: supplier.id, name: "Meercats", - meta_keywords: "Wild Fresh") - } - let(:variant3) { - create(:variant, product: product2, supplier:, price: 40, display_name: "Ferrets") - } - let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } - - before do - exchange.update_attribute :pickup_time, "frogs" - add_variant_to_order_cycle(exchange, variant) - add_variant_to_order_cycle(exchange, variant1) - add_variant_to_order_cycle(exchange, variant2) - add_variant_to_order_cycle(exchange, variant3) - order.order_cycle = oc1 - end - - context "adjusting the price" do - before do - enterprise_fee1 = create(:enterprise_fee, amount: 20) - enterprise_fee2 = create(:enterprise_fee, amount: 3) - exchange.enterprise_fees = [enterprise_fee1, enterprise_fee2] - exchange.save - visit shop_path - end - it "displays the correct price" do - # Page should not have product.price (with or without fee) - expect(page).not_to have_price with_currency(10.00) - expect(page).not_to have_price with_currency(33.00) - - # Page should have variant prices (with fee) - expect(page).to have_price with_currency(43.00) - expect(page).to have_price with_currency(53.00) - - # Product price should be listed as the lesser of these - expect(page).to have_price with_currency(43.00) - end - end - - context "filtering search results" do - it "returns results when successful" do - visit shop_path - # When we see the Add button, it means product are loaded on the page - expect(page).to have_content("Add", count: 4) - - fill_in "search", with: "74576345634XXXXXX" - expect(page).to have_content "Sorry, no results found" - expect(page).not_to have_content 'Meercats' - - click_on "Clear search" # clears search by clicking text - expect(page).to have_content("Add", count: 4) - - fill_in "search", with: "Meer" # For product named "Meercats" - expect(page).to have_content 'Meercats' - expect(page).not_to have_content product.name - - find("a.clear").click # clears search by clicking the X button - expect(page).to have_content("Add", count: 4) - end - - it "returns results by looking at different columns in DB" do - visit shop_path - # When we see the Add button, it means product are loaded on the page - expect(page).to have_content("Add", count: 4) - - # by keyword model: meta_keywords - fill_in "search", with: "Wild" # For product named "Meercats" - expect(page).to have_content 'Wild' - find("a.clear").click - - # by variant display name model: variant display_name - fill_in "search", with: "Ferrets" # For variants named "Ferrets" - within('div.pad-top') do - expect(page).to have_content 'Ferrets' - expect(page).not_to have_content 'Badgers' - end - - # model: variant display_as - fill_in "search", with: "displayedunder" # "Badgers" - within('div.pad-top') do - expect(page).not_to have_content 'Ferrets' - expect(page).to have_content 'Badgers' - end - - # model: Enterprise name - fill_in "search", with: "Enterp" # Enterprise 1 sells nothing - within('p.no-results') do - expect(page).to have_content "Sorry, no results found for Enterp" - end - end - end - - context "when supplier uses property" do - let(:product3) { - create(:simple_product, supplier_id: supplier.id, inherits_properties: false) - } - - before do - add_variant_to_order_cycle(exchange, product3.variants.first) - property = create(:property, presentation: 'certified') - supplier.update!(properties: [property]) - end - - it "filters product by properties" do - visit shop_path - - expect(page).to have_content product2.name - expect(page).to have_content product3.name - - expect(page).to have_selector( - ".sticky-shop-filters-container .property-selectors span", text: "certified" - ) - find(".sticky-shop-filters-container .property-selectors span", text: 'certified').click - expect(page).to have_content "Results for certified" - - expect(page).to have_content product2.name - expect(page).not_to have_content product3.name - end - end - - it "returns search results for products where the search term matches one of the product's " \ - "variant names" do - visit shop_path - fill_in "search", with: "Badg" # For variant with display_name "Badgers" - - within('div.pad-top') do - expect(page).not_to have_content product2.name - expect(page).not_to have_content variant3.display_name - expect(page).to have_content product.name - expect(page).to have_content variant2.display_name - end - end - - context "when the distributor has no available payment/shipping methods" do - before do - distributor.update shipping_methods: [], payment_methods: [] - end - - # Display only shops are a very useful hack that is described in the user guide - it "still renders a display only shop" do - visit shop_path - expect(page).to have_content product.name - - click_add_to_cart variant - expect(page).to have_in_cart product.name - - # Try to go to cart - visit main_app.cart_path - expect(page).to have_content "The hub you have selected is temporarily closed for " \ - "orders. Please try again later." - end - end - end - describe "group buy products" do let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } let(:product) { create(:simple_product, group_buy: true, on_hand: 15) } @@ -702,121 +282,115 @@ RSpec.describe "As a consumer I want to shop with a distributor" do end end - context "when no order cycles are available" do - it "tells us orders are closed" do - visit shop_path - expect(page).to have_content "Orders are closed" - end - - it "shows the last order cycle" do - oc1 = create(:simple_order_cycle, distributors: [distributor], - orders_open_at: 17.days.ago, - orders_close_at: 10.days.ago) - visit shop_path - expect(page).to have_content "The last cycle closed 10 days ago" - end - - it "shows the next order cycle" do - oc1 = create(:simple_order_cycle, distributors: [distributor], - orders_open_at: 10.days.from_now, - orders_close_at: 17.days.from_now) - visit shop_path - expect(page).to have_content "The next cycle opens in 10 days" + it "shows a distributor with images" do + # Given the distributor has a logo + distributor.update!(logo: white_logo_file) + # Then we should see the distributor and its logo + visit shop_path + expect(page).to have_text distributor.name + within ".tab-buttons" do + click_link "About" end + expect(first("distributor img")['src']).to include "logo-white.png" end - context "when shopping requires a customer" do - let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } - let(:product) { create(:simple_product) } - let(:variant) { create(:variant, product:) } - let(:unregistered_customer) { create(:customer, user: nil, enterprise: distributor) } + describe "shop tabs for a distributor" do + default_tabs = ["Shop", "About", "Producers", "Contact"].freeze + all_tabs = (default_tabs + ["Groups", "Home"]).freeze before do + visit shop_path + end + + shared_examples_for "reveal all right tabs" do |tabs, default| + tabs.each do |tab| + it "shows the #{tab} tab" do + within ".tab-buttons" do + expect(page).to have_content tab + end + end + end + + (all_tabs - tabs).each do |tab| + it "does not show the #{tab} tab" do + within ".tab-buttons" do + expect(page).not_to have_content tab + end + end + end + + it "shows the #{default} tab by default" do + within ".tab-buttons" do + expect(page).to have_selector ".selected", text: default + end + end + end + + context "default" do + it_behaves_like "reveal all right tabs", default_tabs, "Shop" + end + + context "when the distributor has a shopfront message" do + before do + distributor.update_attribute(:preferred_shopfront_message, "Hello") + visit shop_path + end + + it_behaves_like "reveal all right tabs", default_tabs + ["Home"], "Home" + end + + context "when the distributor has a custom tab" do + let(:custom_tab) { create(:custom_tab, title: "Custom") } + + before do + distributor.update(custom_tab:) + visit shop_path + end + + it_behaves_like "reveal all right tabs", default_tabs + ["Custom"], "Shop" + end + end + + describe "producers tab" do + before do + exchange = Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) add_variant_to_order_cycle(exchange, variant) - set_order_cycle(order, oc1) - distributor.require_login = true - distributor.save! - end - - context "when not logged in" do - it "tells us to login" do - visit shop_path - expect(page).to have_content "Only approved customers can access this shop." - expect(page).to have_content "login to proceed" - expect(page).not_to have_content product.name - expect(page).not_to have_selector "ordercycle" + visit shop_path + within ".tab-buttons" do + click_link "Producers" end end - context "when logged in" do - let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } - let(:user) { create(:user, bill_address: address, ship_address: address) } + it "shows the producers for a distributor" do + expect(page).to have_content supplier.name + find("a", text: supplier.name).click + within ".reveal-modal" do + expect(page).to have_content supplier.name + end + end + context "when the producer visibility is set to 'hidden'" do before do - login_as user - end - - context "as non-customer" do - it "tells us to contact enterprise" do - visit shop_path - expect(page).to have_content "Only approved customers can access this shop." - expect(page).to have_content "please contact #{distributor.name}" - expect(page).not_to have_content product.name - expect(page).not_to have_selector "ordercycle" - end - end - - context "as customer" do - let!(:customer) { create(:customer, user:, enterprise: distributor) } - - it "shows just products" do - visit shop_path - shows_products_without_customer_warning - end - end - - context "as a manager" do - let!(:role) { create(:enterprise_role, user:, enterprise: distributor) } - - it "shows just products" do - visit shop_path - shows_products_without_customer_warning - end - end - - context "as the owner" do - before do - distributor.owner = user - distributor.save! - end - - it "shows just products" do - visit shop_path - shows_products_without_customer_warning - end - end - end - - context "when previously unregistered customer registers" do - let!(:returning_user) { create(:user, email: unregistered_customer.email) } - - before do - login_as returning_user - end - - it "shows the products without customer only message" do + supplier.visible = "hidden" + supplier.save visit shop_path - shows_products_without_customer_warning + within ".tab-buttons" do + click_link "Producers" + end + end + + it "shows the producer name" do + expect(page).to have_content supplier.name + end + + it "does not show the producer modal" do + expect(page).not_to have_link supplier.name + expect(page).not_to have_selector ".reveal-modal" end end end end - def shows_products_without_customer_warning - expect(page).not_to have_content "This shop is for customers only." - expect(page).to have_content product.name - end - def expect_out_of_stock_behavior # Shows an "out of stock" modal, with helpful user feedback within(".out-of-stock-modal") do