diff --git a/app/controllers/api/v0/orders_controller.rb b/app/controllers/api/v0/orders_controller.rb index 2ffb4304b3..d3fe7070c4 100644 --- a/app/controllers/api/v0/orders_controller.rb +++ b/app/controllers/api/v0/orders_controller.rb @@ -67,7 +67,8 @@ module Api def serialized_orders(orders) ActiveModel::ArraySerializer.new( orders, - each_serializer: Api::Admin::OrderSerializer + each_serializer: Api::Admin::OrderSerializer, + current_user: current_api_user ) end diff --git a/app/controllers/spree/admin/variants_controller.rb b/app/controllers/spree/admin/variants_controller.rb index 765a011dff..deda271867 100644 --- a/app/controllers/spree/admin/variants_controller.rb +++ b/app/controllers/spree/admin/variants_controller.rb @@ -64,7 +64,10 @@ module Spree end def search - scoper = OpenFoodNetwork::ScopeVariantsForSearch.new(variant_search_params) + scoper = OpenFoodNetwork::ScopeVariantsForSearch.new( + variant_search_params, + spree_current_user + ) @variants = scoper.search render json: @variants, each_serializer: ::Api::Admin::VariantSerializer end diff --git a/app/helpers/spree/admin/orders_helper.rb b/app/helpers/spree/admin/orders_helper.rb index fc4f21baa8..e80b6d9d13 100644 --- a/app/helpers/spree/admin/orders_helper.rb +++ b/app/helpers/spree/admin/orders_helper.rb @@ -142,6 +142,32 @@ module Spree end number_field_tag :quantity, manifest_item.quantity, html_options end + + def prepare_shipment_manifest(shipment) + manifest = shipment.manifest + + if filter_by_supplier?(shipment.order) + supplier_ids = spree_current_user.enterprises.ids + manifest.select! { |mi| supplier_ids.include?(mi.variant.supplier_id) } + end + + manifest + end + + def filter_by_supplier?(order) + order.distributor&.enable_producers_to_edit_orders && + spree_current_user.can_manage_line_items_in_orders_only? + end + + def display_value_for_producer(order, value) + return value unless filter_by_supplier?(order) + + if order.distributor&.show_customer_names_to_suppliers + value + else + t("admin.reports.hidden_field") + end + end end end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index b369712fb1..049eefef65 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -381,6 +381,10 @@ class Enterprise < ApplicationRecord sells == 'any' end + def is_producer_only + is_primary_producer && sells == 'none' + end + # Simplify enterprise categories for frontend logic and icons, and maybe other things. def category # Make this crazy logic human readable so we can argue about it sanely. diff --git a/app/models/spree/ability.rb b/app/models/spree/ability.rb index 3bce37ff4d..08e27d60d7 100644 --- a/app/models/spree/ability.rb +++ b/app/models/spree/ability.rb @@ -47,7 +47,11 @@ module Spree add_group_management_abilities user if can_manage_groups? user add_product_management_abilities user if can_manage_products? user add_order_cycle_management_abilities user if can_manage_order_cycles? user - add_order_management_abilities user if can_manage_orders? user + if can_manage_orders? user + add_order_management_abilities user + elsif can_manage_line_items_in_orders? user + add_manage_line_items_abilities user + end add_relationship_management_abilities user if can_manage_relationships? user end @@ -81,7 +85,13 @@ module Spree # Users can manage orders if they have a sells own/any enterprise. def can_manage_orders?(user) - ( user.enterprises.map(&:sells) & %w(own any) ).any? + user.can_manage_orders? + end + + # Users can manage line items in orders if they have producer enterprise and + # any of order distributors allow them to edit their orders. + def can_manage_line_items_in_orders?(user) + user.can_manage_line_items_in_orders? end def can_manage_relationships?(user) @@ -343,6 +353,28 @@ module Spree end end + def can_edit_order(order, user) + return unless order.distributor&.enable_producers_to_edit_orders + + order.variants.any? { |variant| user.enterprises.ids.include?(variant.supplier_id) } + end + + def add_manage_line_items_abilities(user) + can [:admin, :read, :index, :edit, :update, :bulk_management], Spree::Order do |order| + can_edit_order(order, user) + end + can [:admin, :index, :create, :destroy, :update], Spree::LineItem do |item| + can_edit_order(item.order, user) + end + can [:index, :create, :add, :read, :edit, :update], Spree::Shipment do |shipment| + can_edit_order(shipment.order, user) + end + can [:admin, :index], OrderCycle do |order_cycle| + can_edit_order(order_cycle.order, user) + end + can [:visible], Enterprise + end + def add_relationship_management_abilities(user) can [:admin, :index, :create], EnterpriseRelationship can [:destroy], EnterpriseRelationship do |enterprise_relationship| diff --git a/app/models/spree/line_item.rb b/app/models/spree/line_item.rb index b2913e331f..2d9444e18d 100644 --- a/app/models/spree/line_item.rb +++ b/app/models/spree/line_item.rb @@ -108,6 +108,13 @@ module Spree where(spree_adjustments: { id: nil }) } + scope :editable_by_producers, ->(enterprises_ids) { + joins(:variant, order: :distributor).where( + distributor: { enable_producers_to_edit_orders: true }, + spree_variants: { supplier_id: enterprises_ids } + ) + } + def copy_price return unless variant diff --git a/app/models/spree/order.rb b/app/models/spree/order.rb index 9619faeb27..0199156469 100644 --- a/app/models/spree/order.rb +++ b/app/models/spree/order.rb @@ -140,6 +140,15 @@ module Spree end } + scope :editable_by_producers, ->(enterprises) { + joins( + :distributor, line_items: :supplier + ).where( + supplier: { id: enterprises }, + distributor: { enable_producers_to_edit_orders: true } + ) + } + scope :distributed_by_user, lambda { |user| if user.admin? where(nil) diff --git a/app/models/spree/user.rb b/app/models/spree/user.rb index 0bbe9d60f2..94ad89ab15 100644 --- a/app/models/spree/user.rb +++ b/app/models/spree/user.rb @@ -147,6 +147,25 @@ module Spree Enterprise.joins(:connected_apps).merge(ConnectedApps::AffiliateSalesData.ready) end + # Users can manage orders if they have a sells own/any enterprise. or is admin + def can_manage_orders? + @can_manage_orders ||= (enterprises.pluck(:sells).intersect?(%w(own any)) or admin?) + end + + # Users can manage line items in orders if they have producer enterprise and + # any of order distributors allow them to edit their orders. + def can_manage_line_items_in_orders? + return @can_manage_line_items_in_orders if defined? @can_manage_line_items_in_orders + + @can_manage_line_items_in_orders = + enterprises.any?(&:is_producer_only) && + Spree::Order.editable_by_producers(enterprises).exists? + end + + def can_manage_line_items_in_orders_only? + !can_manage_orders? && can_manage_line_items_in_orders? + end + protected def password_required? diff --git a/app/serializers/api/admin/order_serializer.rb b/app/serializers/api/admin/order_serializer.rb index db3a72f0b5..b1b4740786 100644 --- a/app/serializers/api/admin/order_serializer.rb +++ b/app/serializers/api/admin/order_serializer.rb @@ -15,8 +15,14 @@ module Api has_one :distributor, serializer: Api::Admin::IdSerializer has_one :order_cycle, serializer: Api::Admin::IdSerializer + def full_name_for_sorting + value = [last_name, first_name].compact_blank.join(", ") + display_value_for_producer(object, value) + end + def full_name - object.billing_address.nil? ? "" : ( object.billing_address.full_name || "" ) + value = object.billing_address.nil? ? "" : ( object.billing_address.full_name || "" ) + display_value_for_producer(object, value) end def first_name @@ -65,11 +71,12 @@ module Api end def email - object.email || "" + display_value_for_producer(object, object.email || "") end def phone - object.billing_address.nil? ? "a" : ( object.billing_address.phone || "" ) + value = object.billing_address.nil? ? "a" : ( object.billing_address.phone || "" ) + display_value_for_producer(object, value) end def created_at @@ -93,6 +100,19 @@ module Api def spree_routes_helper Spree::Core::Engine.routes.url_helpers end + + def display_value_for_producer(order, value) + filter_by_supplier = + order.distributor&.enable_producers_to_edit_orders && + options[:current_user]&.can_manage_line_items_in_orders_only? + return value unless filter_by_supplier + + if order.distributor&.show_customer_names_to_suppliers + value + else + I18n.t("admin.reports.hidden_field") + end + end end end end diff --git a/app/services/permissions/order.rb b/app/services/permissions/order.rb index d4c2f4b0ea..29d63b5a2f 100644 --- a/app/services/permissions/order.rb +++ b/app/services/permissions/order.rb @@ -23,9 +23,16 @@ module Permissions # Any orders that the user can edit def editable_orders - orders = Spree::Order. - where(managed_orders_where_values. - or(coordinated_orders_where_values)) + orders = if @user.can_manage_line_items_in_orders_only? + Spree::Order.joins(:distributor).where( + id: produced_orders.select(:id), + distributor: { enable_producers_to_edit_orders: true } + ) + else + Spree::Order.where( + managed_orders_where_values.or(coordinated_orders_where_values) + ) + end filtered_orders(orders) end @@ -36,7 +43,13 @@ module Permissions # Any line items that I can edit def editable_line_items - Spree::LineItem.where(order_id: editable_orders.select(:id)) + if @user.can_manage_line_items_in_orders_only? + Spree::LineItem.editable_by_producers( + @permissions.managed_enterprises.select("enterprises.id") + ) + else + Spree::LineItem.where(order_id: editable_orders.select(:id)) + end end private @@ -79,6 +92,13 @@ module Permissions reduce(:and) end + def produced_orders + Spree::Order.with_line_items_variants_and_products_outer. + where( + spree_variants: { supplier_id: @permissions.managed_enterprises.select("enterprises.id") } + ) + end + def produced_orders_where_values Spree::Order.with_line_items_variants_and_products_outer. where( diff --git a/app/services/permitted_attributes/enterprise.rb b/app/services/permitted_attributes/enterprise.rb index c25c82a585..5d780bfea3 100644 --- a/app/services/permitted_attributes/enterprise.rb +++ b/app/services/permitted_attributes/enterprise.rb @@ -39,6 +39,7 @@ module PermittedAttributes :preferred_product_low_stock_display, :hide_ofn_navigation, :white_label_logo, :white_label_logo_link, :hide_groups_tab, :external_billing_id, + :enable_producers_to_edit_orders ] end end diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml index 33f4a5f792..ce36dc1c08 100644 --- a/app/views/admin/enterprises/form/_shop_preferences.html.haml +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -123,3 +123,15 @@ .five.columns.omega = f.radio_button :show_customer_contacts_to_suppliers, false = f.label :show_customer_contacts_to_suppliers, t('.customer_contacts_false'), value: :false + +.row + .three.columns.alpha + %label= t('.producers_to_edit_orders') + %div{'ofn-with-tip' => t('.producers_to_edit_orders_tip')} + %a= t 'admin.whats_this' + .three.columns + = radio_button :enterprise, :enable_producers_to_edit_orders, true + = label :enterprise_enable_producers_to_edit_orders, t('.producers_edit_orders_true'), value: :true + .five.columns.omega + = radio_button :enterprise, :enable_producers_to_edit_orders, false + = label :enterprise_enable_producers_to_edit_orders, t('.producers_edit_orders_false'), value: :false diff --git a/app/views/spree/admin/orders/_form.html.haml b/app/views/spree/admin/orders/_form.html.haml index 79b86cbbfe..4f1866f330 100644 --- a/app/views/spree/admin/orders/_form.html.haml +++ b/app/views/spree/admin/orders/_form.html.haml @@ -8,23 +8,24 @@ - if @order.shipments.any? = render :partial => "spree/admin/orders/shipment", :collection => @order.shipments, :locals => { :order => @order } - - if @order.line_items.exists? - = render partial: "spree/admin/orders/note", locals: { order: @order } + - if spree_current_user.can_manage_orders? + - if @order.line_items.exists? + = render partial: "spree/admin/orders/note", locals: { order: @order } - = render :partial => "spree/admin/orders/_form/adjustments", :locals => { :adjustments => @order.line_item_adjustments, :title => t(".line_item_adjustments")} - = render :partial => "spree/admin/orders/_form/adjustments", :locals => { :adjustments => order_adjustments_for_display(@order), :title => t(".order_adjustments")} + = render :partial => "spree/admin/orders/_form/adjustments", :locals => { :adjustments => @order.line_item_adjustments, :title => t(".line_item_adjustments")} + = render :partial => "spree/admin/orders/_form/adjustments", :locals => { :adjustments => order_adjustments_for_display(@order), :title => t(".order_adjustments")} - - if @order.line_items.exists? - %fieldset#order-total.no-border-bottom.order-details-total - %legend{ align: 'center' }= t(".order_total") - %span.order-total= @order.display_total + - if @order.line_items.exists? + %fieldset#order-total.no-border-bottom.order-details-total + %legend{ align: 'center' }= t(".order_total") + %span.order-total= @order.display_total - = form_for @order, url: spree.admin_order_url(@order), method: :put do |f| - = render partial: 'spree/admin/orders/_form/distribution_fields' + = form_for @order, url: spree.admin_order_url(@order), method: :put do |f| + = render partial: 'spree/admin/orders/_form/distribution_fields' - .filter-actions.actions{"ng-show" => "distributionChosen()"} - = button t(:update_and_recalculate_fees), 'icon-refresh' - = link_to_with_icon 'button icon-arrow-left', t(:back), spree.admin_orders_url + .filter-actions.actions{"ng-show" => "distributionChosen()"} + = button t(:update_and_recalculate_fees), 'icon-refresh' + = link_to_with_icon 'button icon-arrow-left', t(:back), spree.admin_orders_url = javascript_tag do var order_number = '#{@order.number}'; diff --git a/app/views/spree/admin/orders/_shipment.html.haml b/app/views/spree/admin/orders/_shipment.html.haml index a992fe38fd..6ed1042554 100644 --- a/app/views/spree/admin/orders/_shipment.html.haml +++ b/app/views/spree/admin/orders/_shipment.html.haml @@ -62,7 +62,7 @@ - if shipment.fee_adjustment.present? && shipment.can_modify? %td.actions - - if can? :update, shipment + - if can? :update, shipment.shipping_method = link_to '', '', :class => 'edit-method icon_link icon-edit no-text with-tip', :data => { :action => 'edit' }, :title => Spree.t('edit') %tr.edit-tracking.hidden.total @@ -86,7 +86,7 @@ = Spree.t(:no_tracking_present) %td.actions - - if can?(:update, shipment) && shipment.can_modify? + - if spree_current_user.can_manage_orders? && can?(:update, shipment) && shipment.can_modify? = link_to '', '', :class => 'edit-tracking icon_link icon-edit no-text with-tip', :data => { :action => 'edit' }, :title => Spree.t('edit') - if shipment.tracking.present? = link_to '', '', :class => 'delete-tracking icon_link icon-trash no-text with-tip', :data => { 'shipment-number' => shipment.number, :action => 'remove' }, :title => Spree.t('delete') diff --git a/app/views/spree/admin/orders/_shipment_manifest.html.haml b/app/views/spree/admin/orders/_shipment_manifest.html.haml index fd95d95dbc..d08cf371c9 100644 --- a/app/views/spree/admin/orders/_shipment_manifest.html.haml +++ b/app/views/spree/admin/orders/_shipment_manifest.html.haml @@ -1,4 +1,4 @@ -- shipment.manifest.each do |item| +- prepare_shipment_manifest(shipment).each do |item| - line_item = order.find_line_item_by_variant(item.variant) - if line_item.present? diff --git a/app/views/spree/admin/orders/_table_row.html.haml b/app/views/spree/admin/orders/_table_row.html.haml index fd202f76d7..6705719168 100644 --- a/app/views/spree/admin/orders/_table_row.html.haml +++ b/app/views/spree/admin/orders/_table_row.html.haml @@ -34,10 +34,11 @@ %span.state{ class: order.shipment_state.to_s} = t('js.admin.orders.shipment_states.' + order.shipment_state.to_s) %td - %a{ href: "mailto:#{order.email}", target: "_blank" } - = order.email + - email_value = display_value_for_producer(order, order.email) + %a{ href: "mailto:#{email_value}", target: "_blank" } + = email_value %td - = order&.bill_address&.full_name_for_sorting + = display_value_for_producer(order, order.bill_address&.full_name_for_sorting) %td.align-left %span = order.display_total @@ -46,10 +47,10 @@ - if local_assigns[:success] %i.success.icon-ok-sign{"data-controller": "ephemeral"} = render AdminTooltipComponent.new(text: t('spree.admin.orders.index.edit'), link_text: "", link: edit_admin_order_path(order), link_class: "icon_link with-tip icon-edit no-text") - - if order.ready_to_ship? + - if spree_current_user.can_manage_orders? && order.ready_to_ship? %form = render ShipOrderComponent.new(order: order) = render partial: 'admin/shared/tooltip_button', locals: {button_class: "icon-road icon_link with-tip no-text", reflex_data_id: order.id.to_s, tooltip_text: t('spree.admin.orders.index.ship'), shipment: true} - - if order.payment_required? && order.pending_payments.reject(&:requires_authorization?).any? + - if can?(:update, Spree::Payment) && order.payment_required? && order.pending_payments.reject(&:requires_authorization?).any? = render partial: 'admin/shared/tooltip_button', locals: {button_class: "icon-capture icon_link no-text", button_reflex: "click->Admin::OrdersReflex#capture", reflex_data_id: order.id.to_s, tooltip_text: t('spree.admin.orders.index.capture')} diff --git a/app/views/spree/admin/orders/edit.html.haml b/app/views/spree/admin/orders/edit.html.haml index d1d0dc73a9..8d9023581d 100644 --- a/app/views/spree/admin/orders/edit.html.haml +++ b/app/views/spree/admin/orders/edit.html.haml @@ -6,14 +6,16 @@ - content_for :page_actions do - if can?(:fire, @order) %li= event_links(@order) - = render partial: 'spree/admin/shared/order_links' + - if spree_current_user.can_manage_orders? + = render partial: 'spree/admin/shared/order_links' - if can?(:admin, Spree::Order) %li %a.button.icon-arrow-left{icon: 'icon-arrow-left', href: admin_orders_path } = t(:back_to_orders_list) = render partial: "spree/admin/shared/order_page_title" -= render partial: "spree/admin/shared/order_tabs", locals: { current: 'Order Details' } +- if spree_current_user.can_manage_orders? + = render partial: "spree/admin/shared/order_tabs", locals: { current: 'Order Details' } %div = render partial: "spree/shared/error_messages", locals: { target: @order } diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 26a7965a69..26d9c002ac 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -3,9 +3,10 @@ - content_for :minimal_js, true -- content_for :page_actions do - %li - = button_link_to t('.new_order'), spree.new_admin_order_url, icon: 'icon-plus', id: 'admin_new_order' +- if can?(:create, Spree::Order) && spree_current_user.can_manage_orders? + - content_for :page_actions do + %li + = button_link_to t('.new_order'), spree.new_admin_order_url, icon: 'icon-plus', id: 'admin_new_order' = render partial: 'spree/admin/shared/order_sub_menu' diff --git a/config/locales/en.yml b/config/locales/en.yml index e7fd2b7466..7945ba1a25 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1359,12 +1359,16 @@ en: enable_subscriptions_true: "Enabled" customer_names_in_reports: "Customer Names in Reports" customer_names_tip: "Enable your suppliers to see your customers names in reports" + producers_to_edit_orders: "Ability for producers to edit orders" + producers_to_edit_orders_tip: "Enable your suppliers to see orders containing their products, and edit quantity and weight for their own products only." customer_names_false: "Disabled" customer_names_true: "Enabled" customer_contacts_in_reports: "Customer contact details in reports" customer_contacts_tip: "Enable your suppliers to see your customer email and phone numbers in reports" customer_contacts_false: "Disabled" customer_contacts_true: "Enabled" + producers_edit_orders_false: "Disabled" + producers_edit_orders_true: "Enabled" shopfront_message: "Shopfront Message" shopfront_message_placeholder: > An optional message to welcome customers and explain how to shop with you. If text is entered here it will be displayed in a home tab when customers first arrive at your shopfront. diff --git a/db/migrate/20250202121858_add_enable_producers_to_edit_orders_to_enterprises.rb b/db/migrate/20250202121858_add_enable_producers_to_edit_orders_to_enterprises.rb new file mode 100644 index 0000000000..842427aca4 --- /dev/null +++ b/db/migrate/20250202121858_add_enable_producers_to_edit_orders_to_enterprises.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddEnableProducersToEditOrdersToEnterprises < ActiveRecord::Migration[7.0] + def change + add_column :enterprises, :enable_producers_to_edit_orders, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index fe7789113b..69728aa208 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -230,6 +230,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_03_04_234657) do t.text "white_label_logo_link" t.boolean "hide_groups_tab", default: false t.string "external_billing_id", limit: 128 + t.boolean "enable_producers_to_edit_orders", default: false, null: false t.boolean "show_customer_contacts_to_suppliers", default: false, null: false t.index ["address_id"], name: "index_enterprises_on_address_id" t.index ["is_primary_producer", "sells"], name: "index_enterprises_on_is_primary_producer_and_sells" diff --git a/lib/open_food_network/scope_variants_for_search.rb b/lib/open_food_network/scope_variants_for_search.rb index 7c51cd8b61..465e501c10 100644 --- a/lib/open_food_network/scope_variants_for_search.rb +++ b/lib/open_food_network/scope_variants_for_search.rb @@ -9,8 +9,9 @@ require 'open_food_network/scope_variant_to_hub' module OpenFoodNetwork class ScopeVariantsForSearch - def initialize(params) + def initialize(params, spree_current_user) @params = params + @spree_current_user = spree_current_user end def search @@ -20,13 +21,14 @@ module OpenFoodNetwork scope_to_schedule if params[:schedule_id] scope_to_order_cycle if params[:order_cycle_id] scope_to_distributor if params[:distributor_id] + scope_to_supplier if spree_current_user.can_manage_line_items_in_orders_only? @variants end private - attr_reader :params + attr_reader :params, :spree_current_user def search_params { product_name_cont: params[:q], sku_cont: params[:q], product_sku_cont: params[:q] } @@ -96,5 +98,9 @@ module OpenFoodNetwork # Filtering could be a problem on scoped variants. variants.each { |v| scoper.scope(v) } end + + def scope_to_supplier + @variants = @variants.where(supplier_id: spree_current_user.enterprises.ids) + end end end diff --git a/spec/controllers/admin/bulk_line_items_controller_spec.rb b/spec/controllers/admin/bulk_line_items_controller_spec.rb index 89303ca6d4..23e42284ac 100644 --- a/spec/controllers/admin/bulk_line_items_controller_spec.rb +++ b/spec/controllers/admin/bulk_line_items_controller_spec.rb @@ -120,11 +120,26 @@ RSpec.describe Admin::BulkLineItemsController, type: :controller do context "producer enterprise" do before do allow(controller).to receive_messages spree_current_user: supplier.owner - get :index, as: :json end - it "does not display line items for which my enterprise is a supplier" do - expect(response).to redirect_to unauthorized_path + context "with no distributor allows to edit orders" do + before { get :index, as: :json } + + it "does not display line items for which my enterprise is a supplier" do + expect(response).to redirect_to unauthorized_path + end + end + + context "with distributor allows to edit orders" do + before do + distributor1.update_columns(enable_producers_to_edit_orders: true) + get :index, as: :json + end + + it "retrieves a list of line_items from the supplier" do + keys = json_response['line_items'].first.keys.map(&:to_sym) + expect(line_item_attributes.all?{ |attr| keys.include? attr }).to eq(true) + end end end diff --git a/spec/controllers/api/v0/orders_controller_spec.rb b/spec/controllers/api/v0/orders_controller_spec.rb index a6d15774fa..558f14696b 100644 --- a/spec/controllers/api/v0/orders_controller_spec.rb +++ b/spec/controllers/api/v0/orders_controller_spec.rb @@ -84,11 +84,25 @@ module Api context 'producer enterprise' do before do allow(controller).to receive(:spree_current_user) { supplier.owner } - get :index end - it "does not display line items for which my enterprise is a supplier" do - assert_unauthorized! + context "with no distributor allows to edit orders" do + before { get :index } + + it "does not display line items for which my enterprise is a supplier" do + assert_unauthorized! + end + end + + context "with distributor allows to edit orders" do + before do + distributor.update_columns(enable_producers_to_edit_orders: true) + get :index + end + + it "retrieves a list of orders which have my supplied products" do + returns_orders(json_response) + end end end diff --git a/spec/controllers/spree/admin/mail_methods_controller_spec.rb b/spec/controllers/spree/admin/mail_methods_controller_spec.rb index 5221204c73..25592aa25b 100644 --- a/spec/controllers/spree/admin/mail_methods_controller_spec.rb +++ b/spec/controllers/spree/admin/mail_methods_controller_spec.rb @@ -22,7 +22,8 @@ RSpec.describe Spree::Admin::MailMethodsController do owned_groups: nil) allow(user).to receive_messages(enterprises: [create(:enterprise)], admin?: true, - locale: nil) + locale: nil, + can_manage_orders?: true) allow(controller).to receive_messages(spree_current_user: user) expect { diff --git a/spec/controllers/spree/admin/variants_controller_spec.rb b/spec/controllers/spree/admin/variants_controller_spec.rb index a443e4d449..dda0c01dab 100644 --- a/spec/controllers/spree/admin/variants_controller_spec.rb +++ b/spec/controllers/spree/admin/variants_controller_spec.rb @@ -5,151 +5,199 @@ require 'spec_helper' module Spree module Admin RSpec.describe VariantsController, type: :controller do - before { controller_login_as_admin } + context "log in as admin user" do + before { controller_login_as_admin } - describe "#index" do - describe "deleted variants" do - let(:product) { create(:product, name: 'Product A') } - let(:deleted_variant) do - deleted_variant = product.variants.create( - unit_value: "2", variant_unit: "weight", variant_unit_scale: 1, price: 1, - primary_taxon: create(:taxon), supplier: create(:supplier_enterprise) - ) - deleted_variant.delete - deleted_variant - end + describe "#index" do + describe "deleted variants" do + let(:product) { create(:product, name: 'Product A') } + let(:deleted_variant) do + deleted_variant = product.variants.create( + unit_value: "2", variant_unit: "weight", variant_unit_scale: 1, price: 1, + primary_taxon: create(:taxon), supplier: create(:supplier_enterprise) + ) + deleted_variant.delete + deleted_variant + end - it "lists only non-deleted variants with params[:deleted] == off" do - spree_get :index, product_id: product.id, deleted: "off" - expect(assigns(:variants)).to eq(product.variants) - end + it "lists only non-deleted variants with params[:deleted] == off" do + spree_get :index, product_id: product.id, deleted: "off" + expect(assigns(:variants)).to eq(product.variants) + end - it "lists only deleted variants with params[:deleted] == on" do - spree_get :index, product_id: product.id, deleted: "on" - expect(assigns(:variants)).to eq([deleted_variant]) + it "lists only deleted variants with params[:deleted] == on" do + spree_get :index, product_id: product.id, deleted: "on" + expect(assigns(:variants)).to eq([deleted_variant]) + end end end - end - describe "#update" do - let!(:variant) { create(:variant, display_name: "Tomatoes", sku: 123, supplier: producer) } - let(:producer) { create(:enterprise) } - - it "updates the variant" do - expect { - spree_put( - :update, - id: variant.id, - product_id: variant.product.id, - variant: { display_name: "Better tomatoes", sku: 456 } + describe "#update" do + let!(:variant) { + create( + :variant, + display_name: "Tomatoes", + sku: 123, + supplier: producer ) - variant.reload - }.to change { variant.display_name }.to("Better tomatoes") - .and change { variant.sku }.to(456.to_s) - end + } + let(:producer) { create(:enterprise) } - context "when updating supplier" do - let(:new_producer) { create(:enterprise) } - - it "updates the supplier" do + it "updates the variant" do expect { + spree_put( + :update, + id: variant.id, + product_id: variant.product.id, + variant: { display_name: "Better tomatoes", sku: 456 } + ) + variant.reload + }.to change { variant.display_name }.to("Better tomatoes") + .and change { variant.sku }.to(456.to_s) + end + + context "when updating supplier" do + let(:new_producer) { create(:enterprise) } + + it "updates the supplier" do + expect { + spree_put( + :update, + id: variant.id, + product_id: variant.product.id, + variant: { supplier_id: new_producer.id } + ) + variant.reload + }.to change { variant.supplier_id }.to(new_producer.id) + end + + it "removes associated product from existing Order Cycles" do + distributor = create(:distributor_enterprise) + order_cycle = create( + :simple_order_cycle, + variants: [variant], + coordinator: distributor, + distributors: [distributor] + ) + spree_put( :update, id: variant.id, product_id: variant.product.id, variant: { supplier_id: new_producer.id } ) - variant.reload - }.to change { variant.supplier_id }.to(new_producer.id) + + expect(order_cycle.reload.distributed_variants).not_to include variant + end + end + end + + describe "#search" do + let(:supplier) { create(:supplier_enterprise) } + let!(:p1) { create(:simple_product, name: 'Product 1', supplier_id: supplier.id) } + let!(:p2) { create(:simple_product, name: 'Product 2', supplier_id: supplier.id) } + let!(:v1) { p1.variants.first } + let!(:v2) { p2.variants.first } + let!(:vo) { create(:variant_override, variant: v1, hub: d, count_on_hand: 44) } + let!(:d) { create(:distributor_enterprise) } + let!(:oc) { create(:simple_order_cycle, distributors: [d], variants: [v1]) } + + it "filters by distributor" do + spree_get :search, q: 'Prod', distributor_id: d.id.to_s + expect(assigns(:variants)).to eq([v1]) end - it "removes associated product from existing Order Cycles" do - distributor = create(:distributor_enterprise) - order_cycle = create( - :simple_order_cycle, - variants: [variant], - coordinator: distributor, - distributors: [distributor] - ) + it "applies variant overrides" do + spree_get :search, q: 'Prod', distributor_id: d.id.to_s + expect(assigns(:variants)).to eq([v1]) + expect(assigns(:variants).first.on_hand).to eq(44) + end - spree_put( - :update, - id: variant.id, - product_id: variant.product.id, - variant: { supplier_id: new_producer.id } - ) + it "filters by order cycle" do + spree_get :search, q: 'Prod', order_cycle_id: oc.id.to_s + expect(assigns(:variants)).to eq([v1]) + end - expect(order_cycle.reload.distributed_variants).not_to include variant + it "does not filter when no distributor or order cycle is specified" do + spree_get :search, q: 'Prod' + expect(assigns(:variants)).to match_array [v1, v2] + end + end + + describe '#destroy' do + let(:variant) { create(:variant) } + + context 'when requesting with html' do + before do + allow(Spree::Variant).to receive(:find).with(variant.id.to_s) { variant } + allow(variant).to receive(:destroy).and_call_original + end + + it 'deletes the variant' do + spree_delete :destroy, id: variant.id, product_id: variant.product.id, + format: 'html' + expect(variant).to have_received(:destroy) + end + + it 'shows a success flash message' do + spree_delete :destroy, id: variant.id, product_id: variant.product.id, + format: 'html' + expect(flash[:success]).to be + end + + it 'redirects to admin_product_variants_url' do + spree_delete :destroy, id: variant.id, product_id: variant.product.id, + format: 'html' + expect(response).to redirect_to spree.admin_product_variants_url(variant.product.id) + end + + it 'destroys all its exchanges' do + exchange = create(:exchange) + variant.exchanges << exchange + + spree_delete :destroy, id: variant.id, product_id: variant.product.id, + format: 'html' + expect(variant.exchanges.reload).to be_empty + end end end end - describe "#search" do - let(:supplier) { create(:supplier_enterprise) } - let!(:p1) { create(:simple_product, name: 'Product 1', supplier_id: supplier.id) } - let!(:p2) { create(:simple_product, name: 'Product 2', supplier_id: supplier.id) } + context "log in as supplier and distributor enable_producers_to_edit_orders" do + let(:supplier1) { create(:supplier_enterprise) } + let(:supplier2) { create(:supplier_enterprise) } + let!(:p1) { create(:simple_product, name: 'Product 1', supplier_id: supplier1.id) } + let!(:p2) { create(:simple_product, name: 'Product 2', supplier_id: supplier2.id) } let!(:v1) { p1.variants.first } let!(:v2) { p2.variants.first } - let!(:vo) { create(:variant_override, variant: v1, hub: d, count_on_hand: 44) } - let!(:d) { create(:distributor_enterprise) } - let!(:oc) { create(:simple_order_cycle, distributors: [d], variants: [v1]) } + let!(:d) { create(:distributor_enterprise, enable_producers_to_edit_orders: true) } + let!(:oc) { create(:simple_order_cycle, distributors: [d], variants: [v1, v2]) } - it "filters by distributor" do - spree_get :search, q: 'Prod', distributor_id: d.id.to_s - expect(assigns(:variants)).to eq([v1]) + before do + order = create(:order_with_line_items, distributor: d, line_items_count: 1) + order.line_items.take.variant.update_attribute(:supplier_id, supplier1.id) + controller_login_as_enterprise_user([supplier1]) end - it "applies variant overrides" do - spree_get :search, q: 'Prod', distributor_id: d.id.to_s - expect(assigns(:variants)).to eq([v1]) - expect(assigns(:variants).first.on_hand).to eq(44) + describe "#search" do + it "filters by distributor and supplier1 products" do + spree_get :search, q: 'Prod', distributor_id: d.id.to_s + expect(assigns(:variants)).to eq([v1]) + end end - it "filters by order cycle" do - spree_get :search, q: 'Prod', order_cycle_id: oc.id.to_s - expect(assigns(:variants)).to eq([v1]) - end - - it "does not filter when no distributor or order cycle is specified" do - spree_get :search, q: 'Prod' - expect(assigns(:variants)).to match_array [v1, v2] - end - end - - describe '#destroy' do - let(:variant) { create(:variant) } - - context 'when requesting with html' do - before do - allow(Spree::Variant).to receive(:find).with(variant.id.to_s) { variant } - allow(variant).to receive(:destroy).and_call_original - end - - it 'deletes the variant' do - spree_delete :destroy, id: variant.id, product_id: variant.product.id, - format: 'html' - expect(variant).to have_received(:destroy) - end - - it 'shows a success flash message' do - spree_delete :destroy, id: variant.id, product_id: variant.product.id, - format: 'html' - expect(flash[:success]).to be - end - - it 'redirects to admin_product_variants_url' do - spree_delete :destroy, id: variant.id, product_id: variant.product.id, - format: 'html' - expect(response).to redirect_to spree.admin_product_variants_url(variant.product.id) - end - - it 'destroys all its exchanges' do - exchange = create(:exchange) - variant.exchanges << exchange - - spree_delete :destroy, id: variant.id, product_id: variant.product.id, - format: 'html' - expect(variant.exchanges.reload).to be_empty + describe "#update" do + it "updates the variant" do + expect { + spree_put( + :update, + id: v1.id, + product_id: v1.product.id, + variant: { display_name: "Better tomatoes", sku: 456 } + ) + v1.reload + }.to change { v1.display_name }.to("Better tomatoes") + .and change { v1.sku }.to(456.to_s) end end end diff --git a/spec/lib/open_food_network/scope_variants_for_search_spec.rb b/spec/lib/open_food_network/scope_variants_for_search_spec.rb index fc16580d15..35453826bf 100644 --- a/spec/lib/open_food_network/scope_variants_for_search_spec.rb +++ b/spec/lib/open_food_network/scope_variants_for_search_spec.rb @@ -19,8 +19,9 @@ RSpec.describe OpenFoodNetwork::ScopeVariantsForSearch do let!(:oc3) { create(:simple_order_cycle, distributors: [d2], variants: [v4]) } let!(:s1) { create(:schedule, order_cycles: [oc1]) } let!(:s2) { create(:schedule, order_cycles: [oc2]) } + let!(:spree_current_user) { create(:user) } - let(:scoper) { OpenFoodNetwork::ScopeVariantsForSearch.new(params) } + let(:scoper) { OpenFoodNetwork::ScopeVariantsForSearch.new(params, spree_current_user) } describe "search" do let(:result) { scoper.search } @@ -68,8 +69,11 @@ RSpec.describe OpenFoodNetwork::ScopeVariantsForSearch do expect{ result }.to query_database [ "Enterprise Load", "VariantOverride Load", - "SQL" + "SQL", + "Enterprise Pluck", + "Enterprise Load" ] + expect(result).to include v4 expect(result).not_to include v1, v2, v3 end @@ -179,6 +183,25 @@ RSpec.describe OpenFoodNetwork::ScopeVariantsForSearch do to eq(["Product 1", "Product a", "Product b", "Product c"]) end end + + context "when search is done by the producer allowing to edit orders" do + let(:params) { { q: "product" } } + let(:producer) { create(:supplier_enterprise) } + let!(:spree_current_user) { + instance_double('Spree::User', enterprises: Enterprise.where(id: producer.id), + can_manage_line_items_in_orders_only?: true) + } + + it "returns products distributed by distributors allowing producers to edit orders" do + v1.supplier_id = producer.id + v2.supplier_id = producer.id + v1.save! + v2.save! + + expect(result).to include v1, v2 + expect(result).not_to include v3, v4 + end + end end private diff --git a/spec/lib/reports/orders_and_distributors_report_spec.rb b/spec/lib/reports/orders_and_distributors_report_spec.rb index 98f4943f29..07c68b55ea 100644 --- a/spec/lib/reports/orders_and_distributors_report_spec.rb +++ b/spec/lib/reports/orders_and_distributors_report_spec.rb @@ -151,6 +151,7 @@ RSpec.describe Reporting::Reports::OrdersAndDistributors::Base do subject # build context first expect { subject.table_rows }.to query_database [ + "Enterprise Pluck", "SQL", "Spree::LineItem Load", "Spree::PaymentMethod Load", diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 1d05f99bd6..7f790e096f 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -1012,6 +1012,29 @@ RSpec.describe Enterprise do expect(expected).to include(sender) end end + + describe "#is_producer_only" do + context "when enterprise is_primary_producer and sells none" do + it "returns true" do + enterprise = build(:supplier_enterprise) + expect(enterprise.is_producer_only).to be true + end + end + + context "when enterprise is_primary_producer and sells any" do + it "returns false" do + enterprise = build(:enterprise, is_primary_producer: true, sells: "any") + expect(enterprise.is_producer_only).to be false + end + end + + context "when enterprise is_primary_producer and sells own" do + it "returns false" do + enterprise = build(:enterprise, is_primary_producer: true, sells: "own") + expect(enterprise.is_producer_only).to be false + end + end + end end def enterprise_name_error(owner_email) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 1339a39328..6060d7bfc5 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -240,6 +240,24 @@ RSpec.describe Spree::Ability do it { expect(subject.can_manage_enterprises?(user)).to be true } it { expect(subject.can_manage_orders?(user)).to be false } it { expect(subject.can_manage_order_cycles?(user)).to be false } + + context "with no distributor allows me to edit orders" do + it { expect(subject.can_manage_orders?(user)).to be false } + it { expect(subject.can_manage_line_items_in_orders?(user)).to be false } + end + + context "with any distributor allows me to edit orders containing my product" do + before do + order = create( + :order_with_line_items, + line_items_count: 1, + distributor: create(:distributor_enterprise, enable_producers_to_edit_orders: true) + ) + order.line_items.first.variant.update!(supplier_id: enterprise_none_producer.id) + end + + it { expect(subject.can_manage_line_items_in_orders?(user)).to be true } + end end context "as a profile" do @@ -260,6 +278,7 @@ RSpec.describe Spree::Ability do it { expect(subject.can_manage_products?(user)).to be false } it { expect(subject.can_manage_enterprises?(user)).to be false } it { expect(subject.can_manage_orders?(user)).to be false } + it { expect(subject.can_manage_line_items_in_orders?(user)).to be false } it { expect(subject.can_manage_order_cycles?(user)).to be false } it "can create enterprises straight off the bat" do diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index ea9531f707..6d123b6990 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -298,4 +298,40 @@ RSpec.describe Spree::User do end end end + + describe "#can_manage_line_items_in_orders_only?" do + let(:producer) { create(:supplier_enterprise) } + let(:order) { create(:order, distributor:) } + + subject { user.can_manage_line_items_in_orders_only? } + + context "when user has producer" do + let(:user) { create(:user, enterprises: [producer]) } + + context "order containing their product" do + before do + order.line_items << create(:line_item, + product: create(:product, supplier_id: producer.id)) + end + context "order distributor allow producer to edit orders" do + let(:distributor) do + create(:distributor_enterprise, enable_producers_to_edit_orders: true) + end + + it { is_expected.to be_truthy } + end + + context "order distributor doesn't allow producer to edit orders" do + let(:distributor) { create(:distributor_enterprise) } + it { is_expected.to be_falsey } + end + end + end + + context "no order containing their product" do + let(:user) { create(:user, enterprises: [create(:distributor_enterprise)]) } + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/services/permissions/order_spec.rb b/spec/services/permissions/order_spec.rb index d85777a583..5f7246d75b 100644 --- a/spec/services/permissions/order_spec.rb +++ b/spec/services/permissions/order_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' module Permissions RSpec.describe Order do - let(:user) { double(:user) } let(:permissions) { Permissions::Order.new(user) } let!(:basic_permissions) { OpenFoodNetwork::Permissions.new(user) } let(:distributor) { create(:distributor_enterprise) } @@ -28,68 +27,24 @@ module Permissions before { allow(OpenFoodNetwork::Permissions).to receive(:new) { basic_permissions } } - describe "finding orders that are visible in reports" do - let(:random_enterprise) { create(:distributor_enterprise) } - let(:order) { create(:order, order_cycle:, distributor: ) } - let!(:line_item) { create(:line_item, order:) } - let!(:producer) { create(:supplier_enterprise) } + context "with user cannot only manage line_items in orders" do + let(:user) { instance_double('Spree::User', can_manage_line_items_in_orders_only?: false) } - before do - allow(basic_permissions).to receive(:coordinated_order_cycles) { Enterprise.where("1=0") } - end + describe "finding orders that are visible in reports" do + let(:random_enterprise) { create(:distributor_enterprise) } + let(:order) { create(:order, order_cycle:, distributor: ) } + let!(:line_item) { create(:line_item, order:) } + let!(:producer) { create(:supplier_enterprise) } - context "as the hub through which the order was placed" do before do - allow(basic_permissions).to receive(:managed_enterprises) { - Enterprise.where(id: distributor) - } + allow(basic_permissions).to receive(:coordinated_order_cycles) { Enterprise.where("1=0") } end - it "should let me see the order" do - expect(permissions.visible_orders).to include order - end - end - - context "as the coordinator of the order cycle through which the order was placed" do - before do - allow(basic_permissions).to receive(:managed_enterprises) { - Enterprise.where(id: coordinator) - } - allow(basic_permissions).to receive(:coordinated_order_cycles) { - OrderCycle.where(id: order_cycle) - } - end - - it "should let me see the order" do - expect(permissions.visible_orders).to include order - end - - context "with search params" do - let(:search_params) { { completed_at_gt: Time.zone.now.yesterday.strftime('%Y-%m-%d') } } - let(:permissions) { Permissions::Order.new(user, search_params) } - - it "only returns completed, non-cancelled orders within search filter range" do - expect(permissions.visible_orders).to include order_completed - expect(permissions.visible_orders).not_to include order_cancelled - expect(permissions.visible_orders).not_to include order_cart - expect(permissions.visible_orders).not_to include order_from_last_year - end - end - end - - context "as a producer which has granted P-OC to the distributor of an order" do - before do - allow(basic_permissions).to receive(:managed_enterprises) { - Enterprise.where(id: producer) - } - create(:enterprise_relationship, parent: producer, child: distributor, - permissions_list: [:add_to_order_cycle]) - end - - context "which contains my products" do + context "as the hub through which the order was placed" do before do - line_item.variant.supplier = producer - line_item.variant.save + allow(basic_permissions).to receive(:managed_enterprises) { + Enterprise.where(id: distributor) + } end it "should let me see the order" do @@ -97,118 +52,208 @@ module Permissions end end - context "which does not contain my products" do + context "as the coordinator of the order cycle through which the order was placed" do + before do + allow(basic_permissions).to receive(:managed_enterprises) { + Enterprise.where(id: coordinator) + } + allow(basic_permissions).to receive(:coordinated_order_cycles) { + OrderCycle.where(id: order_cycle) + } + end + + it "should let me see the order" do + expect(permissions.visible_orders).to include order + end + + context "with search params" do + let(:search_params) { + { completed_at_gt: Time.zone.now.yesterday.strftime('%Y-%m-%d') } + } + let(:permissions) { Permissions::Order.new(user, search_params) } + + it "only returns completed, non-cancelled orders within search filter range" do + expect(permissions.visible_orders).to include order_completed + expect(permissions.visible_orders).not_to include order_cancelled + expect(permissions.visible_orders).not_to include order_cart + expect(permissions.visible_orders).not_to include order_from_last_year + end + end + end + + context "as a producer which has granted P-OC to the distributor of an order" do + before do + allow(basic_permissions).to receive(:managed_enterprises) { + Enterprise.where(id: producer) + } + create(:enterprise_relationship, parent: producer, child: distributor, + permissions_list: [:add_to_order_cycle]) + end + + context "which contains my products" do + before do + line_item.variant.supplier = producer + line_item.variant.save + end + + it "should let me see the order" do + expect(permissions.visible_orders).to include order + end + end + + context "which does not contain my products" do + it "should not let me see the order" do + expect(permissions.visible_orders).not_to include order + end + end + end + + context "as an enterprise that is a distributor in the order cycle, " \ + "but not the distributor of the order" do + before do + allow(basic_permissions).to receive(:managed_enterprises) { + Enterprise.where(id: random_enterprise) + } + end + it "should not let me see the order" do expect(permissions.visible_orders).not_to include order end end end - context "as an enterprise that is a distributor in the order cycle, " \ - "but not the distributor of the order" do + describe "finding line items that are visible in reports" do + let(:random_enterprise) { create(:distributor_enterprise) } + let(:order) { create(:order, order_cycle:, distributor: ) } + let!(:line_item1) { create(:line_item, order:) } + let!(:line_item2) { create(:line_item, order:) } + let!(:producer) { create(:supplier_enterprise) } + before do - allow(basic_permissions).to receive(:managed_enterprises) { - Enterprise.where(id: random_enterprise) - } + allow(basic_permissions).to receive(:coordinated_order_cycles) { Enterprise.where("1=0") } end - it "should not let me see the order" do - expect(permissions.visible_orders).not_to include order + context "as the hub through which the parent order was placed" do + before do + allow(basic_permissions).to receive(:managed_enterprises) { + Enterprise.where(id: distributor) + } + end + + it "should let me see the line_items" do + expect(permissions.visible_line_items).to include line_item1, line_item2 + end + end + + context "as the coordinator of the order cycle through which the parent order was placed" do + before do + allow(basic_permissions).to receive(:managed_enterprises) { + Enterprise.where(id: coordinator) + } + allow(basic_permissions).to receive(:coordinated_order_cycles) { + OrderCycle.where(id: order_cycle) + } + end + + it "should let me see the line_items" do + expect(permissions.visible_line_items).to include line_item1, line_item2 + end + end + + context "as the manager producer which has granted P-OC to the distributor " \ + "of the parent order" do + before do + allow(basic_permissions).to receive(:managed_enterprises) { + Enterprise.where(id: producer) + } + create(:enterprise_relationship, parent: producer, child: distributor, + permissions_list: [:add_to_order_cycle]) + + line_item1.variant.supplier = producer + line_item1.variant.save + end + + it "should let me see the line_items pertaining to variants I produce" do + ps = permissions.visible_line_items + expect(ps).to include line_item1 + expect(ps).not_to include line_item2 + end + end + + context "as an enterprise that is a distributor in the order cycle, " \ + "but not the distributor of the parent order" do + before do + allow(basic_permissions).to receive(:managed_enterprises) { + Enterprise.where(id: random_enterprise) + } + end + + it "should not let me see the line_items" do + expect(permissions.visible_line_items).not_to include line_item1, line_item2 + end + end + + context "with search params" do + let!(:line_item3) { create(:line_item, order: order_completed) } + let!(:line_item4) { create(:line_item, order: order_cancelled) } + let!(:line_item5) { create(:line_item, order: order_cart) } + let!(:line_item6) { create(:line_item, order: order_from_last_year) } + + let(:search_params) { { completed_at_gt: Time.zone.now.yesterday.strftime('%Y-%m-%d') } } + let(:permissions) { Permissions::Order.new(user, search_params) } + + before do + allow(user).to receive(:admin?) { "admin" } + end + + it "only returns line items from completed, " \ + "non-cancelled orders within search filter range" do + expect(permissions.visible_line_items).to include order_completed.line_items.first + expect(permissions.visible_line_items).not_to include order_cancelled.line_items.first + expect(permissions.visible_line_items).not_to include order_cart.line_items.first + expect(permissions.visible_line_items) + .not_to include order_from_last_year.line_items.first + end end end end - describe "finding line items that are visible in reports" do - let(:random_enterprise) { create(:distributor_enterprise) } - let(:order) { create(:order, order_cycle:, distributor: ) } - let!(:line_item1) { create(:line_item, order:) } - let!(:line_item2) { create(:line_item, order:) } - let!(:producer) { create(:supplier_enterprise) } - - before do - allow(basic_permissions).to receive(:coordinated_order_cycles) { Enterprise.where("1=0") } + context "with user can only manage line_items in orders" do + let(:producer) { create(:supplier_enterprise) } + let(:user) do + create(:user, enterprises: [producer]) end + let!(:order_by_distributor_allow_edits) do + order = create( + :order_with_line_items, + distributor: create( + :distributor_enterprise, + enable_producers_to_edit_orders: true + ), + line_items_count: 1 + ) + order.line_items.first.variant.update_attribute(:supplier_id, producer.id) - context "as the hub through which the parent order was placed" do - before do - allow(basic_permissions).to receive(:managed_enterprises) { - Enterprise.where(id: distributor) - } - end - - it "should let me see the line_items" do - expect(permissions.visible_line_items).to include line_item1, line_item2 + order + end + let!(:order_by_distributor_disallow_edits) do + create( + :order_with_line_items, + distributor: create(:distributor_enterprise), + line_items_count: 1 + ) + end + describe "#editable_orders" do + it "returns orders where the distributor allows producers to edit" do + expect(permissions.editable_orders.count).to eq 1 + expect(permissions.editable_orders).to include order_by_distributor_allow_edits end end - context "as the coordinator of the order cycle through which the parent order was placed" do - before do - allow(basic_permissions).to receive(:managed_enterprises) { - Enterprise.where(id: coordinator) - } - allow(basic_permissions).to receive(:coordinated_order_cycles) { - OrderCycle.where(id: order_cycle) - } - end - - it "should let me see the line_items" do - expect(permissions.visible_line_items).to include line_item1, line_item2 - end - end - - context "as the manager producer which has granted P-OC to the distributor " \ - "of the parent order" do - before do - allow(basic_permissions).to receive(:managed_enterprises) { - Enterprise.where(id: producer) - } - create(:enterprise_relationship, parent: producer, child: distributor, - permissions_list: [:add_to_order_cycle]) - - line_item1.variant.supplier = producer - line_item1.variant.save - end - - it "should let me see the line_items pertaining to variants I produce" do - ps = permissions.visible_line_items - expect(ps).to include line_item1 - expect(ps).not_to include line_item2 - end - end - - context "as an enterprise that is a distributor in the order cycle, " \ - "but not the distributor of the parent order" do - before do - allow(basic_permissions).to receive(:managed_enterprises) { - Enterprise.where(id: random_enterprise) - } - end - - it "should not let me see the line_items" do - expect(permissions.visible_line_items).not_to include line_item1, line_item2 - end - end - - context "with search params" do - let!(:line_item3) { create(:line_item, order: order_completed) } - let!(:line_item4) { create(:line_item, order: order_cancelled) } - let!(:line_item5) { create(:line_item, order: order_cart) } - let!(:line_item6) { create(:line_item, order: order_from_last_year) } - - let(:search_params) { { completed_at_gt: Time.zone.now.yesterday.strftime('%Y-%m-%d') } } - let(:permissions) { Permissions::Order.new(user, search_params) } - - before do - allow(user).to receive(:admin?) { "admin" } - end - - it "only returns line items from completed, " \ - "non-cancelled orders within search filter range" do - expect(permissions.visible_line_items).to include order_completed.line_items.first - expect(permissions.visible_line_items).not_to include order_cancelled.line_items.first - expect(permissions.visible_line_items).not_to include order_cart.line_items.first - expect(permissions.visible_line_items) - .not_to include order_from_last_year.line_items.first + describe "#editable_line_items" do + it "returns line items from orders where the distributor allows producers to edit" do + expect(permissions.editable_line_items.count).to eq 1 + expect(permissions.editable_line_items.first.order).to eq order_by_distributor_allow_edits end end end diff --git a/spec/system/admin/orders/producer_actions_spec.rb b/spec/system/admin/orders/producer_actions_spec.rb new file mode 100644 index 0000000000..88e8d5bd34 --- /dev/null +++ b/spec/system/admin/orders/producer_actions_spec.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +require 'system_helper' + +RSpec.describe 'As a producer who have the ability to update orders' do + include AdminHelper + include AuthenticationHelper + include WebHelper + + let!(:supplier1) { create(:supplier_enterprise, name: 'My supplier1') } + let!(:supplier2) { create(:supplier_enterprise, name: 'My supplier2') } + let!(:supplier1_v1) { create(:variant, supplier_id: supplier1.id) } + let!(:supplier1_v2) { create(:variant, supplier_id: supplier1.id) } + let!(:supplier2_v1) { create(:variant, supplier_id: supplier2.id) } + let(:order_cycle) do + create(:simple_order_cycle, distributors: [distributor], variants: [supplier1_v1, supplier1_v2]) + end + let!(:order_containing_supplier1_products) do + o = create( + :completed_order_with_totals, + distributor:, order_cycle:, + user: supplier1_ent_user, line_items_count: 1 + ) + o.line_items.first.update_columns(variant_id: supplier1_v1.id) + o + end + let!(:order_containing_supplier2_v1_products) do + o = create( + :completed_order_with_totals, + distributor:, order_cycle:, + user: supplier2_ent_user, line_items_count: 1 + ) + o.line_items.first.update_columns(variant_id: supplier2_v1.id) + o + end + let(:supplier1_ent_user) { create(:user, enterprises: [supplier1]) } + let(:supplier2_ent_user) { create(:user, enterprises: [supplier2]) } + + context "As supplier1 enterprise user" do + before { login_as(supplier1_ent_user) } + let(:order) { order_containing_supplier1_products } + let(:user) { supplier1_ent_user } + + describe 'orders index page' do + before { visit spree.admin_orders_path } + + context "when no distributor allow the producer to edit orders" do + let(:distributor) { create(:distributor_enterprise) } + + it "should not allow producer to view orders page" do + expect(page).to have_content 'Unauthorized' + end + end + + context "when distributor allows the producer to edit orders" do + let(:distributor) { create(:distributor_enterprise, enable_producers_to_edit_orders: true) } + it "should not allow to add new orders" do + expect(page).not_to have_link('New Order') + end + + context "when distributor doesn't allow to view customer details" do + it "should allow producer to view orders page with HIDDEN customer details" do + within('#listing_orders tbody') do + expect(page).to have_selector('tr', count: 1) # Only one order + # One for Email, one for Name + expect(page).to have_selector('td', text: '< Hidden >', count: 2) + end + end + end + + context "when distributor allows to view customer details" do + let(:distributor) do + create( + :distributor_enterprise, + enable_producers_to_edit_orders: true, + show_customer_names_to_suppliers: true + ) + end + it "should allow producer to view orders page with customer details" do + within('#listing_orders tbody') do + name = order.bill_address&.full_name_for_sorting + email = order.email + expect(page).to have_selector('tr', count: 1) # Only one order + expect(page).to have_selector('td', text: name, count: 1) + expect(page).to have_selector('td', text: email, count: 1) + within 'td.actions' do + # to have edit button + expect(page).to have_selector("a.icon-edit") + # not to have ship button + expect(page).not_to have_selector('button.icon-road') + end + end + end + end + end + end + + describe 'orders edit page' do + before { visit spree.edit_admin_order_path(order) } + + context "when no distributor allow the producer to edit orders" do + let(:distributor) { create(:distributor_enterprise) } + + it "should not allow producer to view orders page" do + expect(page).to have_content 'Unauthorized' + end + end + + context "when distributor allows to edit orders" do + let(:distributor) { create(:distributor_enterprise, enable_producers_to_edit_orders: true) } + let(:product) { supplier1_v2.product } + + it "should allow me to manage my products in the order" do + expect(page).to have_content 'Add Product' + + # Add my product + add_product(product) + expect_product_change(product, :add) + + # Edit my product + edit_product(product) + expect_product_change(product, :update, 2) + + # Delete my product + delete_product(product) + expect_product_change(product, :remove) + end + end + + def expect_product_change(product, action, expected_qty = 0) + # JS for this page sometimes take more than 2 seconds (default timeout for cappybara) + timeout = 5 + + within('table.index tbody tr', wait: timeout) do + case action + when :add + expect(page).to have_text(product.name, wait: timeout) + when :update + expect(page).to have_text(expected_qty.to_s, wait: timeout) + when :remove + expect(page).not_to have_text(product.name, wait: timeout) + else + raise 'Invalid action' + end + end + end + + def add_product(product) + select2_select product.name, from: 'add_variant_id', search: true + find('button.add_variant').click + end + + def edit_product(product) + find('a.edit-item.icon_link.icon-edit.no-text.with-tip').click + fill_in 'quantity', with: 2 + find("a[data-variant-id='#{product.variants.last.id}'][data-action='save']").click + end + + def delete_product(product) + find("a[data-variant-id='#{product.variants.last.id}'][data-action='remove']").click + click_button 'OK' + end + end + end +end diff --git a/spec/system/admin/producer_bulk_order_management.rb b/spec/system/admin/producer_bulk_order_management.rb new file mode 100644 index 0000000000..575bef0ffb --- /dev/null +++ b/spec/system/admin/producer_bulk_order_management.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'system_helper' + +RSpec.describe 'As a producer who have the ability to update orders' do + include AdminHelper + include AuthenticationHelper + include WebHelper + + let!(:supplier1) { create(:supplier_enterprise, name: 'My supplier1') } + let!(:supplier2) { create(:supplier_enterprise, name: 'My supplier2') } + let!(:supplier1_v1) { create(:variant, supplier_id: supplier1.id) } + let!(:supplier1_v2) { create(:variant, supplier_id: supplier1.id) } + let!(:supplier2_v1) { create(:variant, supplier_id: supplier2.id) } + let(:order_cycle) do + create(:simple_order_cycle, distributors: [distributor], variants: [supplier1_v1, supplier1_v2]) + end + let!(:order_containing_supplier1_products) do + o = create( + :completed_order_with_totals, + distributor:, order_cycle:, + user: supplier1_ent_user, line_items_count: 1 + ) + o.line_items.first.update_columns(variant_id: supplier1_v1.id) + o + end + + let(:supplier1_ent_user) { create(:user, enterprises: [supplier1]) } + + context "As supplier1 enterprise user" do + before { login_as(supplier1_ent_user) } + let(:order) { order_containing_supplier1_products } + let(:user) { supplier1_ent_user } + + describe 'bulk orders index page' do + before { visit spree.admin_bulk_order_management_path } + + context "when no distributor allow the producer to edit orders" do + let(:distributor) { create(:distributor_enterprise) } + + it "should not allow producer to view orders page" do + expect(page).to have_content 'Unauthorized' + end + end + + context "when distributor allows the producer to edit orders" do + let(:distributor) { create(:distributor_enterprise, enable_producers_to_edit_orders: true) } + + context "when distributor doesn't allow to view customer details" do + it "should allow producer to view bulk orders page with HIDDEN customer details" do + within('tbody') do + expect(page).to have_selector('tr', count: 1) + expect(page).to have_selector('td', text: '< Hidden >', count: 1) + end + end + end + + context "when distributor allows to view customer details" do + let(:distributor) do + create( + :distributor_enterprise, + enable_producers_to_edit_orders: true, + show_customer_names_to_suppliers: true + ) + end + it "should allow producer to view bulk orders page with customer details" do + within('tbody') do + expect(page).to have_selector('tr', count: 1) + expect(page).to have_selector('td', text: order.bill_address.full_name_for_sorting, + count: 1) + end + end + end + end + end + end +end diff --git a/spec/views/spree/admin/orders/edit.html.haml_spec.rb b/spec/views/spree/admin/orders/edit.html.haml_spec.rb index 644af45b88..ca22a3c226 100644 --- a/spec/views/spree/admin/orders/edit.html.haml_spec.rb +++ b/spec/views/spree/admin/orders/edit.html.haml_spec.rb @@ -21,7 +21,7 @@ RSpec.describe "spree/admin/orders/edit.html.haml" do end end - allow(view).to receive_messages spree_current_user: create(:user) + allow(view).to receive_messages spree_current_user: create(:admin_user) end context "when order is complete" do