diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index 2721c92961..769cec1c18 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -1,5 +1,6 @@ -angular.module("admin.customers").controller "customersCtrl", ($scope, $q, $filter, Customers, TagRuleResource, CurrentShop, RequestMonitor, Columns, pendingChanges, shops) -> +angular.module("admin.customers").controller "customersCtrl", ($scope, $q, $filter, Customers, TagRuleResource, CurrentShop, RequestMonitor, Columns, pendingChanges, shops, availableCountries) -> $scope.shops = shops + $scope.availableCountries = availableCountries $scope.RequestMonitor = RequestMonitor $scope.submitAll = pendingChanges.submitAll $scope.add = Customers.add diff --git a/app/assets/javascripts/admin/customers/directives/edit_address_dialog.js.coffee b/app/assets/javascripts/admin/customers/directives/edit_address_dialog.js.coffee new file mode 100644 index 0000000000..5fb55eaeba --- /dev/null +++ b/app/assets/javascripts/admin/customers/directives/edit_address_dialog.js.coffee @@ -0,0 +1,36 @@ +angular.module("admin.customers").directive 'editAddressDialog', ($compile, $templateCache, $filter, DialogDefaults, Customers, StatusMessage) -> + restrict: 'A' + scope: true + link: (scope, element, attr) -> + scope.errors = [] + + scope.$watch 'address.country_id', (newVal) -> + if newVal + scope.states = scope.filter_states(newVal) + + scope.updateAddress = -> + scope.edit_address_form.$setPristine() + if scope.edit_address_form.$valid + Customers.update(scope.address, scope.customer, scope.addressType).$promise.then (data) -> + scope.customer = data + template.dialog('close') + StatusMessage.display('success', t('admin.customers.index.update_address_success')) + else + scope.errors.push(t('admin.customers.index.update_address_error')) + + + template = $compile($templateCache.get('admin/edit_address_dialog.html'))(scope) + template.dialog(DialogDefaults) + + element.bind 'click', (e) -> + if e.target.id == 'bill-address-link' + scope.addressType = 'bill_address' + else + scope.addressType = 'ship_address' + scope.address = scope.customer[scope.addressType] + + template.dialog('open') + scope.$apply() + + scope.filter_states = (countryID) -> + $filter('filter')(scope.availableCountries, {id: countryID})[0].states diff --git a/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee b/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee index f2dae5836b..60a19838d6 100644 --- a/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee +++ b/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.customers").directive 'newCustomerDialog', ($compile, $injector, $templateCache, DialogDefaults, CurrentShop, Customers) -> +angular.module("admin.customers").directive 'newCustomerDialog', ($compile, $templateCache, DialogDefaults, CurrentShop, Customers) -> restrict: 'A' scope: true link: (scope, element, attr) -> diff --git a/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee b/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee index 5b6c1ab205..3904d0333d 100644 --- a/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee +++ b/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee @@ -14,4 +14,8 @@ angular.module("admin.customers").factory 'CustomerResource', ($resource) -> method: 'DELETE' params: id: '@id' + 'update': + method: 'PUT' + params: + id: '@id' }) diff --git a/app/assets/javascripts/admin/customers/services/customers.js.coffee b/app/assets/javascripts/admin/customers/services/customers.js.coffee index cd4c9bbb44..f4c6f6f3cd 100644 --- a/app/assets/javascripts/admin/customers/services/customers.js.coffee +++ b/app/assets/javascripts/admin/customers/services/customers.js.coffee @@ -25,3 +25,11 @@ angular.module("admin.customers").factory "Customers", ($q, InfoDialog, RequestM request = CustomerResource.index(params, (data) => @customers = data) RequestMonitor.load(request.$promise) request.$promise + + update: (address, customer, addressType) -> + params = + id: customer.id + customer: + "#{addressType}_attributes": address + CustomerResource.update params + diff --git a/app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee b/app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee index c2a3419e2c..0be1f2ec40 100644 --- a/app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/switch_class.js.coffee @@ -1,5 +1,5 @@ angular.module("admin.indexUtils").factory "switchClass", ($timeout) -> - return (element,classToAdd,removeClasses,timeout) -> + return (element, classToAdd, removeClasses, timeout) -> $timeout.cancel element.timeout if element.timeout element.removeClass className for className in removeClasses element.addClass classToAdd diff --git a/app/assets/javascripts/darkswarm/filters/properties.js.coffee b/app/assets/javascripts/darkswarm/filters/properties.js.coffee index 51b02e6634..1453cac053 100644 --- a/app/assets/javascripts/darkswarm/filters/properties.js.coffee +++ b/app/assets/javascripts/darkswarm/filters/properties.js.coffee @@ -1,5 +1,5 @@ -Darkswarm.filter 'properties', ()-> - # Filter anything that responds to object.properties +Darkswarm.filter 'properties', -> + # Filter anything that responds to object.supplied_properties (objects, ids) -> objects ||= [] ids ?= [] @@ -7,10 +7,7 @@ Darkswarm.filter 'properties', ()-> # No properties selected, pass all objects through. objects else - objects.filter (obj)-> - properties = obj.properties - # Combine object properties with supplied properties, if they exist. - # properties = properties.concat obj.supplied_properties if obj.supplied_properties - # Match property array. - properties.some (property)-> + objects.filter (obj) -> + properties = obj.supplied_properties || obj.properties + properties.some (property) -> property.id in ids diff --git a/app/assets/javascripts/darkswarm/filters/properties_of.js.coffee b/app/assets/javascripts/darkswarm/filters/properties_of.js.coffee index 4ab8c94bfd..7eac12bf1e 100644 --- a/app/assets/javascripts/darkswarm/filters/properties_of.js.coffee +++ b/app/assets/javascripts/darkswarm/filters/properties_of.js.coffee @@ -1,7 +1,12 @@ Darkswarm.filter 'propertiesOf', -> - (objects)-> + (objects) -> properties = {} for object in objects - for property in object.properties - properties[property.id] = property + if object.supplied_properties? + for property in object.supplied_properties + properties[property.id] = property + else + for property in object.properties + properties[property.id] = property + properties diff --git a/app/assets/javascripts/templates/admin/edit_address_dialog.html.haml b/app/assets/javascripts/templates/admin/edit_address_dialog.html.haml new file mode 100644 index 0000000000..351a267bd3 --- /dev/null +++ b/app/assets/javascripts/templates/admin/edit_address_dialog.html.haml @@ -0,0 +1,61 @@ +#edit-address-dialog + %h2 {{ addressType === 'bill_address' ? "#{t('admin.customers.index.edit_bill_address')}" : "#{t('admin.customers.index.edit_ship_address')}" }} + %form{ name: 'edit_address_form', novalidate: true, ng: { submit: 'updateAddress()'}} + .row + = t('admin.customers.index.required_fileds') + ( + %span.required * + ) + .error{ ng: { repeat: "error in errors", bind: "error" } } + + %table.no-borders + %tr + %td{style: 'width: 30%'} + = t('spree.street_address') + %span.required * + %td + %input{ type: 'text', name: 'address1', required: true, ng: { model: 'address.address1'} } + %tr + %td + = t('spree.street_address_1') + %td + %input{ type: 'text', name: 'address2', ng: { model: 'address.address2'} } + %tr + %td + = t('spree.phone') + %span.required * + %td + %input{ type: 'text', name: 'phone', required: true, ng: { model: 'address.phone'} } + %tr + %td + = t('spree.city') + %span.required * + %td + %input{ type: 'text', name: 'city', required: true, ng: { model: 'address.city'} } + %tr + %td + = t('spree.zipcode') + %span.required * + %td + %input{ type: 'text', name: 'zipcode', required: true, ng: { model: 'address.zipcode'} } + %tr + %td + = t('spree.country') + %span.required * + %td + %select{name: 'country', required: true, ng: {model: 'address.country_id', options: 'country.id as country.name for country in availableCountries'}} + %option{value: ''} + = t('admin.customers.index.select_country') + %tr + %td + = t('spree.state') + %span.required * + %td + %select{name: 'state', required: true, ng: {model: 'address.state_id', options: 'state.id as state.name for state in states'}} + %option{value: ''} + = t('admin.customers.index.select_state') + + .text-center + %input.button.red.icon-plus{ type: 'submit', value: 'Update Address'} + + diff --git a/app/assets/javascripts/templates/partials/enterprise_details.html.haml b/app/assets/javascripts/templates/partials/enterprise_details.html.haml index 46a37050e2..c454312f18 100644 --- a/app/assets/javascripts/templates/partials/enterprise_details.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_details.html.haml @@ -10,7 +10,7 @@ %span.filter-shopfront.property-selectors.pad-top %ul.inline-block - %li{"ng-repeat" => "property in enterprise.properties"} + %li{"ng-repeat" => "property in enterprise.supplied_properties"} %a.button.tiny{"ng-bind" => "property.presentation"} .about-container.pad-top diff --git a/app/assets/stylesheets/admin/components/trial_progess_bar.sass b/app/assets/stylesheets/admin/components/trial_progess_bar.sass index 37ef4642d9..cb6a648273 100644 --- a/app/assets/stylesheets/admin/components/trial_progess_bar.sass +++ b/app/assets/stylesheets/admin/components/trial_progess_bar.sass @@ -1,7 +1,8 @@ #trial_progress_bar position: fixed + left: 0px bottom: 0px - width: 100% + width: 100vw padding: 8px 10px font-weight: bold background-color: #5498da diff --git a/app/assets/stylesheets/admin/validation.css.scss b/app/assets/stylesheets/admin/validation.css.scss index c97335a6c4..f8fa261fdb 100644 --- a/app/assets/stylesheets/admin/validation.css.scss +++ b/app/assets/stylesheets/admin/validation.css.scss @@ -5,3 +5,8 @@ input.ng-invalid { border: solid 1px red; } } + +select.ng-invalid { + border: solid 1px red; +} + diff --git a/app/controllers/admin/contents_controller.rb b/app/controllers/admin/contents_controller.rb index 87c86997d9..c8d04efebb 100644 --- a/app/controllers/admin/contents_controller.rb +++ b/app/controllers/admin/contents_controller.rb @@ -6,9 +6,9 @@ module Admin {name: 'Producer signup page', preferences: [:producer_signup_pricing_table_html, :producer_signup_case_studies_html, :producer_signup_detail_html]}, {name: 'Hub signup page', preferences: [:hub_signup_pricing_table_html, :hub_signup_case_studies_html, :hub_signup_detail_html]}, {name: 'Group signup page', preferences: [:group_signup_pricing_table_html, :group_signup_case_studies_html, :group_signup_detail_html]}, - {name: 'Footer', preferences: [:footer_logo, + {name: 'Footer and External Links', preferences: [:footer_logo, :footer_facebook_url, :footer_twitter_url, :footer_instagram_url, :footer_linkedin_url, :footer_googleplus_url, :footer_pinterest_url, - :footer_email, :footer_links_md, :footer_about_url, :footer_tos_url]}] + :footer_email, :community_forum_url, :footer_links_md, :footer_about_url, :footer_tos_url]}] end def update diff --git a/app/helpers/admin/business_model_configuration_helper.rb b/app/helpers/admin/business_model_configuration_helper.rb index b1a2f1ac12..de17761924 100644 --- a/app/helpers/admin/business_model_configuration_helper.rb +++ b/app/helpers/admin/business_model_configuration_helper.rb @@ -16,6 +16,10 @@ module Admin private + def free_use? + Spree::Config[:account_invoices_monthly_fixed] == 0 && Spree::Config[:account_invoices_monthly_rate] == 0 + end + def fixed_description fixed_amount = Spree::Money.new(Spree::Config[:account_invoices_monthly_fixed], {currency: Spree::Config[:currency]} ).rounded monthly_bill_includes_fixed? ? "#{fixed_amount}" : "" diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 67105b6b34..b528eb84a4 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -39,6 +39,10 @@ module Admin admin_inject_json_ams_array ngModule, "shops", @shops, Api::Admin::IdNameSerializer end + def admin_inject_available_countries(ngModule='admin.customers') + admin_inject_json_ams_array ngModule, 'availableCountries', available_countries, Api::CountrySerializer + end + def admin_inject_hubs(opts={module: 'ofn.admin'}) admin_inject_json_ams_array opts[:module], "hubs", @hubs, Api::Admin::IdNameSerializer end diff --git a/app/models/calculator/flat_percent_per_item.rb b/app/models/calculator/flat_percent_per_item.rb new file mode 100644 index 0000000000..17089b2f2c --- /dev/null +++ b/app/models/calculator/flat_percent_per_item.rb @@ -0,0 +1,28 @@ +require_dependency 'spree/calculator' + +class Calculator::FlatPercentPerItem < Spree::Calculator + # Spree's FlatPercentItemTotal calculator sums all amounts, and then calculates a percentage + # on them. + # In the cart, we display line item individual amounts rounded, so to have consistent + # calculations we do the same internally. Here, we round adjustments at the individual + # item level first, then multiply by the item quantity. + + preference :flat_percent, :decimal, :default => 0 + + attr_accessible :preferred_flat_percent + + def self.description + I18n.t(:flat_percent_per_item) + end + + def compute(object) + line_items_for(object).sum do |li| + unless li.price.present? && li.quantity.present? + raise ArgumentError, "object must respond to #price and #quantity" + end + + value = (li.price * BigDecimal(self.preferred_flat_percent.to_s) / 100.0).round(2) + value * li.quantity + end + end +end diff --git a/app/models/content_configuration.rb b/app/models/content_configuration.rb index e20279f5ee..fc35a2b0ba 100644 --- a/app/models/content_configuration.rb +++ b/app/models/content_configuration.rb @@ -34,6 +34,8 @@ class ContentConfiguration < Spree::Preferences::FileConfiguration # Footer preference :footer_logo, :file has_attached_file :footer_logo, default_url: "/assets/ofn-logo-footer.png" + + #Other preference :footer_facebook_url, :string, default: "https://www.facebook.com/OpenFoodNet" preference :footer_twitter_url, :string, default: "https://twitter.com/OpenFoodNet" preference :footer_instagram_url, :string, default: "" @@ -41,6 +43,7 @@ class ContentConfiguration < Spree::Preferences::FileConfiguration preference :footer_googleplus_url, :string, default: "" preference :footer_pinterest_url, :string, default: "" preference :footer_email, :string, default: "hello@openfoodnetwork.org" + preference :community_forum_url, :string, default: "http://community.openfoodnetwork.org" preference :footer_links_md, :text, default: <<-EOS [Newsletter sign-up](/) diff --git a/app/models/customer.rb b/app/models/customer.rb index c208c619ba..3003acb848 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -6,8 +6,17 @@ class Customer < ActiveRecord::Base has_many :orders, class_name: Spree::Order before_destroy :check_for_orders + belongs_to :bill_address, foreign_key: :bill_address_id, class_name: Spree::Address + alias_attribute :billing_address, :bill_address + accepts_nested_attributes_for :bill_address + + belongs_to :ship_address, foreign_key: :ship_address_id, class_name: Spree::Address + alias_attribute :shipping_address, :ship_address + accepts_nested_attributes_for :ship_address + before_validation :downcase_email before_validation :empty_code + before_validation :set_unused_address_fields validates :code, uniqueness: { scope: :enterprise_id, allow_nil: true } validates :email, presence: true, uniqueness: { scope: :enterprise_id, message: I18n.t('validation_msg_is_associated_with_an_exising_customer') } @@ -27,6 +36,11 @@ class Customer < ActiveRecord::Base self.code = nil if code.blank? end + def set_unused_address_fields + bill_address.firstname = bill_address.lastname = 'unused' if bill_address.present? + ship_address.firstname = ship_address.lastname = 'unused' if ship_address.present? + end + def associate_user self.user = user || Spree::User.find_by_email(email) end diff --git a/app/models/producer_property.rb b/app/models/producer_property.rb index 5b9fdd6d1c..bf1f083936 100644 --- a/app/models/producer_property.rb +++ b/app/models/producer_property.rb @@ -8,6 +8,15 @@ class ProducerProperty < ActiveRecord::Base after_destroy :refresh_products_cache_from_destroy + scope :sold_by, ->(shop) { + joins(producer: {supplied_products: {variants: {exchanges: :order_cycle}}}). + merge(Exchange.outgoing). + merge(Exchange.to_enterprise(shop)). + merge(OrderCycle.active). + select('DISTINCT producer_properties.*') + } + + def property_name property.name if property end diff --git a/app/models/spree/gateway/pay_pal_express_decorator.rb b/app/models/spree/gateway/pay_pal_express_decorator.rb deleted file mode 100644 index dc8239a107..0000000000 --- a/app/models/spree/gateway/pay_pal_express_decorator.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Spree - class Gateway::PayPalExpress < Gateway - # Something odd is happening with class inheritance here, this class (defined in spree_paypal_express gem) - # doesn't seem to pick up attr_accessible from the Gateway class, so we redefine the attrs we need here - attr_accessible :tag_list - end -end diff --git a/app/models/spree/gateway_decorator.rb b/app/models/spree/gateway_decorator.rb index 9125ef1d2d..bc4312961e 100644 --- a/app/models/spree/gateway_decorator.rb +++ b/app/models/spree/gateway_decorator.rb @@ -1,18 +1,5 @@ Spree::Gateway.class_eval do - acts_as_taggable - - # Due to class load order, when config.cache_classes is enabled (ie. staging and production - # environments), this association isn't inherited from PaymentMethod. As a result, creating - # payment methods using payment gateways results in: - # undefined method `association_class' for nil:NilClass - # To avoid that, we redefine this association here. - - has_and_belongs_to_many :distributors, join_table: 'distributors_payment_methods', :class_name => 'Enterprise', foreign_key: 'payment_method_id', association_foreign_key: 'distributor_id' - - # Default to live preference :server, :string, :default => 'live' preference :test_mode, :boolean, :default => false - - attr_accessible :tag_list end diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index 77e22c591f..4a718dfc16 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -1,7 +1,6 @@ Spree::PaymentMethod.class_eval do acts_as_taggable - # See gateway_decorator.rb when modifying this association has_and_belongs_to_many :distributors, join_table: 'distributors_payment_methods', :class_name => 'Enterprise', association_foreign_key: 'distributor_id' attr_accessible :distributor_ids, :tag_list @@ -45,26 +44,20 @@ Spree::PaymentMethod.class_eval do def has_distributor?(distributor) self.distributors.include?(distributor) end -end -# Ensure that all derived classes also allow distributor_ids -Spree::Gateway.providers.each do |p| - p.attr_accessible :distributor_ids - p.instance_eval do - def clean_name - case name - when "Spree::PaymentMethod::Check" - "Cash/EFT/etc. (payments for which automatic validation is not required)" - when "Spree::Gateway::Migs" - "MasterCard Internet Gateway Service (MIGS)" - when "Spree::Gateway::Pin" - "Pin Payments" - when "Spree::Gateway::PayPalExpress" - "PayPal Express" - else - i = name.rindex('::') + 2 - name[i..-1] - end + def self.clean_name + case name + when "Spree::PaymentMethod::Check" + "Cash/EFT/etc. (payments for which automatic validation is not required)" + when "Spree::Gateway::Migs" + "MasterCard Internet Gateway Service (MIGS)" + when "Spree::Gateway::Pin" + "Pin Payments" + when "Spree::Gateway::PayPalExpress" + "PayPal Express" + else + i = name.rindex('::') + 2 + name[i..-1] end end end diff --git a/app/models/spree/property_decorator.rb b/app/models/spree/property_decorator.rb index f6eaefc75f..50e450a03b 100644 --- a/app/models/spree/property_decorator.rb +++ b/app/models/spree/property_decorator.rb @@ -1,11 +1,22 @@ module Spree Property.class_eval do + has_many :producer_properties + scope :applied_by, ->(enterprise) { select('DISTINCT spree_properties.*'). joins(:product_properties). where('spree_product_properties.product_id IN (?)', enterprise.supplied_product_ids) } + scope :sold_by, ->(shop) { + joins(products: {variants: {exchanges: :order_cycle}}). + merge(Exchange.outgoing). + merge(Exchange.to_enterprise(shop)). + merge(OrderCycle.active). + select('DISTINCT spree_properties.*') + } + + after_save :refresh_products_cache # When a Property is destroyed, dependent-destroy will destroy all ProductProperties, diff --git a/app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface b/app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface deleted file mode 100644 index eb9b44ad10..0000000000 --- a/app/overrides/spree/layouts/admin/add_trial_progress_bar.html.haml.deface +++ /dev/null @@ -1,5 +0,0 @@ -/ insert_top "[data-hook='admin_footer_scripts']" - -- enterprise = spree_current_user.enterprises.first if OpenFoodNetwork::Permissions.new(spree_current_user).manages_one_enterprise? - -= render 'spree/admin/shared/trial_progress_bar', enterprise: enterprise \ No newline at end of file diff --git a/app/serializers/api/admin/customer_serializer.rb b/app/serializers/api/admin/customer_serializer.rb index 4634625c11..1c9d19306d 100644 --- a/app/serializers/api/admin/customer_serializer.rb +++ b/app/serializers/api/admin/customer_serializer.rb @@ -1,5 +1,8 @@ class Api::Admin::CustomerSerializer < ActiveModel::Serializer - attributes :id, :email, :enterprise_id, :user_id, :code, :tags, :tag_list + attributes :id, :email, :enterprise_id, :user_id, :code, :tags, :tag_list, :name + + has_one :ship_address, serializer: Api::AddressSerializer + has_one :bill_address, serializer: Api::AddressSerializer def tag_list object.tag_list.join(",") diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index d30c2d8677..f68c8be679 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -21,7 +21,8 @@ end class Api::UncachedEnterpriseSerializer < ActiveModel::Serializer attributes :orders_close_at, :active - has_many :properties, serializer: Api::PropertySerializer + has_many :supplied_properties, serializer: Api::PropertySerializer + has_many :distributed_properties, serializer: Api::PropertySerializer def orders_close_at options[:data].earliest_closing_times[object.id] @@ -31,13 +32,22 @@ class Api::UncachedEnterpriseSerializer < ActiveModel::Serializer options[:data].active_distributors.andand.include? object end - def properties + def supplied_properties # This results in 3 queries per enterprise product_properties = Spree::Property.applied_by(object) producer_properties = object.properties OpenFoodNetwork::PropertyMerge.merge product_properties, producer_properties end + + def distributed_properties + # This results in 3 queries per enterprise + product_properties = Spree::Property.sold_by(object) + ids = ProducerProperty.sold_by(object).pluck(:property_id) + producer_properties = Spree::Property.where(id: ids) + + OpenFoodNetwork::PropertyMerge.merge product_properties, producer_properties + end end class Api::CachedEnterpriseSerializer < ActiveModel::Serializer diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index 98e8b3c414..7e08c2e4b3 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -12,6 +12,7 @@ = admin_inject_column_preferences module: 'admin.customers' = admin_inject_shops += admin_inject_available_countries %div{ ng: { controller: 'customersCtrl' } } .row.filters @@ -37,6 +38,7 @@ %img.spinner{ src: "/assets/spinning-circles.svg" } %h1 =t :loading_customers + .row{ :class => "sixteen columns alpha", 'ng-show' => '!RequestMonitor.loading && filteredCustomers.length == 0'} %h1#no_results =t :no_customers_found @@ -48,18 +50,25 @@ %table.index#customers %col.email{ width: "20%", 'ng-show' => 'columns.email.visible' } - %col.code{ width: "20%", 'ng-show' => 'columns.code.visible' } - %col.tags{ width: "50%", 'ng-show' => 'columns.tags.visible' } + %col.name{ width: "20%", 'ng-show' => 'columns.name.visible' } + %col.code{ width: "10%", 'ng-show' => 'columns.code.visible' } + %col.tags{ width: "20%", 'ng-show' => 'columns.tags.visible' } + %col.bill_address{ width: "10%", 'ng-show' => 'columns.bill_address.visible' } + %col.ship_address{ width: "10%", 'ng-show' => 'columns.ship_address.visible' } %col.actions{ width: "10%"} %thead %tr{ ng: { controller: "ColumnsCtrl" } } -# %th.bulk -# %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } %th.email{ 'ng-show' => 'columns.email.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'customer.email'; reverse = !reverse" } Email + %a{ :href => '', 'ng-click' => "predicate = 'customer.email'; reverse = !reverse" }=t('admin.email') + %th.name{ 'ng-show' => 'columns.name.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'customer.name'; reverse = !reverse" }=t('admin.name') %th.code{ 'ng-show' => 'columns.code.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'customer.code'; reverse = !reverse" } Code - %th.tags{ 'ng-show' => 'columns.tags.visible' } Tags + %a{ :href => '', 'ng-click' => "predicate = 'customer.code'; reverse = !reverse" }=t('admin.customers.index.code') + %th.tags{ 'ng-show' => 'columns.tags.visible' }=t('admin.tags') + %th.bill_address{ 'ng-show' => 'columns.bill_address.visible' }=t('admin.customers.index.bill_address') + %th.ship_address{ 'ng-show' => 'columns.ship_address.visible' }=t('admin.customers.index.ship_address') %th.actions Ask?  %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } @@ -67,6 +76,8 @@ -# %td.bulk -# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } %td.email{ 'ng-show' => 'columns.email.visible', "ng-bind" => '::customer.email' } + %td.name{ 'ng-show' => 'columns.name.visible'} + %input{ type: 'text', name: 'name', ng: { model: 'customer.name' }, 'obj-for-update' => 'customer', 'attr-for-update' => 'name'} %td.code{ 'ng-show' => 'columns.code.visible' } %input{ type: 'text', name: 'code', ng: {model: 'customer.code', change: 'checkForDuplicateCodes()'}, "obj-for-update" => "customer", "attr-for-update" => "code" } %i.icon-warning-sign{ ng: {if: 'duplicate'} } @@ -74,6 +85,10 @@ %td.tags{ 'ng-show' => 'columns.tags.visible' } .tag_watcher{ 'obj-for-update' => "customer", "attr-for-update" => "tag_list"} %tags_with_translation{ object: 'customer', 'find-tags' => 'findTags(query)' } + %td.bill_address{ 'ng-show' => 'columns.bill_address.visible' } + %a{ id: 'bill-address-link', href: '#', "ng-bind" => "customer.bill_address ? customer.bill_address.address1 : '#{t('admin.customers.index.edit')}' | limitTo: 15", 'edit-address-dialog' => true } + %td.ship_address{ 'ng-show' => 'columns.ship_address.visible' } + %a{ id: 'ship-address-link', href: '#', "ng-bind" => "customer.ship_address ? customer.ship_address.address1 : '#{t('admin.customers.index.edit')}' | limitTo: 15", 'edit-address-dialog' => true } %td.actions %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } diff --git a/app/views/admin/enterprises/_change_type_form.html.haml b/app/views/admin/enterprises/_change_type_form.html.haml index 6474af1344..ac5ca55821 100644 --- a/app/views/admin/enterprises/_change_type_form.html.haml +++ b/app/views/admin/enterprises/_change_type_form.html.haml @@ -50,7 +50,9 @@ -# At the end of your trial, there is a one-off $200 fee to fully activate your account. Then you will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). - else - .shop_profile.option.one-third.column.alpha + .two.columns.alpha +   + .shop_profile.option.six.columns %a.full-width.button.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } .top %h3 Profile Only @@ -59,25 +61,29 @@ %p.description People can find and contact you on the Open Food Network. Your enterprise will be visible on the map, and will be searchable in listings. - .full_hub.option.one-third.column + .full_hub.option.six.columns %a.full-width.button.selector{ ng: { click: "sells='any'", class: "{selected: sells=='any'}" } } .top %h3 Hub Shop %p Sell produce from others .bottom %monthly-pricing-description{ joiner: "newline" } - %p.description Your enterprise is the backbone of your local food system. You aggregate produce from other enterprises and can sell it through your shop on the Open Food Network. - + .two.columns.omega +   .row .sixteen.columns.alpha %span.error{ ng: { show: "(change_type.sells.$error.required || change_type.sells.$error.pattern) && submitted" } } Please choose one of the options above. - if @enterprise.sells == 'unspecified' && @enterprise.shop_trial_start_date.nil? - %input.button.big{ type: 'submit', value: 'Start 30 Day Trial', ng: { click: "submit(change_type)", show: "sells=='own' || sells=='any'" } } - %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)", hide: "sells=='own' || sells=='any'" } } + -if free_use? + %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)" } } + - else + - trial_length = Spree::Config[:shop_trial_length_days] + %input.button.big{ type: 'submit', value: "Start #{trial_length}-Day Shop Trial", ng: { click: "submit(change_type)", show: "sells=='own' || sells=='any'" } } + %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)", hide: "sells=='own' || sells=='any'" } } - elsif @enterprise.sells == 'unspecified' %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)" } } - else diff --git a/app/views/admin/shared/_user_guide_link.html.haml b/app/views/admin/shared/_user_guide_link.html.haml index bfae6002e7..6081422aba 100644 --- a/app/views/admin/shared/_user_guide_link.html.haml +++ b/app/views/admin/shared/_user_guide_link.html.haml @@ -1 +1 @@ -= button_link_to "User Guide", "http://www.openfoodnetwork.org/platform/user-guide/", :icon => 'icon-external-link', :id => 'user_guide_link', target: '_blank' += button_link_to "User Guide", "http://www.openfoodnetwork.org/platform/user-guide/", :icon => 'icon-external-link', target: '_blank' diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index 97b350de45..33db02ed98 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -13,7 +13,7 @@ = t :email_admin_html, link: link_to('Admin Panel', spree.admin_url) %p - = t :email_community_html, link: link_to('Join the community.', 'http://community.openfoodnetwork.org/') + = t :email_community_html, link: link_to(t(:join_the_community), ContentConfig.community_forum_url) %p = t :email_help diff --git a/app/views/home/_fat.html.haml b/app/views/home/_fat.html.haml index 4e90771cc1..d5703f84d8 100644 --- a/app/views/home/_fat.html.haml +++ b/app/views/home/_fat.html.haml @@ -4,9 +4,13 @@ %label = t :hubs_buy .trans-sentence - %span.fat-taxons{"ng-repeat" => "taxon in hub.taxons"} - %render-svg{path: "{{taxon.icon}}"} - %span{"ng-bind" => "::taxon.name"} + %div + %span.fat-taxons{"ng-repeat" => "taxon in hub.taxons"} + %render-svg{path: "{{taxon.icon}}"} + %span{"ng-bind" => "::taxon.name"} + %div + %span.fat-properties{"ng-repeat" => "property in hub.distributed_properties"} + %span{"ng-bind" => "property.presentation"} %div.show-for-medium-up{"ng-if" => "::hub.taxons.length==0"}   .columns.small-12.medium-3.large-2.fat diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml index 28ed0c985f..8d68a2b84f 100644 --- a/app/views/producers/_fat.html.haml +++ b/app/views/producers/_fat.html.haml @@ -21,7 +21,7 @@ %render-svg{path: "{{taxon.icon}}"} %span{"ng-bind" => "::taxon.name"} %div - %span.fat-properties{"ng-repeat" => "property in producer.properties"} + %span.fat-properties{"ng-repeat" => "property in producer.supplied_properties"} %span{"ng-bind" => "property.presentation"} %div.show-for-medium-up{"ng-if" => "producer.supplied_taxons.length==0"} diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index 91c0934e21..76377f32e2 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -1,11 +1,11 @@ - content_for :page_title do - %h1 - = @enterprise.name - %span.small< - = "(#{enterprise_type_name(@enterprise)})" + = @enterprise.name + %span.small< + = "(#{enterprise_type_name(@enterprise)})" - content_for :page_actions do - = render 'admin/shared/user_guide_link' + %li#user_guide_link + = render 'admin/shared/user_guide_link' :javascript function toggleType(){ @@ -17,7 +17,7 @@ } $("#package_selection").slideToggle() } - #package_button + %li#package_button %button#toggle_type{ onClick: 'toggleType()' } = t "change_package" %i.icon-chevron-down @@ -98,3 +98,5 @@ %a.button.bottom{href: main_app.admin_order_cycles_path} = t "manage_order_cycles" %span.icon-arrow-right + += render 'spree/admin/shared/trial_progress_bar', enterprise: @enterprise diff --git a/app/views/spree/user_mailer/signup_confirmation.html.haml b/app/views/spree/user_mailer/signup_confirmation.html.haml index e45b99a000..273d497c5d 100644 --- a/app/views/spree/user_mailer/signup_confirmation.html.haml +++ b/app/views/spree/user_mailer/signup_confirmation.html.haml @@ -21,9 +21,7 @@ %p.lead = t :email_signup_text %p - = t :email_signup_help_html - %a{:href => "mailto:hello@openfoodnetwork.org", :target => "_blank"} - hello@openfoodnetwork.org + = t :email_signup_help_html, email: mail_to(ContentConfig.footer_email) = render 'shared/mailers/signoff' diff --git a/config/application.rb b/config/application.rb index 9abd0a3b9b..f9d6151e10 100644 --- a/config/application.rb +++ b/config/application.rb @@ -28,7 +28,7 @@ module Openfoodnetwork initializer "spree.register.calculators" do |app| app.config.spree.calculators.shipping_methods << OpenFoodNetwork::Calculator::Weight - app.config.spree.calculators.enterprise_fees = [Spree::Calculator::FlatPercentItemTotal, + app.config.spree.calculators.enterprise_fees = [Calculator::FlatPercentPerItem, Spree::Calculator::FlatRate, Spree::Calculator::FlexiRate, Spree::Calculator::PerItem, diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index 92ba3db249..30182ef63a 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -9,6 +9,9 @@ require 'spree/product_filters' +require "#{Rails.root}/app/models/spree/payment_method_decorator" +require "#{Rails.root}/app/models/spree/gateway_decorator" + Spree.config do |config| config.shipping_instructions = true config.address_requires_state = true diff --git a/config/locales/en.yml b/config/locales/en.yml index 386f44ae8b..1d8707a7b9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -118,6 +118,16 @@ en: add_a_new_customer_for: Add a new customer for %{shop_name} code: Code duplicate_code: "This code is used already." + bill_address: "Billing Address" + ship_address: "Shipping Address" + update_address_success: 'Address updated successfully.' + update_address_error: 'Sorry! Please input all of the required fields!' + edit_bill_address: 'Edit Billing Address' + edit_ship_address: 'Edit Shipping Address' + required_fileds: 'Required fields are denoted with an asterisk ' + select_country: 'Select Country' + select_state: 'Select State' + edit: 'Edit' products: bulk_edit: @@ -411,6 +421,7 @@ en: email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration." email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. %{link}" + join_community: "Join the community" email_help: "If you have any difficulties, check out our FAQs, browse the forum or post a 'Support' topic and someone will help you out!" email_confirmation_greeting: "Hi, %{contact}!" email_confirmation_profile_created: "A profile for %{name} has been successfully created! @@ -458,7 +469,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using email_signup_text: "Thanks for joining the network. If you are a customer, we look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! If you are a producer or food enterprise, we are excited to have you as a part of the network." - email_signup_help_html: "We welcome all your questions and feedback; you can use the Send Feedback button on the site or email us at" + email_signup_help_html: "We welcome all your questions and feedback; you can use the Send Feedback button on the site or email us at %{email}" producer_mail_greeting: "Dear" producer_mail_text_before: "We now have all the consumer orders for the next food drop." @@ -900,6 +911,7 @@ Please follow the instructions there to make your enterprise visible on the Open tax_category: "Tax Category" calculator: "Calculator" calculator_values: "Calculator values" + flat_percent_per_item: "Flat Percent (per item)" new_order_cycles: "New Order Cycles" select_a_coordinator_for_your_order_cycle: "Select a coordinator for your order cycle" edit_order_cycle: "Edit Order Cycle" diff --git a/db/migrate/20160713003535_add_bill_address_and_ship_address_to_customer.rb b/db/migrate/20160713003535_add_bill_address_and_ship_address_to_customer.rb new file mode 100644 index 0000000000..306276bcc0 --- /dev/null +++ b/db/migrate/20160713003535_add_bill_address_and_ship_address_to_customer.rb @@ -0,0 +1,12 @@ +class AddBillAddressAndShipAddressToCustomer < ActiveRecord::Migration + def change + add_column :customers, :bill_address_id, :integer + add_column :customers, :ship_address_id, :integer + + add_index :customers, :bill_address_id + add_index :customers, :ship_address_id + + add_foreign_key :customers, :spree_addresses, column: :bill_address_id + add_foreign_key :customers, :spree_addresses, column: :ship_address_id + end +end diff --git a/db/migrate/20160713013358_add_name_to_customer.rb b/db/migrate/20160713013358_add_name_to_customer.rb new file mode 100644 index 0000000000..832816657e --- /dev/null +++ b/db/migrate/20160713013358_add_name_to_customer.rb @@ -0,0 +1,5 @@ +class AddNameToCustomer < ActiveRecord::Migration + def change + add_column :customers, :name, :string + end +end diff --git a/db/migrate/20160819065331_swap_calculator_to_flat_percent_per_item.rb b/db/migrate/20160819065331_swap_calculator_to_flat_percent_per_item.rb new file mode 100644 index 0000000000..eb8e206e8f --- /dev/null +++ b/db/migrate/20160819065331_swap_calculator_to_flat_percent_per_item.rb @@ -0,0 +1,29 @@ +class SwapCalculatorToFlatPercentPerItem < ActiveRecord::Migration + class Spree::Calculator < ActiveRecord::Base + end + + def up + Spree::Calculator.where(calculable_type: "EnterpriseFee", type: 'Spree::Calculator::FlatPercentItemTotal').each do |c| + swap_calculator_type c, 'Calculator::FlatPercentPerItem' + end + end + + def down + Spree::Calculator.where(calculable_type: "EnterpriseFee", type: 'Spree::Calculator::FlatPercentPerItem').each do |c| + swap_calculator_type c, 'Calculator::FlatPercentItemTotal' + end + end + + + private + + def swap_calculator_type(calculator, to_class) + value = calculator.preferred_flat_percent + + calculator.type = to_class + calculator.save + + calculator = Spree::Calculator.find calculator.id + calculator.preferred_flat_percent = value + end +end diff --git a/db/schema.rb b/db/schema.rb index d0403533fd..48b07e3cdb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20160707023818) do +ActiveRecord::Schema.define(:version => 20160819065331) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -79,16 +79,21 @@ ActiveRecord::Schema.define(:version => 20160707023818) do add_index "coordinator_fees", ["order_cycle_id"], :name => "index_coordinator_fees_on_order_cycle_id" create_table "customers", :force => true do |t| - t.string "email", :null => false - t.integer "enterprise_id", :null => false + t.string "email", :null => false + t.integer "enterprise_id", :null => false t.string "code" t.integer "user_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.integer "bill_address_id" + t.integer "ship_address_id" + t.string "name" end + add_index "customers", ["bill_address_id"], :name => "index_customers_on_bill_address_id" add_index "customers", ["email"], :name => "index_customers_on_email" add_index "customers", ["enterprise_id", "code"], :name => "index_customers_on_enterprise_id_and_code", :unique => true + add_index "customers", ["ship_address_id"], :name => "index_customers_on_ship_address_id" add_index "customers", ["user_id"], :name => "index_customers_on_user_id" create_table "delayed_jobs", :force => true do |t| @@ -577,9 +582,9 @@ ActiveRecord::Schema.define(:version => 20160707023818) do t.string "email" t.text "special_instructions" t.integer "distributor_id" + t.integer "order_cycle_id" t.string "currency" t.string "last_ip_address" - t.integer "order_cycle_id" t.integer "cart_id" t.integer "customer_id" end @@ -1111,6 +1116,8 @@ ActiveRecord::Schema.define(:version => 20160707023818) do add_foreign_key "coordinator_fees", "order_cycles", name: "coordinator_fees_order_cycle_id_fk" add_foreign_key "customers", "enterprises", name: "customers_enterprise_id_fk" + add_foreign_key "customers", "spree_addresses", name: "customers_bill_address_id_fk", column: "bill_address_id" + add_foreign_key "customers", "spree_addresses", name: "customers_ship_address_id_fk", column: "ship_address_id" add_foreign_key "customers", "spree_users", name: "customers_user_id_fk", column: "user_id" add_foreign_key "distributors_payment_methods", "enterprises", name: "distributors_payment_methods_distributor_id_fk", column: "distributor_id" diff --git a/lib/open_food_network/column_preference_defaults.rb b/lib/open_food_network/column_preference_defaults.rb index 09dc197832..41eef63c09 100644 --- a/lib/open_food_network/column_preference_defaults.rb +++ b/lib/open_food_network/column_preference_defaults.rb @@ -27,9 +27,12 @@ module OpenFoodNetwork def customers_index_columns node = 'admin.customers.index' { - email: { name: I18n.t("admin.email"), visible: true }, - code: { name: I18n.t("#{node}.code"), visible: true }, - tags: { name: I18n.t("admin.tags"), visible: true } + email: { name: I18n.t("admin.email"), visible: true }, + name: { name: I18n.t("admin.name"), visible: true }, + code: { name: I18n.t("#{node}.code"), visible: true }, + tags: { name: I18n.t("admin.tags"), visible: true }, + bill_address: { name: I18n.t("#{node}.bill_address"), visible: true }, + ship_address: { name: I18n.t("#{node}.ship_address"), visible: true } } end diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb index 7222abad1c..e72f643ff2 100644 --- a/lib/open_food_network/enterprise_fee_calculator.rb +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -104,8 +104,7 @@ module OpenFoodNetwork def calculate_fee_for(variant, enterprise_fee) # Spree's Calculator interface accepts Orders or LineItems, # so we meet that interface with a struct. - # Amount is faked, this is a method on LineItem - line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price + line_item = OpenStruct.new variant: variant, quantity: 1, price: variant.price, amount: variant.price enterprise_fee.compute_amount(line_item) end diff --git a/spec/factories.rb b/spec/factories.rb index 3b0c548923..9875d22652 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -270,6 +270,7 @@ FactoryGirl.define do enterprise code { SecureRandom.base64(150) } user + bill_address { create(:address) } end factory :billable_period do diff --git a/spec/features/admin/customers_spec.rb b/spec/features/admin/customers_spec.rb index 89893860df..07352ad151 100644 --- a/spec/features/admin/customers_spec.rb +++ b/spec/features/admin/customers_spec.rb @@ -80,8 +80,10 @@ feature 'Customers' do within "tr#c_#{customer1.id}" do fill_in "code", with: "new-customer-code" expect(page).to have_css "input[name=code].update-pending" - end - within "tr#c_#{customer1.id}" do + + fill_in "name", with: "customer abc" + expect(page).to have_css "input[name=name].update-pending" + find(:css, "tags-input .tags input").set "awesome\n" expect(page).to have_css ".tag_watcher.update-pending" end @@ -89,18 +91,22 @@ feature 'Customers' do # Every says it updated expect(page).to have_css "input[name=code].update-success" + expect(page).to have_css "input[name=name].update-success" expect(page).to have_css ".tag_watcher.update-success" # And it actually did expect(customer1.reload.code).to eq "new-customer-code" + expect(customer1.reload.name).to eq "customer abc" expect(customer1.tag_list).to eq ["awesome"] # Clearing attributes within "tr#c_#{customer1.id}" do fill_in "code", with: "" expect(page).to have_css "input[name=code].update-pending" - end - within "tr#c_#{customer1.id}" do + + fill_in "name", with: "" + expect(page).to have_css "input[name=name].update-pending" + find("tags-input li.tag-item a.remove-button").trigger('click') expect(page).to have_css ".tag_watcher.update-pending" end @@ -108,10 +114,12 @@ feature 'Customers' do # Every says it updated expect(page).to have_css "input[name=code].update-success" + expect(page).to have_css "input[name=name].update-success" expect(page).to have_css ".tag_watcher.update-success" # And it actually did expect(customer1.reload.code).to be nil + expect(customer1.reload.name).to eq '' expect(customer1.tag_list).to eq [] end @@ -142,6 +150,52 @@ feature 'Customers' do expect(customer2.reload.code).to be nil end + describe 'updating a customer addresses' do + before do + select2_select managed_distributor2.name, from: "shop_id" + end + + it 'updates the existing billing address' do + expect(page).to have_content 'BILLING ADDRESS' + + first('#bill-address-link').click + + expect(page).to have_content 'Edit Billing Address' + fill_in 'address1', with: "New Address1" + click_button 'Update Address' + + expect(page).to have_content 'Address updated successfully.' + expect(page).to have_link 'New Address1' + + expect(customer4.reload.bill_address.address1).to eq 'New Address1' + end + + it 'creates a new shipping address' do + expect(page).to have_content 'SHIPPING ADDRESS' + + first('#ship-address-link').click + expect(page).to have_content 'Edit Shipping Address' + + fill_in 'address1', with: "New Address1" + fill_in 'phone', with: "12345678" + fill_in 'city', with: "Melbourne" + fill_in 'zipcode', with: "3000" + + select 'Australia', from: 'country' + select 'Victoria', from: 'state' + click_button 'Update Address' + + expect(page).to have_content 'Address updated successfully.' + expect(page).to have_link 'New Address1' + + ship_address = customer4.reload.ship_address + + expect(ship_address.address1).to eq 'New Address1' + expect(ship_address.phone).to eq '12345678' + expect(ship_address.city).to eq 'Melbourne' + end + end + describe "creating a new customer" do context "when no shop has been selected" do it "asks the user to select a shop" do diff --git a/spec/features/admin/enterprise_fees_spec.rb b/spec/features/admin/enterprise_fees_spec.rb index 7025b54766..cd24e8e3a3 100644 --- a/spec/features/admin/enterprise_fees_spec.rb +++ b/spec/features/admin/enterprise_fees_spec.rb @@ -77,13 +77,13 @@ feature %q{ page.should have_select "enterprise_fee_set_collection_attributes_0_fee_type", selected: 'Admin' page.should have_selector "input[value='Greetings!']" page.should have_select 'enterprise_fee_set_collection_attributes_0_tax_category_id', selected: 'Inherit From Product' - page.should have_selector "option[selected]", text: 'Flat Percent' + page.should have_selector "option[selected]", text: 'Flat Percent (per item)' fee.reload fee.enterprise.should == enterprise fee.name.should == 'Greetings!' fee.fee_type.should == 'admin' - fee.calculator_type.should == "Spree::Calculator::FlatPercentItemTotal" + fee.calculator_type.should == "Calculator::FlatPercentPerItem" # Sets tax_category and inherits_tax_category fee.tax_category.should == nil diff --git a/spec/features/consumer/shopping/cart_spec.rb b/spec/features/consumer/shopping/cart_spec.rb index ed573d0460..b604769502 100644 --- a/spec/features/consumer/shopping/cart_spec.rb +++ b/spec/features/consumer/shopping/cart_spec.rb @@ -10,20 +10,42 @@ feature "full-page cart", js: true do let!(:zone) { create(:zone_with_member) } let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } let(:supplier) { create(:supplier_enterprise) } - let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) } - let(:enterprise_fee) { create(:enterprise_fee, amount: 11.00, tax_category: product.tax_category) } - let(:product) { create(:taxed_product, supplier: supplier, zone: zone, price: 110.00, tax_rate_amount: 0.1) } - let(:variant) { product.variants.first } + let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product_tax.variants.first, product_fee.variants.first]) } + let(:enterprise_fee) { create(:enterprise_fee, amount: 11.00, tax_category: product_tax.tax_category) } + let(:product_tax) { create(:taxed_product, supplier: supplier, zone: zone, price: 110.00, tax_rate_amount: 0.1) } + let(:product_fee) { create(:simple_product, supplier: supplier, price: 0.86, on_hand: 100) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } before do - add_enterprise_fee enterprise_fee set_order order - add_product_to_cart - visit spree.cart_path + end + + describe "fees" do + let(:percentage_fee) { create(:enterprise_fee, calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 20)) } + + before do + add_enterprise_fee percentage_fee + add_product_to_cart order, product_fee, quantity: 8 + visit spree.cart_path + end + + it "rounds fee calculations correctly" do + # $0.86 + 20% = $1.032 + # Fractional cents should be immediately rounded down and not carried through + expect(page).to have_selector '.cart-item-price', text: '$1.03' + expect(page).to have_selector '.cart-item-total', text: '$8.24' + expect(page).to have_selector '.order-total.item-total', text: '$8.24' + expect(page).to have_selector '.order-total.grand-total', text: '$8.24' + end end describe "tax" do + before do + add_enterprise_fee enterprise_fee + add_product_to_cart order, product_tax + visit spree.cart_path + end + it "shows the total tax for the order, including product tax and tax on fees" do page.should have_selector '.tax-total', text: '11.00' # 10 + 1 end @@ -31,12 +53,15 @@ feature "full-page cart", js: true do describe "updating quantities with insufficient stock available" do let(:li) { order.line_items(true).last } + let(:variant) { product_tax.variants.first } before do - variant.update_attributes! on_hand: 2 + add_product_to_cart order, product_tax end it "prevents me from entering an invalid value" do + # Given we have 2 on hand, and we've loaded the page after that fact + variant.update_attributes! on_hand: 2 visit spree.cart_path accept_alert 'Insufficient stock available, only 2 remaining' do @@ -47,6 +72,10 @@ feature "full-page cart", js: true do end it "shows the quantities saved, not those submitted" do + # Given we load the page with 3 on hand, then the number available drops to 2 + visit spree.cart_path + variant.update_attributes! on_hand: 2 + fill_in "order_line_items_attributes_0_quantity", with: '4' click_button 'Update' diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index 0b0683def0..e23e4c15a8 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -18,7 +18,7 @@ feature "As a consumer I want to check out my cart", js: true do before do set_order order - add_product_to_cart + add_product_to_cart order, product end it "does not not render the login form when logged in" do diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 10147b21a4..6055cb47d1 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -23,7 +23,7 @@ feature "As a consumer I want to check out my cart", js: true do add_enterprise_fee enterprise_fee set_order order - add_product_to_cart + add_product_to_cart order, product end describe "with shipping and payment methods" do diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index ecb0c4efe8..f4ee8e6275 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -28,7 +28,7 @@ feature "shopping with variant overrides defined", js: true, retry: 3 do let!(:vo4) { create(:variant_override, hub: hub, variant: v4, count_on_hand: 3, default_stock: nil, resettable: false) } let!(:vo5) { create(:variant_override, hub: hub, variant: v5, count_on_hand: 0, default_stock: nil, resettable: false) } let!(:vo6) { create(:variant_override, hub: hub, variant: v6, count_on_hand: 6, default_stock: nil, resettable: false) } - let(:ef) { create(:enterprise_fee, enterprise: hub, fee_type: 'packing', calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) } + let(:ef) { create(:enterprise_fee, enterprise: hub, fee_type: 'packing', calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 10)) } before do outgoing_exchange.variants = [v1, v2, v3, v4, v5, v6] @@ -91,17 +91,17 @@ feature "shopping with variant overrides defined", js: true, retry: 3 do page.should have_field "order[line_items_attributes][0][quantity]", with: '2' page.should have_selector "tr.line-item.variant-#{v1.id} .cart-item-total", text: '$122.22' - page.should have_selector "#edit-cart .item-total", text: '$122.21' - page.should have_selector "#edit-cart .grand-total", text: '$122.21' + page.should have_selector "#edit-cart .item-total", text: '$122.22' + page.should have_selector "#edit-cart .grand-total", text: '$122.22' end it "shows the correct prices in the checkout" do fill_in "variants[#{v1.id}]", with: "2" click_checkout - page.should have_selector 'form.edit_order .cart-total', text: '$122.21' + page.should have_selector 'form.edit_order .cart-total', text: '$122.22' page.should have_selector 'form.edit_order .shipping', text: '$0.00' - page.should have_selector 'form.edit_order .total', text: '$122.21' + page.should have_selector 'form.edit_order .total', text: '$122.22' end end @@ -115,7 +115,7 @@ feature "shopping with variant overrides defined", js: true, retry: 3 do o = Spree::Order.complete.last o.line_items.first.price.should == 55.55 - o.total.should == 122.21 + o.total.should == 122.22 end it "subtracts stock from the override" do diff --git a/spec/features/consumer/shops_spec.rb b/spec/features/consumer/shops_spec.rb index adbbf05f9a..2a65a73aef 100644 --- a/spec/features/consumer/shops_spec.rb +++ b/spec/features/consumer/shops_spec.rb @@ -56,12 +56,36 @@ feature 'Shops', js: true do expect(page).to have_current_path enterprise_shop_path(distributor) end + describe "property badges" do + let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) } + let(:product) { create(:simple_product, supplier: producer) } + + before do + product.set_property 'Local', 'XYZ 123' + end + + it "shows property badges" do + # Given a shop with a product with a property + # And the product's producer has a producer property + + # When I go to the shops path + visit shops_path + + # And I open the shop + expand_active_table_node distributor.name + + # Then I should see both properties + expect(page).to have_content 'Local' # Product property + expect(page).to have_content 'Organic' # Producer property + end + end + describe "hub producer modal" do let!(:product) { create(:simple_product, supplier: producer, taxons: [taxon]) } let!(:taxon) { create(:taxon, name: 'Fruit') } let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) } - it "should show hub producer modals" do + it "shows hub producer modals" do expand_active_table_node distributor.name expect(page).to have_content producer.name open_enterprise_modal producer diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee index 5f190b9382..f97ff35d36 100644 --- a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -15,11 +15,15 @@ describe "CustomersCtrl", -> { name: "Shop 3", id: 3 } ] + availableCountries = [ + {id: 109, name: "Australia", states: [{id: 55, name: "ACT", abbr: "ACT"}]} + ] + inject ($controller, $rootScope, _CustomerResource_, $httpBackend) -> scope = $rootScope http = $httpBackend - $controller 'customersCtrl', {$scope: scope, CustomerResource: _CustomerResource_, shops: shops} + $controller 'customersCtrl', {$scope: scope, CustomerResource: _CustomerResource_, shops: shops, availableCountries: availableCountries} jasmine.addMatchers toDeepEqual: (util, customEqualityTesters) -> compare: (actual, expected) -> diff --git a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb index a31295ac9d..0fb42f3052 100644 --- a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb @@ -66,7 +66,7 @@ module OpenFoodNetwork end describe "summing percentage fees for the variant" do - let!(:enterprise_fee1) { create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) } + let!(:enterprise_fee1) { create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: ::Calculator::FlatPercentPerItem.new(preferred_flat_percent: 20)) } let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, enterprise_fees: [enterprise_fee1], variants: [product1.master]) } diff --git a/spec/models/calculator/flat_percent_per_item_spec.rb b/spec/models/calculator/flat_percent_per_item_spec.rb new file mode 100644 index 0000000000..aaeff2a328 --- /dev/null +++ b/spec/models/calculator/flat_percent_per_item_spec.rb @@ -0,0 +1,13 @@ +describe Calculator::FlatPercentPerItem do + let(:calculator) { Calculator::FlatPercentPerItem.new preferred_flat_percent: 20 } + + it "calculates for a simple line item" do + line_item = Spree::LineItem.new price: 50, quantity: 2 + expect(calculator.compute(line_item)).to eq 20 + end + + it "rounds fractional cents before summing" do + line_item = Spree::LineItem.new price: 0.86, quantity: 8 + expect(calculator.compute(line_item)).to eq 1.36 + end +end diff --git a/spec/models/customer_spec.rb b/spec/models/customer_spec.rb index 32e380f4d3..37134e9574 100644 --- a/spec/models/customer_spec.rb +++ b/spec/models/customer_spec.rb @@ -18,6 +18,26 @@ describe Customer, type: :model do end end + describe 'update shipping address' do + let(:customer) { create(:customer) } + + it 'updates the shipping address' do + expect(customer.shipping_address).to be_nil + + ship_address = {zipcode: "3127", + city: "Melbourne", + state_id: 1, + phone: "455500146", + address1: "U 3/32 Florence Road Surrey Hills2", + country_id: 1} + customer.update_attributes!(ship_address_attributes: ship_address) + + expect(customer.ship_address.city).to eq 'Melbourne' + expect(customer.ship_address.firstname).to eq 'unused' + expect(customer.ship_address.lastname).to eq 'unused' + end + end + describe 'creation callbacks' do let!(:user1) { create(:user) } let!(:user2) { create(:user) } diff --git a/spec/models/enterprise_fee_spec.rb b/spec/models/enterprise_fee_spec.rb index 7fac566f4b..e0df4ce5d0 100644 --- a/spec/models/enterprise_fee_spec.rb +++ b/spec/models/enterprise_fee_spec.rb @@ -75,7 +75,7 @@ describe EnterpriseFee do it "returns fees with any other calculator" do ef1 = create(:enterprise_fee, calculator: Spree::Calculator::DefaultTax.new) - ef2 = create(:enterprise_fee, calculator: Spree::Calculator::FlatPercentItemTotal.new) + ef2 = create(:enterprise_fee, calculator: Calculator::FlatPercentPerItem.new) ef3 = create(:enterprise_fee, calculator: Spree::Calculator::PerItem.new) ef4 = create(:enterprise_fee, calculator: Spree::Calculator::PriceSack.new) @@ -93,7 +93,7 @@ describe EnterpriseFee do it "does not return fees with any other calculator" do ef1 = create(:enterprise_fee, calculator: Spree::Calculator::DefaultTax.new) - ef2 = create(:enterprise_fee, calculator: Spree::Calculator::FlatPercentItemTotal.new) + ef2 = create(:enterprise_fee, calculator: Calculator::FlatPercentPerItem.new) ef3 = create(:enterprise_fee, calculator: Spree::Calculator::PerItem.new) ef4 = create(:enterprise_fee, calculator: Spree::Calculator::PriceSack.new) diff --git a/spec/models/producer_property_spec.rb b/spec/models/producer_property_spec.rb index eaa7f0de0c..2d0009ce12 100644 --- a/spec/models/producer_property_spec.rb +++ b/spec/models/producer_property_spec.rb @@ -1,14 +1,70 @@ require 'spec_helper' describe ProducerProperty do - describe "products caching" do - let(:producer) { create(:supplier_enterprise) } - let(:pp) { producer.producer_properties.first } + let(:producer) { create(:supplier_enterprise) } + let(:pp) { producer.producer_properties.first } + + before do + producer.set_producer_property 'Organic Certified', 'NASAA 54321' + end + + describe ".sold_by" do + let!(:shop) { create(:distributor_enterprise) } + let!(:oc) { create(:simple_order_cycle, distributors: [shop], variants: [product.variants.first]) } + let(:product) { create(:simple_product, supplier: producer) } + let(:producer_other) { create(:supplier_enterprise) } + let(:product_other) { create(:simple_product, supplier: producer_other) } + let(:pp_other) { producer_other.producer_properties.first } before do - producer.set_producer_property 'Organic Certified', 'NASAA 54321' + producer_other.set_producer_property 'Spiffy', 'Ya' end + describe "with an associated producer property" do + it "returns the producer property" do + expect(ProducerProperty.sold_by(shop)).to eq [pp] + end + end + + describe "with a producer property for a producer not carried by that shop" do + let!(:exchange) { create(:exchange, order_cycle: oc, incoming: true, sender: producer_other, receiver: oc.coordinator) } + + it "doesn't return the producer property" do + expect(ProducerProperty.sold_by(shop)).not_to include pp_other + end + end + + describe "with a producer property for a product in a different shop" do + let(:shop_other) { create(:distributor_enterprise) } + let!(:oc) { create(:simple_order_cycle, distributors: [shop], variants: [product.variants.first]) } + let!(:exchange) { create(:exchange, order_cycle: oc, incoming: false, sender: oc.coordinator, receiver: shop_other, variants: [product_other.variants.first]) } + + it "doesn't return the producer property" do + expect(ProducerProperty.sold_by(shop)).not_to include pp_other + end + end + + describe "with a producer property for a product in a closed order cycle" do + before do + oc.update_attributes! orders_close_at: 1.week.ago + end + + it "doesn't return the producer property" do + expect(ProducerProperty.sold_by(shop)).not_to include pp + end + end + + describe "with a duplicate producer property" do + let(:product2) { create(:simple_product, supplier: producer) } + let!(:oc) { create(:simple_order_cycle, distributors: [shop], variants: [product.variants.first, product2.variants.first]) } + + it "doesn't return duplicates" do + expect(ProducerProperty.sold_by(shop).to_a.count).to eq 1 + end + end + end + + describe "products caching" do it "refreshes the products cache on change" do expect(OpenFoodNetwork::ProductsCache).to receive(:producer_property_changed).with(pp) pp.value = 123 diff --git a/spec/models/spree/property_spec.rb b/spec/models/spree/property_spec.rb index 6867b9de1f..6e6ed2d780 100644 --- a/spec/models/spree/property_spec.rb +++ b/spec/models/spree/property_spec.rb @@ -30,6 +30,58 @@ module Spree expect(Spree::Property.applied_by(producer).to_a.count).to eq 1 end end + + describe ".sold_by" do + let!(:shop) { create(:distributor_enterprise) } + let!(:shop_other) { create(:distributor_enterprise) } + let!(:product) { create(:simple_product) } + let!(:product_other_ex) { create(:simple_product) } + let!(:product_no_oc) { create(:simple_product) } + let!(:product_closed_oc) { create(:simple_product) } + let!(:oc) { create(:simple_order_cycle, distributors: [shop], variants: [product.variants.first]) } + let!(:oc_closed) { create(:closed_order_cycle, distributors: [shop], variants: [product_closed_oc.variants.first]) } + let!(:exchange_other_shop) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: shop_other, variants: [product_other_ex.variants.first]) } + let(:property) { product.properties.last } + let(:property_other_ex) { product_other_ex.properties.last } + let(:property_no_oc) { product_no_oc.properties.last } + let(:property_closed_oc) { product_closed_oc.properties.last } + + before do + product.set_property 'Organic', 'NASAA 12345' + product_other_ex.set_property 'Biodynamic', 'ASDF 12345' + product_no_oc.set_property 'Shiny', 'Very' + product_closed_oc.set_property 'Spiffy', 'Ooh yeah' + end + + it "returns the property" do + expect(Property.sold_by(shop)).to eq [property] + end + + it "doesn't return the property from another exchange" do + expect(Property.sold_by(shop)).not_to include property_other_ex + end + + it "doesn't return the property with no order cycle" do + expect(Property.sold_by(shop)).not_to include property_no_oc + end + + it "doesn't return the property from a closed order cycle" do + expect(Property.sold_by(shop)).not_to include property_closed_oc + end + + context "with another product in the order cycle" do + let!(:product2) { create(:simple_product) } + let!(:oc) { create(:simple_order_cycle, distributors: [shop], variants: [product.variants.first, product2.variants.first]) } + + before do + product2.set_property 'Organic', 'NASAA 12345' + end + + it "doesn't return duplicates" do + expect(Property.sold_by(shop).to_a.count).to eq 1 + end + end + end end describe "callbacks" do diff --git a/spec/serializers/admin/customer_serializer_spec.rb b/spec/serializers/admin/customer_serializer_spec.rb index b8e43f5fcc..5694b1004f 100644 --- a/spec/serializers/admin/customer_serializer_spec.rb +++ b/spec/serializers/admin/customer_serializer_spec.rb @@ -11,5 +11,9 @@ describe Api::Admin::CustomerSerializer do expect(tags.length).to eq 3 expect(tags[0]).to eq({ "text" => 'one', "rules" => nil }) expect(tags[1]).to eq({ "text" => 'two', "rules" => 1 }) + + expect(result['bill_address']['id']).to eq customer.bill_address.id + expect(result['bill_address']['address1']).to eq customer.bill_address.address1 + expect(result['ship_address']).to be nil end end diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index 01c24bee05..3fc7ac53a6 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -16,9 +16,9 @@ module ShopWorkflow ApplicationController.any_instance.stub(:session).and_return({order_id: order.id, access_token: order.token}) end - def add_product_to_cart + def add_product_to_cart(order, product, quantity: 1) populator = Spree::OrderPopulator.new(order, order.currency) - populator.populate(variants: {product.variants.first.id => 1}) + populator.populate(variants: {product.variants.first.id => quantity}) # Recalculate fee totals order.update_distribution_charge!