Merge pull request #3479 from luisramos0/2-0-stable-Feb12

[Spree Upgrade] Merging master into 2-0-stable (2nd run in Feb2019)
This commit is contained in:
Pau Pérez Fabregat
2019-02-14 09:40:28 +01:00
committed by GitHub
106 changed files with 6795 additions and 271 deletions

61
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,61 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## Description
<!-- Provide a more detailed introduction to the issue itself, and why you consider it to be a bug -->
## Expected Behavior
<!-- Tell us what should happen -->
## Actual Behaviour
<!-- Tell us what happens instead -->
## Steps to Reproduce
<!-- Provide an unambiguous set of steps to reproduce this bug -->
<!-- Include code to reproduce if relevant -->
1.
2.
3.
4.
## Animated Gif/Screenshot
<!-- Provide a screenshot or brief animated gif reproducing the bug. Linux users can use
[Peek](https://github.com/phw/peek#ubuntu) while Mac users can use [Recordit](http://recordit.co/) -->
## Context
<!-- How has this bug affected you? What were you trying to accomplish? -->
## Severity
<!-- Assign a label and explain the impact.
bug-s1: a critical feature is broken: checkout, payments, signup, login
bug-s2: a non-critical feature is broken, no workaround
bug-s3: a feature is broken but there is a workaround
bug-s4: it's annoying, but you can use it
bug-s5: we can live with it, only a few users impacted
https://github.com/openfoodfoundation/openfoodnetwork/wiki/Bug-severity
-->
## Your Environment
<!-- Include relevant details about the environment you experienced the bug in -->
* Version used:
* Browser name and version:
* Operating System and version (desktop or mobile):
* OFN Platform instance where you discovered the bug, and which version of the software they are using.
## Possible Fix
<!-- Not obligatory, but suggest a fix or reason for the bug -->

View File

@@ -0,0 +1,35 @@
---
name: Feature template
about: Create feature epics that detail the larger feature or functionality to be
delivered.
title: ''
labels: ''
assignees: ''
---
## What is the problem we are solving
<!-- Describe the problem this feature is supposed to solve. Should be described in the icebox item (in Discourse). -->
## Success factors = expected outcome
<!-- Describe what is the expected outcome: when the feature is released, what would it look like? What will make you say "the problem is solved"? Can be metrics like "Platform has less than 1% service interruption", or yes/no statements like "People can checkout on mobiles. -->
## Useful information for inception
<!-- List here any information that can be useful for the inception stage. -->
## Link to the "Product Development - Backlog" item in Discourse
<!-- Put the link here, and put this epic link in the Discourse item as well for cross-referencing. -->
Add a custom footer
Pages 70
Home
Development environment setup
macOS (Sierra, HighSierra and Mojave)
OS X (El Capitan)
OS X (Mavericks)
Ubuntu
On Heroku
Rubocop
General guidelines
Spree Commerce customisation

View File

@@ -0,0 +1,18 @@
---
name: Story template
about: Create stories that are small chunks of work that devs will pick up and deliver
title: ''
labels: ''
assignees: ''
---
**## Description**
<!-- Describe the story in detail:
**- As a:** (enterprise user, super admin, user...)
**- On page:** (provide url of the page you want to modify. If not provide where will be created the new url and the name we want to give it)
**- I want to be able to do:** (specify the desired behavior)
(Link to others issues or resources to provide context > only if really necessary). -->
**## Acceptance Criteria**
<!-- Document the outcomes that need to be achieved before this component can be considered complete. -->

View File

@@ -194,6 +194,8 @@ Metrics/BlockNesting:
Metrics/ClassLength:
Max: 100
Exclude:
- engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb
Metrics/ModuleLength:
Max: 100
@@ -203,6 +205,8 @@ Metrics/CyclomaticComplexity:
Metrics/MethodLength:
Max: 10
Exclude:
- engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb
Metrics/ParameterLists:
Max: 5

View File

@@ -10,6 +10,7 @@ gem 'i18n-js', '~> 3.1.0'
# Patched version. See http://rubysec.com/advisories/CVE-2015-5312/.
gem 'nokogiri', '>= 1.6.7.1'
gem "order_management", path: "./engines/order_management"
gem 'web', path: './engines/web'
gem 'pg'
@@ -26,13 +27,13 @@ gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '2-0-stable'
# - Pass customer email and phone number to PayPal (merged to upstream master)
# - Change type of password from string to password to hide it in the form
gem 'spree_paypal_express', github: "openfoodfoundation/better_spree_paypal_express", branch: "2-0-stable"
gem 'stripe', '~> 3.3.2'
gem 'stripe', '~> 4.5.0'
# We need at least this version to have Digicert's root certificate
# which is needed for Pin Payments (and possibly others).
gem 'activemerchant', '~> 1.78'
gem 'oauth2', '~> 1.4.1' # Used for Stripe Connect
gem 'jwt', '~> 1.5'
gem 'jwt', '~> 2.1'
gem 'delayed_job_active_record'
gem 'daemons'

View File

@@ -118,6 +118,11 @@ GIT
activemodel (>= 3.0)
railties (>= 3.0)
PATH
remote: engines/order_management
specs:
order_management (0.0.1)
PATH
remote: engines/web
specs:
@@ -240,6 +245,7 @@ GEM
compass (~> 1.0.0)
sass-rails (< 5.1)
sprockets (< 4.0)
connection_pool (2.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
css_parser (1.6.0)
@@ -501,7 +507,7 @@ GEM
json_spec (1.1.5)
multi_json (~> 1.0)
rspec (>= 2.0, < 4.0)
jwt (1.5.6)
jwt (2.1.0)
kaminari (0.14.1)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
@@ -532,6 +538,8 @@ GEM
multi_xml (0.6.0)
multipart-post (2.0.0)
nenv (0.3.0)
net-http-persistent (3.0.0)
connection_pool (~> 2.2)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
notiffany (0.1.1)
@@ -623,7 +631,7 @@ GEM
trollop (~> 2.1)
rdoc (3.12.2)
json (~> 1.4)
redcarpet (3.2.3)
redcarpet (3.4.0)
ref (2.0.0)
request_store (1.4.1)
rack (>= 1.4)
@@ -711,8 +719,9 @@ GEM
tilt (~> 1.1, != 1.3.0)
state_machine (1.2.0)
stringex (1.5.1)
stripe (3.3.2)
faraday (~> 0.9)
stripe (4.5.0)
faraday (~> 0.13)
net-http-persistent (~> 3.0)
therubyracer (0.12.0)
libv8 (~> 3.16.14.0)
ref
@@ -808,7 +817,7 @@ DEPENDENCIES
jquery-migrate-rails
jquery-rails (= 3.0.0)
json_spec (~> 1.1.4)
jwt (~> 1.5)
jwt (~> 2.1)
knapsack
letter_opener (>= 1.4.1)
listen (= 3.0.8)
@@ -817,6 +826,7 @@ DEPENDENCIES
oauth2 (~> 1.4.1)
ofn-qz!
oj
order_management!
paper_trail (~> 5.2.3)
paperclip (~> 3.4.1)
pg
@@ -848,7 +858,7 @@ DEPENDENCIES
spree_paypal_express!
spring (= 1.7.2)
spring-commands-rspec
stripe (~> 3.3.2)
stripe (~> 4.5.0)
therubyracer (= 0.12.0)
timecop
truncate_html

View File

@@ -6,7 +6,11 @@ angular.module("admin.subscriptions").controller "SubscriptionController", ($sco
$scope.schedules = Schedules.all
$scope.paymentMethods = PaymentMethods.all
$scope.shippingMethods = ShippingMethods.all
$scope.distributor_id = $scope.subscription.shop_id # variant selector requires distributor_id
# Variant selector requires these
$scope.distributor_id = $scope.subscription.shop_id
$scope.eligible_for_subscriptions = true
$scope.view = if $scope.subscription.id? then 'review' else 'details'
$scope.nextCallbacks = {}
$scope.backCallbacks = {}

View File

@@ -22,6 +22,7 @@ angular.module("admin.utils").directive "variantAutocomplete", ($timeout) ->
q: term
distributor_id: scope.distributor_id
order_cycle_id: scope.order_cycle_id
eligible_for_subscriptions: scope.eligible_for_subscriptions
results: (data, page) ->
window.variants = data # this is how spree auto complete JS code picks up variants
results: data

View File

@@ -4,7 +4,7 @@ Darkswarm.filter "date_in_words", ->
Darkswarm.filter "sensible_timeframe", (date_in_wordsFilter)->
(date) ->
if moment().add('days', 2) < moment(date)
if moment().add(2, 'days') < moment(date)
t 'orders_open'
else
t('closing') + date_in_wordsFilter(date)

View File

@@ -0,0 +1,7 @@
@import '../variables';
.admin-subscription-form-subscription-line-items {
.not-in-open-and-upcoming-order-cycles-warning {
color: $warning-red;
}
}

View File

@@ -0,0 +1,7 @@
@import '../variables';
.admin-subscription-review-subscription-line-items {
.not-in-open-and-upcoming-order-cycles-warning {
color: $warning-red;
}
}

View File

@@ -13,7 +13,8 @@ module Admin
def build
@subscription_line_item.assign_attributes(params[:subscription_line_item])
@subscription_line_item.price_estimate = price_estimate
render json: @subscription_line_item, serializer: Api::Admin::SubscriptionLineItemSerializer
render json: @subscription_line_item, serializer: Api::Admin::SubscriptionLineItemSerializer,
shop: @shop, schedule: @schedule
end
private
@@ -26,7 +27,7 @@ module Admin
@shop = Enterprise.managed_by(spree_current_user).find_by_id(params[:shop_id])
@schedule = permissions.editable_schedules.find_by_id(params[:schedule_id])
@order_cycle = @schedule.andand.current_or_next_order_cycle
@variant = Spree::Variant.stockable_by(@shop).find_by_id(params[:subscription_line_item][:variant_id])
@variant = variant_if_eligible(params[:subscription_line_item][:variant_id]) if @shop.present?
end
def new_actions
@@ -50,5 +51,9 @@ module Admin
OpenFoodNetwork::ScopeVariantToHub.new(@shop).scope(@variant)
@variant.price + fee_calculator.indexed_fees_for(@variant)
end
def variant_if_eligible(variant_id)
SubscriptionVariantsService.eligible_variants(@shop).find_by_id(variant_id)
end
end
end

View File

@@ -2,6 +2,7 @@ module Spree
module Admin
class InvoicesController < Spree::Admin::BaseController
respond_to :json
authorize_resource class: false
def create
invoice_service = BulkInvoiceService.new

View File

@@ -0,0 +1,66 @@
module Spree
module Admin
module Reports
class EnterpriseFeeSummariesController < BaseController
before_filter :load_report_parameters
before_filter :load_permissions
def new; end
def create
return respond_to_invalid_parameters unless @report_parameters.valid?
@report_parameters.authorize!(@permissions)
@report = report_klass::ReportService.new(@permissions, @report_parameters)
renderer.render(self)
rescue ::Reports::Authorizer::ParameterNotAllowedError => e
flash[:error] = e.message
render_report_form
end
private
def respond_to_invalid_parameters
flash[:error] = I18n.t("invalid_filter_parameters", scope: i18n_scope)
render_report_form
end
def i18n_scope
"order_management.reports.enterprise_fee_summary"
end
def render_report_form
render action: :new
end
def report_klass
OrderManagement::Reports::EnterpriseFeeSummary
end
def load_report_parameters
@report_parameters = report_klass::Parameters.new(params[:report] || {})
end
def load_permissions
@permissions = report_klass::Permissions.new(spree_current_user)
end
def report_renderer_klass
case params[:report_format]
when "csv"
report_klass::Renderers::CsvRenderer
when nil, "", "html"
report_klass::Renderers::HtmlRenderer
else
raise Reports::UnsupportedReportFormatException
end
end
def renderer
@renderer ||= report_renderer_klass.new(@report)
end
end
end
end
end

View File

@@ -1,4 +1,6 @@
require 'csv'
require 'open_food_network/reports/list'
require 'open_food_network/order_and_distributor_report'
require 'open_food_network/products_and_inventory_report'
require 'open_food_network/lettuce_share_report'
@@ -24,35 +26,7 @@ Spree::Admin::ReportsController.class_eval do
before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management, :packing]
def report_types
{
orders_and_fulfillment: [
[I18n.t('admin.reports.supplier_totals'), :order_cycle_supplier_totals],
[I18n.t('admin.reports.supplier_totals_by_distributor'), :order_cycle_supplier_totals_by_distributor],
[I18n.t('admin.reports.totals_by_supplier'), :order_cycle_distributor_totals_by_supplier],
[I18n.t('admin.reports.customer_totals'), :order_cycle_customer_totals]
],
products_and_inventory: [
[I18n.t('admin.reports.all_products'), :all_products],
[I18n.t('admin.reports.inventory'), :inventory],
[I18n.t('admin.reports.lettuce_share'), :lettuce_share]
],
customers: [
[I18n.t('admin.reports.mailing_list'), :mailing_list],
[I18n.t('admin.reports.addresses'), :addresses]
],
order_cycle_management: [
[I18n.t('admin.reports.payment_methods'), :payment_methods],
[I18n.t('admin.reports.delivery'), :delivery]
],
sales_tax: [
[I18n.t('admin.reports.tax_types'), :tax_types],
[I18n.t('admin.reports.tax_rates'), :tax_rates]
],
packing: [
[I18n.t('admin.reports.pack_by_customer'), :pack_by_customer],
[I18n.t('admin.reports.pack_by_supplier'), :pack_by_supplier]
]
}
OpenFoodNetwork::Reports::List.all
end
# Override spree reports list.
@@ -275,6 +249,7 @@ Spree::Admin::ReportsController.class_eval do
:products_and_inventory,
:sales_total,
:users_and_enterprises,
:enterprise_fee_summary,
:order_cycle_management,
:sales_tax,
:xero_invoices,
@@ -295,7 +270,13 @@ Spree::Admin::ReportsController.class_eval do
locals: { report_types: report_types[report] }
).html_safe
end
{ name: name, description: description }
{ name: name, url: url_for_report(report), description: description }
end
def url_for_report(report)
public_send("#{report}_admin_reports_url".to_sym)
rescue NoMethodError
url_for([:new, :admin, :reports, report.to_s.singularize])
end
def timestamp

View File

@@ -14,6 +14,13 @@ class FeatureFlags
superadmin?
end
# Checks whether the "Enterprise Fee Summary" is enabled for the specified user
#
# @return [Boolean]
def enterprise_fee_summary_enabled?
superadmin?
end
private
attr_reader :user

View File

@@ -193,7 +193,7 @@ module ProductImport
products.flat_map(&:variants).each do |existing_variant|
unit_scale = existing_variant.product.variant_unit_scale
unscaled_units = entry.unscaled_units || 0
entry.unit_value = unscaled_units * unit_scale
entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil?
if entry_matches_existing_variant?(entry, existing_variant)
variant_override = create_inventory_item(entry, existing_variant)

View File

@@ -182,7 +182,10 @@ class AbilityDecorator
can [:admin, :index, :guide, :import, :save, :save_data, :validate_data, :reset_absent_products], ProductImport::ProductImporter
# Reports page
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing], :report
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments,
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing],
:report
add_enterprise_fee_summary_abilities(user)
end
def add_order_cycle_management_abilities(user)
@@ -208,9 +211,10 @@ class AbilityDecorator
# during the order creation process from the admin backend
order.distributor.nil? || user.enterprises.include?(order.distributor) || order.order_cycle.andand.coordinated_by?(user)
end
can [:admin, :bulk_management, :managed, :bulk_invoice], Spree::Order do
can [:admin, :bulk_management, :managed], Spree::Order do
user.admin? || user.enterprises.any?(&:is_distributor)
end
can [:admin, :create, :show, :poll], :invoice
can [:admin, :visible], Enterprise
can [:admin, :index, :create, :update, :destroy], :line_item
can [:admin, :index, :create], Spree::LineItem
@@ -254,7 +258,10 @@ class AbilityDecorator
end
# Reports page
can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], :report
can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments,
:orders_and_distributors, :orders_and_fulfillment, :products_and_inventory,
:order_cycle_management, :xero_invoices], :report
add_enterprise_fee_summary_abilities(user)
can [:create], Customer
can [:admin, :index, :update, :destroy, :show], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id)
@@ -277,6 +284,16 @@ class AbilityDecorator
user.enterprises.include? enterprise_relationship.parent
end
end
def add_enterprise_fee_summary_abilities(user)
feature_enabled = FeatureFlags.new(user).enterprise_fee_summary_enabled?
return unless feature_enabled
# Reveal the report link in spree/admin/reports#index
can [:enterprise_fee_summary], :report
# Allow direct access to the report resource
can [:admin, :new, :create], :enterprise_fee_summary
end
end
Spree::Ability.register_ability(AbilityDecorator)

View File

@@ -25,6 +25,11 @@ Spree::PaymentMethod.class_eval do
end
}
scope :for_distributors, ->(distributors) {
non_unique_matches = unscoped.joins(:distributors).where(enterprises: { id: distributors })
where(id: non_unique_matches.map(&:id))
}
scope :for_distributor, lambda { |distributor|
joins(:distributors).
where('enterprises.id = ?', distributor)

View File

@@ -20,6 +20,10 @@ Spree::ShippingMethod.class_eval do
end
}
scope :for_distributors, ->(distributors) {
non_unique_matches = unscoped.joins(:distributors).where(enterprises: { id: distributors })
where(id: non_unique_matches.map(&:id))
}
scope :for_distributor, lambda { |distributor|
joins(:distributors).
where('enterprises.id = ?', distributor)

View File

@@ -1,7 +1,8 @@
module Api
module Admin
class SubscriptionLineItemSerializer < ActiveModel::Serializer
attributes :id, :variant_id, :quantity, :description, :price_estimate
attributes :id, :variant_id, :quantity, :description, :price_estimate,
:in_open_and_upcoming_order_cycles
def description
"#{object.variant.product.name} - #{object.variant.full_name}"
@@ -10,6 +11,22 @@ module Api
def price_estimate
object.price_estimate.andand.to_f || "?"
end
def in_open_and_upcoming_order_cycles
SubscriptionVariantsService.in_open_and_upcoming_order_cycles?(option_or_assigned_shop,
option_or_assigned_schedule,
object.variant)
end
private
def option_or_assigned_shop
@options[:shop] || object.subscription.andand.shop
end
def option_or_assigned_schedule
@options[:schedule] || object.subscription.andand.schedule
end
end
end
end

View File

@@ -97,15 +97,12 @@ class SubscriptionValidator
errors.add(:subscription_line_items, :not_available, name: name)
end
# TODO: Extract this into a separate class
def available_variant_ids
@available_variant_ids ||=
Spree::Variant.joins(exchanges: { order_cycle: :schedules })
.where(id: subscription_line_items.map(&:variant_id))
.where(schedules: { id: schedule }, exchanges: { incoming: false, receiver_id: shop })
.merge(OrderCycle.not_closed)
.select('DISTINCT spree_variants.id')
.pluck(:id)
return @available_variant_ids if @available_variant_ids.present?
subscription_variant_ids = subscription_line_items.map(&:variant_id)
@available_variant_ids = SubscriptionVariantsService.eligible_variants(shop)
.where(id: subscription_variant_ids).pluck(:id)
end
def build_msg_from(k, msg)

View File

@@ -0,0 +1,39 @@
class SubscriptionVariantsService
# Includes the following variants:
# - Variants of permitted producers
# - Variants of hub
# - Variants that are in outgoing exchanges where the hub is receiver
def self.eligible_variants(distributor)
variant_conditions = ["spree_products.supplier_id IN (?)", permitted_producer_ids(distributor)]
exchange_variant_ids = outgoing_exchange_variant_ids(distributor)
if exchange_variant_ids.present?
variant_conditions[0] << " OR spree_variants.id IN (?)"
variant_conditions << exchange_variant_ids
end
Spree::Variant.joins(:product).where(is_master: false).where(*variant_conditions)
end
def self.in_open_and_upcoming_order_cycles?(distributor, schedule, variant)
scope = ExchangeVariant.joins(exchange: { order_cycle: :schedules })
.where(variant_id: variant, exchanges: { incoming: false, receiver_id: distributor })
.merge(OrderCycle.not_closed)
scope = scope.where(schedules: { id: schedule })
scope.any?
end
def self.permitted_producer_ids(distributor)
other_permitted_producer_ids = EnterpriseRelationship.joins(:parent)
.permitting(distributor).with_permission(:add_to_order_cycle)
.merge(Enterprise.is_primary_producer)
.pluck(:parent_id)
other_permitted_producer_ids | [distributor.id]
end
def self.outgoing_exchange_variant_ids(distributor)
ExchangeVariant.select("DISTINCT exchange_variants.variant_id").joins(:exchange)
.where(exchanges: { incoming: false, receiver_id: distributor.id })
.pluck(:variant_id)
end
end

View File

@@ -0,0 +1,63 @@
# Validates a datetime string with relaxed rules
#
# This uses ActiveSupport::TimeZone.parse behind the scenes.
#
# https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-parse
#
# === Example
#
# class Post
# include ActiveModel::Validations
#
# attr_accessor :published_at
# validates :published_at, date_time_string: true
# end
#
# post = Post.new
#
# post.published_at = nil
# post.valid? # => true
#
# post.published_at = ""
# post.valid? # => true
#
# post.published_at = []
# post.valid? # => false
# post.errors[:published_at] # => ["must be a string"]
#
# post.published_at = 1
# post.valid? # => false
# post.errors[:published_at] # => ["must be a string"]
#
# post.published_at = "2018-09-20 01:02:00 +10:00"
# post.valid? # => true
#
# post.published_at = "Not Valid"
# post.valid? # => false
# post.errors[:published_at] # => ["must be valid"]
class DateTimeStringValidator < ActiveModel::EachValidator
NOT_STRING_ERROR = I18n.t("validators.date_time_string_validator.not_string_error")
INVALID_FORMAT_ERROR = I18n.t("validators.date_time_string_validator.invalid_format_error")
def validate_each(record, attribute, value)
return if value.nil? || value == ""
validate_attribute_is_string(record, attribute, value)
validate_attribute_is_datetime_string(record, attribute, value)
end
protected
def validate_attribute_is_string(record, attribute, value)
return if value.is_a?(String)
record.errors.add(attribute, NOT_STRING_ERROR)
end
def validate_attribute_is_datetime_string(record, attribute, value)
return unless value.is_a?(String)
datetime = Time.zone.parse(value)
record.errors.add(attribute, INVALID_FORMAT_ERROR) if datetime.blank?
end
end

View File

@@ -0,0 +1,63 @@
# Validates an integer array
#
# This uses Integer() behind the scenes.
#
# === Example
#
# class Post
# include ActiveModel::Validations
#
# attr_accessor :related_post_ids
# validates :related_post_ids, integer_array: true
# end
#
# post = Post.new
#
# post.related_post_ids = nil
# post.valid? # => true
#
# post.related_post_ids = []
# post.valid? # => true
#
# post.related_post_ids = 1
# post.valid? # => false
# post.errors[:related_post_ids] # => ["must be an array"]
#
# post.related_post_ids = [1, 2, 3]
# post.valid? # => true
#
# post.related_post_ids = ["1", "2", "3"]
# post.valid? # => true
#
# post.related_post_ids = [1, "2", "Not Integer", 3]
# post.valid? # => false
# post.errors[:related_post_ids] # => ["must contain only valid integers"]
class IntegerArrayValidator < ActiveModel::EachValidator
NOT_ARRAY_ERROR = I18n.t("validators.integer_array_validator.not_array_error")
INVALID_ELEMENT_ERROR = I18n.t("validators.integer_array_validator.invalid_element_error")
def validate_each(record, attribute, value)
return if value.nil?
validate_attribute_is_array(record, attribute, value)
validate_attribute_elements_are_integer(record, attribute, value)
end
protected
def validate_attribute_is_array(record, attribute, value)
return if value.is_a?(Array)
record.errors.add(attribute, NOT_ARRAY_ERROR)
end
def validate_attribute_elements_are_integer(record, attribute, array)
return unless array.is_a?(Array)
array.each do |element|
Integer(element)
end
rescue ArgumentError
record.errors.add(attribute, INVALID_ELEMENT_ERROR)
end
end

View File

@@ -56,7 +56,7 @@
%input#edit-products{ type: "button", value: t(:edit), ng: { click: "setView('products')" } }
.row
.seven.columns.alpha.omega
%table#subscription-line-items
%table#subscription-line-items.admin-subscription-review-subscription-line-items
%colgroup
%col{:style => "width: 62%;"}/
%col{:style => "width: 14%;"}/
@@ -71,7 +71,10 @@
%span= t(:total)
%tbody
%tr.item{ id: "sli_{{$index}}", ng: { repeat: "item in subscription.subscription_line_items | filter:{ _destroy: '!true' }", class: { even: 'even', odd: 'odd' } } }
%td.description {{ item.description }}
%td
.description {{ item.description }}
.not-in-open-and-upcoming-order-cycles-warning{ ng: { if: '!item.in_open_and_upcoming_order_cycles' } }
= t(".no_open_or_upcoming_order_cycle")
%td.price.align-center {{ item.price_estimate | currency }}
%td.quantity {{ item.quantity }}
%td.total.align-center {{ (item.price_estimate * item.quantity) | currency }}

View File

@@ -1,4 +1,4 @@
%table#subscription-line-items
%table#subscription-line-items.admin-subscription-form-subscription-line-items
%colgroup
%col{:style => "width: 49%;"}/
%col{:style => "width: 14%;"}/
@@ -15,7 +15,10 @@
%th.orders-actions.actions
%tbody
%tr.item{ id: "sli_{{$index}}", ng: { repeat: "item in subscription.subscription_line_items | filter:{ _destroy: '!true' }", class: { even: 'even', odd: 'odd' } } }
%td.description {{ item.description }}
%td
.description {{ item.description }}
.not-in-open-and-upcoming-order-cycles-warning{ ng: { if: '!item.in_open_and_upcoming_order_cycles' } }
= t(".not_in_open_and_upcoming_order_cycles_warning")
%td.price.align-center {{ item.price_estimate | currency }}
%td.quantity
%input{ name: 'quantity', type: 'number', min: 0, ng: { model: 'item.quantity' } }

View File

@@ -112,7 +112,7 @@
.margin-bottom-50{ 'ng-hide' => 'RequestMonitor.loading || filteredLineItems.length == 0' }
%form{ name: 'bulk_order_form' }
%table.index#listing_orders.bulk{ :class => "sixteen columns alpha" }
%table.index#listing_orders.bulk{ :class => "sixteen columns alpha", ng: { show: "initialized" } }
%thead
%tr{ ng: { controller: "ColumnsCtrl" } }
%th.bulk
@@ -157,7 +157,7 @@
= t("admin.orders.bulk_management.ask")
%input{ :type => 'checkbox', 'ng-model' => "confirmDelete" }
%tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:sorting.predicate:sorting.reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" }
%tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:sorting.predicate:sorting.reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", ng: { attr: { id: "li_{{line_item.id}}" } } }
%td.bulk
%input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked', 'ignore-dirty' => true }
%td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }}
@@ -181,6 +181,6 @@
%input.show-dirty{ :type => 'text', :name => 'price', :id => 'price', :ng => { value: 'line_item.price * line_item.quantity | currency:""', readonly: "true", class: '{"update-error": line_item.errors.price}' } }
%span.error{ ng: { bind: 'line_item.errors.price' } }
%td.actions
%a{ href: "/admin/orders/{{line_item.order.number}}/edit", :class => "edit-order icon-edit no-text", 'confirm-link-click' => 'confirmRefresh()' }
%a{ ng: { href: "/admin/orders/{{line_item.order.number}}/edit" }, :class => "edit-order icon-edit no-text", 'confirm-link-click' => 'confirmRefresh()' }
%td.actions
%a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" }

View File

@@ -12,7 +12,7 @@
- if @product_count > 0
%div.seven.columns.alpha.list-item
%span.six.columns.alpha
= "You have #{@product_count} active product#{@product_count > 1 ? "s" : ""}."
= t(".active_products", count: @product_count )
%span.one.column.omega
%span.icon-ok-sign
%a.seven.columns.alpha.button.bottom.blue{ href: "#{admin_products_path}" }
@@ -21,7 +21,7 @@
- else
%div.seven.columns.alpha.list-item.red
%span.six.columns.alpha
= t "spree_admin_enterprises_any_active_products_text"
= t(".active_products", count: @product_count )
%span.one.column.omega
%span.icon-remove-sign
%a.seven.columns.alpha.button.bottom.red{ href: "#{new_admin_product_path}" }

View File

@@ -0,0 +1,50 @@
= form_for @report_parameters, as: :report, url: spree.admin_reports_enterprise_fee_summary_path, method: :post do |f|
.row.date-range-filter
.sixteen.columns.alpha
= label_tag nil, t(".date_range")
%br
= f.label :start_at, class: "inline"
= f.text_field :start_at, class: "datetimepicker datepicker-from"
%span.range-divider
%i.icon-arrow-right
= f.text_field :end_at, class: "datetimepicker datepicker-to"
= f.label :end_at, class: "inline"
.row
.sixteen.columns.alpha
= f.label :distributor_ids
= f.collection_select(:distributor_ids, @permissions.allowed_distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.row
.sixteen.columns.alpha
= f.label :producer_ids
= f.collection_select(:producer_ids, @permissions.allowed_producers, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.row
.sixteen.columns.alpha
= f.label :order_cycle_ids
= f.collection_select(:order_cycle_ids, @permissions.allowed_order_cycles, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.row
.eight.columns.alpha
= f.label :enterprise_fee_ids
= f.collection_select(:enterprise_fee_ids, @permissions.allowed_enterprise_fees, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.eight.columns.omega
= f.label :shipping_method_ids
= f.collection_select(:shipping_method_ids, @permissions.allowed_shipping_methods, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.row
.eight.columns.alpha &nbsp;
.eight.columns.omega
= f.label :payment_method_ids
= f.collection_select(:payment_method_ids, @permissions.allowed_payment_methods, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.row
.sixteen.columns.alpha
= check_box_tag :report_format, "csv", false, id: "report_format_csv"
= label_tag :report_format_csv, t(".report_format_csv")
= button t(".generate_report")

View File

@@ -0,0 +1,19 @@
- if @report.present?
%table#enterprise_fee_summary_report.report__table
%thead
%tr
- @renderer.header.each do |heading|
%th= heading
%tbody
- @renderer.data_rows.each do |row|
%tr
- row.each do |cell_value|
%td= cell_value
- if @renderer.data_rows.empty?
%tr
%td{colspan: @renderer.header.length}= t(:none)
- else
%p.report__message
= t(".select_and_search")

View File

@@ -0,0 +1,2 @@
= render "filters"
= render "report"

View File

@@ -0,0 +1 @@
= render "filters"

View File

@@ -0,0 +1,21 @@
<% content_for :page_title do %>
<%= t(:listing_reports) %>
<% end %>
<table class="index">
<thead>
<tr data-hook="reports_header">
<th><%= t(:name) %></th>
<th><%= t(:description) %></th>
</tr>
</thead>
<tbody>
<% @reports.each do |key, value| %>
<tr data-hook="reports_row">
<td><%= link_to value[:name], value[:url] %></td>
<td><%= value[:description] %></td>
</tr>
<% end %>
</tbody>
</table>

2649
config/locales/ca.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -54,6 +54,16 @@ en:
on_demand_but_count_on_hand_set: "must be blank if on demand"
limited_stock_but_no_count_on_hand: "must be specified because forcing limited stock"
activemodel:
attributes:
order_management/reports/enterprise_fee_summary/parameters:
start_at: "Start"
end_at: "End"
distributor_ids: "Hubs"
producer_ids: "Producers"
order_cycle_ids: "Order Cycles"
enterprise_fee_ids: "Fees Names"
shipping_method_ids: "Shipping Methods"
payment_method_ids: "Payment Methods"
errors:
models:
subscription_validator:
@@ -100,6 +110,14 @@ en:
order_cycle:
cloned_order_cycle_name: "COPY OF %{order_cycle}"
validators:
date_time_string_validator:
not_string_error: "must be a string"
invalid_format_error: "must be valid"
integer_array_validator:
not_array_error: "must be an array"
invalid_element_error: "must contain only valid integers"
enterprise_mailer:
confirmation_instructions:
subject: "Please confirm the email address for %{enterprise}"
@@ -262,6 +280,7 @@ en:
actions:
create_and_add_another: "Create and Add Another"
create: "Create"
admin:
# Common properties / models
begins_at: Begins At
@@ -1055,7 +1074,9 @@ en:
description: Invoices for import into Xero
packing:
name: Packing Reports
enterprise_fee_summary:
name: "Enterprise Fee Summary"
description: "Summary of Enterprise Fees collected"
subscriptions:
subscriptions: Subscriptions
new: New Subscription
@@ -1089,6 +1110,7 @@ en:
this_is_an_estimate: |
The displayed prices are only an estimate and calculated at the time the subscription is changed.
If you change prices or fees, orders will be updated, but the subscription will still display the old values.
not_in_open_and_upcoming_order_cycles_warning: "There are no open or upcoming order cycles for this product."
details:
details: Details
invalid_error: Oops! Please fill in all of the required fields...
@@ -1103,6 +1125,7 @@ en:
details: Details
address: Address
products: Products
no_open_or_upcoming_order_cycle: "No Upcoming Order Cycle"
product_already_in_order: This product has already been added to the order. Please edit the quantity directly.
orders:
number: Number
@@ -1348,7 +1371,6 @@ en:
ie_warning_firefox: Download Firefox
ie_warning_ie: Upgrade Internet Explorer
ie_warning_other: "Can't upgrade your browser? Try Open Food Network on your smartphone :-)"
legal:
cookies_policy:
header: "How We Use Cookies"
@@ -2156,7 +2178,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using
spree_admin_enterprises_none_text: "You don't have any enterprises yet"
spree_admin_enterprises_tabs_hubs: "HUBS"
spree_admin_enterprises_producers_manage_products: "MANAGE PRODUCTS"
spree_admin_enterprises_any_active_products_text: "You don't have any active products."
spree_admin_enterprises_create_new_product: "CREATE A NEW PRODUCT"
spree_admin_single_enterprise_alert_mail_confirmation: "Please confirm the email address for"
spree_admin_single_enterprise_alert_mail_sent: "We've sent an email to"
@@ -2684,6 +2705,44 @@ See the %{link} to find out more about %{sitename}'s features and to start using
signup:
start_free_profile: "Start with a free profile, and expand when you're ready!"
order_management:
reports:
enterprise_fee_summary:
date_end_before_start_error: "must be after start"
parameter_not_allowed_error: "You are not authorized to use one or more selected filters for this report."
fee_calculated_on_transfer_through_all: "All"
fee_type:
payment_method: "Payment Transaction"
shipping_method: "Shipment"
fee_placements:
supplier: "Incoming"
distributor: "Outgoing"
coordinator: "Coordinator"
tax_category_name:
shipping_instance_rate: "Platform Rate"
formats:
csv:
header:
fee_type: "Fee Type"
enterprise_name: "Enterprise Owner"
fee_name: "Fee Name"
customer_name: "Customer"
fee_placement: "Fee Placement"
fee_calculated_on_transfer_through_name: "Fee Calc on Transfer Through"
tax_category_name: "Tax Category"
total_amount: "$$ SUM"
html:
header:
fee_type: "Fee Type"
enterprise_name: "Enterprise Owner"
fee_name: "Fee Name"
customer_name: "Customer"
fee_placement: "Fee Placement"
fee_calculated_on_transfer_through_name: "Fee Calc on Transfer Through"
tax_category_name: "Tax Category"
total_amount: "$$ SUM"
invalid_filter_parameters: "The filters you selected for this report are invalid."
spree:
# TODO: remove `email` key once we get to Spree 2.0
email: Email
@@ -2752,6 +2811,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using
distributor: "Distributor:"
order_cycle: "Order cycle:"
overview:
products:
active_products:
zero: "You don't have any active products."
one: "You have one active product"
other: "You have %{count} active products"
order_cycles:
order_cycles: "Order Cycles"
order_cycles_tip: "Order cycles determine when and where your products are available to customers."
@@ -2825,6 +2889,14 @@ See the %{link} to find out more about %{sitename}'s features and to start using
bulk_coop_allocation: 'Bulk Co-op - Allocation'
bulk_coop_packing_sheets: 'Bulk Co-op - Packing Sheets'
bulk_coop_customer_payments: 'Bulk Co-op - Customer Payments'
enterprise_fee_summaries:
filters:
date_range: "Date Range"
report_format_csv: "Download as CSV"
generate_report: "Generate Report"
report:
none: "None"
select_and_search: "Select filters and click on GENERATE REPORT to access your data."
users:
index:
listing_users: "Listing Users"

View File

@@ -1105,7 +1105,7 @@ fr_BE:
footer_legal_call: "Lire nos"
footer_legal_tos: "Termes et conditions"
footer_legal_visit: "Nous trouver sur"
footer_legal_text_html: "Open Food Network est une plateforme logicielle open source, libre et gratuite. Nos données sont protégées sous licence %{content_license} et notre code sous %{code_license}."
footer_legal_text_html: "Open Food Network est une plateforme logicielle open source et libre. Nos données sont protégées sous licence %{content_license} et notre code sous %{code_license}."
footer_data_text_with_privacy_policy_html: "Nous prenons soin de vos données. Voir notre %{privacy_policy} et %{cookies_policy}."
footer_data_text_without_privacy_policy_html: "Nous prenons soin de vos données. Voir notre %{cookies_policy}."
footer_data_privacy_policy: "politique de confidentialité"

View File

@@ -602,6 +602,7 @@ it:
desc_long_placeholder: Racconta di te ai consumatori. Questa informazione comparirà nel tuo profilo pubblico.
business_details:
abn: ABN
abn_placeholder: es. 99 123 456 789
acn: ACN
acn_placeholder: es. 123 456 789
display_invoice_logo: Mostra logo nelle fatture
@@ -1131,6 +1132,7 @@ it:
total_excl_tax: "Totale (Tasse escl.):"
total_incl_tax: "Totale (Tasse incl.):"
abn: "CF:"
acn: "ACN:"
invoice_issued_on: "Fattura emessa il"
order_number: "Numero fattura:"
date_of_transaction: "Data della transazione:"
@@ -1149,7 +1151,9 @@ it:
menu_5_title: "About"
menu_5_url: "http://www.openfoodnetwork.org/"
menu_6_title: "Connetti"
menu_6_url: "https://openfoodnetwork.org/au/connect/"
menu_7_title: "Impara"
menu_7_url: "https://openfoodnetwork.org/au/learn/"
logo: "Logo (640x130)"
logo_mobile: "Mobile logo (75x26)"
logo_mobile_svg: "Mobile logo (SVG)"
@@ -2216,6 +2220,9 @@ it:
validation_msg_tax: "^Categoria d'imposta richiesta"
validation_msg_tax_category_cant_be_blank: "^Categoria di tassa non può essere vuoto"
validation_msg_is_associated_with_an_exising_customer: "è associato con un cliente esistente"
content_configuration_pricing_table: "(TODO: tabella dei prezzi)"
content_configuration_case_studies: "(TODO: Casi studio)"
content_configuration_detail: "(TODO: Dettaglio)"
enterprise_name_error: "è già stato utilizzato. Se questo è il nome della tua azienda e vorresti reclamarne la proprietà, o se vuoi contattare questa azienda, puoi contattare l'attuale referente di questo profilo a %{email}."
enterprise_owner_error: "^%{email}non può gestire altre aziende (il limite è %{enterprise_limit})."
enterprise_role_uniqueness_error: "^Questo ruolo è già presente."
@@ -2253,6 +2260,7 @@ it:
back_to_orders_list: "Indietro alla lista delle gentili richieste"
no_orders_found: "Nessuna gentile richiesta trovata"
order_information: "Informazioni Gentile Richiesta"
date_completed: "Data di completamento"
amount: "Quantità"
state_names:
ready: Pronto
@@ -2275,6 +2283,7 @@ it:
choose: Scegli
resolve_errors: 'Per favore risolvi i seguenti errori:'
more_items: "+ %{count} ancora"
default_card_updated: Carta predefinita aggiornata
admin:
enterprise_limit_reached: "Hai raggiunto il limite standard di aziende per account. Scrivi a %{contact_email} se hai bisogno di aumentarlo."
modals:
@@ -2368,14 +2377,18 @@ it:
non_producer_example: es. Botteghe, Food Coop, GAS
enterprise_status:
status_title: "%{name} è impostato e pronto a partire!"
severity: Gravità
description: Descrizione
resolve: Risolvi
new_tag_rule_dialog:
select_rule_type: "Seleziona un tipo di regola:"
orders:
index:
per_page: "%{results} per pagina"
view_file: Vedi file
compiling_invoices: Compilazione fatture
bulk_invoice_created: Fattura all'ingrosso creata
bulk_invoice_failed: Creazione fattura all'ingrosso fallita
please_wait: Si prega di attendere che il PDF sia pronto prima di chiudere questo modale
resend_user_email_confirmation:
resend: "Invia nuovamente"
@@ -2394,6 +2407,7 @@ it:
'yes': "A richiesta"
variant_overrides:
on_demand:
use_producer_settings: "Usa le impostazioni dello stock del produttore"
'yes': "Sì"
'no': "No"
inventory_products: "Inventario Prodotti"
@@ -2401,6 +2415,9 @@ it:
new_products: "Nuovi Prodotti"
reset_stock_levels: Resetta le quantità disponibili alla quantità predefinita
changes_to: Cambia in
one_override: una si è sovrascritta
overrides: sovrascrive
remain_unsaved: restano da memorizzare
no_changes_to_save: Nessuna modifica da salvare.
no_authorisation: "Non abbiamo l'autorizzazione per salvare queste modifiche. "
some_trouble: "Abbiamo avuto problemi durante il salvataggio: %{errors}"
@@ -2449,16 +2466,23 @@ it:
admin:
orders:
index:
listing_orders: "Listino Ordini"
new_order: "Nuovo ordine"
capture: "Cattura"
ship: "Spedizione"
edit: "Modifica"
note: "Nota"
first: "Primo"
last: "Ultimo"
previous: "Precedente"
next: "Prossimo"
loading: "Caricamento"
no_orders_found: "Nessuna gentile richiesta trovata"
results_found: "%{number} Risultati trovati."
viewing: "Guardando da %{start} a %{end}"
print_invoices: "Stampa fatture"
invoice:
issued_on: Emesso il
tax_invoice: FATTURA DELLE TASSE
code: Codice
from: Da
@@ -2490,12 +2514,18 @@ it:
stripe_disabled_msg: I pagamenti Stripe sono stati disabilitati dall'amministratore di sistema.
request_failed_msg: Spiacenti, qualcosa è andato storto mentre cercavamo di verificare i dettagli dell'account con Stripe...
account_missing_msg: Non esiste un account Stripe per questa azienda.
connect_one: Connetti One
access_revoked_msg: L'accesso a questo account Stripe è stato revocato, per favore ricollega il tuo account.
status: Stato
connected: Connesso
account_id: Account ID
business_name: Ragione sociale
charges_enabled: Cambi Consentiti
payments:
source_forms:
stripe:
error_saving_payment: Errore memorizzando il pagamento
submitting_payment: Eseguendo il pagamento
products:
new:
title: 'Nuovo prodotto'
@@ -2514,7 +2544,9 @@ it:
inherits_properties?: Eredita proprietà?
available_on: Disponibile il
av_on: "Disp. il"
import_date: "Data di importazione"
products_variant:
variant_has_n_overrides: "Questa variante ha %{n} sostituzione/i"
new_variant: "Nuova variante"
product_name: Nome Prodotto
primary_taxon_form:
@@ -2523,14 +2555,19 @@ it:
group_buy: "Acquisto di gruppo?"
display_as:
display_as: Visualizza come
reports:
table:
select_and_search: "Seleziona filtri e clicca su %{option} per accedere al tuo dato"
users:
index:
listing_users: "Elenco Utenti"
new_user: "Nuovo utente"
user: "Utente"
enterprise_limit: "Limite azienda"
search: "Cerca"
email: "Email"
edit:
editing_user: "Modificando Utente"
back_to_users_list: "Torna all'elenco degli utenti"
general_settings: "Impostazioni generali"
form:
@@ -2548,6 +2585,7 @@ it:
edit:
legal_settings: "Impostazioni Legali"
cookies_consent_banner_toggle: "Mostra banner di consenso per i cookie"
privacy_policy_url: "Privacy Policy URL"
enterprises_require_tos: "Le aziende devono accettare i Termini di Servizio"
cookies_policy_matomo_section: "Visualizza la sezione di Matomo nella pagina della cookie policy"
cookies_policy_ga_section: "Visualizza la sezione di Google Analytics nella pagina della cookie policy"
@@ -2556,7 +2594,12 @@ it:
payment:
stripe:
choose_one: Scegli uno
enter_new_card: Inserire dettagli per una nuova carta
used_saved_card: "usare la carta salvata"
or_enter_new_card: "Oppure, inserire dettagli di una nuova carta"
remember_this_card: Ricordare questa Carta?
date_picker:
format: '%Y-%m-%d'
js_format: 'aa-mm-gg'
inventory: Inventario
orders:
@@ -2567,6 +2610,7 @@ it:
order_mailer:
invoice_email:
hi: "Ciao %{name}"
invoice_attached_text: 'Aggiunge una fattura per il tuo recente ordine di '
order_state:
address: indirizzo
adjustments: aggiustamenti
@@ -2659,5 +2703,6 @@ it:
authorised_shops: Negozi autorizzati
authorised_shops:
shop_name: "Nome del negozio"
allow_charges?: "Consentire ricarichi"
localized_number:
invalid_format: 'Formato non valido: inserire un numero.'

View File

@@ -1027,6 +1027,7 @@ nb:
this_is_an_estimate: |
De viste prisene er bare et estimat og beregnet på det tidspunktet abonnementet endres.
Hvis du endrer priser eller avgifter, vil ordrer bli oppdatert, men abonnementet vil fortsatt vise de gamle verdiene.
not_in_open_and_upcoming_order_cycles_warning: "Det er ingen åpne eller kommende bestillingsrunder for dette produktet."
details:
details: Detaljer
invalid_error: Oops! Vennligst fyll inn alle obligatoriske felter...
@@ -1041,6 +1042,7 @@ nb:
details: Detaljer
address: Adresse
products: Produkter
no_open_or_upcoming_order_cycle: "Ingen kommende bestillingsrunde"
product_already_in_order: Dette produktet er allerede lagt til i bestillingen. Vennligst rediger mengden direkte.
orders:
number: Antall

View File

@@ -118,8 +118,9 @@ Openfoodnetwork::Application.routes.draw do
get 'sitemap.xml', to: 'sitemap#index', defaults: { format: 'xml' }
# Mount Web engine routes
# Mount engine routes
mount Web::Engine, :at => '/'
mount OrderManagement::Engine, :at => '/'
# Mount Spree's routes
mount Spree::Core::Engine, :at => '/'

View File

@@ -31,7 +31,6 @@ Spree::Core::Engine.routes.prepend do
resources :credit_cards
namespace :api, :defaults => { :format => 'json' } do
resources :users do
get :authorise_api, on: :collection

View File

@@ -0,0 +1,5 @@
# Order Management
This is the rails engine for the Order Management domain.
See our wiki for [more info about domains and engines in OFN](https://github.com/openfoodfoundation/openfoodnetwork/wiki/Tech-Doc:-How-OFN-is-organized-in-Domains-using-Rails-Engines).

View File

@@ -0,0 +1 @@
//= require_tree .

View File

@@ -0,0 +1,5 @@
module OrderManagement
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
end

View File

@@ -0,0 +1,39 @@
module OrderManagement
module Reports
module EnterpriseFeeSummary
class Authorizer < ::Reports::Authorizer
def self.parameter_not_allowed_error_message
i18n_scope = "order_management.reports.enterprise_fee_summary"
I18n.t("parameter_not_allowed_error", scope: i18n_scope)
end
def authorize!
authorize_by_distribution!
authorize_by_fee!
end
private
def authorize_by_distribution!
require_ids_allowed(parameters.order_cycle_ids, permissions.allowed_order_cycles)
require_ids_allowed(parameters.distributor_ids, permissions.allowed_distributors)
require_ids_allowed(parameters.producer_ids, permissions.allowed_producers)
end
def authorize_by_fee!
require_ids_allowed(parameters.enterprise_fee_ids, permissions.allowed_enterprise_fees)
require_ids_allowed(parameters.shipping_method_ids, permissions.allowed_shipping_methods)
require_ids_allowed(parameters.payment_method_ids, permissions.allowed_payment_methods)
end
def require_ids_allowed(array, allowed_objects)
error_klass = ::Reports::Authorizer::ParameterNotAllowedError
error_message = self.class.parameter_not_allowed_error_message
ids_allowed = (array - allowed_objects.map(&:id).map(&:to_s)).blank?
raise error_klass, error_message unless ids_allowed
end
end
end
end
end

View File

@@ -0,0 +1,87 @@
module OrderManagement
module Reports
module EnterpriseFeeSummary
class EnterpriseFeeTypeTotalSummarizer
attr_accessor :data
def initialize(data)
@data = data
end
def fee_type
if for_payment_method?
i18n_translate("fee_type.payment_method")
elsif for_shipping_method?
i18n_translate("fee_type.shipping_method")
else
data["fee_type"].try(:capitalize)
end
end
def enterprise_name
if for_payment_method?
data["hub_name"]
elsif for_shipping_method?
data["hub_name"]
else
data["enterprise_name"]
end
end
def fee_name
if for_payment_method?
data["payment_method_name"]
elsif for_shipping_method?
data["shipping_method_name"]
else
data["fee_name"]
end
end
def customer_name
data["customer_name"]
end
def fee_placement
return if for_payment_method? || for_shipping_method?
i18n_translate("fee_placements.#{data['placement_enterprise_role']}")
end
def fee_calculated_on_transfer_through_name
return if for_payment_method? || for_shipping_method?
transfer_through_all_string = i18n_translate("fee_calculated_on_transfer_through_all")
data["incoming_exchange_enterprise_name"] || data["outgoing_exchange_enterprise_name"] ||
(transfer_through_all_string if data["placement_enterprise_role"] == "coordinator")
end
def tax_category_name
return if for_payment_method?
return i18n_translate("tax_category_name.shipping_instance_rate") if for_shipping_method?
data["tax_category_name"] || data["product_tax_category_name"]
end
def total_amount
data["total_amount"]
end
private
def for_payment_method?
data["payment_method_name"].present?
end
def for_shipping_method?
data["shipping_method_name"].present?
end
def i18n_translate(translation_key)
I18n.t("order_management.reports.enterprise_fee_summary.#{translation_key}")
end
end
end
end
end

View File

@@ -0,0 +1,68 @@
module OrderManagement
module Reports
module EnterpriseFeeSummary
class Parameters < ::Reports::Parameters::Base
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::Validations
attr_accessor :start_at, :end_at, :distributor_ids, :producer_ids, :order_cycle_ids,
:enterprise_fee_ids, :shipping_method_ids, :payment_method_ids
before_validation :cleanup_arrays
validates :start_at, :end_at, date_time_string: true
validates :distributor_ids, :producer_ids, integer_array: true
validates :order_cycle_ids, integer_array: true
validates :enterprise_fee_ids, integer_array: true
validates :shipping_method_ids, :payment_method_ids, integer_array: true
validate :require_valid_datetime_range
def self.date_end_before_start_error_message
i18n_scope = "order_management.reports.enterprise_fee_summary"
I18n.t("date_end_before_start_error", scope: i18n_scope)
end
def initialize(attributes = {})
self.distributor_ids = []
self.producer_ids = []
self.order_cycle_ids = []
self.enterprise_fee_ids = []
self.shipping_method_ids = []
self.payment_method_ids = []
super(attributes)
end
def authorize!(permissions)
authorizer = Authorizer.new(self, permissions)
authorizer.authorize!
end
protected
def require_valid_datetime_range
return if start_at.blank? || end_at.blank?
error_message = self.class.date_end_before_start_error_message
errors.add(:end_at, error_message) unless start_at < end_at
end
# Remove the blank strings that Rails multiple selects add by default to
# make sure that blank lists are still submitted to the server as arrays
# instead of nil.
#
# https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-select
def cleanup_arrays
distributor_ids.reject!(&:blank?)
producer_ids.reject!(&:blank?)
order_cycle_ids.reject!(&:blank?)
enterprise_fee_ids.reject!(&:blank?)
shipping_method_ids.reject!(&:blank?)
payment_method_ids.reject!(&:blank?)
end
end
end
end
end

View File

@@ -0,0 +1,45 @@
module OrderManagement
module Reports
module EnterpriseFeeSummary
class Permissions < ::Reports::Permissions
def allowed_order_cycles
@allowed_order_cycles ||= OrderCycle.accessible_by(user)
end
def allowed_distributors
outgoing_exchanges = Exchange.outgoing.where(order_cycle_id: allowed_order_cycle_ids)
@allowed_distributors ||= Enterprise.where(id: outgoing_exchanges.pluck(:receiver_id))
end
def allowed_producers
incoming_exchanges = Exchange.incoming.where(order_cycle_id: allowed_order_cycle_ids)
@allowed_producers ||= Enterprise.where(id: incoming_exchanges.pluck(:sender_id))
end
def allowed_enterprise_fees
return EnterpriseFee.where("1=0") if allowed_order_cycles.blank?
coordinator_enterprise_fees = EnterpriseFee.joins(:coordinator_fees)
.where(coordinator_fees: { order_cycle_id: allowed_order_cycle_ids })
exchange_enterprise_fees = EnterpriseFee.joins(exchange_fees: :exchange)
.where(exchanges: { order_cycle_id: allowed_order_cycle_ids })
@allowed_enterprise_fees ||= (coordinator_enterprise_fees | exchange_enterprise_fees).uniq
end
def allowed_shipping_methods
@allowed_shipping_methods ||= Spree::ShippingMethod.for_distributors(allowed_distributors)
end
def allowed_payment_methods
@allowed_payment_methods ||= Spree::PaymentMethod.for_distributors(allowed_distributors)
end
private
def allowed_order_cycle_ids
@allowed_order_cycle_ids ||= allowed_order_cycles.map(&:id)
end
end
end
end
end

View File

@@ -0,0 +1,64 @@
module OrderManagement
module Reports
module EnterpriseFeeSummary
module Renderers
class CsvRenderer < ::Reports::Renderers::Base
def render(context)
context.send_data(generate, filename: filename)
end
def generate
CSV.generate do |csv|
render_header(csv)
report_data.list.each do |data|
render_data_row(csv, data)
end
end
end
private
def filename
timestamp = Time.zone.now.strftime("%Y%m%d")
"enterprise_fee_summary_#{timestamp}.csv"
end
def render_header(csv)
csv << [
header_label(:fee_type),
header_label(:enterprise_name),
header_label(:fee_name),
header_label(:customer_name),
header_label(:fee_placement),
header_label(:fee_calculated_on_transfer_through_name),
header_label(:tax_category_name),
header_label(:total_amount)
]
end
def render_data_row(csv, data)
csv << [
data.fee_type,
data.enterprise_name,
data.fee_name,
data.customer_name,
data.fee_placement,
data.fee_calculated_on_transfer_through_name,
data.tax_category_name,
data.total_amount
]
end
def header_label(attribute)
I18n.t("header.#{attribute}", scope: i18n_scope)
end
def i18n_scope
"order_management.reports.enterprise_fee_summary.formats.csv"
end
end
end
end
end
end

View File

@@ -0,0 +1,51 @@
module OrderManagement
module Reports
module EnterpriseFeeSummary
module Renderers
class HtmlRenderer < ::Reports::Renderers::Base
def render(context)
context.instance_variable_set :@renderer, self
context.render(action: :create, renderer: self)
end
def header
data_row_attributes.map do |attribute|
header_label(attribute)
end
end
def data_rows
report_data.list.map do |data|
data_row_attributes.map do |attribute|
data.public_send(attribute)
end
end
end
private
def data_row_attributes
[
:fee_type,
:enterprise_name,
:fee_name,
:customer_name,
:fee_placement,
:fee_calculated_on_transfer_through_name,
:tax_category_name,
:total_amount
]
end
def header_label(attribute)
I18n.t("header.#{attribute}", scope: i18n_scope)
end
def i18n_scope
"order_management.reports.enterprise_fee_summary.formats.csv"
end
end
end
end
end
end

View File

@@ -0,0 +1,29 @@
module OrderManagement
module Reports
module EnterpriseFeeSummary
module ReportData
class EnterpriseFeeTypeTotal < ::Reports::ReportData::Base
attr_accessor :fee_type, :enterprise_name, :fee_name, :customer_name, :fee_placement,
:fee_calculated_on_transfer_through_name, :tax_category_name, :total_amount
def <=>(other)
sortable_data <=> other.sortable_data
end
def sortable_data
[
fee_type,
enterprise_name,
fee_name,
customer_name,
fee_placement,
fee_calculated_on_transfer_through_name,
tax_category_name,
total_amount
].map { |attribute| attribute || "" }
end
end
end
end
end
end

View File

@@ -0,0 +1,47 @@
module OrderManagement
module Reports
module EnterpriseFeeSummary
class ReportService
attr_accessor :permissions, :parameters
def initialize(permissions, parameters)
@permissions = permissions
@parameters = parameters
end
def enterprise_fees_by_customer
Scope.new.apply_filters(permission_filters).apply_filters(parameters).result
end
def list
enterprise_fee_type_total_list.sort
end
private
def permission_filters
Parameters.new(order_cycle_ids: permissions.allowed_order_cycles.map(&:id))
end
def enterprise_fee_type_total_list
enterprise_fees_by_customer.map do |total_data|
summarizer = EnterpriseFeeTypeTotalSummarizer.new(total_data)
ReportData::EnterpriseFeeTypeTotal.new.tap do |total|
enterprise_fee_type_summarizer_to_total_attributes.each do |attribute|
total.public_send("#{attribute}=", summarizer.public_send(attribute))
end
end
end
end
def enterprise_fee_type_summarizer_to_total_attributes
[
:fee_type, :enterprise_name, :fee_name, :customer_name, :fee_placement,
:fee_calculated_on_transfer_through_name, :tax_category_name, :total_amount
]
end
end
end
end
end

View File

@@ -0,0 +1,362 @@
module OrderManagement
module Reports
module EnterpriseFeeSummary
class Scope
attr_accessor :parameters
def initialize
setup_default_scope
end
def apply_filters(params)
filter_by_date(params)
filter_by_distribution(params)
filter_by_fee(params)
self
end
def result
group_data.select_attributes
@scope.all
end
protected
def setup_default_scope
find_supported_adjustments
include_adjustment_metadata
include_order_details
include_payment_fee_details
include_shipping_fee_details
include_enterprise_fee_details
include_line_item_details
include_incoming_exchange_details
include_outgoing_exchange_details
group_data
select_attributes
end
def find_supported_adjustments
find_adjustments.for_orders.for_supported_adjustments
end
def find_adjustments
chain_to_scope do
Spree::Adjustment
end
end
def for_orders
chain_to_scope do
where(adjustable_type: "Spree::Order")
end
end
def for_supported_adjustments
chain_to_scope do
where(originator_type: ["EnterpriseFee", "Spree::PaymentMethod",
"Spree::ShippingMethod"])
end
end
def include_adjustment_metadata
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN adjustment_metadata
ON (adjustment_metadata.adjustment_id = spree_adjustments.id)
JOIN_STRING
)
end
# Includes:
# * Order
# * Customer
# * Hub
def include_order_details
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN spree_orders
ON (
spree_adjustments.adjustable_type = 'Spree::Order'
AND spree_orders.id = spree_adjustments.adjustable_id
AND spree_orders.completed_at IS NOT NULL
)
JOIN_STRING
)
join_scope("LEFT OUTER JOIN customers ON (customers.id = spree_orders.customer_id)")
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN enterprises AS hubs
ON (hubs.id = spree_orders.distributor_id)
JOIN_STRING
)
end
# If for payment fee
#
# Includes:
# * Payment method
def include_payment_fee_details
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN spree_payment_methods
ON (
spree_adjustments.originator_type = 'Spree::PaymentMethod'
AND spree_payment_methods.id = spree_adjustments.originator_id
)
JOIN_STRING
)
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN enterprises AS payment_hubs
ON (
spree_payment_methods.id IS NOT NULL
AND payment_hubs.id = spree_orders.distributor_id
)
JOIN_STRING
)
end
# If for shipping fee
#
# Includes:
# * Shipping method
def include_shipping_fee_details
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN spree_shipping_methods
ON (
spree_adjustments.originator_type = 'Spree::ShippingMethod'
AND spree_shipping_methods.id = spree_adjustments.originator_id
)
JOIN_STRING
)
end
# Includes:
# * Enterprise fee
# * Enterprise
# * Enterprise fee tax category
def include_enterprise_fee_details
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN enterprise_fees
ON (
spree_adjustments.originator_type = 'EnterpriseFee'
AND enterprise_fees.id = spree_adjustments.originator_id
)
JOIN_STRING
)
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN enterprises
ON (enterprises.id = enterprise_fees.enterprise_id)
JOIN_STRING
)
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN spree_tax_categories
ON (spree_tax_categories.id = enterprise_fees.tax_category_id)
JOIN_STRING
)
end
# If for line item - Use data only if spree_line_items.id is present
#
# Includes:
# * Line item
# * Variant
# * Product
# * Tax category of product, if enterprise fee tells to inherit
def include_line_item_details
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN spree_line_items
ON (
spree_adjustments.source_type = 'Spree::LineItem'
AND spree_line_items.id = spree_adjustments.source_id
)
JOIN_STRING
)
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN spree_variants
ON (
spree_adjustments.source_type = 'Spree::LineItem'
AND spree_variants.id = spree_line_items.variant_id
)
JOIN_STRING
)
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN spree_products
ON (spree_products.id = spree_variants.product_id)
JOIN_STRING
)
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN spree_tax_categories AS product_tax_categories
ON (
enterprise_fees.inherits_tax_category IS TRUE
AND product_tax_categories.id = spree_products.tax_category_id
)
JOIN_STRING
)
end
# If incoming exchange - Use data only if incoming_exchange_variants.id is present
#
# Includes:
# * Incoming exchange
# * Incoming exchange enterprise
# * Incoming exchange variant
def include_incoming_exchange_details
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN
(
exchange_variants AS incoming_exchange_variants
LEFT OUTER JOIN exchanges AS incoming_exchanges
ON (
incoming_exchanges.incoming IS TRUE
AND incoming_exchange_variants.exchange_id = incoming_exchanges.id
)
)
ON (
spree_adjustments.source_type = 'Spree::LineItem'
AND adjustment_metadata.enterprise_role = 'supplier'
AND incoming_exchanges.order_cycle_id = spree_orders.order_cycle_id
AND incoming_exchange_variants.id IS NOT NULL
AND incoming_exchange_variants.variant_id = spree_line_items.variant_id
)
JOIN_STRING
)
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN enterprises AS incoming_exchange_enterprises
ON (incoming_exchange_enterprises.id = incoming_exchanges.sender_id)
JOIN_STRING
)
end
# If outgoing exchange - Use data only if outgoing_exchange_variants.id is present
#
# Includes:
# * Outgoing exchange
# * Outgoing exchange enterprise
# * Outgoing exchange variant
def include_outgoing_exchange_details
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN
(
exchange_variants AS outgoing_exchange_variants
LEFT OUTER JOIN exchanges AS outgoing_exchanges
ON (
outgoing_exchanges.incoming IS NOT TRUE
AND outgoing_exchange_variants.exchange_id = outgoing_exchanges.id
)
)
ON (
spree_adjustments.source_type = 'Spree::LineItem'
AND adjustment_metadata.enterprise_role = 'distributor'
AND outgoing_exchanges.order_cycle_id = spree_orders.order_cycle_id
AND outgoing_exchange_variants.id IS NOT NULL
AND outgoing_exchange_variants.variant_id = spree_line_items.variant_id
)
JOIN_STRING
)
join_scope(
<<-JOIN_STRING.strip_heredoc
LEFT OUTER JOIN enterprises AS outgoing_exchange_enterprises
ON (outgoing_exchange_enterprises.id = outgoing_exchanges.receiver_id)
JOIN_STRING
)
end
def filter_by_date(params)
filter_scope("spree_orders.completed_at >= ?", params.start_at) \
if params.start_at.present?
filter_scope("spree_orders.completed_at <= ?", params.end_at) if params.end_at.present?
end
def filter_by_distribution(params)
filter_scope(spree_orders: { distributor_id: params.distributor_ids }) \
if params.distributor_ids.present?
filter_scope(spree_products: { supplier_id: params.producer_ids }) \
if params.producer_ids.present?
filter_scope(spree_orders: { order_cycle_id: params.order_cycle_ids }) \
if params.order_cycle_ids.present?
end
def filter_by_fee(params)
filter_scope(enterprise_fees: { id: params.enterprise_fee_ids }) \
if params.enterprise_fee_ids.present?
filter_scope(spree_shipping_methods: { id: params.shipping_method_ids }) \
if params.shipping_method_ids.present?
filter_scope(spree_payment_methods: { id: params.payment_method_ids }) \
if params.payment_method_ids.present?
end
def group_data
chain_to_scope do
group("enterprise_fees.id", "enterprises.id", "customers.id", "hubs.id",
"spree_payment_methods.id", "spree_shipping_methods.id",
"adjustment_metadata.enterprise_role", "spree_tax_categories.id",
"product_tax_categories.id", "incoming_exchange_enterprises.id",
"outgoing_exchange_enterprises.id")
end
end
def select_attributes
chain_to_scope do
select(
<<-JOIN_STRING.strip_heredoc
SUM(spree_adjustments.amount) AS total_amount, spree_payment_methods.name AS
payment_method_name, spree_shipping_methods.name AS shipping_method_name,
hubs.name AS hub_name, enterprises.name AS enterprise_name,
enterprise_fees.fee_type AS fee_type, customers.name AS customer_name,
customers.email AS customer_email, enterprise_fees.fee_type AS fee_type,
enterprise_fees.name AS fee_name, spree_tax_categories.name AS tax_category_name,
product_tax_categories.name AS product_tax_category_name,
adjustment_metadata.enterprise_role AS placement_enterprise_role,
incoming_exchange_enterprises.name AS incoming_exchange_enterprise_name,
outgoing_exchange_enterprises.name AS outgoing_exchange_enterprise_name
JOIN_STRING
)
end
end
def chain_to_scope(&block)
@scope = @scope.instance_eval(&block)
self
end
def join_scope(join_string)
chain_to_scope do
joins(join_string)
end
end
def filter_scope(*args)
chain_to_scope do
where(*args)
end
end
end
end
end
end

View File

@@ -0,0 +1,3 @@
module Reports
class UnsupportedReportFormatException < StandardError; end
end

View File

@@ -0,0 +1,12 @@
module Reports
class Authorizer
class ParameterNotAllowedError < StandardError; end
attr_accessor :parameters, :permissions
def initialize(parameters, permissions)
@parameters = parameters
@permissions = permissions
end
end
end

View File

@@ -0,0 +1,19 @@
module Reports
module Parameters
class Base
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
def initialize(attributes = {})
attributes.each do |key, value|
public_send("#{key}=", value)
end
end
# The parameters are never persisted.
def to_key; end
end
end
end

View File

@@ -0,0 +1,9 @@
module Reports
class Permissions
attr_accessor :user
def initialize(user)
@user = user
end
end
end

View File

@@ -0,0 +1,11 @@
module Reports
module Renderers
class Base
attr_reader :report_data
def initialize(report_data)
@report_data = report_data
end
end
end
end

View File

@@ -0,0 +1,11 @@
module Reports
module ReportData
class Base
def initialize(attributes = {})
attributes.each do |key, value|
public_send("#{key}=", value)
end
end
end
end
end

View File

@@ -0,0 +1,7 @@
Spree::Core::Engine.routes.prepend do
namespace :admin do
namespace :reports do
resource :enterprise_fee_summary, only: [:new, :create]
end
end
end

View File

@@ -0,0 +1,4 @@
require "order_management/engine"
module OrderManagement
end

View File

@@ -0,0 +1,5 @@
module OrderManagement
class Engine < ::Rails::Engine
isolate_namespace OrderManagement
end
end

View File

@@ -0,0 +1,3 @@
module OrderManagement
VERSION = "0.0.1".freeze
end

View File

@@ -0,0 +1,13 @@
$LOAD_PATH.push File.expand_path('lib', __dir__)
require "order_management/version"
Gem::Specification.new do |s|
s.name = "order_management"
s.version = OrderManagement::VERSION
s.authors = ["developers@ofn"]
s.summary = "Order Management domain of the OFN solution."
s.files = Dir["{app,config,db,lib}/**/*"] + ["LICENSE.txt", "Rakefile", "README.rdoc"]
s.test_files = Dir["spec/**/*"]
end

View File

@@ -0,0 +1,172 @@
require "spec_helper"
describe OrderManagement::Reports::EnterpriseFeeSummary::Authorizer do
let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary }
let(:user) { create(:user) }
let(:parameters) { report_klass::Parameters.new(params) }
let(:permissions) { report_klass::Permissions.new(user) }
let(:authorizer) { described_class.new(parameters, permissions) }
context "for distributors" do
before do
allow(permissions).to receive(:allowed_distributors) do
stub_model_collection(Enterprise, :id, ["1", "2", "3"])
end
end
context "when distributors are allowed" do
let(:params) { { distributor_ids: ["1", "3"] } }
it "does not raise error" do
expect { authorizer.authorize! }.not_to raise_error
end
end
context "when a distributor is not allowed" do
let(:params) { { distributor_ids: ["1", "4"] } }
it "raises ParameterNotAllowedError" do
expect { authorizer.authorize! }
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
end
end
end
context "for producers" do
before do
allow(permissions).to receive(:allowed_producers) do
stub_model_collection(Enterprise, :id, ["1", "2", "3"])
end
end
context "when producers are allowed" do
let(:params) { { producer_ids: ["1", "3"] } }
it "does not raise error" do
expect { authorizer.authorize! }.not_to raise_error
end
end
context "when a producer is not allowed" do
let(:params) { { producer_ids: ["1", "4"] } }
it "raises ParameterNotAllowedError" do
expect { authorizer.authorize! }
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
end
end
end
context "for order cycles" do
before do
allow(permissions).to receive(:allowed_order_cycles) do
stub_model_collection(OrderCycle, :id, ["1", "2", "3"])
end
end
context "when order cycles are allowed" do
let(:params) { { order_cycle_ids: ["1", "3"] } }
it "does not raise error" do
expect { authorizer.authorize! }.not_to raise_error
end
end
context "when an order cycle is not allowed" do
let(:params) { { order_cycle_ids: ["1", "4"] } }
it "raises ParameterNotAllowedError" do
expect { authorizer.authorize! }
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
end
end
end
context "for enterprise fees" do
before do
allow(permissions).to receive(:allowed_enterprise_fees) do
stub_model_collection(EnterpriseFee, :id, ["1", "2", "3"])
end
end
context "when enterprise fees are allowed" do
let(:params) { { enterprise_fee_ids: ["1", "3"] } }
it "does not raise error" do
expect { authorizer.authorize! }.not_to raise_error
end
end
context "when an enterprise fee is not allowed" do
let(:params) { { enterprise_fee_ids: ["1", "4"] } }
it "raises ParameterNotAllowedError" do
expect { authorizer.authorize! }
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
end
end
end
context "for shipping methods" do
before do
allow(permissions).to receive(:allowed_shipping_methods) do
stub_model_collection(Spree::ShippingMethod, :id, ["1", "2", "3"])
end
end
context "when shipping methods are allowed" do
let(:params) { { shipping_method_ids: ["1", "3"] } }
it "does not raise error" do
expect { authorizer.authorize! }.not_to raise_error
end
end
context "when a shipping method is not allowed" do
let(:params) { { shipping_method_ids: ["1", "4"] } }
it "raises ParameterNotAllowedError" do
expect { authorizer.authorize! }
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
end
end
end
context "for payment methods" do
before do
allow(permissions).to receive(:allowed_payment_methods) do
stub_model_collection(Spree::PaymentMethod, :id, ["1", "2", "3"])
end
end
context "when payment methods are allowed" do
let(:params) { { payment_method_ids: ["1", "3"] } }
it "does not raise error" do
expect { authorizer.authorize! }.not_to raise_error
end
end
context "when a payment method is not allowed" do
let(:params) { { payment_method_ids: ["1", "4"] } }
it "raises ParameterNotAllowedError" do
expect { authorizer.authorize! }
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
end
end
end
def stub_model_collection(model, attribute_name, attribute_list)
attribute_list.map do |attribute_value|
stub_model(model, attribute_name => attribute_value)
end
end
def stub_model(model, params)
model.new.tap do |instance|
instance.stub(params)
end
end
end

View File

@@ -0,0 +1,88 @@
require "spec_helper"
require "date_time_string_validator"
describe OrderManagement::Reports::EnterpriseFeeSummary::Parameters do
describe "validation" do
let(:parameters) { described_class.new }
it "allows all parameters to be blank" do
expect(parameters).to be_valid
end
context "for type of parameters" do
it { is_expected.to validate_date_time_format_of(:start_at) }
it { is_expected.to validate_date_time_format_of(:end_at) }
it { is_expected.to validate_integer_array(:distributor_ids) }
it { is_expected.to validate_integer_array(:producer_ids) }
it { is_expected.to validate_integer_array(:order_cycle_ids) }
it { is_expected.to validate_integer_array(:enterprise_fee_ids) }
it { is_expected.to validate_integer_array(:shipping_method_ids) }
it { is_expected.to validate_integer_array(:payment_method_ids) }
it "allows integer arrays to include blank string and cleans it up" do
subject.distributor_ids = ["", "1"]
subject.producer_ids = ["", "1"]
subject.order_cycle_ids = ["", "1"]
subject.enterprise_fee_ids = ["", "1"]
subject.shipping_method_ids = ["", "1"]
subject.payment_method_ids = ["", "1"]
expect(subject).to be_valid
expect(subject.distributor_ids).to eq(["1"])
expect(subject.producer_ids).to eq(["1"])
expect(subject.order_cycle_ids).to eq(["1"])
expect(subject.enterprise_fee_ids).to eq(["1"])
expect(subject.shipping_method_ids).to eq(["1"])
expect(subject.payment_method_ids).to eq(["1"])
end
describe "requiring start_at to be before end_at" do
let(:now) { Time.zone.now }
it "adds error when start_at is after end_at" do
allow(subject).to receive(:start_at) { now.to_s }
allow(subject).to receive(:end_at) { (now - 1.hour).to_s }
expect(subject).not_to be_valid
error_message = described_class.date_end_before_start_error_message
expect(subject.errors[:end_at]).to eq([error_message])
end
it "does not add error when start_at is before end_at" do
allow(subject).to receive(:start_at) { now.to_s }
allow(subject).to receive(:end_at) { (now + 1.hour).to_s }
expect(subject).to be_valid
end
end
end
end
describe "smoke authorization" do
let!(:order_cycle) { create(:order_cycle) }
let!(:user) { create(:user) }
let(:permissions) do
report_klass::Permissions.new(nil).tap do |instance|
instance.stub(allowed_order_cycles: [order_cycle])
end
end
it "does not raise error when the parameters are allowed" do
parameters = described_class.new(order_cycle_ids: [order_cycle.id.to_s])
expect { parameters.authorize!(permissions) }.not_to raise_error
end
it "raises error when the parameters are not allowed" do
parameters = described_class.new(order_cycle_ids: [(order_cycle.id + 1).to_s])
expect { parameters.authorize!(permissions) }
.to raise_error(Reports::Authorizer::ParameterNotAllowedError)
end
end
def report_klass
OrderManagement::Reports::EnterpriseFeeSummary
end
end

View File

@@ -0,0 +1,206 @@
require "spec_helper"
describe OrderManagement::Reports::EnterpriseFeeSummary::Permissions do
let!(:order_cycle) { create(:simple_order_cycle) }
let!(:incoming_exchange) { create(:exchange, incoming: true, order_cycle: order_cycle) }
let!(:outgoing_exchange) { create(:exchange, incoming: false, order_cycle: order_cycle) }
# The factory for order cycle uses the first distributor it finds in the database, if it exists.
# However, for this example group, we need to make sure that the coordinator for the second order
# cycle is not the same as the one in the first.
let!(:another_coordinator) { create(:distributor_enterprise) }
let!(:another_order_cycle) { create(:simple_order_cycle, coordinator: another_coordinator) }
let!(:another_incoming_exchange) do
create(:exchange, incoming: true, order_cycle: another_order_cycle)
end
let!(:another_outgoing_exchange) do
create(:exchange, incoming: false, order_cycle: another_order_cycle)
end
describe "permissions for order cycles" do
it "allows admin" do
user = create(:admin_user)
authorizer = described_class.new(user)
expect(authorizer.allowed_order_cycles).to include(order_cycle)
end
it "allows coordinator of the order cycle" do
user = order_cycle.coordinator.owner
authorizer = described_class.new(user)
expect(authorizer.allowed_order_cycles).to include(order_cycle)
end
it "allows sender of incoming exchange" do
user = incoming_exchange.sender.owner
authorizer = described_class.new(user)
expect(authorizer.allowed_order_cycles).to include(order_cycle)
end
it "allows receiver of outgoing exchange" do
user = outgoing_exchange.receiver.owner
authorizer = described_class.new(user)
expect(authorizer.allowed_order_cycles).to include(order_cycle)
end
it "does not allow coordinator of another order cycle" do
user = another_order_cycle.coordinator.owner
authorizer = described_class.new(user)
expect(authorizer.allowed_order_cycles).not_to include(order_cycle)
end
it "does not allow sender of incoming exchange of another order cycle" do
user = another_incoming_exchange.sender.owner
authorizer = described_class.new(user)
expect(authorizer.allowed_order_cycles).not_to include(order_cycle)
end
it "does not allow receiver of outgoing exchange of another order cycle" do
user = another_outgoing_exchange.receiver.owner
authorizer = described_class.new(user)
expect(authorizer.allowed_order_cycles).not_to include(order_cycle)
end
end
describe "permissions for properties related to the order cycle" do
let(:user) { create(:user) }
let(:authorizer) do
described_class.new(user).tap do |instance|
allow(instance).to receive(:allowed_order_cycles) { [order_cycle] }
end
end
describe "allowed distributors" do
it "includes distributor of allowed order cycle" do
expect(authorizer.allowed_distributors).to include(outgoing_exchange.receiver)
end
it "does not include distributor of order cycle that is not allowed" do
expect(authorizer.allowed_distributors).not_to include(another_outgoing_exchange.receiver)
end
end
describe "allowed producers" do
it "includes supplier of allowed order cycle" do
expect(authorizer.allowed_producers).to include(incoming_exchange.sender)
end
it "does not include supplier of order cycle that is not allowed" do
expect(authorizer.allowed_producers).not_to include(another_incoming_exchange.sender)
end
end
describe "allowed enterprise fees" do
context "when coordinator fee for order cycle" do
let!(:coordinator_fee) do
create(:enterprise_fee, enterprise: order_cycle.coordinator).tap do |fee|
order_cycle.coordinator_fees << fee
end
end
let!(:another_coordinator_fee) do
create(:enterprise_fee, enterprise: another_order_cycle.coordinator).tap do |fee|
another_order_cycle.coordinator_fees << fee
end
end
it "includes enterprise fee in allowed order cycle" do
expect(authorizer.allowed_enterprise_fees).to include(coordinator_fee)
end
it "does not include enterprise fee in order cycle that is not allowed" do
expect(authorizer.allowed_enterprise_fees).not_to include(another_coordinator_fee)
end
end
context "when enterprise fee for incoming exchange" do
let!(:exchange_fee) do
create(:enterprise_fee, enterprise: incoming_exchange.sender).tap do |fee|
incoming_exchange.enterprise_fees << fee
end
end
let!(:another_exchange_fee) do
create(:enterprise_fee, enterprise: another_incoming_exchange.sender).tap do |fee|
another_incoming_exchange.enterprise_fees << fee
end
end
it "includes enterprise fee in allowed order cycle" do
expect(authorizer.allowed_enterprise_fees).to include(exchange_fee)
end
it "does not include enterprise fee in order cycle that is not allowed" do
expect(authorizer.allowed_enterprise_fees).not_to include(another_exchange_fee)
end
end
context "when enterprise fee for outgoing exchange" do
let!(:exchange_fee) do
create(:enterprise_fee, enterprise: outgoing_exchange.receiver).tap do |fee|
outgoing_exchange.enterprise_fees << fee
end
end
let!(:another_exchange_fee) do
create(:enterprise_fee, enterprise: another_outgoing_exchange.receiver).tap do |fee|
another_outgoing_exchange.enterprise_fees << fee
end
end
it "includes enterprise fee in allowed order cycle" do
expect(authorizer.allowed_enterprise_fees).to include(exchange_fee)
end
it "does not include enterprise fee in order cycle that is not allowed" do
expect(authorizer.allowed_enterprise_fees).not_to include(another_exchange_fee)
end
end
end
describe "allowed shipping methods" do
it "includes shipping methods of distributors in allowed order cycle" do
shipping_method = create(:shipping_method, distributors: [outgoing_exchange.receiver])
expect(authorizer.allowed_shipping_methods).to include(shipping_method)
end
it "does not include shipping methods of suppliers in allowed order cycle" do
shipping_method = create(:shipping_method, distributors: [incoming_exchange.sender])
expect(authorizer.allowed_shipping_methods).not_to include(shipping_method)
end
it "does not include shipping methods of coordinator of allowed order cycle" do
shipping_method = create(:shipping_method, distributors: [order_cycle.coordinator])
expect(authorizer.allowed_shipping_methods).not_to include(shipping_method)
end
it "does not include shipping methods of distributors in order cycle that is not allowed" do
shipping_method = create(:shipping_method,
distributors: [another_outgoing_exchange.receiver])
expect(authorizer.allowed_shipping_methods).not_to include(shipping_method)
end
end
describe "allowed payment methods" do
it "includes payment methods of distributors in allowed order cycle" do
payment_method = create(:payment_method, distributors: [outgoing_exchange.receiver])
expect(authorizer.allowed_payment_methods).to include(payment_method)
end
it "does not include payment methods of suppliers in allowed order cycle" do
payment_method = create(:payment_method, distributors: [incoming_exchange.sender])
expect(authorizer.allowed_payment_methods).not_to include(payment_method)
end
it "does not include payment methods of coordinator of allowed order cycle" do
payment_method = create(:payment_method, distributors: [order_cycle.coordinator])
expect(authorizer.allowed_payment_methods).not_to include(payment_method)
end
it "does not include payment methods of distributors in order cycle that is not allowed" do
payment_method = create(:payment_method, distributors: [another_outgoing_exchange.receiver])
expect(authorizer.allowed_payment_methods).not_to include(payment_method)
end
end
end
end

View File

@@ -0,0 +1,89 @@
require "spec_helper"
describe OrderManagement::Reports::EnterpriseFeeSummary::Renderers::CsvRenderer do
let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary }
let!(:permissions) { report_klass::Permissions.new(current_user) }
let!(:parameters) { report_klass::Parameters.new }
let!(:service) { report_klass::ReportService.new(permissions, parameters) }
let!(:renderer) { described_class.new(service) }
# Context which will be passed to the renderer. The response object is not automatically prepared,
# so this has to be assigned explicitly.
let!(:response) { ActionController::TestResponse.new }
let!(:controller) do
ActionController::Base.new.tap do |controller_mock|
controller_mock.instance_variable_set(:@_response, response)
end
end
let!(:enterprise_fee_type_totals) do
[
report_klass::ReportData::EnterpriseFeeTypeTotal.new(
fee_type: "Fee Type A",
enterprise_name: "Enterprise A",
fee_name: "Fee A",
customer_name: "Custoemr A",
fee_placement: "Fee Placement A",
fee_calculated_on_transfer_through_name: "Transfer Enterprise A",
tax_category_name: "Tax Category A",
total_amount: "1.00"
),
report_klass::ReportData::EnterpriseFeeTypeTotal.new(
fee_type: "Fee Type B",
enterprise_name: "Enterprise B",
fee_name: "Fee C",
customer_name: "Custoemr D",
fee_placement: "Fee Placement E",
fee_calculated_on_transfer_through_name: "Transfer Enterprise F",
tax_category_name: "Tax Category G",
total_amount: "2.00"
)
]
end
let(:current_user) { nil }
before do
allow(service).to receive(:list) { enterprise_fee_type_totals }
end
it "generates CSV header" do
renderer.render(controller)
result = response.body
csv = CSV.parse(result)
header_row = csv[0]
# Test all header cells have values
expect(header_row.length).to eq(8)
expect(header_row.all?(&:present?)).to be_truthy
end
it "generates CSV data rows" do
renderer.render(controller)
result = response.body
csv = CSV.parse(result, headers: true)
expect(csv.length).to eq(2)
# Test random cells
expect(csv[0][i18n_translate("header.fee_type")]).to eq("Fee Type A")
expect(csv[0][i18n_translate("header.total_amount")]).to eq("1.00")
expect(csv[1][i18n_translate("header.total_amount")]).to eq("2.00")
end
it "generates filename correctly" do
Timecop.freeze(Time.zone.local(2018, 10, 9, 7, 30, 0)) do
filename = renderer.__send__(:filename)
expect(filename).to eq("enterprise_fee_summary_20181009.csv")
end
end
def i18n_translate(key)
I18n.t(key, scope: i18n_scope)
end
def i18n_scope
"order_management.reports.enterprise_fee_summary.formats.csv"
end
end

View File

@@ -0,0 +1,70 @@
require "spec_helper"
describe OrderManagement::Reports::EnterpriseFeeSummary::Renderers::HtmlRenderer do
let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary }
let!(:permissions) { report_klass::Permissions.new(current_user) }
let!(:parameters) { report_klass::Parameters.new }
let!(:controller) { Spree::Admin::Reports::EnterpriseFeeSummariesController.new }
let!(:service) { report_klass::ReportService.new(permissions, parameters) }
let!(:renderer) { described_class.new(service) }
let!(:enterprise_fee_type_totals) do
[
report_klass::ReportData::EnterpriseFeeTypeTotal.new(
fee_type: "Fee Type A",
enterprise_name: "Enterprise A",
fee_name: "Fee A",
customer_name: "Custoemr A",
fee_placement: "Fee Placement A",
fee_calculated_on_transfer_through_name: "Transfer Enterprise A",
tax_category_name: "Tax Category A",
total_amount: "1.00"
),
report_klass::ReportData::EnterpriseFeeTypeTotal.new(
fee_type: "Fee Type B",
enterprise_name: "Enterprise B",
fee_name: "Fee C",
customer_name: "Custoemr D",
fee_placement: "Fee Placement E",
fee_calculated_on_transfer_through_name: "Transfer Enterprise F",
tax_category_name: "Tax Category G",
total_amount: "2.00"
)
]
end
let(:current_user) { nil }
before do
allow(service).to receive(:list) { enterprise_fee_type_totals }
end
it "generates header values" do
header_row = renderer.header
# Test all header cells have values
expect(header_row.length).to eq(8)
expect(header_row.all?(&:present?)).to be_truthy
end
it "generates data rows" do
header_row = renderer.header
result = renderer.data_rows
expect(result.length).to eq(2)
# Test random cells
expect(result[0][header_row.index(i18n_translate("header.fee_type"))]).to eq("Fee Type A")
expect(result[0][header_row.index(i18n_translate("header.total_amount"))]).to eq("1.00")
expect(result[1][header_row.index(i18n_translate("header.total_amount"))]).to eq("2.00")
end
def i18n_translate(key)
I18n.t(key, scope: i18n_scope)
end
def i18n_scope
"order_management.reports.enterprise_fee_summary.formats.csv"
end
end

View File

@@ -0,0 +1,46 @@
require "spec_helper"
describe OrderManagement::Reports::EnterpriseFeeSummary::ReportData::EnterpriseFeeTypeTotal do
it "sorts instances according to their attributes" do
instance_a = described_class.new(
fee_type: "sales",
enterprise_name: "Enterprise A",
fee_name: "A Sales",
customer_name: "Customer A",
fee_placement: "Incoming",
fee_calculated_on_transfer_through_name: "Transfer Enterprise B",
tax_category_name: "Sales 4%",
total_amount: "12.00"
)
instance_b = described_class.new(
fee_type: "sales",
enterprise_name: "Enterprise A",
fee_name: "B Sales",
customer_name: "Customer A",
fee_placement: "Incoming",
fee_calculated_on_transfer_through_name: "Transfer Enterprise B",
tax_category_name: "Sales 4%",
total_amount: "12.00"
)
instance_c = described_class.new(
fee_type: "admin",
enterprise_name: "Enterprise A",
fee_name: "C Admin",
customer_name: "Customer B",
fee_placement: "Incoming",
fee_calculated_on_transfer_through_name: nil,
tax_category_name: "Sales 6%",
total_amount: "12.00"
)
list = [
instance_a,
instance_b,
instance_c
]
expect(list.sort).to eq([instance_c, instance_a, instance_b])
end
end

View File

@@ -0,0 +1,536 @@
require "spec_helper"
describe OrderManagement::Reports::EnterpriseFeeSummary::ReportService do
let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary }
# Basic data.
let!(:shipping_method) do
create(:shipping_method, :per_item, amount: 1, name: "Sample Shipping Method")
end
let!(:payment_method) do
create(:payment_method, :per_item, amount: 2, name: "Sample Payment Method")
end
# Create enterprises.
let!(:distributor) do
create(:distributor_enterprise, name: "Sample Distributor").tap do |enterprise|
payment_method.distributors << enterprise
shipping_method.distributors << enterprise
end
end
let!(:producer) { create(:supplier_enterprise, name: "Sample Producer") }
let!(:coordinator) { create(:enterprise, name: "Sample Coordinator") }
# Add some fee noise.
let!(:other_distributor_fee) { create(:enterprise_fee, :per_item, enterprise: distributor) }
let!(:other_producer_fee) { create(:enterprise_fee, :per_item, enterprise: producer) }
let!(:other_coordinator_fee) { create(:enterprise_fee, :per_item, enterprise: coordinator) }
# Set up other requirements for ordering.
let!(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) }
let!(:product) { create(:product, tax_category: product_tax_category) }
let!(:product_tax_category) { create(:tax_category, name: "Sample Product Tax") }
let!(:variant) { prepare_variant }
# Create customers.
let!(:customer) { create(:customer, name: "Sample Customer") }
let!(:another_customer) { create(:customer, name: "Another Customer") }
# Setup up permissions and report.
let!(:current_user) { create(:admin_user) }
let(:permissions) { report_klass::Permissions.new(current_user) }
let(:parameters) { report_klass::Parameters.new }
let(:service) { described_class.new(permissions, parameters) }
describe "grouping and sorting of entries" do
let!(:order_cycle) do
create(:simple_order_cycle, coordinator: coordinator, coordinator_fees: order_cycle_fees)
end
let!(:variant) do
prepare_variant(incoming_exchange_fees: variant_incoming_exchange_fees,
outgoing_exchange_fees: variant_outgoing_exchange_fees)
end
let!(:order_cycle_fees) do
[
create(:enterprise_fee, :per_item, name: "Coordinator Fee 1", enterprise: coordinator,
fee_type: "admin", amount: 512.0,
tax_category: coordinator_tax_category),
create(:enterprise_fee, :per_item, name: "Coordinator Fee 2", enterprise: coordinator,
fee_type: "sales", amount: 1024.0,
inherits_tax_category: true)
]
end
let!(:coordinator_tax_category) { create(:tax_category, name: "Sample Coordinator Tax") }
let!(:variant_incoming_exchange_fees) do
[
create(:enterprise_fee, :per_item, name: "Producer Fee 1", enterprise: producer,
fee_type: "sales", amount: 64.0,
tax_category: producer_tax_category),
create(:enterprise_fee, :per_item, name: "Producer Fee 2", enterprise: producer,
fee_type: "sales", amount: 128.0,
inherits_tax_category: true)
]
end
let!(:producer_tax_category) { create(:tax_category, name: "Sample Producer Tax") }
let!(:variant_outgoing_exchange_fees) do
[
create(:enterprise_fee, :per_item, name: "Distributor Fee 1", enterprise: distributor,
fee_type: "admin", amount: 4.0,
tax_category: distributor_tax_category),
create(:enterprise_fee, :per_item, name: "Distributor Fee 2", enterprise: distributor,
fee_type: "sales", amount: 8.0,
inherits_tax_category: true)
]
end
let!(:distributor_tax_category) { create(:tax_category, name: "Sample Distributor Tax") }
let!(:customer_order) { prepare_order(customer: customer) }
let!(:customer_incomplete_order) { prepare_incomplete_order(customer: customer) }
let!(:second_customer_order) { prepare_order(customer: customer) }
let!(:other_customer_order) { prepare_order(customer: another_customer) }
it "groups and sorts entries correctly" do
totals = service.list
expect(totals.length).to eq(16)
# Data is sorted by the following, in order:
# * fee_type
# * enterprise_name
# * fee_name
# * customer_name
# * fee_placement
# * fee_calculated_on_transfer_through_name
# * tax_category_name
# * total_amount
expected_result = [
["Admin", "Sample Coordinator", "Coordinator Fee 1", "Another Customer",
"Coordinator", "All", "Sample Coordinator Tax", "512.00"],
["Admin", "Sample Coordinator", "Coordinator Fee 1", "Sample Customer",
"Coordinator", "All", "Sample Coordinator Tax", "1024.00"],
["Admin", "Sample Distributor", "Distributor Fee 1", "Another Customer",
"Outgoing", "Sample Distributor", "Sample Distributor Tax", "4.00"],
["Admin", "Sample Distributor", "Distributor Fee 1", "Sample Customer",
"Outgoing", "Sample Distributor", "Sample Distributor Tax", "8.00"],
["Payment Transaction", "Sample Distributor", "Sample Payment Method", "Another Customer",
nil, nil, nil, "2.00"],
["Payment Transaction", "Sample Distributor", "Sample Payment Method", "Sample Customer",
nil, nil, nil, "4.00"],
["Sales", "Sample Coordinator", "Coordinator Fee 2", "Another Customer",
"Coordinator", "All", "Sample Product Tax", "1024.00"],
["Sales", "Sample Coordinator", "Coordinator Fee 2", "Sample Customer",
"Coordinator", "All", "Sample Product Tax", "2048.00"],
["Sales", "Sample Distributor", "Distributor Fee 2", "Another Customer",
"Outgoing", "Sample Distributor", "Sample Product Tax", "8.00"],
["Sales", "Sample Distributor", "Distributor Fee 2", "Sample Customer",
"Outgoing", "Sample Distributor", "Sample Product Tax", "16.00"],
["Sales", "Sample Producer", "Producer Fee 1", "Another Customer",
"Incoming", "Sample Producer", "Sample Producer Tax", "64.00"],
["Sales", "Sample Producer", "Producer Fee 1", "Sample Customer",
"Incoming", "Sample Producer", "Sample Producer Tax", "128.00"],
["Sales", "Sample Producer", "Producer Fee 2", "Another Customer",
"Incoming", "Sample Producer", "Sample Product Tax", "128.00"],
["Sales", "Sample Producer", "Producer Fee 2", "Sample Customer",
"Incoming", "Sample Producer", "Sample Product Tax", "256.00"],
["Shipment", "Sample Distributor", "Sample Shipping Method", "Another Customer",
nil, nil, "Platform Rate", "1.00"],
["Shipment", "Sample Distributor", "Sample Shipping Method", "Sample Customer",
nil, nil, "Platform Rate", "2.00"]
]
expected_result.each_with_index do |expected_attributes, row_index|
expect_total_attributes(totals[row_index], expected_attributes)
end
end
end
describe "handling of more complex cases" do
context "with non-sender fee for incoming exchange and non-receiver fee for outgoing" do
let!(:variant) do
prepare_variant(incoming_exchange_fees: variant_incoming_exchange_fees,
outgoing_exchange_fees: variant_outgoing_exchange_fees)
end
let!(:variant_incoming_exchange_fees) { [coordinator_fee, distributor_fee] }
let!(:variant_outgoing_exchange_fees) { [producer_fee, coordinator_fee] }
let!(:producer_fee) do
tax_category = create(:tax_category, name: "Sample Producer Tax")
create(:enterprise_fee, :per_item, name: "Sample Producer Fee", enterprise: producer,
fee_type: "sales", amount: 64.0,
tax_category: tax_category)
end
let!(:coordinator_fee) do
tax_category = create(:tax_category, name: "Sample Coordinator Tax")
create(:enterprise_fee, :per_item, name: "Sample Coordinator Fee", enterprise: coordinator,
fee_type: "admin", amount: 512.0,
tax_category: tax_category)
end
let!(:distributor_fee) do
tax_category = create(:tax_category, name: "Sample Distributor Tax")
create(:enterprise_fee, :per_item, name: "Sample Distributor Fee", enterprise: distributor,
fee_type: "admin", amount: 4.0,
tax_category: tax_category)
end
let!(:customer_order) { prepare_order(customer: customer) }
it "fetches data correctly" do
totals = service.list
expect(totals.length).to eq(6)
expected_result = [
["Admin", "Sample Coordinator", "Sample Coordinator Fee", "Sample Customer",
"Incoming", "Sample Producer", "Sample Coordinator Tax", "512.00"],
["Admin", "Sample Coordinator", "Sample Coordinator Fee", "Sample Customer",
"Outgoing", "Sample Distributor", "Sample Coordinator Tax", "512.00"],
["Admin", "Sample Distributor", "Sample Distributor Fee", "Sample Customer",
"Incoming", "Sample Producer", "Sample Distributor Tax", "4.00"],
["Payment Transaction", "Sample Distributor", "Sample Payment Method", "Sample Customer",
nil, nil, nil, "2.00"],
["Sales", "Sample Producer", "Sample Producer Fee", "Sample Customer",
"Outgoing", "Sample Distributor", "Sample Producer Tax", "64.00"],
["Shipment", "Sample Distributor", "Sample Shipping Method", "Sample Customer",
nil, nil, "Platform Rate", "1.00"]
]
expected_result.each_with_index do |expected_attributes, row_index|
expect_total_attributes(totals[row_index], expected_attributes)
end
end
end
end
describe "filtering results based on permissions" do
let!(:distributor_a) do
create(:distributor_enterprise, name: "Distributor A", payment_methods: [payment_method],
shipping_methods: [shipping_method])
end
let!(:distributor_b) do
create(:distributor_enterprise, name: "Distributor B", payment_methods: [payment_method],
shipping_methods: [shipping_method])
end
let!(:order_cycle_a) { create(:simple_order_cycle, coordinator: coordinator) }
let!(:order_cycle_b) { create(:simple_order_cycle, coordinator: coordinator) }
let!(:variant_a) { prepare_variant(distributor: distributor_a, order_cycle: order_cycle_a) }
let!(:variant_b) { prepare_variant(distributor: distributor_b, order_cycle: order_cycle_b) }
let!(:order_a) { prepare_order(order_cycle: order_cycle_a, distributor: distributor_a) }
let!(:order_b) { prepare_order(order_cycle: order_cycle_b, distributor: distributor_b) }
context "when admin" do
let!(:current_user) { create(:admin_user) }
it "includes all order cycles" do
totals = service.list
expect_total_matches(totals, 2, fee_type: "Shipment")
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A")
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor B")
end
end
context "when enterprise owner for distributor" do
let!(:current_user) { distributor_a.owner }
it "does not include unrelated order cycles" do
totals = service.list
expect_total_matches(totals, 1, fee_type: "Shipment")
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A")
end
end
end
describe "filters entries correctly" do
let(:parameters) { report_klass::Parameters.new(parameters_attributes) }
context "filtering by completion date" do
let(:timestamp) { Time.zone.local(2018, 1, 5, 14, 30, 5) }
let!(:customer_a) { create(:customer, name: "Customer A") }
let!(:customer_b) { create(:customer, name: "Customer B") }
let!(:customer_c) { create(:customer, name: "Customer C") }
let!(:order_placed_before_timestamp) do
prepare_order(customer: customer_a).tap do |order|
order.update_column(:completed_at, timestamp - 1.second)
end
end
let!(:order_placed_during_timestamp) do
prepare_order(customer: customer_b).tap do |order|
order.update_column(:completed_at, timestamp)
end
end
let!(:order_placed_after_timestamp) do
prepare_order(customer: customer_c).tap do |order|
order.update_column(:completed_at, timestamp + 1.second)
end
end
context "on or after start_at" do
let(:parameters_attributes) { { start_at: timestamp } }
it "filters entries" do
totals = service.list
expect_total_matches(totals, 0, fee_type: "Shipment", customer_name: "Customer A")
expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer B")
expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer C")
end
end
context "on or before end_at" do
let(:parameters_attributes) { { end_at: timestamp } }
it "filters entries" do
totals = service.list
expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer A")
expect_total_matches(totals, 1, fee_type: "Shipment", customer_name: "Customer B")
expect_total_matches(totals, 0, fee_type: "Shipment", customer_name: "Customer C")
end
end
end
describe "for specified shops" do
let!(:distributor_a) do
create(:distributor_enterprise, name: "Distributor A", payment_methods: [payment_method],
shipping_methods: [shipping_method])
end
let!(:distributor_b) do
create(:distributor_enterprise, name: "Distributor B", payment_methods: [payment_method],
shipping_methods: [shipping_method])
end
let!(:distributor_c) do
create(:distributor_enterprise, name: "Distributor C", payment_methods: [payment_method],
shipping_methods: [shipping_method])
end
let!(:order_a) { prepare_order(distributor: distributor_a) }
let!(:order_b) { prepare_order(distributor: distributor_b) }
let!(:order_c) { prepare_order(distributor: distributor_c) }
let(:parameters_attributes) { { distributor_ids: [distributor_a.id, distributor_b.id] } }
it "filters entries" do
totals = service.list
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A")
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor B")
expect_total_matches(totals, 0, fee_type: "Shipment", enterprise_name: "Distributor C")
end
end
describe "for specified suppliers" do
let!(:producer_a) { create(:supplier_enterprise, name: "Producer A") }
let!(:producer_b) { create(:supplier_enterprise, name: "Producer B") }
let!(:producer_c) { create(:supplier_enterprise, name: "Producer C") }
let!(:fee_a) { create(:enterprise_fee, name: "Fee A", enterprise: producer_a) }
let!(:fee_b) { create(:enterprise_fee, name: "Fee B", enterprise: producer_b) }
let!(:fee_c) { create(:enterprise_fee, name: "Fee C", enterprise: producer_c) }
let!(:product_a) { create(:product, supplier: producer_a) }
let!(:product_b) { create(:product, supplier: producer_b) }
let!(:product_c) { create(:product, supplier: producer_c) }
let!(:variant_a) do
prepare_variant(product: product_a, producer: producer_a, incoming_exchange_fees: [fee_a])
end
let!(:variant_b) do
prepare_variant(product: product_b, producer: producer_b, incoming_exchange_fees: [fee_b])
end
let!(:variant_c) do
prepare_variant(product: product_c, producer: producer_c, incoming_exchange_fees: [fee_c])
end
let!(:order_a) { prepare_order(variant: variant_a) }
let!(:order_b) { prepare_order(variant: variant_b) }
let!(:order_c) { prepare_order(variant: variant_c) }
let(:parameters_attributes) { { producer_ids: [producer_a.id, producer_b.id] } }
it "filters entries" do
totals = service.list
expect_total_matches(totals, 1, fee_name: "Fee A", enterprise_name: "Producer A")
expect_total_matches(totals, 1, fee_name: "Fee B", enterprise_name: "Producer B")
expect_total_matches(totals, 0, fee_name: "Fee C", enterprise_name: "Producer C")
end
end
describe "for specified order cycles" do
let!(:distributor_a) do
create(:distributor_enterprise, name: "Distributor A", payment_methods: [payment_method],
shipping_methods: [shipping_method])
end
let!(:distributor_b) do
create(:distributor_enterprise, name: "Distributor B", payment_methods: [payment_method],
shipping_methods: [shipping_method])
end
let!(:distributor_c) do
create(:distributor_enterprise, name: "Distributor C", payment_methods: [payment_method],
shipping_methods: [shipping_method])
end
let!(:order_cycle_a) { create(:simple_order_cycle, coordinator: coordinator) }
let!(:order_cycle_b) { create(:simple_order_cycle, coordinator: coordinator) }
let!(:order_cycle_c) { create(:simple_order_cycle, coordinator: coordinator) }
let!(:variant_a) { prepare_variant(distributor: distributor_a, order_cycle: order_cycle_a) }
let!(:variant_b) { prepare_variant(distributor: distributor_b, order_cycle: order_cycle_b) }
let!(:variant_c) { prepare_variant(distributor: distributor_c, order_cycle: order_cycle_c) }
let!(:order_a) { prepare_order(order_cycle: order_cycle_a, distributor: distributor_a) }
let!(:order_b) { prepare_order(order_cycle: order_cycle_b, distributor: distributor_b) }
let!(:order_c) { prepare_order(order_cycle: order_cycle_c, distributor: distributor_c) }
let(:parameters_attributes) { { order_cycle_ids: [order_cycle_a.id, order_cycle_b.id] } }
it "filters entries" do
totals = service.list
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor A")
expect_total_matches(totals, 1, fee_type: "Shipment", enterprise_name: "Distributor B")
expect_total_matches(totals, 0, fee_type: "Shipment", enterprise_name: "Distributor C")
end
end
describe "for specified enterprise fees" do
let!(:fee_a) { create(:enterprise_fee, name: "Fee A", enterprise: distributor) }
let!(:fee_b) { create(:enterprise_fee, name: "Fee B", enterprise: distributor) }
let!(:fee_c) { create(:enterprise_fee, name: "Fee C", enterprise: distributor) }
let!(:variant) { prepare_variant(outgoing_exchange_fees: variant_outgoing_exchange_fees) }
let!(:variant_outgoing_exchange_fees) { [fee_a, fee_b, fee_c] }
let!(:order) { prepare_order(variant: variant) }
let(:parameters_attributes) { { enterprise_fee_ids: [fee_a.id, fee_b.id] } }
it "filters entries" do
totals = service.list
expect_total_matches(totals, 1, fee_name: "Fee A")
expect_total_matches(totals, 1, fee_name: "Fee B")
expect_total_matches(totals, 0, fee_name: "Fee C")
end
end
describe "for specified shipping methods" do
let!(:shipping_method_a) do
create(:shipping_method, name: "Shipping A", distributors: [distributor])
end
let!(:shipping_method_b) do
create(:shipping_method, name: "Shipping B", distributors: [distributor])
end
let!(:shipping_method_c) do
create(:shipping_method, name: "Shipping C", distributors: [distributor])
end
let!(:order_a) { prepare_order(shipping_method: shipping_method_a) }
let!(:order_b) { prepare_order(shipping_method: shipping_method_b) }
let!(:order_c) { prepare_order(shipping_method: shipping_method_c) }
let(:parameters_attributes) do
{ shipping_method_ids: [shipping_method_a.id, shipping_method_b.id] }
end
it "filters entries" do
totals = service.list
expect_total_matches(totals, 1, fee_name: "Shipping A")
expect_total_matches(totals, 1, fee_name: "Shipping B")
expect_total_matches(totals, 0, fee_name: "Shipping C")
end
end
describe "for specified payment methods" do
let!(:payment_method_a) do
create(:payment_method, name: "Payment A", distributors: [distributor])
end
let!(:payment_method_b) do
create(:payment_method, name: "Payment B", distributors: [distributor])
end
let!(:payment_method_c) do
create(:payment_method, name: "Payment C", distributors: [distributor])
end
let!(:order_a) { prepare_order(payment_method: payment_method_a) }
let!(:order_b) { prepare_order(payment_method: payment_method_b) }
let!(:order_c) { prepare_order(payment_method: payment_method_c) }
let(:parameters_attributes) do
{ payment_method_ids: [payment_method_a.id, payment_method_b.id] }
end
it "filters entries" do
totals = service.list
expect_total_matches(totals, 1, fee_name: "Payment A")
expect_total_matches(totals, 1, fee_name: "Payment B")
expect_total_matches(totals, 0, fee_name: "Payment C")
end
end
end
# Helper methods for example group
def expect_total_attributes(total, expected_attribute_list)
actual_attribute_list = [total.fee_type, total.enterprise_name, total.fee_name,
total.customer_name, total.fee_placement,
total.fee_calculated_on_transfer_through_name, total.tax_category_name,
total.total_amount]
expect(actual_attribute_list).to eq(expected_attribute_list)
end
def expect_total_matches(totals, count, attributes)
expect(count_totals(totals, attributes)).to eq(count)
end
def default_order_options
{ customer: customer, distributor: distributor, order_cycle: order_cycle,
shipping_method: shipping_method, variant: variant }
end
def prepare_incomplete_order(options = {})
target_options = default_order_options.merge(options)
create(:order, :with_line_item, target_options)
end
def prepare_order(options = {})
factory_trait_options = { payment_method: payment_method }
target_options = default_order_options.merge(factory_trait_options).merge(options)
create(:order, :with_line_item, :completed, target_options)
end
def default_variant_options
{ product: product, producer: producer, is_master: false, coordinator: coordinator,
distributor: distributor, order_cycle: order_cycle }
end
def prepare_variant(options = {})
target_options = default_variant_options.merge(options)
create(:variant, :with_order_cycle, target_options)
end
def count_totals(totals, attributes)
totals.count do |data|
attributes.all? do |attribute_name, attribute_value|
data.public_send(attribute_name) == attribute_value
end
end
end
end

View File

@@ -0,0 +1,10 @@
ENV["RAILS_ENV"] = "test"
require "order_management"
require "../../spec/spec_helper"
# Require factories in Spree and main application.
require 'spree/testing_support/factories'
require '../../spec/factories'
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }

View File

@@ -0,0 +1,79 @@
module OpenFoodNetwork
module Reports
class List
def self.all
new.all
end
def all
{
orders_and_fulfillment: orders_and_fulfillment_report_types,
products_and_inventory: products_and_inventory_report_types,
customers: customers_report_types,
enterprise_fee_summary: enterprise_fee_summary_report_types,
order_cycle_management: order_cycle_management_report_types,
sales_tax: sales_tax_report_types,
packing: packing_report_types
}
end
protected
def orders_and_fulfillment_report_types
[
[i18n_translate("supplier_totals"), :order_cycle_supplier_totals],
[i18n_translate("supplier_totals_by_distributor"),
:order_cycle_supplier_totals_by_distributor],
[i18n_translate("totals_by_supplier"), :order_cycle_distributor_totals_by_supplier],
[i18n_translate("customer_totals"), :order_cycle_customer_totals]
]
end
def products_and_inventory_report_types
[
[i18n_translate("all_products"), :all_products],
[i18n_translate("inventory"), :inventory],
[i18n_translate("lettuce_share"), :lettuce_share]
]
end
def customers_report_types
[
[i18n_translate("mailing_list"), :mailing_list],
[i18n_translate("addresses"), :addresses]
]
end
def enterprise_fee_summary_report_types
[
[i18n_translate("enterprise_fee_summary"), :enterprise_fee_summary]
]
end
def order_cycle_management_report_types
[
[i18n_translate("payment_methods"), :payment_methods],
[i18n_translate("delivery"), :delivery]
]
end
def sales_tax_report_types
[
[i18n_translate("tax_types"), :tax_types],
[i18n_translate("tax_rates"), :tax_rates]
]
end
def packing_report_types
[
[i18n_translate("pack_by_customer"), :pack_by_customer],
[i18n_translate("pack_by_supplier"), :pack_by_supplier]
]
end
def i18n_translate(key)
I18n.t(key, scope: "admin.reports")
end
end
end
end

View File

@@ -33,6 +33,10 @@ module OpenFoodNetwork
Spree::Variant.where(is_master: false).ransack(search_params.merge(m: 'or')).result
end
def distributor
Enterprise.find params[:distributor_id]
end
def scope_to_schedule
@variants = @variants.in_schedule(params[:schedule_id])
end
@@ -42,12 +46,29 @@ module OpenFoodNetwork
end
def scope_to_distributor
distributor = Enterprise.find params[:distributor_id]
if params[:eligible_for_subscriptions]
scope_to_eligible_for_subscriptions_in_distributor
else
scope_to_available_for_orders_in_distributor
end
end
def scope_to_available_for_orders_in_distributor
@variants = @variants.in_distributor(distributor)
scope_variants_to_distributor(@variants, distributor)
end
def scope_to_eligible_for_subscriptions_in_distributor
eligible_variants_scope = SubscriptionVariantsService.eligible_variants(distributor)
@variants = @variants.merge(eligible_variants_scope)
scope_variants_to_distributor(@variants, distributor)
end
def scope_variants_to_distributor(variants, distributor)
scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor)
# Perform scoping after all filtering is done.
# Filtering could be a problem on scoped variants.
@variants.each { |v| scoper.scope(v) }
variants.each { |v| scoper.scope(v) }
end
end
end

View File

@@ -1 +1 @@
producer,distributor,name,display_name,units,unit_type,price,on_hand
producer,distributor,name,display_name,variant_unit_name,sku,units,unit_type,price,on_hand,on_demand
1 producer distributor name display_name variant_unit_name sku units unit_type price on_hand on_demand

View File

@@ -10,9 +10,9 @@ describe Admin::SubscriptionLineItemsController, type: :controller do
let(:unmanaged_shop) { create(:enterprise) }
let!(:product) { create(:product) }
let!(:variant) { create(:variant, product: product, unit_value: '100', price: 15.00, option_values: []) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant], enterprise_fees: [enterprise_fee]) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 3.50) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let(:unmanaged_schedule) { create(:schedule, order_cycles: [create(:simple_order_cycle, coordinator: unmanaged_shop)]) }
@@ -42,6 +42,8 @@ describe Admin::SubscriptionLineItemsController, type: :controller do
before { params.merge!(shop_id: shop.id) }
context "but the shop doesn't have permission to sell product in question" do
let!(:outgoing_exchange) { }
it "returns an error" do
spree_post :build, params
json_response = JSON.parse(response.body)

View File

@@ -341,7 +341,7 @@ describe Admin::SubscriptionsController, type: :controller do
end
context 'with subscription_line_items params' do
let!(:product2) { create(:product, supplier: shop) }
let!(:product2) { create(:product) }
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
before do

View File

@@ -2,10 +2,11 @@ require 'spec_helper'
xdescribe Spree::Admin::InvoicesController, type: :controller do
let(:order) { create(:order_with_totals_and_distribution) }
let(:user) { create(:admin_user) }
let(:enterprise_user) { create(:user) }
let!(:enterprise) { create(:enterprise, owner: enterprise_user) }
before do
allow(controller).to receive(:spree_current_user) { user }
allow(controller).to receive(:spree_current_user) { enterprise_user }
end
describe "#create" do

View File

@@ -0,0 +1,102 @@
require "spec_helper"
describe Spree::Admin::Reports::EnterpriseFeeSummariesController, type: :controller do
let(:report_klass) { OrderManagement::Reports::EnterpriseFeeSummary }
let!(:distributor) { create(:distributor_enterprise) }
let(:current_user) { distributor.owner }
before do
feature_flags = instance_double(FeatureFlags, enterprise_fee_summary_enabled?: true)
allow(FeatureFlags).to receive(:new).with(current_user) { feature_flags }
allow(controller).to receive(:spree_current_user) { current_user }
end
describe "#new" do
it "renders the report form" do
get :new
expect(response).to be_success
expect(response).to render_template(new_template_path)
end
context "when feature flag is in effect" do
before { allow(FeatureFlags).to receive(:new).with(current_user).and_call_original }
it "is unauthorized" do
get :new
expect(response).to redirect_to spree.unauthorized_path
end
end
end
describe "#create" do
context "when the parameters are valid" do
it "sends the generated report in the correct format" do
post :create, report: { start_at: "2018-10-09 07:30:00" }, report_format: "csv"
expect(response).to be_success
expect(response.body).not_to be_blank
expect(response.header["Content-Type"]).to eq("text/csv")
end
context "when feature flag is in effect" do
before { allow(FeatureFlags).to receive(:new).with(current_user).and_call_original }
it "is unauthorized" do
post :create, report: { start_at: "2018-10-09 07:30:00" }, report_format: "csv"
expect(response).to redirect_to spree.unauthorized_path
end
end
end
context "when the parameters are invalid" do
it "renders the report form with an error" do
post :create, report: { start_at: "invalid date" }, report_format: "csv"
expect(flash[:error]).to eq(I18n.t("invalid_filter_parameters", scope: i18n_scope))
expect(response).to render_template(new_template_path)
end
end
context "when some parameters are now allowed" do
let!(:distributor) { create(:distributor_enterprise) }
let!(:other_distributor) { create(:distributor_enterprise) }
let(:current_user) { distributor.owner }
it "renders the report form with an error" do
post :create, report: { distributor_ids: [other_distributor.id] }, report_format: "csv"
expect(flash[:error]).to eq(report_klass::Authorizer.parameter_not_allowed_error_message)
expect(response).to render_template(new_template_path)
end
end
describe "filtering results based on permissions" do
let!(:distributor) { create(:distributor_enterprise) }
let!(:other_distributor) { create(:distributor_enterprise) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: distributor) }
let!(:other_order_cycle) { create(:simple_order_cycle, coordinator: other_distributor) }
let(:current_user) { distributor.owner }
it "applies permissions to report" do
post :create, report: {}, report_format: "csv"
expect(assigns(:permissions).allowed_order_cycles.to_a).to eq([order_cycle])
end
end
end
def i18n_scope
"order_management.reports.enterprise_fee_summary"
end
def new_template_path
"spree/admin/reports/enterprise_fee_summaries/new"
end
end

View File

@@ -0,0 +1,12 @@
attach_per_item_trait = proc do
trait :per_item do
transient { amount 1 }
calculator { build(:calculator_per_item, preferred_amount: amount) }
end
end
FactoryBot.modify do
factory :payment_method, &attach_per_item_trait
factory :shipping_method, &attach_per_item_trait
factory :enterprise_fee, &attach_per_item_trait
end

View File

@@ -0,0 +1,27 @@
FactoryBot.modify do
factory :order do
trait :with_line_item do
transient do
variant { FactoryGirl.create(:variant) }
end
after(:create) do |order, evaluator|
create(:line_item, order: order, variant: evaluator.variant)
end
end
trait :completed do
transient do
payment_method { create(:payment_method, distributors: [distributor]) }
end
after(:create) do |order, evaluator|
order.create_shipment!
create(:payment, state: "checkout", order: order, amount: order.total,
payment_method: evaluator.payment_method)
order.update_distribution_charge!
while !order.completed? do break unless order.next! end
end
end
end
end

View File

@@ -0,0 +1,34 @@
FactoryBot.modify do
factory :variant do
trait :with_order_cycle do
transient do
order_cycle { create(:order_cycle) }
producer { product.supplier }
coordinator { create(:distributor_enterprise) }
distributor { create(:distributor_enterprise) }
incoming_exchange_fees { [] }
outgoing_exchange_fees { [] }
end
after(:create) do |variant, evaluator|
exchange_attributes = { order_cycle_id: evaluator.order_cycle.id, incoming: true,
sender_id: evaluator.producer.id,
receiver_id: evaluator.coordinator.id }
exchange = Exchange.where(exchange_attributes).first_or_create!(exchange_attributes)
exchange.variants << variant
evaluator.incoming_exchange_fees.each do |enterprise_fee|
exchange.enterprise_fees << enterprise_fee
end
exchange_attributes = { order_cycle_id: evaluator.order_cycle.id, incoming: false,
sender_id: evaluator.coordinator.id,
receiver_id: evaluator.distributor.id }
exchange = Exchange.where(exchange_attributes).first_or_create!(exchange_attributes)
exchange.variants << variant
(evaluator.outgoing_exchange_fees || []).each do |enterprise_fee|
exchange.enterprise_fees << enterprise_fee
end
end
end
end
end

View File

@@ -439,7 +439,7 @@ feature %q{
expect(page).to have_selector "tr#li_#{li3.id}"
fill_in "quick_search", :with => o1.email
expect(page).to have_selector "tr#li_#{li1.id}"
expect(page).to have_no_selector "tr#li_#{li2.id}", true
expect(page).to have_no_selector "tr#li_#{li2.id}"
expect(page).to have_no_selector "tr#li_#{li3.id}"
end
end
@@ -567,6 +567,7 @@ feature %q{
context "when a filter has been applied" do
it "only toggles checkboxes which are in filteredLineItems" do
fill_in "quick_search", with: o1.number
expect(page).to have_no_selector "tr#li_#{li2.id}"
check "toggle_bulk"
fill_in "quick_search", with: ''
expect(find("tr#li_#{li1.id} input[type='checkbox'][name='bulk']").checked?).to be true
@@ -577,11 +578,13 @@ feature %q{
it "only applies the delete action to filteredLineItems" do
check "toggle_bulk"
fill_in "quick_search", with: o1.number
expect(page).to have_no_selector "tr#li_#{li2.id}"
find("div#bulk-actions-dropdown").click
find("div#bulk-actions-dropdown div.menu_item", :text => "Delete Selected" ).click
fill_in "quick_search", with: ''
expect(page).to have_no_selector "tr#li_#{li1.id}"
fill_in "quick_search", with: ''
expect(page).to have_selector "tr#li_#{li2.id}"
expect(page).to have_no_selector "tr#li_#{li1.id}"
end
end
end
@@ -740,10 +743,11 @@ feature %q{
end
def select_date(date)
current_month = Time.zone.today.strftime("%B")
target_month = date.strftime("%B")
# Wait for datepicker to open and be associated to the datepicker trigger.
expect(page).to have_selector("#ui-datepicker-div")
navigate_datepicker_to_month date
find('#ui-datepicker-div .ui-datepicker-header .ui-datepicker-prev').click if current_month != target_month
find('#ui-datepicker-div .ui-datepicker-calendar .ui-state-default', text: date.strftime("%e").to_s.strip, exact_text: true).click
end
end

View File

@@ -776,9 +776,9 @@ feature %q{
# Shows spinner whilst loading
expect(page).to have_css "img.spinner", visible: true
expect(page).to have_no_css "img.spinner", visible: true
end
expect(page).to have_no_css "img.spinner", visible: true
expect(page).to have_no_selector "div.reveal-modal"
within "table#listing_products tr#p_#{product.id}" do

View File

@@ -294,6 +294,49 @@ feature "Product Import", js: true do
expect(page).to have_content 'Cabbage'
end
end
it "handles on_demand and on_hand validations with inventory" do
csv_data = CSV.generate do |csv|
csv << ["name", "distributor", "producer", "category", "on_hand", "price", "units", "on_demand"]
csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", nil, "3.20", "500", "true"]
csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "6", "6.50", "500", "false"]
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "Vegetables", nil, "1.50", "500", nil]
end
File.write('/tmp/test.csv', csv_data)
visit main_app.admin_product_import_path
select2_select I18n.t('admin.product_import.index.inventories'), from: "settings_import_into"
attach_file 'file', '/tmp/test.csv'
click_button 'Upload'
proceed_to_validation
expect(page).to have_selector '.item-count', text: "3"
expect(page).to have_no_selector '.invalid-count'
expect(page).to have_selector '.inv-create-count', text: '2'
expect(page).to have_selector '.inv-update-count', text: '1'
save_data
expect(page).to have_selector '.inv-created-count', text: '2'
expect(page).to have_selector '.inv-updated-count', text: '1'
beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
expect(Float(beans_override.price)).to eq 3.20
expect(beans_override.count_on_hand).to be_nil
expect(beans_override.on_demand).to be_truthy
expect(Float(sprouts_override.price)).to eq 6.50
expect(sprouts_override.count_on_hand).to eq 6
expect(sprouts_override.on_demand).to eq false
expect(Float(cabbage_override.price)).to eq 1.50
expect(cabbage_override.count_on_hand).to be_nil
expect(cabbage_override.on_demand).to be_nil
end
end
describe "when dealing with uploaded files" do

View File

@@ -0,0 +1,159 @@
require "spec_helper"
xfeature "enterprise fee summaries", js: true do
include AuthenticationWorkflow
include WebHelper
let!(:distributor) { create(:distributor_enterprise) }
let!(:other_distributor) { create(:distributor_enterprise) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: distributor) }
let!(:other_order_cycle) { create(:simple_order_cycle, coordinator: other_distributor) }
before do
feature_flags = instance_double(FeatureFlags, enterprise_fee_summary_enabled?: true)
allow(FeatureFlags).to receive(:new).with(current_user) { feature_flags }
login_as current_user
end
describe "navigation" do
context "when accessing the report as an superadmin" do
let(:current_user) { create(:admin_user) }
it "shows link and allows access to the report" do
visit spree.admin_reports_path
click_on I18n.t("admin.reports.enterprise_fee_summary.name")
expect(page).to have_button(I18n.t("filters.generate_report", scope: i18n_scope))
end
end
context "when accessing the report as an admin" do
let(:current_user) { distributor.owner }
it "shows link and allows access to the report" do
visit spree.admin_reports_path
click_on I18n.t("admin.reports.enterprise_fee_summary.name")
expect(page).to have_button(I18n.t("filters.generate_report", scope: i18n_scope))
end
context "when feature flag is in effect" do
before { allow(FeatureFlags).to receive(:new).with(current_user).and_call_original }
it "does not show link now allow direct access to the report" do
visit spree.admin_reports_path
expect(page).to have_no_link I18n.t("admin.reports.enterprise_fee_summary.name")
visit spree.new_admin_reports_enterprise_fee_summary_path
expect(page).to have_no_button(I18n.t("filters.generate_report", scope: i18n_scope))
end
end
end
context "when accessing the report as an enterprise user without sufficient permissions" do
let(:current_user) { create(:user) }
it "does not allow access to the report" do
visit spree.admin_reports_path
expect(page).to have_no_link(I18n.t("admin.reports.enterprise_fee_summary.name"))
visit spree.new_admin_reports_enterprise_fee_summary_path
expect(page).to have_content(I18n.t("unauthorized"))
end
context "when feature flag is in effect" do
before { allow(FeatureFlags).to receive(:new).with(current_user).and_call_original }
it "does not show link now allow direct access to the report" do
visit spree.admin_reports_path
expect(page).to have_no_link I18n.t("admin.reports.enterprise_fee_summary.name")
visit spree.new_admin_reports_enterprise_fee_summary_path
expect(page).to have_no_button(I18n.t("filters.generate_report", scope: i18n_scope))
end
end
end
end
describe "smoke test for filters" do
before do
visit spree.new_admin_reports_enterprise_fee_summary_path
end
context "when logged in as admin" do
let(:current_user) { create(:admin_user) }
it "shows all available options" do
expect(page).to have_select "report_order_cycle_ids", with_options: [order_cycle.name]
end
end
context "when logged in as enterprise user" do
let!(:order) { create(:completed_order_with_fees, order_cycle: order_cycle, distributor: distributor) }
let(:current_user) { distributor.owner }
it "shows available options for the enterprise" do
expect(page).to have_select "report_order_cycle_ids", options: [order_cycle.name]
end
end
end
describe "smoke test for generation of report based on permissions" do
before do
visit spree.new_admin_reports_enterprise_fee_summary_path
end
context "when logged in as admin" do
let!(:order) { create(:completed_order_with_fees, order_cycle: order_cycle, distributor: distributor) }
let(:current_user) { create(:admin_user) }
it "generates file with data for all enterprises" do
check I18n.t("filters.report_format_csv", scope: i18n_scope)
click_on I18n.t("filters.generate_report", scope: i18n_scope)
expect(page.response_headers['Content-Type']).to eq "text/csv"
expect(page.body).to have_content(distributor.name)
end
end
context "when logged in as enterprise user" do
let!(:order) { create(:completed_order_with_fees, order_cycle: order_cycle, distributor: distributor) }
let!(:other_order) { create(:completed_order_with_fees, order_cycle: other_order_cycle, distributor: other_distributor) }
let(:current_user) { distributor.owner }
it "generates file with data for the enterprise" do
check I18n.t("filters.report_format_csv", scope: i18n_scope)
click_on I18n.t("filters.generate_report", scope: i18n_scope)
expect(page.response_headers['Content-Type']).to eq "text/csv"
expect(page.body).to have_content(distributor.name)
expect(page.body).not_to have_content(other_distributor.name)
end
end
end
describe "smoke test for filtering report based on filters" do
let!(:second_distributor) { create(:distributor_enterprise) }
let!(:second_order_cycle) { create(:simple_order_cycle, coordinator: second_distributor) }
let!(:order) { create(:completed_order_with_fees, order_cycle: order_cycle, distributor: distributor) }
let!(:second_order) { create(:completed_order_with_fees, order_cycle: second_order_cycle, distributor: second_distributor) }
let(:current_user) { create(:admin_user) }
before do
visit spree.new_admin_reports_enterprise_fee_summary_path
end
it "generates file with data for selected order cycle" do
select order_cycle.name, from: "report_order_cycle_ids"
check I18n.t("filters.report_format_csv", scope: i18n_scope)
click_on I18n.t("filters.generate_report", scope: i18n_scope)
expect(page.response_headers['Content-Type']).to eq "text/csv"
expect(page.body).to have_content(distributor.name)
expect(page.body).not_to have_content(second_distributor.name)
end
end
def i18n_scope
"spree.admin.reports.enterprise_fee_summaries"
end
end

View File

@@ -31,10 +31,13 @@ feature 'shipping methods' do
check "shipping_method_distributor_ids_#{d1.id}"
check "shipping_method_distributor_ids_#{d2.id}"
check "shipping_method_shipping_categories_"
click_button 'Create'
click_button I18n.t("actions.create")
expect(page).to have_no_button I18n.t("actions.create")
# Then the shipping method should have its distributor set
expect(flash_message).to eq('Shipping method "Carrier Pidgeon" has been successfully created!')
message = "Shipping method \"Carrier Pidgeon\" has been successfully created!"
expect(page).to have_flash_message message
sm = Spree::ShippingMethod.last
expect(sm.name).to eq('Carrier Pidgeon')
@@ -104,9 +107,12 @@ feature 'shipping methods' do
expect(page).to have_css '.tag-item'
end
click_button 'Create'
click_button I18n.t("actions.create")
expect(page).to have_no_button I18n.t("actions.create")
message = "Shipping method \"Teleport\" has been successfully created!"
expect(page).to have_flash_message message
expect(flash_message).to eq('Shipping method "Teleport" has been successfully created!')
expect(first('tags-input .tag-list ti-tag-item')).to have_content "local"
shipping_method = Spree::ShippingMethod.find_by_name('Teleport')

View File

@@ -145,23 +145,25 @@ xfeature 'Subscriptions' do
let!(:customer_user) { create(:user) }
let!(:credit_card1) { create(:credit_card, user: customer_user, cc_type: 'visa', last_digits: 1111, month: 10, year: 2030) }
let!(:customer) { create(:customer, enterprise: shop, bill_address: address, user: customer_user, allow_charges: true) }
let!(:product1) { create(:product, supplier: shop) }
let!(:product2) { create(:product, supplier: shop) }
let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) }
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
let!(:test_product) { create(:product, supplier: shop, distributors: []) }
let!(:test_variant) { create(:variant, product: test_product, unit_value: "100", price: 12.00, option_values: []) }
let!(:shop_product) { create(:product, supplier: shop, distributors: [shop]) }
let!(:shop_variant) { create(:variant, product: shop_product, unit_value: "1000", price: 6.00, option_values: []) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [test_variant, shop_variant], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
it "passes the smoke test" do
before do
visit admin_subscriptions_path
click_link 'New Subscription'
select2_select shop.name, from: 'new_subscription_shop_id'
click_button 'Continue'
click_link "New Subscription"
select2_select shop.name, from: "new_subscription_shop_id"
click_button "Continue"
end
it "passes the smoke test" do
select2_select customer.email, from: 'customer_id'
select2_select schedule.name, from: 'schedule_id'
select2_select payment_method.name, from: 'payment_method_id'
@@ -215,11 +217,9 @@ xfeature 'Subscriptions' do
expect(page).to have_content 'Please add at least one product'
# Adding a product and getting a price estimate
select2_search_async product1.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
fill_in 'add_quantity', with: 2
click_link 'Add'
add_variant_to_subscription test_variant, 2
within 'table#subscription-line-items tr.item', match: :first do
expect(page).to have_selector 'td.description', text: "#{product1.name} - #{variant1.full_name}"
expect(page).to have_selector '.description', text: "#{test_product.name} - #{test_variant.full_name}"
expect(page).to have_selector 'td.price', text: "$13.75"
expect(page).to have_input 'quantity', with: "2"
expect(page).to have_selector 'td.total', text: "$27.50"
@@ -241,11 +241,9 @@ xfeature 'Subscriptions' do
click_button('edit-products')
# Adding a new product
select2_search_async product2.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
fill_in 'add_quantity', with: 3
click_link 'Add'
add_variant_to_subscription shop_variant, 3
within 'table#subscription-line-items tr.item', match: :first do
expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}"
expect(page).to have_selector '.description', text: "#{shop_product.name} - #{shop_variant.full_name}"
expect(page).to have_selector 'td.price', text: "$7.75"
expect(page).to have_input 'quantity', with: "3"
expect(page).to have_selector 'td.total', text: "$23.25"
@@ -264,7 +262,7 @@ xfeature 'Subscriptions' do
# Prices are shown in the index
within 'table#subscription-line-items tr.item', match: :first do
expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}"
expect(page).to have_selector '.description', text: "#{shop_product.name} - #{shop_variant.full_name}"
expect(page).to have_selector 'td.price', text: "$7.75"
expect(page).to have_input 'quantity', with: "3"
expect(page).to have_selector 'td.total', text: "$23.25"
@@ -282,142 +280,249 @@ xfeature 'Subscriptions' do
# Standing Line Items are created
expect(subscription.subscription_line_items.count).to eq 1
subscription_line_item = subscription.subscription_line_items.first
expect(subscription_line_item.variant).to eq variant2
expect(subscription_line_item.variant).to eq shop_variant
expect(subscription_line_item.quantity).to eq 3
end
end
context 'editing an existing subscription' do
let!(:customer) { create(:customer, enterprise: shop) }
let!(:product1) { create(:product, supplier: shop) }
let!(:product2) { create(:product, supplier: shop) }
let!(:product3) { create(:product, supplier: shop) }
let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) }
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
let!(:variant3) { create(:variant, product: product3, unit_value: '10000', price: 22.00, option_values: []) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:variant3_oc) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:variant3_ex) { variant3_oc.exchanges.create(sender: shop, receiver: shop, variants: [variant3]) }
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
let!(:stripe_payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
let!(:subscription) {
create(:subscription,
shop: shop,
customer: customer,
schedule: schedule,
payment_method: payment_method,
shipping_method: shipping_method,
subscription_line_items: [create(:subscription_line_item, variant: variant1, quantity: 2, price_estimate: 13.75)],
with_proxy_orders: true)
}
context 'editing an existing subscription' do
let!(:customer) { create(:customer, enterprise: shop) }
let!(:product1) { create(:product, supplier: shop) }
let!(:product2) { create(:product, supplier: shop) }
let!(:product3) { create(:product, supplier: shop) }
let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) }
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
let!(:variant3) { create(:variant, product: product3, unit_value: '10000', price: 22.00, option_values: []) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:variant3_oc) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:variant3_ex) { variant3_oc.exchanges.create(sender: shop, receiver: shop, variants: [variant3]) }
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
let!(:stripe_payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
let!(:subscription) {
create(:subscription,
shop: shop,
customer: customer,
schedule: schedule,
payment_method: payment_method,
shipping_method: shipping_method,
subscription_line_items: [create(:subscription_line_item, variant: variant1, quantity: 2, price_estimate: 13.75)],
with_proxy_orders: true)
}
it "passes the smoke test" do
visit edit_admin_subscription_path(subscription)
it "passes the smoke test" do
visit edit_admin_subscription_path(subscription)
# Customer and Schedule cannot be edited
click_button 'edit-details'
expect(page).to have_selector '#s2id_customer_id.select2-container-disabled'
expect(page).to have_selector '#s2id_schedule_id.select2-container-disabled'
# Customer and Schedule cannot be edited
click_button 'edit-details'
expect(page).to have_selector '#s2id_customer_id.select2-container-disabled'
expect(page).to have_selector '#s2id_schedule_id.select2-container-disabled'
# Can't use a Stripe payment method because customer does not allow it
select2_select stripe_payment_method.name, from: 'payment_method_id'
expect(page).to have_content I18n.t('admin.subscriptions.details.charges_not_allowed')
click_button 'Save Changes'
expect(page).to have_content 'Credit card charges are not allowed by this customer'
select2_select payment_method.name, from: 'payment_method_id'
click_button 'Review'
# Can't use a Stripe payment method because customer does not allow it
select2_select stripe_payment_method.name, from: 'payment_method_id'
expect(page).to have_content I18n.t('admin.subscriptions.details.charges_not_allowed')
click_button 'Save Changes'
expect(page).to have_content 'Credit card charges are not allowed by this customer'
select2_select payment_method.name, from: 'payment_method_id'
click_button 'Review'
# Existing products should be visible
click_button 'edit-products'
within "#sli_0" do
expect(page).to have_selector 'td.description', text: "#{product1.name} - #{variant1.full_name}"
expect(page).to have_selector 'td.price', text: "$13.75"
expect(page).to have_input 'quantity', with: "2"
expect(page).to have_selector 'td.total', text: "$27.50"
# Existing products should be visible
click_button 'edit-products'
within "#sli_0" do
expect(page).to have_selector '.description', text: "#{product1.name} - #{variant1.full_name}"
expect(page).to have_selector 'td.price', text: "$13.75"
expect(page).to have_input 'quantity', with: "2"
expect(page).to have_selector 'td.total', text: "$27.50"
# Remove variant1 from the subscription
find("a.delete-item").click
end
# Attempting to submit without a product
click_button 'Save Changes'
expect(page).to have_content 'Please add at least one product'
# Add variant2 to the subscription
select2_search_async product2.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
fill_in 'add_quantity', with: 1
click_link 'Add'
within "#sli_0" do
expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}"
expect(page).to have_selector 'td.price', text: "$7.75"
expect(page).to have_input 'quantity', with: "1"
expect(page).to have_selector 'td.total', text: "$7.75"
end
# Total should be $7.75
expect(page).to have_selector '#order_form_total', text: "$7.75"
# Add variant3 to the subscription (even though it is not available)
select2_search_async product3.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
fill_in 'add_quantity', with: 1
click_link 'Add'
within "#sli_1" do
expect(page).to have_selector 'td.description', text: "#{product3.name} - #{variant3.full_name}"
expect(page).to have_selector 'td.price', text: "$22.00"
expect(page).to have_input 'quantity', with: "1"
expect(page).to have_selector 'td.total', text: "$22.00"
end
# Total should be $29.75
expect(page).to have_selector '#order_form_total', text: "$29.75"
click_button 'Save Changes'
expect(page).to have_content "#{product3.name} - #{variant3.full_name} is not available from the selected schedule"
# Remove variant3 from the subscription
within '#sli_1' do
find("a.delete-item").click
end
click_button 'Save Changes'
expect(page).to have_current_path admin_subscriptions_path
select2_select shop.name, from: "shop_id"
expect(page).to have_selector "td.items.panel-toggle"
first("td.items.panel-toggle").click
# Total should be $7.75
expect(page).to have_selector '#order_form_total', text: "$7.75"
expect(page).to have_selector 'tr.item', count: 1
expect(subscription.reload.subscription_line_items.length).to eq 1
expect(subscription.subscription_line_items.first.variant).to eq variant2
# Remove variant1 from the subscription
find("a.delete-item").click
end
context "with initialised order that has been changed" do
let(:proxy_order) { subscription.proxy_orders.first }
let(:order) { proxy_order.initialise_order! }
let(:line_item) { order.line_items.first }
# Attempting to submit without a product
click_button 'Save Changes'
expect(page).to have_content 'Please add at least one product'
before { line_item.update_attributes(quantity: 3) }
# Add variant2 to the subscription
add_variant_to_subscription(variant2, 1)
within "#sli_0" do
expect(page).to have_selector '.description', text: "#{product2.name} - #{variant2.full_name}"
expect(page).to have_selector 'td.price', text: "$7.75"
expect(page).to have_input 'quantity', with: "1"
expect(page).to have_selector 'td.total', text: "$7.75"
end
it "reports issues encountered during the update" do
visit edit_admin_subscription_path(subscription)
click_button 'edit-products'
# Total should be $7.75
expect(page).to have_selector '#order_form_total', text: "$7.75"
within "#sli_0" do
fill_in 'quantity', with: "1"
end
# Add variant3 to the subscription (even though it is not available)
add_variant_to_subscription(variant3, 1)
within "#sli_1" do
expect(page).to have_selector '.description', text: "#{product3.name} - #{variant3.full_name}"
expect(page).to have_selector 'td.price', text: "$22.00"
expect(page).to have_input 'quantity', with: "1"
expect(page).to have_selector 'td.total', text: "$22.00"
end
click_button 'Save Changes'
expect(page).to have_content 'Saved'
# Total should be $29.75
expect(page).to have_selector '#order_form_total', text: "$29.75"
expect(page).to have_selector "#order_update_issues_dialog .message", text: I18n.t("admin.subscriptions.order_update_issues_msg")
# Remove variant3 from the subscription
within '#sli_1' do
find("a.delete-item").click
end
click_button 'Save Changes'
expect(page).to have_current_path admin_subscriptions_path
select2_select shop.name, from: "shop_id"
expect(page).to have_selector "td.items.panel-toggle"
first("td.items.panel-toggle").click
# Total should be $7.75
expect(page).to have_selector '#order_form_total', text: "$7.75"
expect(page).to have_selector 'tr.item', count: 1
expect(subscription.reload.subscription_line_items.length).to eq 1
expect(subscription.subscription_line_items.first.variant).to eq variant2
end
context "with initialised order that has been changed" do
let(:proxy_order) { subscription.proxy_orders.first }
let(:order) { proxy_order.initialise_order! }
let(:line_item) { order.line_items.first }
before { line_item.update_attributes(quantity: 3) }
it "reports issues encountered during the update" do
visit edit_admin_subscription_path(subscription)
click_button 'edit-products'
within "#sli_0" do
fill_in 'quantity', with: "1"
end
click_button 'Save Changes'
expect(page).to have_content 'Saved'
expect(page).to have_selector "#order_update_issues_dialog .message", text: I18n.t("admin.subscriptions.order_update_issues_msg")
end
end
end
describe "allowed variants" do
let!(:customer) { create(:customer, enterprise: shop, allow_charges: true) }
let!(:credit_card) { create(:credit_card, user: customer.user) }
let!(:shop_product) { create(:product, supplier: shop, distributors: [shop]) }
let!(:shop_variant) { create(:variant, product: shop_product, unit_value: "2000") }
let!(:permitted_supplier) do
create(:supplier_enterprise).tap do |supplier|
create(:enterprise_relationship, child: shop, parent: supplier, permissions_list: [:add_to_order_cycle])
end
end
let!(:permitted_supplier_product) { create(:product, supplier: permitted_supplier, distributors: [shop]) }
let!(:permitted_supplier_variant) { create(:variant, product: permitted_supplier_product, unit_value: "2000") }
let!(:incoming_exchange_product) { create(:product, distributors: [shop]) }
let!(:incoming_exchange_variant) do
create(:variant, product: incoming_exchange_product, unit_value: "2000").tap do |variant|
create(:exchange, order_cycle: order_cycle, incoming: true, receiver: shop, variants: [variant])
end
end
let!(:outgoing_exchange_product) { create(:product, distributors: [shop]) }
let!(:outgoing_exchange_variant) do
create(:variant, product: outgoing_exchange_product, unit_value: "2000").tap do |variant|
create(:exchange, order_cycle: order_cycle, incoming: false, receiver: shop, variants: [variant])
end
end
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:payment_method) { create(:stripe_payment_method, distributors: [shop], preferred_enterprise_id: shop.id) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
before do
visit admin_subscriptions_path
click_link "New Subscription"
select2_select shop.name, from: "new_subscription_shop_id"
click_button "Continue"
end
it "permit creating and editing of the subscription" do
# Fill in other details
fill_in_subscription_basic_details
click_button "Next"
expect(page).to have_content "BILLING ADDRESS"
click_button "Next"
# Add products
expect(page).to have_content "NAME OR SKU"
add_variant_to_subscription shop_variant, 3
expect_not_in_open_or_upcoming_order_cycle_warning 1
add_variant_to_subscription permitted_supplier_variant, 4
expect_not_in_open_or_upcoming_order_cycle_warning 2
add_variant_to_subscription incoming_exchange_variant, 5
expect_not_in_open_or_upcoming_order_cycle_warning 3
add_variant_to_subscription outgoing_exchange_variant, 6
expect_not_in_open_or_upcoming_order_cycle_warning 3
click_button "Next"
# Submit form
expect {
click_button "Create Subscription"
expect(page).to have_current_path admin_subscriptions_path
}.to change(Subscription, :count).by(1)
# Subscription line items are created
subscription = Subscription.last
expect(subscription.subscription_line_items.count).to eq 4
# Edit the subscription
visit edit_admin_subscription_path(subscription)
# Remove shop_variant from the subscription
click_button "edit-products"
within "#sli_0" do
expect(page).to have_selector ".description", text: shop_variant.name
find("a.delete-item").click
end
# Submit form
click_button "Save Changes"
expect(page).to have_current_path admin_subscriptions_path
# Subscription is saved
visit edit_admin_subscription_path(subscription)
expect(page).to have_selector "#subscription-line-items .item", count: 3
end
end
end
def fill_in_subscription_basic_details
select2_select customer.email, from: "customer_id"
select2_select schedule.name, from: "schedule_id"
select2_select payment_method.name, from: "payment_method_id"
select2_select shipping_method.name, from: "shipping_method_id"
find_field("begins_at").click
choose_today_from_datepicker
end
def expect_not_in_open_or_upcoming_order_cycle_warning(count)
expect(page).to have_content variant_not_in_open_or_upcoming_order_cycle_warning, count: count
end
def add_variant_to_subscription(variant, quantity)
row_count = all("#subscription-line-items .item").length
variant_name = variant.full_name.present? ? "#{variant.name} - #{variant.full_name}" : variant.name
select2_search variant.name, from: I18n.t(:name_or_sku), dropdown_css: ".select2-drop", select_text: variant_name
fill_in "add_quantity", with: quantity
click_link "Add"
expect(page).to have_selector("#subscription-line-items .item", count: row_count + 1)
end
def variant_not_in_open_or_upcoming_order_cycle_warning
I18n.t("not_in_open_and_upcoming_order_cycles_warning",
scope: "admin.subscriptions.subscription_line_items")
end
end

View File

@@ -1,10 +1,10 @@
require 'spec_helper'
describe FeatureFlags do
describe '.product_import_enabled?' do
let(:user) { build_stubbed(:user) }
let(:feature_flags) { described_class.new(user) }
let(:user) { build_stubbed(:user) }
let(:feature_flags) { described_class.new(user) }
describe '#product_import_enabled?' do
context 'when the user is superadmin' do
before do
allow(user).to receive(:has_spree_role?).with('admin') { true }
@@ -25,4 +25,22 @@ describe FeatureFlags do
end
end
end
describe "#enterprise_fee_summary_enabled?" do
context "when the user is superadmin" do
let!(:user) { create(:admin_user) }
it "returns true" do
expect(feature_flags).to be_enterprise_fee_summary_enabled
end
end
context "when the user is not superadmin" do
let!(:user) { create(:user) }
it "returns false" do
expect(feature_flags).not_to be_enterprise_fee_summary_enabled
end
end
end
end

View File

@@ -26,7 +26,7 @@ xdescribe ProductImport::ProductImporter do
let!(:variant) { create(:variant, product_id: product.id, price: '8.50', on_hand: '100', unit_value: '500', display_name: 'Preexisting Banana') }
let!(:product2) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Beans', unit_value: '500', primary_taxon_id: category.id, description: nil) }
let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts', unit_value: '500', primary_taxon_id: category.id) }
let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '500', primary_taxon_id: category.id) }
let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '1', variant_unit_scale: nil, variant_unit: "items", variant_unit_name: "Whole", primary_taxon_id: category.id) }
let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500', primary_taxon_id: category.id) }
let!(:product6) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Beetroot', unit_value: '500', on_demand: true, variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id, description: nil) }
let!(:product7) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Tomato', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id, description: nil) }
@@ -467,50 +467,106 @@ xdescribe ProductImport::ProductImporter do
end
describe "importing items into inventory" do
before do
csv_data = CSV.generate do |csv|
csv << ["name", "distributor", "producer", "on_hand", "price", "units", "unit_type"]
csv << ["Beans", "Another Enterprise", "User Enterprise", "5", "3.20", "500", "g"]
csv << ["Sprouts", "Another Enterprise", "User Enterprise", "6", "6.50", "500", "g"]
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "2001", "1.50", "500", "g"]
describe "creating and updating inventory" do
before do
csv_data = CSV.generate do |csv|
csv << ["name", "distributor", "producer", "on_hand", "price", "units", "unit_type", "variant_unit_name"]
csv << ["Beans", "Another Enterprise", "User Enterprise", "5", "3.20", "500", "g", ""]
csv << ["Sprouts", "Another Enterprise", "User Enterprise", "6", "6.50", "500", "g", ""]
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "2001", "1.50", "1", "", "Whole"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
settings = {'import_into' => 'inventories'}
@importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings)
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
settings = {'import_into' => 'inventories'}
@importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings)
end
after { File.delete('/tmp/test-m.csv') }
after { File.delete('/tmp/test-m.csv') }
it "validates entries" do
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
it "validates entries" do
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 3
expect(filter('invalid', entries)).to eq 0
expect(filter('create_inventory', entries)).to eq 2
expect(filter('update_inventory', entries)).to eq 1
expect(filter('valid', entries)).to eq 3
expect(filter('invalid', entries)).to eq 0
expect(filter('create_inventory', entries)).to eq 2
expect(filter('update_inventory', entries)).to eq 1
end
it "saves and updates inventory" do
@importer.save_entries
expect(@importer.inventory_created_count).to eq 2
expect(@importer.inventory_updated_count).to eq 1
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 3
beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
expect(Float(beans_override.price)).to eq 3.20
expect(beans_override.count_on_hand).to eq 5
expect(Float(sprouts_override.price)).to eq 6.50
expect(sprouts_override.count_on_hand).to eq 6
expect(Float(cabbage_override.price)).to eq 1.50
expect(cabbage_override.count_on_hand).to eq 2001
end
end
it "saves and updates inventory" do
@importer.save_entries
describe "updating existing inventory referenced by display_name" do
before do
csv_data = CSV.generate do |csv|
csv << ["name", "display_name", "distributor", "producer", "on_hand", "price", "units"]
csv << ["Oats", "Porridge Oats", "Another Enterprise", "User Enterprise", "900", "", "500"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
settings = {'import_into' => 'inventories'}
@importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings)
end
after { File.delete('/tmp/test-m.csv') }
expect(@importer.inventory_created_count).to eq 2
expect(@importer.inventory_updated_count).to eq 1
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 3
it "updates inventory item correctly" do
@importer.save_entries
beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
expect(@importer.inventory_created_count).to eq 1
expect(Float(beans_override.price)).to eq 3.20
expect(beans_override.count_on_hand).to eq 5
override = VariantOverride.where(variant_id: variant2.id, hub_id: enterprise2.id).first
visible = InventoryItem.where(variant_id: variant2.id, enterprise_id: enterprise2.id).first.visible
expect(Float(sprouts_override.price)).to eq 6.50
expect(sprouts_override.count_on_hand).to eq 6
expect(override.count_on_hand).to eq 900
expect(visible).to be_truthy
end
end
expect(Float(cabbage_override.price)).to eq 1.50
expect(cabbage_override.count_on_hand).to eq 2001
describe "updating existing item that was set to hidden in inventory" do
before do
InventoryItem.create(variant_id: product4.variants.first.id, enterprise_id: enterprise2.id, visible: false)
csv_data = CSV.generate do |csv|
csv << ["name", "distributor", "producer", "on_hand", "price", "units", "variant_unit_name"]
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "900", "", "1", "Whole"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
settings = {'import_into' => 'inventories'}
@importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings)
end
after { File.delete('/tmp/test-m.csv') }
it "sets the item to visible in inventory when the item is updated" do
@importer.save_entries
expect(@importer.inventory_updated_count).to eq 1
override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
visible = InventoryItem.where(variant_id: product4.variants.first.id, enterprise_id: enterprise2.id).first.visible
expect(override.count_on_hand).to eq 900
expect(visible).to be_truthy
end
end
end

View File

@@ -4,9 +4,11 @@ require 'support/cancan_helper'
module Spree
describe User do
describe "broad permissions" do
subject { AbilityDecorator.new(user) }
include ::AbilityHelper
let(:user) { create(:user) }
let(:enterprise_any) { create(:enterprise, sells: 'any') }
let(:enterprise_own) { create(:enterprise, sells: 'own') }
@@ -215,6 +217,8 @@ module Spree
should have_ability([:admin, :index, :customers, :bulk_coop, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], for: :report)
end
include_examples "allows access to Enterprise Fee Summary only if feature flag enabled"
it "should not be able to read other reports" do
should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors, :users_and_enterprises, :xero_invoices], for: :report)
end
@@ -406,6 +410,8 @@ module Spree
should have_ability([:admin, :index, :customers, :sales_tax, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], for: :report)
end
include_examples "allows access to Enterprise Fee Summary only if feature flag enabled"
it "should not be able to read other reports" do
should_not have_ability([:sales_total, :users_and_enterprises], for: :report)
end

View File

@@ -43,5 +43,24 @@ module Spree
order.add_variant(product.master)
expect(flat_percent_payment_method.compute_amount(order)).to eq 2.0
end
describe "scope" do
describe "filtering to specified distributors" do
let!(:distributor_a) { create(:distributor_enterprise) }
let!(:distributor_b) { create(:distributor_enterprise) }
let!(:distributor_c) { create(:distributor_enterprise) }
let!(:payment_method_a) { create(:payment_method, distributors: [distributor_a, distributor_b]) }
let!(:payment_method_b) { create(:payment_method, distributors: [distributor_b]) }
let!(:payment_method_c) { create(:payment_method, distributors: [distributor_c]) }
it "includes only unique records under specified distributors" do
result = described_class.for_distributors([distributor_a, distributor_b])
expect(result.length).to eq(2)
expect(result).to include(payment_method_a)
expect(result).to include(payment_method_b)
end
end
end
end
end

View File

@@ -18,13 +18,32 @@ module Spree
sm.reload.distributors.should match_array [d1, d2]
end
it "finds shipping methods for a particular distributor" do
d1 = create(:distributor_enterprise)
d2 = create(:distributor_enterprise)
sm1 = create(:shipping_method, distributors: [d1])
sm2 = create(:shipping_method, distributors: [d2])
describe "scope" do
describe "filtering to specified distributors" do
let!(:distributor_a) { create(:distributor_enterprise) }
let!(:distributor_b) { create(:distributor_enterprise) }
let!(:distributor_c) { create(:distributor_enterprise) }
ShippingMethod.for_distributor(d1).should == [sm1]
let!(:shipping_method_a) { create(:shipping_method, distributors: [distributor_a, distributor_b]) }
let!(:shipping_method_b) { create(:shipping_method, distributors: [distributor_b]) }
let!(:shipping_method_c) { create(:shipping_method, distributors: [distributor_c]) }
it "includes only unique records under specified distributors" do
result = described_class.for_distributors([distributor_a, distributor_b])
expect(result.length).to eq(2)
expect(result).to include(shipping_method_a)
expect(result).to include(shipping_method_b)
end
end
it "finds shipping methods for a particular distributor" do
d1 = create(:distributor_enterprise)
d2 = create(:distributor_enterprise)
sm1 = create(:shipping_method, distributors: [d1])
sm2 = create(:shipping_method, distributors: [d2])
ShippingMethod.for_distributor(d1).should == [sm1]
end
end
it "orders shipping methods by name" do

View File

@@ -1,8 +1,11 @@
require "spec_helper"
describe SubscriptionValidator do
let(:shop) { instance_double(Enterprise, name: "Shop") }
let(:owner) { create(:user) }
let(:shop) { create(:enterprise, name: "Shop", owner: owner) }
describe "delegation" do
let(:subscription) { create(:subscription) }
let(:subscription) { create(:subscription, shop: shop) }
let(:validator) { SubscriptionValidator.new(subscription) }
it "delegates to subscription" do
@@ -438,6 +441,7 @@ describe SubscriptionValidator do
context "but some variants are unavailable" do
let(:product) { instance_double(Spree::Product, name: "some_name") }
before do
allow(validator).to receive(:available_variant_ids) { [variant2.id] }
allow(variant1).to receive(:product) { product }
@@ -451,7 +455,9 @@ describe SubscriptionValidator do
end
context "and all requested variants are available" do
before { allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] } }
before do
allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] }
end
it "returns true" do
expect(validator.valid?).to be true

View File

@@ -0,0 +1,130 @@
require "spec_helper"
describe SubscriptionVariantsService do
describe "variant eligibility for subscription" do
let!(:shop) { create(:distributor_enterprise) }
let!(:producer) { create(:supplier_enterprise) }
let!(:product) { create(:product, supplier: producer) }
let!(:variant) { product.variants.first }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:subscription) { create(:subscription, shop: shop, schedule: schedule) }
let!(:subscription_line_item) do
create(:subscription_line_item, subscription: subscription, variant: variant)
end
let(:current_order_cycle) do
create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.ago,
orders_close_at: 1.week.from_now)
end
let(:future_order_cycle) do
create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.from_now,
orders_close_at: 2.weeks.from_now)
end
let(:past_order_cycle) do
create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.weeks.ago,
orders_close_at: 1.week.ago)
end
let!(:order_cycle) { current_order_cycle }
context "if the shop is the supplier for the product" do
let!(:producer) { shop }
it "is eligible" do
expect(described_class.eligible_variants(shop)).to include(variant)
end
end
context "if the supplier is permitted for the shop" do
let!(:enterprise_relationship) { create(:enterprise_relationship, child: shop, parent: product.supplier, permissions_list: [:add_to_order_cycle]) }
it "is eligible" do
expect(described_class.eligible_variants(shop)).to include(variant)
end
end
context "if the variant is involved in an exchange" do
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
context "if it is an incoming exchange where the shop is the receiver" do
let!(:incoming_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: true, variants: [variant]) }
it "is not eligible" do
expect(described_class.eligible_variants(shop)).to_not include(variant)
end
end
context "if it is an outgoing exchange where the shop is the receiver" do
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: false, variants: [variant]) }
context "if the order cycle is currently open" do
let!(:order_cycle) { current_order_cycle }
it "is eligible" do
expect(described_class.eligible_variants(shop)).to include(variant)
end
end
context "if the order cycle opens in the future" do
let!(:order_cycle) { future_order_cycle }
it "is eligible" do
expect(described_class.eligible_variants(shop)).to include(variant)
end
end
context "if the order cycle closed in the past" do
let!(:order_cycle) { past_order_cycle }
it "is eligible" do
expect(described_class.eligible_variants(shop)).to include(variant)
end
end
end
end
context "if the variant is unrelated" do
it "is not eligible" do
expect(described_class.eligible_variants(shop)).to_not include(variant)
end
end
end
describe "checking if variant in open and upcoming order cycles" do
let!(:shop) { create(:enterprise) }
let!(:product) { create(:product) }
let!(:variant) { product.variants.first }
let!(:schedule) { create(:schedule) }
context "if the variant is involved in an exchange" do
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
context "if it is an incoming exchange where the shop is the receiver" do
let!(:incoming_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: true, variants: [variant]) }
it "is is false" do
expect(described_class).not_to be_in_open_and_upcoming_order_cycles(shop, schedule, variant)
end
end
context "if it is an outgoing exchange where the shop is the receiver" do
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: false, variants: [variant]) }
it "is true" do
expect(described_class).to be_in_open_and_upcoming_order_cycles(shop, schedule, variant)
end
end
end
context "if the variant is unrelated" do
it "is false" do
expect(described_class).to_not be_in_open_and_upcoming_order_cycles(shop, schedule, variant)
end
end
end
end

View File

@@ -135,6 +135,7 @@ RSpec.configure do |config|
config.extend Spree::Api::TestingSupport::Setup, :type => :controller
config.include Spree::Api::TestingSupport::Helpers, :type => :controller
config.include OpenFoodNetwork::ControllerHelper, :type => :controller
config.include Features::DatepickerHelper, type: :feature
config.include OpenFoodNetwork::FeatureToggleHelper
config.include OpenFoodNetwork::FiltersHelper
config.include OpenFoodNetwork::EnterpriseGroupsHelper

View File

@@ -0,0 +1,28 @@
module AbilityHelper
shared_examples "allows access to Enterprise Fee Summary only if feature flag enabled" do
it "should not be able to read Enterprise Fee Summary" do
is_expected.not_to have_link_to_enterprise_fee_summary
is_expected.not_to have_direct_access_to_enterprise_fee_summary
end
context "when feature flag for Enterprise Fee Summary is enabled absolutely" do
before do
feature_flags = instance_double(FeatureFlags, enterprise_fee_summary_enabled?: true)
allow(FeatureFlags).to receive(:new).with(user) { feature_flags }
end
it "should be able to see link and read report" do
is_expected.to have_link_to_enterprise_fee_summary
is_expected.to have_direct_access_to_enterprise_fee_summary
end
end
def have_link_to_enterprise_fee_summary
have_ability([:enterprise_fee_summary], for: :report)
end
def have_direct_access_to_enterprise_fee_summary
have_ability([:admin, :new, :create], for: :enterprise_fee_summary)
end
end
end

View File

@@ -0,0 +1,33 @@
module Features
module DatepickerHelper
def choose_today_from_datepicker
within(".ui-datepicker-calendar") do
find(".ui-datepicker-today").click
end
end
def navigate_datepicker_to_month(date, reference_date = Time.zone.today)
month_and_year = date.strftime("%B %Y")
until datepicker_month_and_year == month_and_year.upcase
if date < reference_date
navigate_datepicker_to_previous_month
elsif date > reference_date
navigate_datepicker_to_next_month
end
end
end
def navigate_datepicker_to_previous_month
find('#ui-datepicker-div .ui-datepicker-header .ui-datepicker-prev').click
end
def navigate_datepicker_to_next_month
find('#ui-datepicker-div .ui-datepicker-header .ui-datepicker-next').click
end
def datepicker_month_and_year
find("#ui-datepicker-div .ui-datepicker-title").text
end
end
end

Some files were not shown because too many files have changed in this diff Show More