diff --git a/app/assets/javascripts/admin/enterprises/directives/terms_and_conditions_warning.js.coffee b/app/assets/javascripts/admin/enterprises/directives/terms_and_conditions_warning.js.coffee new file mode 100644 index 0000000000..d8aafe113d --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/directives/terms_and_conditions_warning.js.coffee @@ -0,0 +1,30 @@ +angular.module("admin.enterprises").directive 'termsAndConditionsWarning', ($compile, $templateCache, DialogDefaults, $timeout) -> + restrict: 'A' + scope: true + + link: (scope, element, attr) -> + # This file input click handler will hold the browser file input dialog and show a warning modal + scope.hold_file_input_and_show_warning_modal = (event) -> + event.preventDefault() + scope.template = $compile($templateCache.get('admin/modals/terms_and_conditions_warning.html'))(scope) + if scope.template.dialog + scope.template.dialog(DialogDefaults) + scope.template.dialog('open') + scope.$apply() + + element.bind 'click', scope.hold_file_input_and_show_warning_modal + + # When the user presses continue in the warning modal, we open the browser file input dialog + scope.continue = -> + scope.template.dialog('close') + + # unbind warning modal handler and click file input again to open the browser file input dialog + element.unbind('click').trigger('click') + # afterwards, bind warning modal handler again so that the warning is shown the next time + $timeout -> + element.bind 'click', scope.hold_file_input_and_show_warning_modal + return + + scope.close = -> + scope.template.dialog('close') + return diff --git a/app/assets/javascripts/darkswarm/services/checkout.js.coffee b/app/assets/javascripts/darkswarm/services/checkout.js.coffee index 69c484ccb4..ef865012c2 100644 --- a/app/assets/javascripts/darkswarm/services/checkout.js.coffee +++ b/app/assets/javascripts/darkswarm/services/checkout.js.coffee @@ -95,6 +95,10 @@ Darkswarm.factory 'Checkout', ($injector, CurrentOrder, ShippingMethods, StripeE last_name: @order.bill_address.lastname save_requested_by_customer: @secrets.save_requested_by_customer } + + if @terms_and_conditions_accepted() + munged_order["terms_and_conditions_accepted"] = true + munged_order shippingMethod: -> @@ -114,3 +118,7 @@ Darkswarm.factory 'Checkout', ($injector, CurrentOrder, ShippingMethods, StripeE cartTotal: -> @order.display_total + @shippingPrice() + @paymentPrice() + + terms_and_conditions_accepted: -> + terms_and_conditions_checkbox = angular.element("#accept_terms")[0] + terms_and_conditions_checkbox? && terms_and_conditions_checkbox.checked diff --git a/app/assets/javascripts/templates/admin/modals/terms_and_conditions_info.html.haml b/app/assets/javascripts/templates/admin/modals/terms_and_conditions_info.html.haml new file mode 100644 index 0000000000..5a3568085e --- /dev/null +++ b/app/assets/javascripts/templates/admin/modals/terms_and_conditions_info.html.haml @@ -0,0 +1,13 @@ +%div + .margin-bottom-30.text-center + .text-big + {{ 'js.admin.modals.terms_and_conditions_info.title' | t }} + .margin-bottom-30 + %p + {{ 'js.admin.modals.terms_and_conditions_info.message_1' | t }} + .margin-bottom-30 + %p + {{ 'js.admin.modals.terms_and_conditions_info.message_2' | t }} + + .text-center + %input.button.red.icon-plus{ type: 'button', value: t('js.admin.modals.got_it'), ng: { click: 'close()' } } diff --git a/app/assets/javascripts/templates/admin/modals/terms_and_conditions_warning.html.haml b/app/assets/javascripts/templates/admin/modals/terms_and_conditions_warning.html.haml new file mode 100644 index 0000000000..13eed6fe85 --- /dev/null +++ b/app/assets/javascripts/templates/admin/modals/terms_and_conditions_warning.html.haml @@ -0,0 +1,14 @@ +%div + .margin-bottom-30.text-center + .text-big + {{ 'js.admin.modals.terms_and_conditions_warning.title' | t }} + .margin-bottom-30 + %p + {{ 'js.admin.modals.terms_and_conditions_warning.message_1' | t }} + .margin-bottom-30 + %p + {{ 'js.admin.modals.terms_and_conditions_warning.message_2' | t }} + + .text-center + %input.button.red{ type: 'button', value: t('js.admin.modals.close'), ng: { click: 'close()' } } + %input.button.red{ type: 'button', value: t('js.admin.modals.continue'), ng: { click: 'continue()' } } diff --git a/app/helpers/terms_and_conditions_helper.rb b/app/helpers/terms_and_conditions_helper.rb new file mode 100644 index 0000000000..ef50459edb --- /dev/null +++ b/app/helpers/terms_and_conditions_helper.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module TermsAndConditionsHelper + def terms_and_conditions_activated? + current_order.distributor.terms_and_conditions.file? + end + + def terms_and_conditions_already_accepted? + customer_terms_and_conditions_accepted_at = spree_current_user&. + customer_of(current_order.distributor)&.terms_and_conditions_accepted_at + + customer_terms_and_conditions_accepted_at.present? && + (customer_terms_and_conditions_accepted_at > + current_order.distributor.terms_and_conditions_updated_at) + end +end diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 2385d996d6..399284d132 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -6,7 +6,8 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer :preferred_product_selection_from_inventory_only, :preferred_show_customer_names_to_suppliers, :owner, :contact, :users, :tag_groups, :default_tag_group, :require_login, :allow_guest_orders, :allow_order_changes, - :logo, :promo_image, :terms_and_conditions, :terms_and_conditions_file_name + :logo, :promo_image, :terms_and_conditions, + :terms_and_conditions_file_name, :terms_and_conditions_updated_at has_one :owner, serializer: Api::Admin::UserSerializer has_many :users, serializer: Api::Admin::UserSerializer @@ -21,9 +22,13 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer end def terms_and_conditions - return unless @object.terms_and_conditions.file? + return unless object.terms_and_conditions.file? - @object.terms_and_conditions.url + object.terms_and_conditions.url + end + + def terms_and_conditions_updated_at + object.terms_and_conditions_updated_at&.to_s end def tag_groups diff --git a/app/services/checkout/post_checkout_actions.rb b/app/services/checkout/post_checkout_actions.rb index f595ab0537..c9defcc9ca 100644 --- a/app/services/checkout/post_checkout_actions.rb +++ b/app/services/checkout/post_checkout_actions.rb @@ -8,6 +8,7 @@ module Checkout end def success(controller, params, current_user) + set_customer_terms_and_conditions_accepted_at(params) save_order_addresses_as_user_default(params, current_user) OrderCompletionReset.new(controller, @order).call end @@ -26,5 +27,11 @@ module Checkout user_default_address_setter.set_default_bill_address if params[:order][:default_bill_address] user_default_address_setter.set_default_ship_address if params[:order][:default_ship_address] end + + def set_customer_terms_and_conditions_accepted_at(params) + return unless params[:order] + + @order.customer.update(terms_and_conditions_accepted_at: Time.zone.now) if params[:order][:terms_and_conditions_accepted] + end end end diff --git a/app/views/admin/enterprises/form/_business_details.html.haml b/app/views/admin/enterprises/form/_business_details.html.haml index d15e3803c0..1bade5b580 100644 --- a/app/views/admin/enterprises/form/_business_details.html.haml +++ b/app/views/admin/enterprises/form/_business_details.html.haml @@ -35,12 +35,15 @@ .row .alpha.three.columns = f.label :terms_and_conditions, t('.terms_and_conditions') + %i.text-big.icon-question-sign.help-modal{ template: 'admin/modals/terms_and_conditions_info.html' } .omega.eight.columns - %a{ href: '{{ Enterprise.terms_and_conditions }}', ng: { if: 'Enterprise.terms_and_conditions' } } + %a{ href: '{{ Enterprise.terms_and_conditions }}', target: '_blank', ng: { if: 'Enterprise.terms_and_conditions' } } = '{{ Enterprise.terms_and_conditions_file_name }}' + = t('.uploaded_on') + = '{{ Enterprise.terms_and_conditions_updated_at }}' .pad-top - = f.file_field :terms_and_conditions + = f.file_field :terms_and_conditions, accept: 'application/pdf', 'terms-and-conditions-warning' => 'true' .pad-top %a.button.red{ href: '', ng: {click: 'removeTermsAndConditions()', if: 'Enterprise.terms_and_conditions'} } = t('.remove_terms_and_conditions') diff --git a/app/views/checkout/_form.html.haml b/app/views/checkout/_form.html.haml index f341a666d9..e755af01e4 100644 --- a/app/views/checkout/_form.html.haml +++ b/app/views/checkout/_form.html.haml @@ -16,6 +16,6 @@ = render "checkout/already_ordered", f: f if show_bought_items? = render "checkout/terms_and_conditions", f: f %p - %button.button.primary{type: :submit} + %button.button.primary{ type: :submit, ng: { disabled: "terms_and_conditions_activated && !terms_and_conditions_accepted" } } = t :checkout_send / {{ checkout.$valid }} diff --git a/app/views/checkout/_terms_and_conditions.html.haml b/app/views/checkout/_terms_and_conditions.html.haml index a99cf44785..05e0122ae7 100644 --- a/app/views/checkout/_terms_and_conditions.html.haml +++ b/app/views/checkout/_terms_and_conditions.html.haml @@ -1,2 +1,4 @@ -%p.small - = t('.message_html', terms_and_conditions_link: link_to( t( '.link_text' ), current_order.distributor.terms_and_conditions.url, target: '_blank')) if current_order.distributor.terms_and_conditions.file? +- if terms_and_conditions_activated? + %p + %input{ type: 'checkbox', id: 'accept_terms', ng: { model: "terms_and_conditions_accepted", init: "terms_and_conditions_activated=#{terms_and_conditions_activated?}; terms_and_conditions_accepted=#{terms_and_conditions_already_accepted?}" } } + %label.small{for: "accept_terms"}= t('.message_html', terms_and_conditions_link: link_to( t( '.link_text' ), current_order.distributor.terms_and_conditions.url, target: '_blank')) diff --git a/config/locales/en.yml b/config/locales/en.yml index 8b9786f1e9..ed2261fcf4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -705,6 +705,7 @@ en: invoice_text: Add customized text at the end of invoices terms_and_conditions: "Terms and Conditions" remove_terms_and_conditions: "Remove File" + uploaded_on: "uploaded on" contact: name: Name name_placeholder: eg. Gustav Plum @@ -1235,8 +1236,8 @@ en: cart: "cart" message_html: "You have an order for this order cycle already. Check the %{cart} to see the items you ordered before. You can also cancel items as long as the order cycle is open." terms_and_conditions: - message_html: "By placing this order you agree to the %{terms_and_conditions_link}." - link_text: "Terms of Service" + message_html: "I agree to the seller's %{terms_and_conditions_link}." + link_text: "Terms and Conditions" failed: "The checkout failed. Please let us know so that we can process your order." shops: hubs: @@ -2517,8 +2518,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using admin: enterprise_limit_reached: "You have reached the standard limit of enterprises per account. Write to %{contact_email} if you need to increase it." modals: - got_it: Got it + got_it: "Got it" close: "Close" + continue: "Continue" invite: "Invite" invite_title: "Invite an unregistered user" tag_rule_help: @@ -2537,6 +2539,14 @@ See the %{link} to find out more about %{sitename}'s features and to start using customer_tagged_rules_text: > By creating rules related to a specific customer tag, you can override the default behaviour (whether it be to show or to hide items) for customers with the specified tag. + terms_and_conditions_info: + title: "Uploading Terms and Conditions" + message_1: "Terms and Conditions are the contract between you, the seller, and the shopper. If you upload a file here shoppers must accept your Terms and Conditions in order to complete checkout. For the shopper this will appear as a checkbox at checkout that must be checked in order to proceed with checkout. We highly recommend you upload Terms and Conditions in alignment with national legislation." + message_2: "Shoppers will only be required to accept Terms and Conditions once. However if you change you Terms and Conditions shoppers will again be required to accept them before they can checkout." + terms_and_conditions_warning: + title: "Uploading Terms and Conditions" + message_1: "All your buyers will have to agree to them once at checkout. If you update the file, all your buyers will have to agree to them again at checkout." + message_2: "For buyers with subscriptions, you need to email them the Terms and Conditions (or the changes to them) for now, nothing will notify them about these new Terms and Conditions." panels: save: SAVE saved: SAVED diff --git a/db/migrate/20200907140555_add_customer_terms_and_conditions_accepted.rb b/db/migrate/20200907140555_add_customer_terms_and_conditions_accepted.rb new file mode 100644 index 0000000000..c48b1c19f2 --- /dev/null +++ b/db/migrate/20200907140555_add_customer_terms_and_conditions_accepted.rb @@ -0,0 +1,5 @@ +class AddCustomerTermsAndConditionsAccepted < ActiveRecord::Migration + def change + add_column :customers, :terms_and_conditions_accepted_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 5f982a1216..30247daf3a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -47,16 +47,17 @@ ActiveRecord::Schema.define(version: 20200912190210) do add_index "coordinator_fees", ["order_cycle_id"], name: "index_coordinator_fees_on_order_cycle_id", using: :btree create_table "customers", force: true do |t| - t.string "email", null: false - t.integer "enterprise_id", null: false + t.string "email", null: false + t.integer "enterprise_id", null: false t.string "code" t.integer "user_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "bill_address_id" t.integer "ship_address_id" t.string "name" - t.boolean "allow_charges", default: false, null: false + t.boolean "allow_charges", default: false, null: false + t.datetime "terms_and_conditions_accepted_at" end add_index "customers", ["bill_address_id"], name: "index_customers_on_bill_address_id", using: :btree diff --git a/lib/tasks/sample_data/order_factory.rb b/lib/tasks/sample_data/order_factory.rb index 9614f685a0..cdb2dfcc4e 100644 --- a/lib/tasks/sample_data/order_factory.rb +++ b/lib/tasks/sample_data/order_factory.rb @@ -57,7 +57,7 @@ class OrderFactory bill_address: order_address, ship_address: order_address ) - order.line_items.create( variant_id: first_variant.id, quantity: 5 ) + order.line_items.create(variant_id: first_variant.id, quantity: 5) order.payments.create(payment_method_id: first_payment_method_id) order end diff --git a/spec/features/admin/enterprises/terms_and_conditions_spec.rb b/spec/features/admin/enterprises/terms_and_conditions_spec.rb index ced7c5deea..476ea6eaad 100644 --- a/spec/features/admin/enterprises/terms_and_conditions_spec.rb +++ b/spec/features/admin/enterprises/terms_and_conditions_spec.rb @@ -16,7 +16,7 @@ feature "Uploading Terms and Conditions PDF" do visit edit_admin_enterprise_path(distributor) end - describe "images for an enterprise" do + describe "with terms and conditions to upload" do def go_to_business_details within(".side_menu") do click_link "Business Details" @@ -43,21 +43,27 @@ feature "Uploading Terms and Conditions PDF" do # Add PDF attach_file "enterprise[terms_and_conditions]", white_pdf_file_name - click_button "Update" + + Timecop.freeze(run_time = Time.zone.local(2002, 4, 13, 0, 0, 0)) do + click_button "Update" + expect(distributor.reload.terms_and_conditions_updated_at).to eq run_time + end expect(page). - to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") + to have_content "Enterprise \"#{distributor.name}\" has been successfully updated!" go_to_business_details - expect(page).to have_selector("a[href*='logo-white.pdf']") + expect(page).to have_selector "a[href*='logo-white.pdf'][target=\"_blank\"]" + expect(page).to have_content "2002-04-13 00:00:00 +1000" # Replace PDF attach_file "enterprise[terms_and_conditions]", black_pdf_file_name click_button "Update" expect(page). - to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") + to have_content "Enterprise \"#{distributor.name}\" has been successfully updated!" + expect(distributor.reload.terms_and_conditions_updated_at).to_not eq run_time go_to_business_details - expect(page).to have_selector("a[href*='logo-black.pdf']") + expect(page).to have_selector "a[href*='logo-black.pdf']" end end end diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 26be7c62a5..bedabaa2c3 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -122,11 +122,11 @@ feature "As a consumer I want to check out my cart", js: true do end it "doesn't show link to terms and conditions" do - expect(page).to have_no_link("Terms of Service") + expect(page).to have_no_link("Terms and Conditions") end end - context "when distributor has terms and conditions" do + context "when distributor has T&Cs" do let(:fake_terms_and_conditions_path) { Rails.root.join("app/assets/images/logo-white.png") } let(:terms_and_conditions_file) { Rack::Test::UploadedFile.new(fake_terms_and_conditions_path, "application/pdf") } @@ -135,9 +135,37 @@ feature "As a consumer I want to check out my cart", js: true do order.distributor.save end - it "shows a link to the terms and conditions" do - visit checkout_path - expect(page).to have_link("Terms of Service", href: order.distributor.terms_and_conditions.url) + describe "when customer has not accepted T&Cs before" do + it "shows a link to the T&Cs and disables checkout button until terms are accepted" do + visit checkout_path + expect(page).to have_link("Terms and Conditions", href: order.distributor.terms_and_conditions.url) + + expect(page).to have_button("Place order now", disabled: true) + + check "accept_terms" + expect(page).to have_button("Place order now", disabled: false) + end + end + + describe "when customer has already accepted T&Cs before" do + before do + customer = create(:customer, enterprise: order.distributor, user: user) + customer.update terms_and_conditions_accepted_at: Time.zone.now + end + + it "enables checkout button (because T&Cs are accepted by default)" do + visit checkout_path + expect(page).to have_button("Place order now", disabled: false) + end + + describe "but afterwards the enterprise has uploaded a new T&Cs file" do + before { order.distributor.update terms_and_conditions_updated_at: Time.zone.now } + + it "disables checkout button until terms are accepted" do + visit checkout_path + expect(page).to have_button("Place order now", disabled: true) + end + end end end diff --git a/spec/javascripts/unit/admin/enterprises/directives/terms_and_conditions_warning_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/directives/terms_and_conditions_warning_spec.js.coffee new file mode 100644 index 0000000000..b7647024e2 --- /dev/null +++ b/spec/javascripts/unit/admin/enterprises/directives/terms_and_conditions_warning_spec.js.coffee @@ -0,0 +1,18 @@ +describe "termsAndConditionsWarning", -> + element = null + templatecache = null + + beforeEach -> + module('admin.enterprises') + + inject ($rootScope, $compile, $templateCache) -> + templatecache = $templateCache + el = angular.element("") + element = $compile(el)($rootScope) + $rootScope.$digest() + + describe "terms and conditions warning", -> + it "should load template", -> + spyOn(templatecache, 'get') + element.triggerHandler('click'); + expect(templatecache.get).toHaveBeenCalledWith('admin/modals/terms_and_conditions_warning.html') diff --git a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee index 8caacdc531..8c5d49633b 100644 --- a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee @@ -65,6 +65,7 @@ describe 'Checkout service', -> inject ($injector, _$httpBackend_, $rootScope)-> $httpBackend = _$httpBackend_ Checkout = $injector.get("Checkout") + spyOn(Checkout, "terms_and_conditions_accepted") scope = $rootScope.$new() scope.Checkout = Checkout Navigation = $injector.get("Navigation") diff --git a/spec/services/checkout/post_checkout_actions_spec.rb b/spec/services/checkout/post_checkout_actions_spec.rb index 916cff69fb..4c5ee6a1c2 100644 --- a/spec/services/checkout/post_checkout_actions_spec.rb +++ b/spec/services/checkout/post_checkout_actions_spec.rb @@ -23,6 +23,21 @@ describe Checkout::PostCheckoutActions do postCheckoutActions.success(controller, params, current_user) end + describe "setting customer terms_and_conditions_accepted_at" do + before { order.customer = build(:customer) } + + it "does not set customer's terms_and_conditions to the current time if terms have not been accepted" do + postCheckoutActions.success(controller, params, current_user) + expect(order.customer.terms_and_conditions_accepted_at).to be_nil + end + + it "sets customer's terms_and_conditions to the current time if terms have been accepted" do + params = { order: { terms_and_conditions_accepted: true } } + postCheckoutActions.success(controller, params, current_user) + expect(order.customer.terms_and_conditions_accepted_at).to_not be_nil + end + end + describe "setting the user default address" do let(:user_default_address_setter) { instance_double(UserDefaultAddressSetter) }