Merge pull request #9262 from cillian/order-cycle-shipping-methods

Let people customise which shipping methods are available to customers on order cycles
This commit is contained in:
Filipe
2022-09-30 14:36:37 +01:00
committed by GitHub
40 changed files with 1201 additions and 302 deletions

View File

@@ -732,7 +732,7 @@ Rails/FilePath:
- 'spec/models/content_configuration_spec.rb'
- 'spec/support/downloads_helper.rb'
# Offense count: 11
# Offense count: 12
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/HasAndBelongsToMany:
@@ -740,6 +740,7 @@ Rails/HasAndBelongsToMany:
- 'app/models/concerns/payment_method_distributors.rb'
- 'app/models/enterprise.rb'
- 'app/models/enterprise_group.rb'
- 'app/models/order_cycle.rb'
- 'app/models/spree/line_item.rb'
- 'app/models/spree/option_value.rb'
- 'app/models/spree/role.rb'

View File

@@ -2,10 +2,10 @@
module Admin
class OrderCyclesController < Admin::ResourceController
include OrderCyclesHelper
include ::OrderCyclesHelper
include PaperTrailLogging
prepend_before_action :set_order_cycle_id, only: [:incoming, :outgoing]
prepend_before_action :set_order_cycle_id, only: [:incoming, :outgoing, :checkout_options]
before_action :load_data_for_index, only: :index
before_action :require_coordinator, only: :new
before_action :remove_protected_attrs, only: [:update]
@@ -67,10 +67,12 @@ module Admin
update_nil_subscription_line_items_price_estimate(@order_cycle)
respond_to do |format|
flash[:notice] = I18n.t(:order_cycles_update_notice) if params[:reloading] == '1'
format.html { redirect_back(fallback_location: root_path) }
format.html { redirect_to_after_update_path }
format.json { render json: { success: true } }
end
else
elsif request.format.html?
render :checkout_options
elsif request.format.json?
render json: { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity
end
end
@@ -190,6 +192,16 @@ module Admin
end
end
def redirect_to_after_update_path
if params[:context] == "checkout_options" && params[:save]
redirect_to main_app.admin_order_cycle_checkout_options_path(@order_cycle)
elsif params[:context] == "checkout_options" && params[:save_and_back_to_list]
redirect_to main_app.admin_order_cycles_path
else
redirect_back(fallback_location: root_path)
end
end
def require_coordinator
@order_cycle.coordinator =
permitted_coordinating_enterprises_for(@order_cycle).find_by(id: params[:coordinator_id])

View File

@@ -16,12 +16,7 @@ class BaseController < ApplicationController
private
def set_order_cycles
unless @distributor.ready_for_checkout?
@order_cycles = OrderCycle.where('false')
return
end
@order_cycles = Shop::OrderCyclesList.new(@distributor, current_customer).call
@order_cycles = Shop::OrderCyclesList.ready_for_checkout_for(@distributor, current_customer)
set_order_cycle
end

View File

@@ -14,15 +14,7 @@ module EnterprisesHelper
end
def available_shipping_methods
return [] if current_distributor.blank?
shipping_methods = current_distributor.shipping_methods.display_on_checkout.to_a
applicator = OpenFoodNetwork::TagRuleApplicator.new(current_distributor,
"FilterShippingMethods", current_customer&.tag_list)
applicator.filter!(shipping_methods)
shipping_methods.uniq
OrderAvailableShippingMethods.new(current_order, current_customer).to_a
end
def available_payment_methods

View File

@@ -56,10 +56,6 @@ module OrderCyclesHelper
@simple_index ||= !OpenFoodNetwork::Permissions.new(spree_current_user).can_manage_complex_order_cycles?
end
def order_cycles_simple_form
@order_cycles_simple_form ||= @order_cycle.coordinator.sells == 'own'
end
def pickup_time(order_cycle = current_order_cycle)
order_cycle.exchanges.to_enterprises(current_distributor).outgoing.first.pickup_time
end

View File

@@ -120,6 +120,7 @@ class Enterprise < ApplicationRecord
joins(:shipping_methods).
joins(:payment_methods).
merge(Spree::PaymentMethod.available).
merge(Spree::ShippingMethod.frontend).
select('DISTINCT enterprises.*')
}
scope :not_ready_for_checkout, lambda {
@@ -387,7 +388,7 @@ class Enterprise < ApplicationRecord
end
def ready_for_checkout?
shipping_methods.any? && payment_methods.available.any?
shipping_methods.frontend.any? && payment_methods.available.any?
end
def self.find_available_permalink(test_permalink)

View File

@@ -22,9 +22,11 @@ class OrderCycle < ApplicationRecord
has_many :suppliers, -> { distinct }, source: :sender, through: :cached_incoming_exchanges
has_many :distributors, -> { distinct }, source: :receiver, through: :cached_outgoing_exchanges
has_many :order_cycle_schedules
has_many :schedules, through: :order_cycle_schedules
has_and_belongs_to_many :selected_distributor_shipping_methods,
class_name: 'DistributorShippingMethod',
join_table: 'order_cycles_distributor_shipping_methods'
has_paper_trail meta: { custom_data: proc { |order_cycle| order_cycle.schedule_ids.to_s } }
attr_accessor :incoming_exchanges, :outgoing_exchanges
@@ -150,6 +152,20 @@ class OrderCycle < ApplicationRecord
]
end
def attachable_payment_methods
Spree::PaymentMethod.available(:both).
joins("INNER JOIN distributors_payment_methods
ON payment_method_id = spree_payment_methods.id").
where("distributor_id IN (?)", distributor_ids).
distinct
end
def attachable_distributor_shipping_methods
DistributorShippingMethod.joins(:shipping_method).
merge(Spree::ShippingMethod.frontend).
where("distributor_id IN (?)", distributor_ids)
end
def clone!
oc = dup
oc.name = I18n.t("models.order_cycle.cloned_order_cycle_name", order_cycle: oc.name)
@@ -161,6 +177,9 @@ class OrderCycle < ApplicationRecord
oc.schedule_ids = schedule_ids
oc.save!
exchanges.each { |e| e.clone!(oc) }
oc.selected_distributor_shipping_method_ids = (
attachable_distributor_shipping_methods.map(&:id) & selected_distributor_shipping_method_ids
)
sync_subscriptions
oc.reload
end
@@ -274,6 +293,22 @@ class OrderCycle < ApplicationRecord
items.each { |li| scoper.scope(li.variant) }
end
def distributor_shipping_methods
if simple? || selected_distributor_shipping_methods.none?
attachable_distributor_shipping_methods
else
attachable_distributor_shipping_methods.where(
"distributors_shipping_methods.id IN (?) OR distributor_id NOT IN (?)",
selected_distributor_shipping_methods.map(&:id),
selected_distributor_shipping_methods.map(&:distributor_id)
)
end
end
def simple?
coordinator.sells == 'own'
end
private
def opening?

View File

@@ -243,7 +243,9 @@ module Spree
end
def add_order_cycle_management_abilities(user)
can [:admin, :index, :read, :edit, :update, :incoming, :outgoing], OrderCycle do |order_cycle|
can [
:admin, :index, :read, :edit, :update, :incoming, :outgoing, :checkout_options
], OrderCycle do |order_cycle|
OrderCycle.visible_by(user).include? order_cycle
end
can [:admin, :index, :create], Schedule

View File

@@ -3,7 +3,10 @@
module Spree
class ShippingMethod < ApplicationRecord
include CalculatedAdjustments
DISPLAY = [:both, :front_end, :back_end].freeze
DISPLAY_ON_OPTIONS = {
both: "",
back_end: "back_end"
}.freeze
acts_as_paranoid
acts_as_taggable
@@ -27,6 +30,7 @@ module Spree
validates :name, presence: true
validate :distributor_validation
validate :at_least_one_shipping_category
validates :display_on, inclusion: { in: DISPLAY_ON_OPTIONS.values }, allow_nil: true
after_save :touch_distributors
@@ -51,9 +55,6 @@ module Spree
}
scope :by_name, -> { order('spree_shipping_methods.name ASC') }
scope :display_on_checkout, -> {
where("spree_shipping_methods.display_on is null OR spree_shipping_methods.display_on = ''")
}
# Here we allow checkout with shipping methods without zones (see issue #3928 for details)
# and also checkout with addresses outside of the zones of the selected shipping method
@@ -101,16 +102,26 @@ module Spree
]
end
def self.on_backend_query
"#{table_name}.display_on != 'front_end' OR #{table_name}.display_on IS NULL"
def self.backend
where(display_on: DISPLAY_ON_OPTIONS[:back_end])
end
def self.on_frontend_query
"#{table_name}.display_on != 'back_end' OR #{table_name}.display_on IS NULL"
def self.frontend
where(display_on: [nil, ""])
end
private
def no_active_or_upcoming_order_cycle_distributors_with_only_one_shipping_method?
return true if new_record?
distributors.
with_order_cycles_as_distributor_outer.
merge(OrderCycle.active.or(OrderCycle.upcoming)).none? do |distributor|
distributor.shipping_method_ids.one?
end
end
def at_least_one_shipping_category
return unless shipping_categories.empty?

View File

@@ -8,14 +8,7 @@ module Spree
scope :frontend,
-> {
includes(:shipping_method).
where(ShippingMethod.on_frontend_query).
references(:shipping_method).
order("cost ASC")
}
scope :backend,
-> {
includes(:shipping_method).
where(ShippingMethod.on_backend_query).
merge(ShippingMethod.frontend).
references(:shipping_method).
order("cost ASC")
}

View File

@@ -0,0 +1,37 @@
# frozen_string_literal: true
class OrderAvailableShippingMethods
attr_reader :order, :customer
delegate :distributor,
:order_cycle,
to: :order
def initialize(order, customer = nil)
@order, @customer = order, customer
end
def to_a
return [] if distributor.blank?
shipping_methods = shipping_methods_before_tag_rules_applied
applicator = OpenFoodNetwork::TagRuleApplicator.new(distributor,
"FilterShippingMethods", customer&.tag_list)
applicator.filter!(shipping_methods)
shipping_methods.uniq
end
private
def shipping_methods_before_tag_rules_applied
if order_cycle.nil? || order_cycle.simple?
distributor.shipping_methods
else
distributor.shipping_methods.where(
id: order_cycle.distributor_shipping_methods.select(:shipping_method_id)
)
end.frontend.to_a
end
end

View File

@@ -34,7 +34,7 @@ class OrderCartReset
end
def reset_order_cycle(current_customer)
listed_order_cycles = Shop::OrderCyclesList.new(distributor, current_customer).call
listed_order_cycles = Shop::OrderCyclesList.active_for(distributor, current_customer)
if order_cycle_not_listed?(order.order_cycle, listed_order_cycles)
order.order_cycle = nil

View File

@@ -8,9 +8,13 @@ class OrderCycleForm
def initialize(order_cycle, order_cycle_params, user)
@order_cycle = order_cycle
@order_cycle_params = order_cycle_params
@specified_params = order_cycle_params.keys
@user = user
@permissions = OpenFoodNetwork::Permissions.new(user)
@schedule_ids = order_cycle_params.delete(:schedule_ids)
@selected_distributor_shipping_method_ids = order_cycle_params.delete(
:selected_distributor_shipping_method_ids
)
end
def save
@@ -20,13 +24,15 @@ class OrderCycleForm
order_cycle.transaction do
order_cycle.save!
order_cycle.schedule_ids = schedule_ids
order_cycle.schedule_ids = schedule_ids if parameter_specified?(:schedule_ids)
order_cycle.save!
apply_exchange_changes
attach_selected_distributor_shipping_methods
sync_subscriptions
true
end
rescue ActiveRecord::RecordInvalid
rescue ActiveRecord::RecordInvalid => e
add_exception_to_order_cycle_errors(e)
false
end
@@ -34,24 +40,50 @@ class OrderCycleForm
attr_accessor :order_cycle, :order_cycle_params, :user, :permissions
def add_exception_to_order_cycle_errors(exception)
error = exception.message.split(":").last.strip
order_cycle.errors.add(:base, error) if order_cycle.errors.to_a.exclude?(error)
end
def apply_exchange_changes
return if exchanges_unchanged?
OpenFoodNetwork::OrderCycleFormApplicator.new(order_cycle, user).go!
end
def attach_selected_distributor_shipping_methods
return if @selected_distributor_shipping_method_ids.nil?
order_cycle.reload # so outgoing exchanges are up-to-date for shipping method validations
order_cycle.selected_distributor_shipping_method_ids = selected_distributor_shipping_method_ids
order_cycle.save!
end
def attachable_distributor_shipping_method_ids
@attachable_distributor_shipping_method_ids ||= order_cycle.attachable_distributor_shipping_methods.map(&:id)
end
def exchanges_unchanged?
[:incoming_exchanges, :outgoing_exchanges].all? do |direction|
order_cycle_params[direction].nil?
end
end
def schedule_ids?
@schedule_ids.present?
def selected_distributor_shipping_method_ids
@selected_distributor_shipping_method_ids = (
attachable_distributor_shipping_method_ids &
@selected_distributor_shipping_method_ids.reject(&:blank?).map(&:to_i)
)
if attachable_distributor_shipping_method_ids.sort == @selected_distributor_shipping_method_ids.sort
@selected_distributor_shipping_method_ids = []
end
@selected_distributor_shipping_method_ids
end
def build_schedule_ids
return unless schedule_ids?
return unless parameter_specified?(:schedule_ids)
result = existing_schedule_ids
result |= (requested_schedule_ids & permitted_schedule_ids) # Add permitted and requested
@@ -60,7 +92,7 @@ class OrderCycleForm
end
def sync_subscriptions
return unless schedule_ids?
return unless parameter_specified?(:schedule_ids)
return unless schedule_sync_required?
OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscriptions_to_sync).sync!
@@ -78,6 +110,10 @@ class OrderCycleForm
@schedule_ids.map(&:to_i)
end
def parameter_specified?(key)
@specified_params.map(&:to_s).include?(key.to_s)
end
def permitted_schedule_ids
Schedule.where(id: requested_schedule_ids | existing_schedule_ids)
.merge(permissions.editable_schedules).pluck(:id)

View File

@@ -17,7 +17,7 @@ module PermittedAttributes
:name, :orders_open_at, :orders_close_at, :coordinator_id,
:preferred_product_selection_from_coordinator_inventory_only,
:automatic_notifications,
{ schedule_ids: [], coordinator_fee_ids: [] }
{ schedule_ids: [], selected_distributor_shipping_method_ids: [], coordinator_fee_ids: [] }
]
end

View File

@@ -3,6 +3,16 @@
# Lists available order cycles for a given customer in a given distributor
module Shop
class OrderCyclesList
def self.active_for(distributor, customer)
new(distributor, customer).call
end
def self.ready_for_checkout_for(distributor, customer)
return OrderCycle.none unless distributor.ready_for_checkout?
new(distributor, customer).call
end
def initialize(distributor, customer)
@distributor = distributor
@customer = customer

View File

@@ -6,6 +6,8 @@
= t("admin.order_cycles.wizard_progress.incoming")
%li
= t("admin.order_cycles.wizard_progress.outgoing")
%li
= t("admin.order_cycles.wizard_progress.checkout_options")
- else
%li{ class: "#{'current' if action_name == 'edit'}" }
%a{ href: main_app.edit_admin_order_cycle_path(@order_cycle) }
@@ -16,3 +18,6 @@
%li{ class: "#{'current' if action_name == 'outgoing'}" }
%a{ href: main_app.admin_order_cycle_outgoing_path(@order_cycle) }
= t("admin.order_cycles.wizard_progress.outgoing")
%li{ class: "#{'current' if action_name == 'checkout_options'}" }
%a{ href: main_app.admin_order_cycle_checkout_options_path(@order_cycle) }
= t("admin.order_cycles.wizard_progress.checkout_options")

View File

@@ -0,0 +1,74 @@
= render partial: "/admin/order_cycles/order_cycle_top_buttons"
- content_for :page_title do
= t :edit_order_cycle
= form_for [main_app, :admin, @order_cycle], html: { class: "order_cycle" } do |f|
= render 'wizard_progress'
%fieldset.no-border-bottom
%legend{ align: 'center'}= t('.checkout_options')
= hidden_field_tag "order_cycle[selected_distributor_shipping_method_ids][]", ""
.row
.three.columns
&nbsp;
.ten.columns
%table.checkout-options
%thead
%tr
%th{ colspan: 2 }= t('.shipping_methods')
- @order_cycle.distributors.each do |distributor|
- distributor_shipping_methods = @order_cycle.attachable_distributor_shipping_methods.where("distributor_id = ?", distributor.id).includes(:shipping_method)
%tr{ "data-controller": "select-all" }
%td.text-center
- if distributor_shipping_methods.many?
%label
= check_box_tag nil, nil, nil, { "data-action": "change->select-all#toggleAll", "data-select-all-target": "all" }
= t(".select_all")
%td
%em= distributor.name
- distributor_shipping_methods.each do |distributor_shipping_method|
%p
%label{ class: ("disabled" if distributor_shipping_methods.one? || !distributor_shipping_method.shipping_method.frontend?) }
= check_box_tag "order_cycle[selected_distributor_shipping_method_ids][]",
distributor_shipping_method.id,
@order_cycle.distributor_shipping_methods.include?(distributor_shipping_method),
id: "order_cycle_selected_distributor_shipping_method_ids_#{distributor_shipping_method.id}",
data: ({ "action" => "change->select-all#toggleCheckbox", "select-all-target" => "checkbox" } if distributor_shipping_method.shipping_method.frontend?)
= distributor_shipping_method.shipping_method.name
- distributor.shipping_methods.backend.each do |shipping_method|
%label.disabled
= check_box_tag nil, nil, false, disabled: true
= shipping_method.name
= "(#{t('.back_end')})"
- if distributor.shipping_methods.frontend.none?
%p
= t('.no_shipping_methods')
%tr
%th{ colspan: 2 }= t('.payment_methods')
%tr
%td
%td
- if @order_cycle.attachable_payment_methods.available(:both).any?
%ul
- @order_cycle.attachable_payment_methods.available(:both).each do |payment_method|
%li= payment_method.name
- else
%p
= t('.no_payment_methods')
%div#save-bar
%div.container
%div.seven.columns.alpha
- if @order_cycle.errors.any?
%h5#status-message.error
= @order_cycle.errors.to_a.to_sentence
%div.nine.columns.omega.text-right
= hidden_field_tag :context, :checkout_options
= f.submit t('.save'), class: "red", name: :save
= f.submit t('.save_and_back_to_list'), class: "red", name: :save_and_back_to_list
%a.button.cancel{ href: main_app.admin_order_cycles_path }
= t('.cancel')

View File

@@ -13,20 +13,20 @@
- content_for :page_title do
= t :edit_order_cycle
- ng_controller = order_cycles_simple_form ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl'
- ng_controller = @order_cycle.simple? ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl'
= admin_inject_order_cycle_instance
= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form'} do |f|
%save-bar{ dirty: "order_cycle_form.$dirty", persist: "true" }
%input.red{ type: "button", value: t('.save'), ng: { click: "submit($event, null)", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } }
- if order_cycles_simple_form
- if @order_cycle.simple?
%input.red{ type: "button", value: t('.save_and_back_to_list'), ng: { click: "submit($event, '#{main_app.admin_order_cycles_path}')", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } }
- else
%input.red{ type: "button", value: t('.save_and_next'), ng: { click: "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } }
%input{ type: "button", value: t('.next'), ng: { click: "cancel('#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", disabled: "order_cycle_form.$dirty" } }
%input{ type: "button", ng: { value: "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", click: "cancel('#{main_app.admin_order_cycles_path}')" } }
- if order_cycles_simple_form
- if @order_cycle.simple?
= render 'simple_form', f: f
- else
= render 'form', f: f

View File

@@ -1,18 +1,18 @@
- content_for :page_title do
=t('new_order_cycle')
- ng_controller = order_cycles_simple_form ? 'AdminSimpleCreateOrderCycleCtrl' : 'AdminCreateOrderCycleCtrl'
- ng_controller = @order_cycle.simple? ? 'AdminSimpleCreateOrderCycleCtrl' : 'AdminCreateOrderCycleCtrl'
= admin_inject_order_cycle_instance
= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form'} do |f|
%save-bar{ dirty: "order_cycle_form.$dirty", persist: "true" }
- if order_cycles_simple_form
- if @order_cycle.simple?
- custom_redirect_path = main_app.admin_order_cycles_path
%input.red{ type: "button", value: t('.create'), ng: { click: "submit($event, '#{custom_redirect_path}')", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } }
%input{ type: "button", ng: { value: "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", click: "cancel('#{main_app.admin_order_cycles_path}')" } }
- if order_cycles_simple_form
- if @order_cycle.simple?
= render 'simple_form', f: f
- else
= render 'form', f: f

View File

@@ -10,7 +10,8 @@
%save-bar{ dirty: "order_cycle_form.$dirty", persist: "true" }
%input.red{ type: "button", value: t('.save'), ng: { click: "submit($event, null)", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } }
%input.red{ type: "button", value: t('.save_and_back_to_list'), ng: { click: "submit($event, '#{main_app.admin_order_cycles_path}')", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } }
%input.red{ type: "button", value: t('.save_and_next'), ng: { click: "submit($event, '#{main_app.admin_order_cycle_checkout_options_path(@order_cycle)}')", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } }
%input{ type: "button", value: t('.next'), ng: { click: "cancel('#{main_app.admin_order_cycle_checkout_options_path(@order_cycle)}')", disabled: "order_cycle_form.$dirty" } }
%input{ type: "button", ng: { value: "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", click: "cancel('#{main_app.admin_order_cycles_path}')" } }
%fieldset.no-border-bottom

View File

@@ -38,7 +38,7 @@
%td{ :colspan => "5" }
%div.field.alpha.five.columns
= label_tag 'selected_shipping_rate_id', Spree.t(:shipping_method)
= select_tag :selected_shipping_rate_id, options_for_select(shipment.shipping_rates.backend.map { |sr| ["#{sr.name} #{sr.display_price}", sr.id] }, shipment.selected_shipping_rate_id), { :class => 'select2 fullwidth', :data => { 'shipment-number' => shipment.number } }
= select_tag :selected_shipping_rate_id, options_for_select(shipment.shipping_rates.map { |sr| ["#{sr.name} #{sr.display_price}", sr.id] }, shipment.selected_shipping_rate_id), { :class => 'select2 fullwidth', :data => { 'shipment-number' => shipment.number } }
%td.actions
- if can? :update, shipment

View File

@@ -19,7 +19,7 @@
.alpha.four.columns
= f.label :display_on, t(:display)
.omega.twelve.columns
= select(:shipping_method, :display_on, [[t(".both"), nil], [t(".back_end"), "back_end"]], {}, {class: 'select2 fullwidth'})
= select(:shipping_method, :display_on, Spree::ShippingMethod::DISPLAY_ON_OPTIONS.map { |key, value| [t(".#{key}"), value] }, {}, {class: 'select2 fullwidth'})
= error_message_on :shipping_method, :display_on
.row

View File

@@ -0,0 +1,19 @@
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ["all", "checkbox"];
connect() {
this.toggleCheckbox()
}
toggleAll() {
this.checkboxTargets.forEach(checkbox => {
checkbox.checked = this.allTarget.checked;
});
}
toggleCheckbox() {
this.allTarget.checked = this.checkboxTargets.every(checkbox => checkbox.checked);
}
}

View File

@@ -11,6 +11,10 @@
h5 {
color: $spree-blue;
&.error {
color: $red-500;
}
}
input {

View File

@@ -96,6 +96,14 @@ form.order_cycle {
.icon-question-sign {
font-size: 18px;
}
table.checkout-options {
ul {
margin-left: 1em;
}
p, li {
margin: 0.5em 0;
}
}
table.exchanges {
tr td.active {
width: 20px;

View File

@@ -1156,15 +1156,29 @@ en:
tags: "Tags"
delivery_details: "Delivery Details"
fees: "Fees"
next: "Next"
previous: "Previous"
save: "Save"
save_and_back_to_list: "Save and Back to List"
save_and_next: "Save and Next"
cancel: "Cancel"
back_to_list: "Back To List"
checkout_options:
back_end: "Back office only"
cancel: "Cancel"
checkout_options: "Checkout options"
distributor: "Distributor"
no_payment_methods: Each distributor on this order cycle requires at least one payment method.
no_shipping_methods: Each distributor on this order cycle requires at least one shipping method.
payment_methods: "Payment Methods"
save: "Save"
save_and_back_to_list: "Save and Back to List"
select_all: "Select all"
shipping_methods: "Shipping Methods"
wizard_progress:
edit: "1. General Settings"
incoming: "2. Incoming Products"
outgoing: "3. Outgoing Products"
checkout_options: "4. Checkout Options"
exchange_form:
pickup_time_tip: When orders from this OC will be ready for the customer
pickup_instructions_placeholder: "Pick-up instructions"

View File

@@ -15,6 +15,7 @@ Openfoodnetwork::Application.routes.draw do
post :bulk_update, on: :collection, as: :bulk_update
get :incoming
get :outgoing
get :checkout_options
member do
get :clone

View File

@@ -0,0 +1,17 @@
class CreateOrderCyclesDistributorShippingMethods < ActiveRecord::Migration[6.1]
def up
create_table :order_cycles_distributor_shipping_methods, id: false do |t|
t.belongs_to :order_cycle,
index: { name: "index_oc_id_on_order_cycles_distributor_shipping_methods" }
t.belongs_to :distributor_shipping_method,
index: { name: "index_dsm_id_on_order_cycles_distributor_shipping_methods" }
t.index [:order_cycle_id, :distributor_shipping_method_id],
name: "order_cycles_distributor_shipping_methods_join_index",
unique: true
end
end
def down
drop_table :order_cycles_distributor_shipping_methods
end
end

View File

@@ -330,6 +330,14 @@ ActiveRecord::Schema.define(version: 2022_09_07_055044) do
t.boolean "mails_sent", default: false
end
create_table "order_cycles_distributor_shipping_methods", id: false, force: :cascade do |t|
t.bigint "order_cycle_id"
t.bigint "distributor_shipping_method_id"
t.index ["distributor_shipping_method_id"], name: "index_dsm_id_on_order_cycles_distributor_shipping_methods"
t.index ["order_cycle_id", "distributor_shipping_method_id"], name: "order_cycles_distributor_shipping_methods_join_index", unique: true
t.index ["order_cycle_id"], name: "index_oc_id_on_order_cycles_distributor_shipping_methods"
end
create_table "producer_properties", id: :serial, force: :cascade do |t|
t.string "value", limit: 255
t.integer "producer_id"

View File

@@ -59,6 +59,10 @@ FactoryBot.define do
end
end
# Note: Order cycles are sometimes referred to as 'simple if they are for a shop selling their
# own produce i.e. :sells = 'own'. However the 'simple_order_cycle' name does not mean this
# and may need to be renamed to avoid potential confusion because it actually can create
# 'non-simple' order cycles too for distributors selling produce from other enterprises.
factory :simple_order_cycle, class: OrderCycle do
sequence(:name) { |n| "Order Cycle #{n}" }
@@ -116,4 +120,12 @@ FactoryBot.define do
orders_open_at { 2.weeks.ago }
orders_close_at { 1.week.ago }
end
factory :distributor_order_cycle, parent: :simple_order_cycle do
coordinator { create(:distributor_enterprise) }
end
factory :sells_own_order_cycle, parent: :simple_order_cycle do
coordinator { create(:enterprise, sells: "own") }
end
end

View File

@@ -9,140 +9,6 @@ describe EnterprisesHelper, type: :helper do
before { allow(helper).to receive(:spree_current_user) { user } }
describe "loading available shipping methods" do
let!(:distributor_shipping_method) {
create(:shipping_method, require_ship_address: false, distributors: [distributor])
}
let!(:other_distributor_shipping_method) {
create(:shipping_method, require_ship_address: false, distributors: [some_other_distributor])
}
context "when the order has no current_distributor" do
before do
allow(helper).to receive(:current_distributor) { nil }
end
it "returns an empty array" do
expect(helper.available_shipping_methods).to eq []
end
end
context "when no tag rules are in effect" do
before { allow(helper).to receive(:current_distributor) { distributor } }
it "finds the shipping methods for the current distributor" do
expect(helper.available_shipping_methods).to_not include other_distributor_shipping_method
expect(helper.available_shipping_methods).to include distributor_shipping_method
end
it "does not return 'back office only' shipping method" do
backoffice_only_shipping_method = create(:shipping_method, require_ship_address: false,
distributors: [distributor], display_on: 'back_end')
expect(helper.available_shipping_methods).to_not include backoffice_only_shipping_method
expect(helper.available_shipping_methods).to_not include other_distributor_shipping_method
expect(helper.available_shipping_methods).to include distributor_shipping_method
end
end
context "when FilterShippingMethods tag rules are in effect" do
let(:customer) { create(:customer, user: user, enterprise: distributor) }
let!(:tag_rule) {
create(:filter_shipping_methods_tag_rule,
enterprise: distributor,
preferred_customer_tags: "local",
preferred_shipping_method_tags: "local-delivery")
}
let!(:default_tag_rule) {
create(:filter_shipping_methods_tag_rule,
enterprise: distributor,
is_default: true,
preferred_shipping_method_tags: "local-delivery")
}
let!(:tagged_sm) { distributor_shipping_method }
let!(:untagged_sm) { other_distributor_shipping_method }
before do
tagged_sm.update_attribute(:tag_list, 'local-delivery')
distributor.shipping_methods = [tagged_sm, untagged_sm]
allow(helper).to receive(:current_distributor) { distributor }
end
context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do
before {
tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'visible')
}
before {
default_tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility,
'hidden')
}
context "when the customer is nil" do
it "applies default action (hide)" do
expect(helper.current_customer).to be nil
expect(helper.available_shipping_methods).to include untagged_sm
expect(helper.available_shipping_methods).to_not include tagged_sm
end
end
context "when the customer's tags match" do
before { customer.update_attribute(:tag_list, 'local') }
it "applies the action (show)" do
expect(helper.current_customer).to eq customer
expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm
end
end
context "when the customer's tags don't match" do
before { customer.update_attribute(:tag_list, 'something') }
it "applies the default action (hide)" do
expect(helper.current_customer).to eq customer
expect(helper.available_shipping_methods).to include untagged_sm
expect(helper.available_shipping_methods).to_not include tagged_sm
end
end
end
context "with a preferred visiblity of 'hidden', default visibility of 'visible'" do
before {
tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'hidden')
}
before {
default_tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility,
'visible')
}
context "when the customer is nil" do
it "applies default action (show)" do
expect(helper.current_customer).to be nil
expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm
end
end
context "when the customer's tags match" do
before { customer.update_attribute(:tag_list, 'local') }
it "applies the action (hide)" do
expect(helper.current_customer).to eq customer
expect(helper.available_shipping_methods).to include untagged_sm
expect(helper.available_shipping_methods).to_not include tagged_sm
end
end
context "when the customer's tags don't match" do
before { customer.update_attribute(:tag_list, 'something') }
it "applies the default action (show)" do
expect(helper.current_customer).to eq customer
expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm
end
end
end
end
end
describe "loading available payment methods" do
let!(:pm1) { create(:payment_method, distributors: [distributor]) }
let!(:pm2) { create(:payment_method, distributors: [some_other_distributor]) }

View File

@@ -0,0 +1,113 @@
/**
* @jest-environment jsdom
*/
import { Application } from "stimulus";
import select_all_controller from "../../../app/webpacker/controllers/select_all_controller";
describe("SelectAllController", () => {
beforeAll(() => {
const application = Application.start();
application.register("select-all", select_all_controller);
});
beforeEach(() => {
document.body.innerHTML = `
<div data-controller="select-all">
<input
id="selectAllCheckbox"
type="checkbox"
data-action="change->select-all#toggleAll"
data-select-all-target="all">
<input
id="checkboxA"
type="checkbox"
data-action="change->select-all#toggleCheckbox"
data-select-all-target="checkbox">
<input
id="checkboxB"
type="checkbox"
data-action="change->select-all#toggleCheckbox"
data-select-all-target="checkbox">
</div>
`;
});
describe("#toggleAll", () => {
it("checks all checkboxes when it's checked and unchecks them all when unchecked", () => {
const selectAllCheckbox = document.getElementById("selectAllCheckbox");
const checkboxA = document.getElementById("checkboxA");
const checkboxB = document.getElementById("checkboxB");
expect(selectAllCheckbox.checked).toBe(false);
expect(checkboxA.checked).toBe(false);
expect(checkboxB.checked).toBe(false);
selectAllCheckbox.click()
expect(selectAllCheckbox.checked).toBe(true);
expect(checkboxA.checked).toBe(true);
expect(checkboxB.checked).toBe(true);
selectAllCheckbox.click()
expect(selectAllCheckbox.checked).toBe(false);
expect(checkboxA.checked).toBe(false);
expect(checkboxB.checked).toBe(false);
});
});
describe("#toggleCheckbox", () => {
it("checks the individual checkbox and checks the select all checkbox if all checkboxes are checked and vice versa", () => {
const selectAllCheckbox = document.getElementById("selectAllCheckbox");
const checkboxA = document.getElementById("checkboxA");
const checkboxB = document.getElementById("checkboxB");
checkboxA.click()
expect(selectAllCheckbox.checked).toBe(false);
expect(checkboxA.checked).toBe(true);
expect(checkboxB.checked).toBe(false);
checkboxB.click()
expect(selectAllCheckbox.checked).toBe(true);
expect(checkboxA.checked).toBe(true);
expect(checkboxB.checked).toBe(true);
checkboxB.click()
expect(selectAllCheckbox.checked).toBe(false);
expect(checkboxA.checked).toBe(true);
expect(checkboxB.checked).toBe(false);
});
});
describe("#connect", () => {
beforeEach(() => {
document.body.innerHTML = `
<div data-controller="select-all">
<input
id="selectAllCheckbox"
type="checkbox"
data-action="change->select-all#toggleAll"
data-select-all-target="all">
<input
id="checkboxA"
type="checkbox"
data-action="change->select-all#toggleCheckbox"
data-select-all-target="checkbox"
checked="checked">
<input
id="checkboxB"
type="checkbox"
data-action="change->select-all#toggleCheckbox"
data-select-all-target="checkbox"
checked="checked">
</div>
`;
});
it("checks the select all checkbox on page load if all checkboxes are checked", () => {
const selectAllCheckbox = document.getElementById("selectAllCheckbox");
expect(selectAllCheckbox.checked).toBe(true);
});
});
});

View File

@@ -276,6 +276,13 @@ describe Enterprise do
expect(Enterprise.ready_for_checkout).not_to include e
end
it "does not show enterprises wchich only have backend shipping methods" do
create(:shipping_method, distributors: [e],
display_on: Spree::ShippingMethod::DISPLAY_ON_OPTIONS[:back_end])
create(:payment_method, distributors: [e])
expect(Enterprise.ready_for_checkout).not_to include e
end
it "shows enterprises with available payment and shipping methods" do
create(:shipping_method, distributors: [e])
create(:payment_method, distributors: [e])
@@ -302,6 +309,13 @@ describe Enterprise do
expect(Enterprise.not_ready_for_checkout).to include e
end
it "shows enterprises which only have backend shipping methods" do
create(:shipping_method, distributors: [e],
display_on: Spree::ShippingMethod::DISPLAY_ON_OPTIONS[:back_end])
create(:payment_method, distributors: [e])
expect(Enterprise.not_ready_for_checkout).to include e
end
it "does not show enterprises with available payment and shipping methods" do
create(:shipping_method, distributors: [e])
create(:payment_method, distributors: [e])
@@ -328,6 +342,13 @@ describe Enterprise do
expect(e.reload).not_to be_ready_for_checkout
end
it "returns false for enterprises which only have backend shipping methods" do
create(:shipping_method, distributors: [e],
display_on: Spree::ShippingMethod::DISPLAY_ON_OPTIONS[:back_end])
create(:payment_method, distributors: [e])
expect(e.reload).not_to be_ready_for_checkout
end
it "returns true for enterprises with available payment and shipping methods" do
create(:shipping_method, distributors: [e])
create(:payment_method, distributors: [e])

View File

@@ -368,39 +368,66 @@ describe OrderCycle do
end
end
it "clones itself" do
coordinator = create(:enterprise);
oc = create(:simple_order_cycle,
coordinator_fees: [create(:enterprise_fee, enterprise: coordinator)],
preferred_product_selection_from_coordinator_inventory_only: true,
automatic_notifications: true, processed_at: Time.zone.now, mails_sent: true)
schedule = create(:schedule, order_cycles: [oc])
ex1 = create(:exchange, order_cycle: oc)
ex2 = create(:exchange, order_cycle: oc)
oc.clone!
describe "clone!" do
it "clones itself" do
coordinator = create(:enterprise);
oc = create(:simple_order_cycle,
coordinator_fees: [create(:enterprise_fee, enterprise: coordinator)],
preferred_product_selection_from_coordinator_inventory_only: true,
automatic_notifications: true, processed_at: Time.zone.now, mails_sent: true)
schedule = create(:schedule, order_cycles: [oc])
ex1 = create(:exchange, order_cycle: oc)
ex2 = create(:exchange, order_cycle: oc)
oc.clone!
occ = OrderCycle.last
expect(occ.name).to eq("COPY OF #{oc.name}")
expect(occ.orders_open_at).to be_nil
expect(occ.orders_close_at).to be_nil
expect(occ.coordinator).not_to be_nil
expect(occ.preferred_product_selection_from_coordinator_inventory_only).to be true
expect(occ.automatic_notifications).to eq(oc.automatic_notifications)
expect(occ.processed_at).to eq(nil)
expect(occ.mails_sent).to eq(nil)
expect(occ.coordinator).to eq(oc.coordinator)
occ = OrderCycle.last
expect(occ.name).to eq("COPY OF #{oc.name}")
expect(occ.orders_open_at).to be_nil
expect(occ.orders_close_at).to be_nil
expect(occ.coordinator).not_to be_nil
expect(occ.preferred_product_selection_from_coordinator_inventory_only).to be true
expect(occ.automatic_notifications).to eq(oc.automatic_notifications)
expect(occ.processed_at).to eq(nil)
expect(occ.mails_sent).to eq(nil)
expect(occ.coordinator).to eq(oc.coordinator)
expect(occ.coordinator_fee_ids).not_to be_empty
expect(occ.coordinator_fee_ids).to eq(oc.coordinator_fee_ids)
expect(occ.preferred_product_selection_from_coordinator_inventory_only).to eq(oc.preferred_product_selection_from_coordinator_inventory_only)
expect(occ.schedule_ids).not_to be_empty
expect(occ.schedule_ids).to eq(oc.schedule_ids)
expect(occ.coordinator_fee_ids).not_to be_empty
expect(occ.coordinator_fee_ids).to eq(oc.coordinator_fee_ids)
expect(occ.preferred_product_selection_from_coordinator_inventory_only).to eq(oc.preferred_product_selection_from_coordinator_inventory_only)
expect(occ.schedule_ids).not_to be_empty
expect(occ.schedule_ids).to eq(oc.schedule_ids)
# Check that the exchanges have been cloned.
original_exchange_attributes = oc.exchanges.map { |ex| core_exchange_attributes(ex) }
cloned_exchange_attributes = occ.exchanges.map { |ex| core_exchange_attributes(ex) }
# Check that the exchanges have been cloned.
original_exchange_attributes = oc.exchanges.map { |ex| core_exchange_attributes(ex) }
cloned_exchange_attributes = occ.exchanges.map { |ex| core_exchange_attributes(ex) }
expect(cloned_exchange_attributes).to match_array original_exchange_attributes
expect(cloned_exchange_attributes).to match_array original_exchange_attributes
end
context "when it has preferred shipping methods which can longer be applied validly
e.g. shipping method is backoffice only" do
it "only attaches the valid ones to the clone" do
distributor = create(:distributor_enterprise)
distributor_shipping_method_i = create(
:shipping_method,
distributors: [distributor]
).distributor_shipping_methods.first
distributor_shipping_method_ii = create(
:shipping_method,
distributors: [distributor],
display_on: Spree::ShippingMethod::DISPLAY_ON_OPTIONS[:back_end]
).distributor_shipping_methods.first
order_cycle = create(:distributor_order_cycle, distributors: [distributor])
order_cycle.selected_distributor_shipping_methods = [
distributor_shipping_method_i,
distributor_shipping_method_ii
]
cloned_order_cycle = order_cycle.clone!
expect(cloned_order_cycle.distributor_shipping_methods).to eq [distributor_shipping_method_i]
end
end
end
describe "finding recently closed order cycles" do
@@ -610,6 +637,112 @@ describe OrderCycle do
end
end
describe "#attachable_distributor_shipping_methods" do
it "includes distributor shipping methods from the distributors on the order cycle" do
shipping_method = create(:shipping_method)
oc = create(:simple_order_cycle, distributors: [shipping_method.distributors.first])
distributor_shipping_method = shipping_method.distributor_shipping_methods.first
expect(oc.attachable_distributor_shipping_methods).to eq([distributor_shipping_method])
end
it "does not include backoffice only distributor shipping methods" do
shipping_method = create(:shipping_method, display_on: "back_end")
enterprise = create(:enterprise, shipping_methods: [shipping_method])
oc = create(:simple_order_cycle, distributors: [enterprise])
expect(oc.attachable_distributor_shipping_methods).to be_empty
end
end
describe "#distributor_shipping_methods" do
let(:distributor) { create(:distributor_enterprise) }
it "returns all attachable distributor shipping methods if the order cycle is simple" do
oc = create(:sells_own_order_cycle, distributors: [distributor])
distributor_shipping_method = create(
:shipping_method,
distributors: [distributor]
).distributor_shipping_methods.first
expect(oc.distributor_shipping_methods).to eq [distributor_shipping_method]
end
context "distributor order cycle i.e. non-simple" do
let(:oc) { create(:distributor_order_cycle, distributors: [distributor]) }
it "returns all attachable distributor shipping methods if no distributor shipping methods
have been selected specifically" do
distributor_shipping_method = create(
:shipping_method,
distributors: [distributor]
).distributor_shipping_methods.first
expect(oc.selected_distributor_shipping_methods).to be_empty
expect(oc.distributor_shipping_methods).to eq [distributor_shipping_method]
end
it "returns selected distributor shipping methods if they have been specified" do
distributor_shipping_method_i = create(
:shipping_method,
distributors: [distributor]
).distributor_shipping_methods.first
distributor_shipping_method_ii = create(
:shipping_method,
distributors: [distributor]
).distributor_shipping_methods.first
oc.selected_distributor_shipping_methods << distributor_shipping_method_ii
expect(oc.distributor_shipping_methods).to eq [distributor_shipping_method_ii]
end
context "with multiple distributors" do
let(:other_distributor) { create(:distributor_enterprise) }
let(:oc) { create(:distributor_order_cycle, distributors: [distributor, other_distributor]) }
it "returns all attachable distributor shipping methods for a distributor if no distributor
shipping methods have been selected specifically for that distributor, even if
distributor shipping methods have been selected specifically for a different distributor
on the order cycle" do
distributor_shipping_method = create(
:shipping_method,
distributors: [distributor]
).distributor_shipping_methods.first
other_distributor_shipping_method_i = create(
:shipping_method,
distributors: [other_distributor]
).distributor_shipping_methods.first
other_distributor_shipping_method_ii = create(
:shipping_method,
distributors: [other_distributor]
).distributor_shipping_methods.first
oc.selected_distributor_shipping_methods << other_distributor_shipping_method_i
expect(oc.distributor_shipping_methods).to eq [
distributor_shipping_method,
other_distributor_shipping_method_i
]
end
end
end
end
describe "#simple?" do
it "returns true if the coordinator sells their own products i.e. shops" do
order_cycle = build(:simple_order_cycle, coordinator: build(:enterprise, sells: "own"))
expect(order_cycle).to be_simple
end
it "returns false if the coordinator can sell other people's products i.e. hubs" do
order_cycle = build(:simple_order_cycle, coordinator: build(:enterprise, sells: "any"))
expect(order_cycle).not_to be_simple
end
end
def core_exchange_attributes(exchange)
exterior_attribute_keys = %w(id order_cycle_id created_at updated_at)
exchange.attributes.

View File

@@ -143,6 +143,25 @@ module Spree
expect(shipping_method.errors[:name].first).to eq "can't be blank"
end
describe "#display_on" do
it "is valid when it's set to nil, an empty string or 'back_end'" do
shipping_method = build_stubbed(
:shipping_method,
shipping_categories: [Spree::ShippingCategory.new(name: 'Test')]
)
[nil, "", "back_end"].each do |display_on_option|
shipping_method.display_on = display_on_option
expect(shipping_method).to be_valid
end
end
it "is not valid when it's set to an unknown value" do
shipping_method = build_stubbed(:shipping_method, display_on: "front_end")
expect(shipping_method).not_to be_valid
expect(shipping_method.errors[:display_on]).to eq ["is not included in the list"]
end
end
context "shipping category" do
it "validates presence of at least one" do
shipping_method = build_stubbed(

View File

@@ -0,0 +1,193 @@
# frozen_string_literal: true
require 'spec_helper'
describe OrderAvailableShippingMethods do
context "when the order has no current_distributor" do
it "returns an empty array" do
order_cycle = create(:sells_own_order_cycle)
order = build(:order, distributor: nil, order_cycle: order_cycle)
expect(OrderAvailableShippingMethods.new(order).to_a).to eq []
end
end
it "does not return 'back office only' shipping method" do
distributor = create(:distributor_enterprise)
frontend_shipping_method = create(:shipping_method, distributors: [distributor])
backoffice_only_shipping_method = create(:shipping_method,
distributors: [distributor], display_on: 'back_end')
order_cycle = create(:sells_own_order_cycle)
order = build(:order, distributor: distributor, order_cycle: order_cycle)
available_shipping_methods = OrderAvailableShippingMethods.new(order).to_a
expect(available_shipping_methods).to eq [frontend_shipping_method]
end
context "when no tag rules are in effect" do
context "sells own order cycle i.e. simple" do
it "only returns the shipping methods which are available on the order cycle
and belong to the order distributor" do
distributor_i = create(:distributor_enterprise)
distributor_ii = create(:distributor_enterprise)
distributor_iii = create(:distributor_enterprise)
shipping_method_i = create(:shipping_method, distributors: [distributor_i])
shipping_method_ii = create(:shipping_method, distributors: [distributor_ii])
shipping_method_iii = create(:shipping_method, distributors: [distributor_iii])
order_cycle = create(:sells_own_order_cycle, distributors: [distributor_i, distributor_ii])
order = build(:order, distributor: distributor_i, order_cycle: order_cycle)
available_shipping_methods = OrderAvailableShippingMethods.new(order).to_a
expect(available_shipping_methods).to eq [shipping_method_i]
end
end
context "distributor order cycle i.e. not simple" do
it "only returns the shipping methods which are available on the order cycle
and belong to the order distributor" do
distributor_i = create(:distributor_enterprise, shipping_methods: [])
distributor_ii = create(:distributor_enterprise, shipping_methods: [])
shipping_method_i = create(:shipping_method, distributors: [distributor_i])
shipping_method_ii = create(:shipping_method, distributors: [distributor_i])
shipping_method_iii = create(:shipping_method, distributors: [distributor_ii])
shipping_method_iv = create(:shipping_method, distributors: [distributor_ii])
order_cycle = create(:distributor_order_cycle,
distributors: [distributor_i, distributor_ii])
order_cycle.selected_distributor_shipping_methods << [
distributor_i.distributor_shipping_methods.first,
distributor_ii.distributor_shipping_methods.first,
]
order = build(:order, distributor: distributor_i, order_cycle: order_cycle)
available_shipping_methods = OrderAvailableShippingMethods.new(order).to_a
expect(available_shipping_methods).to eq [shipping_method_i]
end
end
end
context "when FilterShippingMethods tag rules are in effect" do
let(:user) { create(:user) }
let(:distributor) { create(:distributor_enterprise) }
let(:other_distributor) { create(:distributor_enterprise) }
let!(:distributor_shipping_method) { create(:shipping_method, distributors: [distributor]) }
let!(:other_distributor_shipping_method) do
create(:shipping_method, distributors: [other_distributor])
end
let(:customer) { create(:customer, user: user, enterprise: distributor) }
let!(:tag_rule) {
create(:filter_shipping_methods_tag_rule,
enterprise: distributor,
preferred_customer_tags: "local",
preferred_shipping_method_tags: "local-delivery")
}
let!(:default_tag_rule) {
create(:filter_shipping_methods_tag_rule,
enterprise: distributor,
is_default: true,
preferred_shipping_method_tags: "local-delivery")
}
let!(:tagged_sm) { distributor_shipping_method }
let!(:untagged_sm) { other_distributor_shipping_method }
before do
tagged_sm.update_attribute(:tag_list, 'local-delivery')
distributor.shipping_methods = [tagged_sm, untagged_sm]
end
context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do
before {
tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'visible')
}
before {
default_tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility,
'hidden')
}
let(:order_cycle) { create(:sells_own_order_cycle) }
let(:order) { build(:order, distributor: distributor, order_cycle: order_cycle) }
context "when the customer is nil" do
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order).to_a }
it "applies default action (hide)" do
expect(available_shipping_methods).to include untagged_sm
expect(available_shipping_methods).to_not include tagged_sm
end
end
context "when a customer is present" do
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order, customer).to_a }
context "and the customer's tags match" do
before do
customer.update_attribute(:tag_list, 'local')
end
it "applies the action (show)" do
expect(available_shipping_methods).to include tagged_sm, untagged_sm
end
end
context "and the customer's tags don't match" do
before do
customer.update_attribute(:tag_list, 'something')
end
it "applies the default action (hide)" do
expect(available_shipping_methods).to include untagged_sm
expect(available_shipping_methods).to_not include tagged_sm
end
end
end
end
context "with a preferred visiblity of 'hidden', default visibility of 'visible'" do
before {
tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'hidden')
}
before {
default_tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility,
'visible')
}
let(:order_cycle) { create(:sells_own_order_cycle) }
let(:order) { build(:order, distributor: distributor, order_cycle: order_cycle) }
context "when the customer is nil" do
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order).to_a }
it "applies default action (show)" do
expect(available_shipping_methods).to include tagged_sm, untagged_sm
end
end
context "when a customer is present" do
let(:available_shipping_methods) { OrderAvailableShippingMethods.new(order, customer).to_a }
context "and the customer's tags match" do
before do
customer.update_attribute(:tag_list, 'local')
end
it "applies the action (hide)" do
expect(available_shipping_methods).to include untagged_sm
expect(available_shipping_methods).to_not include tagged_sm
end
end
context "and the customer's tags don't match" do
before do
customer.update_attribute(:tag_list, 'something')
end
it "applies the default action (show)" do
expect(available_shipping_methods).to include tagged_sm, untagged_sm
end
end
end
end
end
end

View File

@@ -55,6 +55,17 @@ describe OrderCycleForm do
end.to_not change{ order_cycle.reload.name }
end
end
context "when schedules are present but updating something other than the :schedule_ids" do
let(:params) { { name: "New Order Cycle Name" } }
before { create(:schedule, order_cycles: [order_cycle]) }
it "doesn't delete the schedules" do
expect(order_cycle.schedules).to be_present
form.save
expect(order_cycle.schedules).to be_present
end
end
end
end
@@ -119,34 +130,128 @@ describe OrderCycleForm do
end
end
describe "updating exchanges" do
let(:user) { instance_double(Spree::User) }
let(:order_cycle) { create(:simple_order_cycle) }
let(:form_applicator_mock) { instance_double(OpenFoodNetwork::OrderCycleFormApplicator) }
let(:form) { OrderCycleForm.new(order_cycle, params, user) }
context "distributor order cycle" do
let(:order_cycle) { create(:distributor_order_cycle) }
let(:distributor) { order_cycle.coordinator }
let(:supplier) { create(:supplier_enterprise) }
let(:user) { distributor.owner }
let(:shipping_method) { create(:shipping_method, distributors: [distributor]) }
let(:distributor_shipping_method) { shipping_method.distributor_shipping_methods.first }
let(:variant) { create(:variant, product: create(:product, supplier: supplier)) }
let(:params) { { name: 'Some new name' } }
before do
allow(OpenFoodNetwork::OrderCycleFormApplicator).to receive(:new) { form_applicator_mock }
allow(form_applicator_mock).to receive(:go!)
let(:form) { OrderCycleForm.new(order_cycle, params, user) }
let(:outgoing_exchange_params) do
{
enterprise_id: distributor.id,
incoming: false,
active: true,
variants: { variant.id => true },
pickup_time: "Saturday morning",
enterprise_fee_ids: []
}
end
context "when exchange params are provided" do
let(:exchange_params) { { incoming_exchanges: [], outgoing_exchanges: [] } }
before { params.merge!(exchange_params) }
it "runs the OrderCycleFormApplicator, and saves other changes" do
context "basic update i.e. without exchanges or shipping methods" do
it do
expect(form.save).to be true
expect(form_applicator_mock).to have_received(:go!)
expect(order_cycle.name).to eq 'Some new name'
end
end
context "when no exchange params are provided" do
it "does not run the OrderCycleFormApplicator, but saves other changes" do
context "updating basics, incoming exchanges, outcoming exchanges
and shipping methods simultaneously" do
before do
params.merge!(
incoming_exchanges: [{
enterprise_id: supplier.id,
incoming: true,
active: true,
variants: { variant.id => true },
receival_instructions: "Friday evening",
enterprise_fee_ids: []
}],
outgoing_exchanges: [outgoing_exchange_params],
selected_distributor_shipping_method_ids: [distributor_shipping_method.id]
)
end
it "saves everything i.e. the basics, incoming and outgoing exchanges and shipping methods" do
expect(form.save).to be true
expect(form_applicator_mock).to_not have_received(:go!)
expect(order_cycle.name).to eq 'Some new name'
expect(order_cycle.cached_incoming_exchanges.count).to eq 1
expect(order_cycle.cached_outgoing_exchanges.count).to eq 1
expect(order_cycle.distributor_shipping_methods).to eq [distributor_shipping_method]
end
end
context "updating outgoing exchanges and shipping methods simultaneously but the shipping
method doesn't belong to the new or any existing order cycle distributor" do
let(:other_distributor_shipping_method) do
create(
:shipping_method,
distributors: [create(:distributor_enterprise)]
).distributor_shipping_methods.first
end
before do
params.merge!(
outgoing_exchanges: [outgoing_exchange_params],
selected_distributor_shipping_method_ids: [other_distributor_shipping_method.id]
)
end
it "saves the outgoing exchange but ignores the shipping method" do
expect(form.save).to be true
expect(order_cycle.distributors).to eq [distributor]
expect(order_cycle.distributor_shipping_methods).to be_empty
end
end
context "updating shipping methods" do
context "and it's valid" do
it "saves the changes" do
distributor = create(:distributor_enterprise)
distributor_shipping_method = create(
:shipping_method,
distributors: [distributor]
).distributor_shipping_methods.first
order_cycle = create(:distributor_order_cycle, distributors: [distributor])
form = OrderCycleForm.new(
order_cycle,
{ selected_distributor_shipping_method_ids: [distributor_shipping_method.id] },
order_cycle.coordinator
)
expect(form.save).to be true
expect(order_cycle.distributor_shipping_methods).to eq [distributor_shipping_method]
end
end
context "with a shipping method which doesn't belong to any distributor on the order cycle" do
it "ignores it" do
distributor_i = create(:distributor_enterprise)
distributor_ii = create(:distributor_enterprise)
distributor_shipping_method_i = create(
:shipping_method,
distributors: [distributor_i]
).distributor_shipping_methods.first
distributor_shipping_method_ii = create(
:shipping_method,
distributors: [distributor_ii]
).distributor_shipping_methods.first
order_cycle = create(:distributor_order_cycle,
distributors: [distributor_i])
form = OrderCycleForm.new(
order_cycle,
{ selected_distributor_shipping_method_ids: [distributor_shipping_method_ii.id] },
order_cycle.coordinator
)
expect(form.save).to be true
expect(order_cycle.distributor_shipping_methods).to eq [distributor_shipping_method_i]
end
end
end
end

View File

@@ -0,0 +1,67 @@
# frozen_string_literal: true
require 'spec_helper'
describe Shop::OrderCyclesList do
describe ".active_for" do
let(:customer) { nil }
context "when the order cycle is open and the distributor belongs to the order cycle" do
context "and the distributor is ready for checkout" do
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) }
it "returns the order cycle" do
open_order_cycle = create(:open_order_cycle, distributors: [distributor])
expect(Shop::OrderCyclesList.active_for(distributor, customer)).to eq [open_order_cycle]
end
end
context "and the distributor is not ready for checkout" do
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: false) }
it "returns the order cycle" do
open_order_cycle = create(:open_order_cycle, distributors: [distributor])
expect(Shop::OrderCyclesList.active_for(distributor, customer)).to eq [open_order_cycle]
end
end
end
it "doesn't returns closed order cycles or ones belonging to other distributors" do
distributor = create(:distributor_enterprise)
closed_order_cycle = create(:closed_order_cycle, distributors: [distributor])
other_distributor_order_cycle = create(:open_order_cycle)
expect(Shop::OrderCyclesList.active_for(distributor, customer)).to be_empty
end
end
describe ".ready_for_checkout_for" do
let(:customer) { nil }
context "when the order cycle is open and belongs to the distributor" do
context "and the distributor is ready for checkout" do
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) }
it "returns the order cycle" do
open_order_cycle = create(:open_order_cycle, distributors: [distributor])
expect(Shop::OrderCyclesList.ready_for_checkout_for(distributor, customer)).to eq [
open_order_cycle
]
end
end
context "but the distributor not is ready for checkout" do
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: false) }
it "doesn't return the order cycle" do
open_order_cycle = create(:open_order_cycle, distributors: [distributor])
expect(Shop::OrderCyclesList.ready_for_checkout_for(distributor, customer)).to be_empty
end
end
end
end
end

View File

@@ -13,16 +13,27 @@ describe '
let(:order_cycle_opening_time) { 1.day.from_now(Time.zone.now) }
let(:order_cycle_closing_time) { 2.days.from_now(Time.zone.now) }
it "creating an order cycle with full interface", js: true do
# Given coordinating, supplying and distributing enterprises with some products with variants
coordinator = create(:distributor_enterprise, name: 'My coordinator')
supplier = create(:supplier_enterprise, name: 'My supplier')
product = create(:product, supplier: supplier)
v1 = create(:variant, product: product)
v2 = create(:variant, product: product)
distributor = create(:distributor_enterprise, name: 'My distributor',
with_payment_and_shipping: true)
# Given coordinating, supplying and distributing enterprises with some products with variants
let!(:coordinator) { create(:distributor_enterprise, name: 'My coordinator') }
let!(:supplier) { create(:supplier_enterprise, name: 'My supplier') }
let!(:product) { create(:product, supplier: supplier) }
let!(:v1) { create(:variant, product: product) }
let!(:v2) { create(:variant, product: product) }
let!(:distributor) {
create(:distributor_enterprise, name: 'My distributor', with_payment_and_shipping: true)
}
let!(:shipping_method_i) { distributor.shipping_methods.first }
let!(:shipping_method_ii) { create(:shipping_method, distributors: [distributor]) }
let(:oc) { OrderCycle.last }
# And some enterprise fees
let!(:supplier_fee) { create(:enterprise_fee, enterprise: supplier, name: 'Supplier fee') }
let!(:coordinator_fee) { create(:enterprise_fee, enterprise: coordinator, name: 'Coord fee') }
let!(:distributor_fee) do
create(:enterprise_fee, enterprise: distributor, name: 'Distributor fee')
end
before do
# Relationships required for interface to work
create(:enterprise_relationship, parent: supplier, child: coordinator,
permissions_list: [:add_to_order_cycle])
@@ -31,12 +42,12 @@ describe '
create(:enterprise_relationship, parent: supplier, child: distributor,
permissions_list: [:add_to_order_cycle])
# And some enterprise fees
supplier_fee = create(:enterprise_fee, enterprise: supplier, name: 'Supplier fee')
coordinator_fee = create(:enterprise_fee, enterprise: coordinator, name: 'Coord fee')
distributor_fee = create(:enterprise_fee, enterprise: distributor, name: 'Distributor fee')
shipping_method_i.update!(name: "Pickup - always available")
shipping_method_ii.update!(name: "Delivery - sometimes available")
end
# When I go to the new order cycle page
it "creating an order cycle with full interface", js: true do
## CREATE
login_as_admin_and_visit admin_order_cycles_path
click_link 'New Order Cycle'
@@ -44,46 +55,53 @@ describe '
select2_select 'My coordinator', from: 'coordinator_id'
click_button "Continue >"
# I cannot save before filling in the required fields
expect(page).to have_button("Create", disabled: true)
fill_in_order_cycle_name
select_opening_and_closing_times
# The Create button is enabled once Name is entered
fill_in 'order_cycle_name', with: 'Plums & Avos'
expect(page).to have_button("Create", disabled: false)
# If I fill in the basic fields
find('#order_cycle_orders_open_at').click
# select date
select_date_from_datepicker Time.zone.at(order_cycle_opening_time)
# select time
within(".flatpickr-calendar.open .flatpickr-time") do
find('.flatpickr-hour').set('%02d' % order_cycle_opening_time.hour)
find('.flatpickr-minute').set('%02d' % order_cycle_opening_time.min)
end
# hide the datetimepicker
find("body").send_keys(:escape)
find('#order_cycle_orders_close_at').click
# select date
select_date_from_datepicker Time.zone.at(order_cycle_closing_time)
# select time
within(".flatpickr-calendar.open .flatpickr-time") do
find('.flatpickr-hour').set('%02d' % order_cycle_closing_time.hour)
find('.flatpickr-minute').set('%02d' % order_cycle_closing_time.min)
end
# hide the datetimepicker
find("body").send_keys(:escape)
# And I add a coordinator fee
click_button 'Add coordinator fee'
select 'Coord fee', from: 'order_cycle_coordinator_fee_0_id'
click_button 'Create'
expect(page).to have_content 'Your order cycle has been created.'
# I should not be able to add a blank supplier
expect(page).to have_select 'new_supplier_id', selected: ''
expect(page).to have_button 'Add supplier', disabled: true
## UPDATE
add_supplier_with_fees
add_distributor_with_fees
select_distributor_shipping_methods
expect_all_data_saved
end
def fill_in_order_cycle_name
# I cannot save before filling in the required fields
expect(page).to have_button("Create", disabled: true)
# The Create button is enabled once Name is entered
fill_in 'order_cycle_name', with: "Plums & Avos"
expect(page).to have_button("Create", disabled: false)
end
def select_opening_and_closing_times
select_time("#order_cycle_orders_open_at", order_cycle_opening_time)
select_time("#order_cycle_orders_close_at", order_cycle_closing_time)
end
def select_time(selector, time)
# If I fill in the basic fields
find(selector).click
# select date
select_date_from_datepicker Time.zone.at(time)
# select time
within(".flatpickr-calendar.open .flatpickr-time") do
find('.flatpickr-hour').set('%02d' % time.hour)
find('.flatpickr-minute').set('%02d' % time.min)
end
# hide the datetimepicker
find("body").send_keys(:escape)
end
def add_supplier_with_fees
expect_not_able_to_add_blank_supplier
# And I add a supplier and some products
select 'My supplier', from: 'new_supplier_id'
@@ -93,10 +111,7 @@ describe '
check "order_cycle_incoming_exchange_0_variants_#{v1.id}"
check "order_cycle_incoming_exchange_0_variants_#{v2.id}"
# I should not be able to re-add the supplier
expect(page).not_to have_select 'new_supplier_id', with_options: ['My supplier']
expect(page).to have_button 'Add supplier', disabled: true
expect(page.all("td.supplier_name").map(&:text)).to eq(['My supplier'])
expect_not_able_to_readd_supplier('My supplier')
# And I add a supplier fee
within("tr.supplier-#{supplier.id}") { click_button 'Add fee' }
@@ -105,11 +120,25 @@ describe '
from: 'order_cycle_incoming_exchange_0_enterprise_fees_0_enterprise_fee_id'
click_button 'Save and Next'
end
def expect_not_able_to_add_blank_supplier
expect(page).to have_select 'new_supplier_id', selected: ''
expect(page).to have_button 'Add supplier', disabled: true
end
def expect_not_able_to_readd_supplier(supplier_name)
expect(page).not_to have_select 'new_supplier_id', with_options: [supplier_name]
expect(page).to have_button 'Add supplier', disabled: true
expect(page.all("td.supplier_name").map(&:text)).to eq([supplier_name])
end
def add_distributor_with_fees
# And I add a distributor with the same products
select 'My distributor', from: 'new_distributor_id'
click_button 'Add distributor'
expect(page).to have_field "order_cycle_outgoing_exchange_0_pickup_time"
fill_in 'order_cycle_outgoing_exchange_0_pickup_time', with: 'pickup time'
fill_in 'order_cycle_outgoing_exchange_0_pickup_instructions', with: 'pickup instructions'
@@ -129,37 +158,89 @@ describe '
select 'Distributor fee',
from: 'order_cycle_outgoing_exchange_0_enterprise_fees_0_enterprise_fee_id'
click_button 'Save and Back to List'
click_button 'Save and Next'
end
oc = OrderCycle.last
def select_distributor_shipping_methods
expect(page).to have_checked_field "Select all"
expect(page).to have_checked_field "Pickup - always available"
expect(page).to have_checked_field "Delivery - sometimes available"
uncheck "Delivery - sometimes available"
expect(page).to have_unchecked_field "Select all"
expect_checking_select_all_shipping_methods_works
expect_unchecking_select_all_shipping_methods_works
# Our final selection:
check "Pickup - always available"
click_button 'Save and Back to List'
end
def expect_checking_select_all_shipping_methods_works
# Now test that the "Select all" input is doing what it's supposed to:
check "Select all"
expect(page).to have_checked_field "Pickup - always available"
expect(page).to have_checked_field "Delivery - sometimes available"
end
def expect_unchecking_select_all_shipping_methods_works
uncheck "Select all"
expect(page).to have_unchecked_field "Pickup - always available"
expect(page).to have_unchecked_field "Delivery - sometimes available"
end
def expect_all_data_saved
toggle_columns "Producers", "Shops"
expect(page).to have_input "oc#{oc.id}[name]", value: "Plums & Avos"
expect(page).to have_content "My coordinator"
expect_opening_and_closing_times_saved
expect(page).to have_selector 'td.producers', text: 'My supplier'
expect(page).to have_selector 'td.shops', text: 'My distributor'
expect_fees_saved
expect_variants_saved
expect_receival_instructions_saved
expect_pickup_time_and_instructions_saved
expect_distributor_shipping_methods_saved
end
def expect_opening_and_closing_times_saved
expect(page).to have_input "oc#{oc.id}[orders_open_at]",
value: Time.zone.at(order_cycle_opening_time), visible: false
expect(page).to have_input "oc#{oc.id}[orders_close_at]",
value: Time.zone.at(order_cycle_closing_time), visible: false
expect(page).to have_content "My coordinator"
end
expect(page).to have_selector 'td.producers', text: 'My supplier'
expect(page).to have_selector 'td.shops', text: 'My distributor'
# And it should have some fees
def expect_fees_saved
expect(oc.exchanges.incoming.first.enterprise_fees).to eq([supplier_fee])
expect(oc.coordinator_fees).to eq([coordinator_fee])
expect(oc.exchanges.outgoing.first.enterprise_fees).to eq([distributor_fee])
end
# And it should have some variants selected
def expect_variants_saved
expect(oc.exchanges.first.variants.count).to eq(2)
expect(oc.exchanges.last.variants.count).to eq(2)
end
# And my receival and pickup time and instructions should have been saved
def expect_receival_instructions_saved
exchange = oc.exchanges.incoming.first
expect(exchange.receival_instructions).to eq('receival instructions')
end
def expect_pickup_time_and_instructions_saved
exchange = oc.exchanges.outgoing.first
expect(exchange.pickup_time).to eq('pickup time')
expect(exchange.pickup_instructions).to eq('pickup instructions')
expect(exchange.tag_list).to eq(['wholesale'])
end
def expect_distributor_shipping_methods_saved
expect(oc.distributor_shipping_methods).to eq(shipping_method_i.distributor_shipping_methods)
end
end

View File

@@ -260,6 +260,11 @@ describe '
find(:css, "tags-input .tags input").set "wholesale\n"
end
click_button 'Save and Next'
expect_shipping_methods_to_be_checked_for(distributor_managed)
expect_shipping_methods_to_be_checked_for(distributor_permitted)
click_button 'Save and Back to List'
order_cycle = OrderCycle.find_by(name: 'My order cycle')
expect(page).to have_input "oc#{order_cycle.id}[name]", value: order_cycle.name
@@ -270,6 +275,9 @@ describe '
expect(order_cycle.schedules).to eq([schedule])
exchange = order_cycle.exchanges.outgoing.to_enterprise(distributor_managed).first
expect(exchange.tag_list).to eq(["wholesale"])
expect(order_cycle.distributor_shipping_methods).to match_array(
order_cycle.attachable_distributor_shipping_methods
)
end
context "editing an order cycle" do
@@ -308,6 +316,7 @@ describe '
# And I remove all outgoing exchanges
page.find("tr.distributor-#{distributor_managed.id} a.remove-exchange").click
page.find("tr.distributor-#{distributor_permitted.id} a.remove-exchange").click
click_button 'Save and Next'
click_button 'Save and Back to List'
expect(page).to have_input "oc#{oc.id}[name]", value: oc.name
@@ -706,6 +715,14 @@ describe '
private
def expect_shipping_methods_to_be_checked_for(distributor)
distributor.distributor_shipping_method_ids.each do |distributor_shipping_method_id|
expect(page).to have_checked_field(
"order_cycle_selected_distributor_shipping_method_ids_#{distributor_shipping_method_id}"
)
end
end
def wait_for_edit_form_to_load_order_cycle(order_cycle)
expect(page).to have_field "order_cycle_name", with: order_cycle.name
end