Merge pull request #5994 from openfoodfoundation/tcs_checkbox

Terms and Conditions checkbox on checkout
This commit is contained in:
Luis Ramos
2020-10-30 15:10:06 +00:00
committed by GitHub
19 changed files with 210 additions and 28 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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()' } }

View File

@@ -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()' } }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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 }}

View File

@@ -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'))

View File

@@ -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

View File

@@ -0,0 +1,5 @@
class AddCustomerTermsAndConditionsAccepted < ActiveRecord::Migration
def change
add_column :customers, :terms_and_conditions_accepted_at, :datetime
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,18 @@
describe "termsAndConditionsWarning", ->
element = null
templatecache = null
beforeEach ->
module('admin.enterprises')
inject ($rootScope, $compile, $templateCache) ->
templatecache = $templateCache
el = angular.element("<input terms-and-conditions-warning=\"true\"></input>")
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')

View File

@@ -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")

View File

@@ -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) }