diff --git a/Gemfile b/Gemfile index 48ff4349cb..d581c8d10b 100644 --- a/Gemfile +++ b/Gemfile @@ -13,9 +13,10 @@ gem 'spree', github: 'openfoodfoundation/spree', branch: '1-3-stable' gem 'spree_i18n', github: 'spree/spree_i18n', branch: '1-3-stable' gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '1-3-stable' -# Waiting on merge of PR #117 -# https://github.com/spree-contrib/better_spree_paypal_express/pull/117 -gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "1-3-stable" +# Our branch contains two changes +# - 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 => "hide-password" #gem 'spree_paypal_express', :github => "spree-contrib/better_spree_paypal_express", :branch => "1-3-stable" gem 'delayed_job_active_record' diff --git a/Gemfile.lock b/Gemfile.lock index fcd0633483..0e31157d96 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,8 +14,8 @@ GIT GIT remote: git://github.com/openfoodfoundation/better_spree_paypal_express.git - revision: cdd61161ccd27cd8d183f9321422c7be113796b8 - branch: 1-3-stable + revision: 840d973cd5bd3250b17674a624dad494aeb09eb3 + branch: hide-password specs: spree_paypal_express (2.0.3) paypal-sdk-merchant (= 1.106.1) diff --git a/app/assets/images/ofn-logo-footer.png b/app/assets/images/ofn-logo-footer.png new file mode 100644 index 0000000000..f612d5aa87 Binary files /dev/null and b/app/assets/images/ofn-logo-footer.png differ diff --git a/app/assets/images/ofn-logo-mobile.svg b/app/assets/images/ofn-logo-mobile.svg new file mode 100644 index 0000000000..7c48b00b1b --- /dev/null +++ b/app/assets/images/ofn-logo-mobile.svg @@ -0,0 +1,80 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/app/assets/images/ofn-logo.png b/app/assets/images/ofn-logo.png new file mode 100644 index 0000000000..f53680c342 Binary files /dev/null and b/app/assets/images/ofn-logo.png differ diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 1e2b6f3898..9f99dc1dcd 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -38,6 +38,7 @@ //= require ./products/products //= require ./shipping_methods/shipping_methods //= require ./side_menu/side_menu +//= require ./tag_rules/tag_rules //= require ./taxons/taxons //= require ./utils/utils //= require ./users/users 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 c475f1e4df..d2e6d58562 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,5 @@ -angular.module("admin.customers").controller "customersCtrl", ($scope, Customers, Columns, pendingChanges, shops) -> - $scope.shop = null +angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, Columns, pendingChanges, shops) -> + $scope.shop = {} $scope.shops = shops $scope.submitAll = pendingChanges.submitAll @@ -8,10 +8,26 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, Customers code: { name: "Code", visible: true } tags: { name: "Tags", visible: true } - $scope.$watch "shop", -> - if $scope.shop? - Customers.loaded = false - $scope.customers = Customers.index(enterprise_id: $scope.shop.id) + $scope.$watch "shop.id", -> + if $scope.shop.id? + $scope.customers = index {enterprise_id: $scope.shop.id} - $scope.loaded = -> - Customers.loaded + $scope.add = (email) -> + params = + enterprise_id: $scope.shop.id + email: email + CustomerResource.create params, (customer) => + if customer.id + $scope.customers.push customer + $scope.quickSearch = customer.email + + $scope.deleteCustomer = (customer) -> + params = id: customer.id + CustomerResource.destroy params, -> + i = $scope.customers.indexOf customer + $scope.customers.splice i, 1 unless i < 0 + + index = (params) -> + $scope.loaded = false + CustomerResource.index params, => + $scope.loaded = true diff --git a/app/assets/javascripts/admin/customers/customers.js.coffee b/app/assets/javascripts/admin/customers/customers.js.coffee index 3733fe2eea..1e8ae9b988 100644 --- a/app/assets/javascripts/admin/customers/customers.js.coffee +++ b/app/assets/javascripts/admin/customers/customers.js.coffee @@ -1 +1 @@ -angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.dropdown']) \ No newline at end of file +angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.utils', 'admin.dropdown']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee deleted file mode 100644 index e15ec10342..0000000000 --- a/app/assets/javascripts/admin/customers/directives/tags_with_translation.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -angular.module("admin.customers").directive "tagsWithTranslation", -> - restrict: "E" - template: "" - scope: - object: "=" - link: (scope, element, attrs) -> - scope.$watchCollection "object.tags", -> - scope.object.tag_list = (tag.text for tag in scope.object.tags).join(",") 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 523e0c1495..5b6c1ab205 100644 --- a/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee +++ b/app/assets/javascripts/admin/customers/services/customer_resource.js.coffee @@ -1,8 +1,17 @@ angular.module("admin.customers").factory 'CustomerResource', ($resource) -> - $resource('/admin/customers.json', {}, { + $resource('/admin/customers/:id.json', {}, { 'index': method: 'GET' isArray: true params: enterprise_id: '@enterprise_id' + 'create': + method: 'POST' + params: + enterprise_id: '@enterprise_id' + email: '@email' + 'destroy': + method: 'DELETE' + 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 deleted file mode 100644 index 9acfa317d2..0000000000 --- a/app/assets/javascripts/admin/customers/services/customers.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -angular.module("admin.customers").factory 'Customers', (CustomerResource) -> - new class Customers - customers: [] - customers_by_id: {} - loaded: false - - index: (params={}, callback=null) -> - CustomerResource.index params, (data) => - for customer in data - @customers.push customer - @customers_by_id[customer.id] = customer - - @loaded = true - (callback || angular.noop)(@customers) - - @customers diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index 7981e498b6..a0105fefa4 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -17,8 +17,9 @@ angular.module("admin.enterprises") { name: t('shipping_methods'), icon_class: "icon-truck", show: "showShippingMethods()" } { name: t('payment_methods'), icon_class: "icon-money", show: "showPaymentMethods()" } { name: t('enterprise_fees'), icon_class: "icon-tasks", show: "showEnterpriseFees()" } - { name: t('inventory_settings'), icon_class: "icon-list-ol", show: "showInventorySettings()" } - { name: t('shop_preferences'), icon_class: "icon-shopping-cart", show: "showShopPreferences()" } + { name: t('inventory_settings'), icon_class: "icon-list-ol", show: "enterpriseIsShop()" } + { name: t('tag_rules'), icon_class: "icon-random", show: "enterpriseIsShop()" } + { name: t('shop_preferences'), icon_class: "icon-shopping-cart", show: "enterpriseIsShop()" } ] $scope.select(0) @@ -42,8 +43,5 @@ angular.module("admin.enterprises") $scope.showEnterpriseFees = -> enterprisePermissions.can_manage_enterprise_fees && ($scope.Enterprise.sells != "none" || $scope.Enterprise.is_primary_producer) - $scope.showInventorySettings = -> - $scope.Enterprise.sells != "none" - - $scope.showShopPreferences = -> + $scope.enterpriseIsShop = -> $scope.Enterprise.sells != "none" diff --git a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee index 6be7e00ffa..2074a1ea05 100644 --- a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', 'admin.dropdown', 'pasvaz.bindonce', 'ngSanitize'] ) \ No newline at end of file +angular.module("admin.enterprises", [ "admin.paymentMethods", "admin.utils", "admin.shippingMethods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', 'admin.tagRules', 'admin.dropdown', 'pasvaz.bindonce', 'ngSanitize'] ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee b/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee index ec454e9216..132480d987 100644 --- a/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee @@ -15,8 +15,6 @@ angular.module("admin.indexUtils").directive "ofnSelect2", ($sanitize, $timeout) element.select2 minimumResultsForSearch: scope.minSearch || 0 data: { results: scope.data, text: scope.text } - initSelection: (element, callback) -> - callback scope.data[0] formatSelection: (item) -> item[scope.text] formatResult: (item) -> diff --git a/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee b/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee index 092fd5bbd2..c2595faa6d 100644 --- a/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.payment_methods") +angular.module("admin.paymentMethods") .controller "paymentMethodCtrl", ($scope, PaymentMethods) -> $scope.findPaymentMethodByID = (id) -> - $scope.PaymentMethod = PaymentMethods.findByID(id) \ No newline at end of file + $scope.PaymentMethod = PaymentMethods.findByID(id) diff --git a/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee b/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee index e75142ae0d..01553647d4 100644 --- a/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee @@ -1 +1 @@ -angular.module("admin.payment_methods", []) \ No newline at end of file +angular.module("admin.paymentMethods", []) diff --git a/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee b/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee index 21e557cac3..c31a20d96f 100644 --- a/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/services/payment_methods.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.payment_methods") +angular.module("admin.paymentMethods") .factory "PaymentMethods", (paymentMethods) -> new class PaymentMethods paymentMethods: paymentMethods diff --git a/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee index dabe52574e..cc7bd4ee3e 100644 --- a/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee @@ -1,4 +1,2 @@ -angular.module("admin.shipping_methods") - .controller "shippingMethodCtrl", ($scope, ShippingMethods) -> - $scope.findShippingMethodByID = (id) -> - $scope.ShippingMethod = ShippingMethods.findByID(id) \ No newline at end of file +angular.module("admin.shippingMethods").controller "shippingMethodCtrl", ($scope, shippingMethod) -> + $scope.shippingMethod = shippingMethod diff --git a/app/assets/javascripts/admin/shipping_methods/controllers/shipping_methods_controller.js.coffee b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_methods_controller.js.coffee new file mode 100644 index 0000000000..91569b2256 --- /dev/null +++ b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_methods_controller.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.shippingMethods") + .controller "shippingMethodsCtrl", ($scope, ShippingMethods) -> + $scope.findShippingMethodByID = (id) -> + $scope.ShippingMethod = ShippingMethods.findByID(id) diff --git a/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee b/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee index 556445c869..c691f5dae5 100644 --- a/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/services/shipping_methods.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.shipping_methods") +angular.module("admin.shippingMethods") .factory "ShippingMethods", (shippingMethods) -> new class ShippingMethods shippingMethods: shippingMethods diff --git a/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee b/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee index 99aeb9566d..232eee7045 100644 --- a/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee @@ -1 +1 @@ -angular.module("admin.shipping_methods", []) \ No newline at end of file +angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils']) diff --git a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee new file mode 100644 index 0000000000..7209147b66 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee @@ -0,0 +1,48 @@ +angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, enterprise) -> + $scope.tagGroups = enterprise.tag_groups + + $scope.visibilityOptions = [ { id: "visible", name: "VISIBLE" }, { id: "hidden", name: "NOT VISIBLE" } ] + + updateRuleCounts = -> + index = 0 + for tagGroup in $scope.tagGroups + tagGroup.startIndex = index + index = index + tagGroup.rules.length + + updateRuleCounts() + + $scope.updateTagsRulesFor = (tagGroup) -> + for tagRule in tagGroup.rules + tagRule.preferred_customer_tags = (tag.text for tag in tagGroup.tags).join(",") + + $scope.addNewRuleTo = (tagGroup, ruleType) -> + newRule = + id: null + preferred_customer_tags: (tag.text for tag in tagGroup.tags).join(",") + type: "TagRule::#{ruleType}" + switch ruleType + when "DiscountOrder" + newRule.calculator = { preferred_flat_percent: 0 } + when "FilterShippingMethods" + newRule.peferred_shipping_method_tags = [] + newRule.preferred_matched_shipping_methods_visibility = "visible" + tagGroup.rules.push(newRule) + updateRuleCounts() + + $scope.addNewTag = -> + $scope.tagGroups.push { tags: [], rules: [] } + + $scope.deleteTagRule = (tagGroup, tagRule) -> + index = tagGroup.rules.indexOf(tagRule) + return unless index >= 0 + if tagRule.id is null + tagGroup.rules.splice(index, 1) + updateRuleCounts() + else + if confirm("Are you sure?") + $http + method: "DELETE" + url: "/admin/enterprises/#{enterprise.id}/tag_rules/#{tagRule.id}.json" + .success -> + tagGroup.rules.splice(index, 1) + updateRuleCounts() diff --git a/app/assets/javascripts/admin/tag_rules/directives/invert_number.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/invert_number.js.coffee new file mode 100644 index 0000000000..2412eec18c --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/invert_number.js.coffee @@ -0,0 +1,11 @@ +angular.module("admin.tagRules").directive "invertNumber", -> + restrict: "A" + require: "ngModel" + link: (scope, element, attrs, ngModel) -> + ngModel.$parsers.push (viewValue) -> + return -parseInt(viewValue) unless isNaN(parseInt(viewValue)) + viewValue + + ngModel.$formatters.push (modelValue) -> + return -parseInt(modelValue) unless isNaN(parseInt(modelValue)) + modelValue diff --git a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee new file mode 100644 index 0000000000..54e33006e4 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee @@ -0,0 +1,34 @@ +angular.module("admin.tagRules").directive 'newTagRuleDialog', ($compile, $templateCache, $window) -> + restrict: 'A' + scope: true + link: (scope, element, attr) -> + # Compile modal template + template = $compile($templateCache.get('admin/new_tag_rule_dialog.html'))(scope) + + scope.ruleTypes = [ + # { id: "DiscountOrder", name: 'Apply a discount to orders' } + { id: "FilterShippingMethods", name: 'Show/Hide shipping methods' } + ] + + scope.ruleType = "DiscountOrder" + + # Set Dialog options + template.dialog + show: { effect: "fade", duration: 400 } + hide: { effect: "fade", duration: 300 } + autoOpen: false + resizable: false + width: $window.innerWidth * 0.4; + modal: true + open: (event, ui) -> + $('.ui-widget-overlay').bind 'click', -> + $(this).siblings('.ui-dialog').find('.ui-dialog-content').dialog('close') + + # Link opening of dialog to click event on element + element.bind 'click', (e) -> + template.dialog('open') + + scope.addRule = (tagGroup, ruleType) -> + scope.addNewRuleTo(tagGroup, ruleType) + template.dialog('close') + return diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/discount_order.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/discount_order.js.coffee new file mode 100644 index 0000000000..b374f88782 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/discount_order.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.tagRules").directive "discountOrder", -> + restrict: "E" + replace: true + templateUrl: "admin/tag_rules/discount_order.html" diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_shipping_methods.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_shipping_methods.js.coffee new file mode 100644 index 0000000000..1a75cf8ff2 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_shipping_methods.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.tagRules").directive "filterShippingMethods", -> + restrict: "E" + replace: true + templateUrl: "admin/tag_rules/filter_shipping_methods.html" diff --git a/app/assets/javascripts/admin/tag_rules/tag_rules.js.coffee b/app/assets/javascripts/admin/tag_rules/tag_rules.js.coffee new file mode 100644 index 0000000000..88c7734c33 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/tag_rules.js.coffee @@ -0,0 +1 @@ +angular.module("admin.tagRules", ['ngTagsInput']) diff --git a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee new file mode 100644 index 0000000000..6ce7953608 --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee @@ -0,0 +1,15 @@ +angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) -> + restrict: "E" + template: "" + scope: + object: "=" + tagsAttr: "@?" + tagListAttr: "@?" + link: (scope, element, attrs) -> + $timeout -> + scope.tagsAttr ||= "tags" + scope.tagListAttr ||= "tag_list" + + watchString = "object.#{scope.tagsAttr}" + scope.$watchCollection watchString, -> + scope.object[scope.tagListAttr] = (tag.text for tag in scope.object[scope.tagsAttr]).join(",") diff --git a/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee index db995170dd..fa11c0e92f 100644 --- a/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee @@ -8,6 +8,7 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterpris $scope.show_profiles = false $scope.filtersActive = false $scope.distanceMatchesShown = false + $scope.filterExpression = {active: true} $scope.$watch "query", (query)-> @@ -44,8 +45,8 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterpris $scope.filterEnterprises = -> es = Enterprises.hubs $scope.nameMatches = enterpriseMatchesNameQueryFilter(es, true) - $scope.distanceMatches = enterpriseMatchesNameQueryFilter(es, false) - $scope.distanceMatches = distanceWithinKmFilter($scope.distanceMatches, 50) + noNameMatches = enterpriseMatchesNameQueryFilter(es, false) + $scope.distanceMatches = distanceWithinKmFilter(noNameMatches, 50) $scope.updateVisibleMatches = -> @@ -65,3 +66,9 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterpris $scope.nameMatchesFiltered[0] else undefined + + $scope.showClosedShops = -> + delete $scope.filterExpression['active'] + + $scope.hideClosedShops = -> + $scope.filterExpression['active'] = true diff --git a/app/assets/javascripts/darkswarm/directives/auth.js.coffee b/app/assets/javascripts/darkswarm/directives/auth.js.coffee new file mode 100644 index 0000000000..46ae301651 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/auth.js.coffee @@ -0,0 +1,5 @@ +Darkswarm.directive 'auth', (AuthenticationService) -> + restrict: 'A' + link: (scope, elem, attrs) -> + elem.bind "click", -> + AuthenticationService.open '/' + attrs.auth diff --git a/app/assets/javascripts/templates/admin/new_tag_rule_dialog.html.haml b/app/assets/javascripts/templates/admin/new_tag_rule_dialog.html.haml new file mode 100644 index 0000000000..653e0d175d --- /dev/null +++ b/app/assets/javascripts/templates/admin/new_tag_rule_dialog.html.haml @@ -0,0 +1,10 @@ +#new-tag-rule-dialog + .text-normal.margin-bottom-30.text-center + Select a rule type: + + .text-center.margin-bottom-30 + -# %select.fullwidth{ 'select2-min-search' => 5, 'ng-model' => 'newRuleType', 'ng-options' => 'ruleType.id as ruleType.name for ruleType in availableRuleTypes' } + %input.ofn-select2.fullwidth{ :id => 'rule_type_selector', ng: { model: "ruleType" }, data: "ruleTypes", 'min-search' => "5" } + + .text-center + %input.button.red.icon-plus{ type: 'button', value: "Add Rule", ng: { click: 'addRule(tagGroup, ruleType)' } } diff --git a/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml b/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml new file mode 100644 index 0000000000..358d9ce1a6 --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml @@ -0,0 +1,37 @@ +%div + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", + ng: { value: "rule.id" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", + value: "TagRule::DiscountOrder" } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_type]", + value: "Spree::Calculator::FlatPercentItemTotal" } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_attributes][id]", + ng: { value: "rule.calculator.id" } } + + %input{ type: "hidden", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_attributes][preferred_flat_percent]", + ng: { value: "rule.calculator.preferred_flat_percent" } } + + %span.text-normal {{ $index + 1 }}. Orders are discounted by + %input{ type: "number", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_preferred_flat_percent", + min: -100, + max: 100, + ng: { model: "rule.calculator.preferred_flat_percent" }, 'invert-number' => true } + %span.text-normal % diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods.html.haml new file mode 100644 index 0000000000..6552d834b5 --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods.html.haml @@ -0,0 +1,27 @@ +%div + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", + ng: { value: "rule.id" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", + value: "TagRule::FilterShippingMethods" } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_shipping_method_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_shipping_method_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %span.text-normal {{ $index + 1 }}. Shipping methods with matching tags are + %input.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]", + ng: { model: "rule.preferred_matched_shipping_methods_visibility"}, + data: 'visibilityOptions', "min-search" => 5 } + -# %tags-with-translation{ object: "rule", "tags-attr" => "shipping_method_tags", "tag-list-attr" => "preferred_shipping_method_tags" } diff --git a/app/assets/stylesheets/admin/components/jquery_dialog.scss b/app/assets/stylesheets/admin/components/jquery_dialog.scss new file mode 100644 index 0000000000..2e36db6e33 --- /dev/null +++ b/app/assets/stylesheets/admin/components/jquery_dialog.scss @@ -0,0 +1,88 @@ +/** +Main colors: +dark: #545454 +light: #ccc +*/ +.ui-dialog { + border: 2px solid #4a4a4a; + border-radius:3px; + padding:0px; + -moz-box-shadow: 3px 3px 4px #797979; + -webkit-box-shadow: 3px 3px 4px #797979; + box-shadow: 3px 3px 4px #797979; + + /* For IE 8 */ + -ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#545454')"; + + /* For IE 5.5 - 7 */ + filter: progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#545454'); +} + +.ui-dialog .ui-dialog-titlebar{ + border-radius: 3px; +} + +.ui-dialog .ui-state-hover { + &.ui-dialog-titlebar-close{ + + } +} + +/*.ui-dialog .ui-icon-closethick{background:url(/static/assets/dialogCloseButton.png);}*/ + +.ui-dialog .ui-widget-header{ + background-image: none; + background-color: #ffffff; + border:0px; + border-radius: 3px; + padding: 0px 5px 0px 5px; +} +.ui-dialog .ui-widget-content{ + border: none; + border-radius: 3px; + padding: 0px 50px 30px 50px; +} + +.ui-dialog .ui-corner-all{ + border-radius:0px; +} +.ui-dialog { + .ui-state-hover, .ui-state-focus{ + border: none; + background: none; + color: #545454; + } +} + +.ui-state-hover, .ui-widget-header .ui-state-hover, .ui-widget-content .ui-state-hover { + background-color: #ffffff; + background: none; +} + +.ui-dialog-titlebar-close { + float: right; + &:before { + color: #000000; + font-size: 2em; + font-weight: 400; + content: '\00d7'; + display: inline; + } + + &:hover { + &:before { + color: #da5354; + } + } + + .ui-icon { + &.ui-icon-closethick { + display: none; + } + } +} + +.ui-widget-overlay { + background: #e9e9e9; + opacity: 0.6; +} diff --git a/app/assets/stylesheets/admin/offsets.css.scss b/app/assets/stylesheets/admin/offsets.css.scss index 762b7469f6..190ed49243 100644 --- a/app/assets/stylesheets/admin/offsets.css.scss +++ b/app/assets/stylesheets/admin/offsets.css.scss @@ -2,6 +2,10 @@ margin-bottom: 20px; } +.margin-bottom-30 { + margin-bottom: 30px; +} + .margin-bottom-50 { margin-bottom: 50px; } diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index 9a2dd4385c..761ccbc014 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -69,6 +69,21 @@ div#group_buy_calculation { text-indent:1em; } } + + &.after { + + span { + position: absolute; + transform: translate(0,-55%); + top:50%; + right: 0.5em; + pointer-events:none; + } + + input { + padding-right: 1.2em; + } + } } th.actions { diff --git a/app/assets/stylesheets/admin/select2.css.scss b/app/assets/stylesheets/admin/select2.css.scss index daba11d099..f94515a54c 100644 --- a/app/assets/stylesheets/admin/select2.css.scss +++ b/app/assets/stylesheets/admin/select2.css.scss @@ -1,5 +1,8 @@ .select2-container { .select2-choice { + .select2-search-choice-close { + display: none; + } .select2-arrow { width: 22px; border: none; @@ -7,4 +10,36 @@ background-color: transparent; } } + + &.light { + .select2-choice{ + background-color: #ffffff; + font-weight: normal; + border: 1px solid #5498da !important; + color: #5498da !important; + + .select2-arrow { + &:before { + color: #5498da; + font-size: 1rem; + font-weight: 400; + content: '\25be'; + display: inline; + } + } + } + + &:hover, &.select2-container-active { + .select2-choice{ + color: #ffffff !important; + background-color: #5498da !important; + + .select2-arrow { + &:before { + color: #ffffff; + } + } + } + } + } } diff --git a/app/assets/stylesheets/admin/tag_rules.css.scss b/app/assets/stylesheets/admin/tag_rules.css.scss new file mode 100644 index 0000000000..028ad6ce1c --- /dev/null +++ b/app/assets/stylesheets/admin/tag_rules.css.scss @@ -0,0 +1,68 @@ +.no_tags { + margin-bottom: 40px; + color: #aeaeae; + font-size: 1rem; + font-weight: bold; +} + +.customer_tag { + border: 1px solid #cee1f4; + margin-bottom: 40px; + + .header { + padding: 8px 10px; + background-color: #eff5fc; + border-bottom: 1px solid #cee1f4; + + table { + padding: 0px; + margin: 0px 0px 0px 0px; + tr { + td { + border: none; + } + } + } + } + + .no_rules { + padding: 8px 10px; + margin-bottom: 10px; + color: #aeaeae; + font-size: 1rem; + font-weight: bold; + } + + table { + padding: 0px; + margin: 0px 0px 10px 0px; + + tr.tag_rule { + border: none; + padding: 0px; + margin: 0px; + + td { + border: none; + padding: 4px 10px 10px 10px; + margin: 0px; + + input { + width: auto; + } + } + } + } + + .add_rule { + padding: 8px 10px; + margin-bottom: 10px; + } +} + +#new-tag-rule-dialog{ + .select2-chosen, .select2-result-label{ + font-size: 1rem; + font-weight: lighter; + } +} diff --git a/app/assets/stylesheets/admin/typography.css.scss b/app/assets/stylesheets/admin/typography.css.scss index 20148df3f1..761058fb1d 100644 --- a/app/assets/stylesheets/admin/typography.css.scss +++ b/app/assets/stylesheets/admin/typography.css.scss @@ -7,3 +7,12 @@ font-size: 1.2rem; font-weight: 300; } + +.text-red { + color: #DA5354; +} + + +input.text-big { + font-size: 1.1rem; +} diff --git a/app/assets/stylesheets/darkswarm/footer.sass b/app/assets/stylesheets/darkswarm/footer.sass index 76dd0f4384..a0d2244c0e 100644 --- a/app/assets/stylesheets/darkswarm/footer.sass +++ b/app/assets/stylesheets/darkswarm/footer.sass @@ -30,7 +30,7 @@ footer background-color: transparent border: none padding: 0 - a.big-alert + a.alert-cta @include csstrans width: 100% border: 1px solid rgba($dark-grey, 0.35) diff --git a/app/assets/stylesheets/darkswarm/hubs.css.sass b/app/assets/stylesheets/darkswarm/hubs.css.sass index a351170d99..5946d6ee77 100644 --- a/app/assets/stylesheets/darkswarm/hubs.css.sass +++ b/app/assets/stylesheets/darkswarm/hubs.css.sass @@ -7,4 +7,7 @@ @include sidepaddingSm .name-matches, .distance-matches - margin-top: 4em \ No newline at end of file + margin-top: 4em + + .more-controls + text-align: center diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 5422c4463a..eab074369e 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -84,9 +84,11 @@ padding-right: 0rem font-size: 0.8rem - .shopfront_message, .shopfront_closed_message + .shopfront_message, .shopfront_closed_message, .shopfront_hidden_message padding: 15px border-radius: 5px + + .shopfront_message, .shopfront_closed_message border: 2px solid #eb4c46 .shopfront_message @@ -94,3 +96,7 @@ .shopfront_closed_message margin: 2em 0em + + .shopfront_hidden_message + border: 2px solid #db4 + margin: 2em 0em diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index b1ceb88c2f..bd865c8130 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -7,13 +7,25 @@ module Admin respond_to do |format| format.html format.json do - render json: ActiveModel::ArraySerializer.new( @collection, - each_serializer: Api::Admin::CustomerSerializer, spree_current_user: spree_current_user - ).to_json + serialised = ActiveModel::ArraySerializer.new( + @collection, + each_serializer: Api::Admin::CustomerSerializer, + spree_current_user: spree_current_user) + render json: serialised.to_json end end end + def create + @customer = Customer.new(params[:customer]) + if user_can_create_customer? + @customer.save + render json: Api::Admin::CustomerSerializer.new(@customer).to_json + else + redirect_to '/unauthorized' + end + end + private def collection @@ -25,5 +37,10 @@ module Admin def load_managed_shops @shops = Enterprise.managed_by(spree_current_user).is_distributor end + + def user_can_create_customer? + spree_current_user.admin? || + spree_current_user.enterprises.include?(@customer.enterprise) + end end end diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 55eaabd7d8..377186b604 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -4,7 +4,7 @@ module Admin class EnterprisesController < ResourceController before_filter :load_enterprise_set, :only => :index before_filter :load_countries, :except => [:index, :register, :check_permalink] - before_filter :load_methods_and_fees, :only => [:new, :edit, :update, :create] + before_filter :load_methods_and_fees, :only => [:edit, :update] before_filter :load_groups, :only => [:new, :edit, :update, :create] before_filter :load_taxons, :only => [:new, :edit, :update, :create] before_filter :check_can_change_sells, only: :update @@ -35,6 +35,8 @@ module Admin def update invoke_callbacks(:update, :before) + tag_rules_attributes = params[object_name].delete :tag_rules_attributes + update_tag_rules(tag_rules_attributes) if tag_rules_attributes.present? if @object.update_attributes(params[object_name]) invoke_callbacks(:update, :after) flash[:success] = flash_message_for(@object, :successfully_updated) @@ -180,6 +182,27 @@ module Admin @taxons = Spree::Taxon.order(:name) end + def update_tag_rules(tag_rules_attributes) + # Due to the combination of trying to use nested attributes and type inheritance + # we cannot apply all attributes to tag rules in one hit because mass assignment + # methods that are specific to each class do not become available until after the + # record is persisted. This problem is compounded by the use of calculators. + @object.transaction do + tag_rules_attributes.select{ |i, attrs| attrs[:type].present? }.each do |i, attrs| + rule = @object.tag_rules.find_by_id(attrs.delete :id) || attrs[:type].constantize.new(enterprise: @object) + create_calculator_for(rule, attrs) if rule.type == "TagRule::DiscountOrder" && rule.calculator.nil? + rule.update_attributes(attrs) + end + end + end + + def create_calculator_for(rule, attrs) + if attrs[:calculator_type].present? && attrs[:calculator_attributes].present? + rule.update_attributes(calculator_type: attrs[:calculator_type]) + attrs[:calculator_attributes].merge!( { id: rule.calculator.id } ) + end + end + def check_can_change_bulk_sells unless spree_current_user.admin? params[:enterprise_set][:collection_attributes].each do |i, enterprise_params| diff --git a/app/controllers/admin/tag_rules_controller.rb b/app/controllers/admin/tag_rules_controller.rb new file mode 100644 index 0000000000..7d60cb4888 --- /dev/null +++ b/app/controllers/admin/tag_rules_controller.rb @@ -0,0 +1,10 @@ +module Admin + class TagRulesController < ResourceController + + respond_to :json + + respond_override destroy: { json: { + success: lambda { render nothing: true, :status => 204 } + } } + end +end diff --git a/app/controllers/api/statuses_controller.rb b/app/controllers/api/statuses_controller.rb new file mode 100644 index 0000000000..c8844b868b --- /dev/null +++ b/app/controllers/api/statuses_controller.rb @@ -0,0 +1,17 @@ +module Api + class StatusesController < BaseController + respond_to :json + + def job_queue + render json: {alive: job_queue_alive?} + end + + + private + + def job_queue_alive? + Spree::Config.last_job_queue_heartbeat_at.present? && + Time.parse(Spree::Config.last_job_queue_heartbeat_at) > 6.minutes.ago + end + end +end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 511eced707..0c032eaa9e 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -20,11 +20,15 @@ module Admin end def admin_inject_payment_methods - admin_inject_json_ams_array "admin.payment_methods", "paymentMethods", @payment_methods, Api::Admin::IdNameSerializer + admin_inject_json_ams_array "admin.paymentMethods", "paymentMethods", @payment_methods, Api::Admin::IdNameSerializer end def admin_inject_shipping_methods - admin_inject_json_ams_array "admin.shipping_methods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer + admin_inject_json_ams_array "admin.shippingMethods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer + end + + def admin_inject_shipping_method + admin_inject_json_ams "admin.shippingMethods", "shippingMethod", @shipping_method, Api::Admin::ShippingMethodSerializer end def admin_inject_shops(ngModule='admin.customers') diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index 85d1167ab7..5aa36624cd 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -4,7 +4,11 @@ module EnterprisesHelper end def available_shipping_methods - current_distributor.shipping_methods.uniq + shipping_methods = current_distributor.shipping_methods + if current_distributor.present? + current_distributor.apply_tag_rules_to(shipping_methods, customer: current_order.customer) + end + shipping_methods.uniq end def managed_enterprises diff --git a/app/helpers/shop_helper.rb b/app/helpers/shop_helper.rb index 3066bfbe05..920d41d65e 100644 --- a/app/helpers/shop_helper.rb +++ b/app/helpers/shop_helper.rb @@ -7,4 +7,16 @@ module ShopHelper ] end end + + def require_customer? + current_distributor.require_login? && !user_is_related_to_distributor? + end + + def user_is_related_to_distributor? + spree_current_user.present? && ( + spree_current_user.admin? || + spree_current_user.enterprises.include?(current_distributor) || + spree_current_user.customer_of(current_distributor) + ) + end end diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index bbc184d800..aaed402a6f 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -11,11 +11,17 @@ module Spree end def report_payment_method_options(orders) - orders.map { |o| o.payments.first.payment_method.andand.name }.uniq + orders.map do |o| + pm = o.payments.first.payment_method + [pm.andand.name, pm.andand.id] + end.uniq end def report_shipping_method_options(orders) - orders.map { |o| o.shipping_method.andand.name }.uniq + orders.map do |o| + sm = o.shipping_method + [sm.andand.name, sm.andand.id] + end.uniq end def xero_report_types diff --git a/app/jobs/heartbeat_job.rb b/app/jobs/heartbeat_job.rb new file mode 100644 index 0000000000..93e835905f --- /dev/null +++ b/app/jobs/heartbeat_job.rb @@ -0,0 +1,5 @@ +class HeartbeatJob + def perform + Spree::Config.last_job_queue_heartbeat_at = Time.now + end +end diff --git a/app/models/content_configuration.rb b/app/models/content_configuration.rb index 2357f8dfa3..e20279f5ee 100644 --- a/app/models/content_configuration.rb +++ b/app/models/content_configuration.rb @@ -7,14 +7,14 @@ class ContentConfiguration < Spree::Preferences::FileConfiguration preference :logo, :file preference :logo_mobile, :file preference :logo_mobile_svg, :file - has_attached_file :logo + has_attached_file :logo, default_url: "/assets/ofn-logo.png" has_attached_file :logo_mobile - has_attached_file :logo_mobile_svg + has_attached_file :logo_mobile_svg, default_url: "/assets/ofn-logo-mobile.svg" # Home page preference :home_hero, :file preference :home_show_stats, :boolean, default: true - has_attached_file :home_hero + has_attached_file :home_hero, default_url: "/assets/home/home.jpg" # Producer sign-up page preference :producer_signup_pricing_table_html, :text, default: "(TODO: Pricing table)" @@ -33,7 +33,7 @@ class ContentConfiguration < Spree::Preferences::FileConfiguration # Footer preference :footer_logo, :file - has_attached_file :footer_logo + has_attached_file :footer_logo, default_url: "/assets/ofn-logo-footer.png" 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: "" diff --git a/app/models/customer.rb b/app/models/customer.rb index bcafd3246b..34f62a6aa6 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -4,6 +4,8 @@ class Customer < ActiveRecord::Base belongs_to :enterprise belongs_to :user, class_name: Spree.user_class + before_validation :downcase_email + validates :code, uniqueness: { scope: :enterprise_id, allow_blank: true, allow_nil: true } validates :email, presence: true, uniqueness: { scope: :enterprise_id, message: I18n.t('validation_msg_is_associated_with_an_exising_customer') } validates :enterprise_id, presence: true @@ -14,6 +16,10 @@ class Customer < ActiveRecord::Base private + def downcase_email + email.andand.downcase! + end + def associate_user self.user = user || Spree::User.find_by_email(email) end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 432b53bd4d..dfc5050a38 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -42,11 +42,13 @@ class Enterprise < ActiveRecord::Base has_many :customers has_many :billable_periods has_many :inventory_items + has_many :tag_rules delegate :latitude, :longitude, :city, :state_name, :to => :address accepts_nested_attributes_for :address accepts_nested_attributes_for :producer_properties, allow_destroy: true, reject_if: lambda { |pp| pp[:property_name].blank? } + accepts_nested_attributes_for :tag_rules, allow_destroy: true, reject_if: lambda { |tag_rule| tag_rule[:preferred_customer_tags].blank? } has_attached_file :logo, styles: { medium: "300x300>", small: "180x180>", thumb: "100x100>" }, @@ -176,17 +178,6 @@ class Enterprise < ActiveRecord::Base end } - def self.find_near(suburb) - enterprises = [] - - unless suburb.nil? - addresses = Spree::Address.near([suburb.latitude, suburb.longitude], ENTERPRISE_SEARCH_RADIUS, :units => :km).joins(:enterprise).limit(10) - enterprises = addresses.collect(&:enterprise) - end - - enterprises - end - # Force a distinct count to work around relation count issue https://github.com/rails/rails/issues/5554 def self.distinct_count count(distinct: true) @@ -354,6 +345,13 @@ class Enterprise < ActiveRecord::Base abn.present? end + def apply_tag_rules_to(subject, context) + tag_rules.each do |rule| + rule.set_context(subject,context) + rule.apply + end + end + protected def devise_mailer diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 8c93fe4b71..867d3c9e2b 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -72,6 +72,10 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty + can [:admin, :destroy], TagRule do |tag_rule| + user.enterprises.include? tag_rule.enterprise + end + can [:admin, :index, :create], Enterprise can [:read, :edit, :update, :bulk_update, :resend_confirmation], Enterprise do |enterprise| OpenFoodNetwork::Permissions.new(user).editable_enterprises.include? enterprise @@ -97,6 +101,11 @@ class AbilityDecorator can [:print], Spree::Order do |order| order.user == user end + + can [:create], Customer + can [:destroy], Customer do |customer| + user.enterprises.include? customer.enterprise + end end def add_product_management_abilities(user) diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb index fc7a8171cc..6ef1e7b848 100644 --- a/app/models/spree/app_configuration_decorator.rb +++ b/app/models/spree/app_configuration_decorator.rb @@ -20,4 +20,7 @@ Spree::AppConfiguration.class_eval do preference :account_invoices_monthly_rate, :decimal, default: 0 preference :account_invoices_monthly_cap, :decimal, default: 0 preference :account_invoices_tax_rate, :decimal, default: 0 + + # Monitoring + preference :last_job_queue_heartbeat_at, :string, default: nil end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 8f1c0381b1..f0e9324185 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -17,7 +17,8 @@ Spree::Order.class_eval do attr_accessible :order_cycle_id, :distributor_id before_validation :shipping_address_from_distributor - before_validation :associate_customer, unless: :customer_is_valid? + before_validation :associate_customer, unless: :customer_id? + before_validation :ensure_customer, unless: :customer_is_valid? checkout_flow do go_to_state :address @@ -69,13 +70,6 @@ Spree::Order.class_eval do where("state != ?", state) } - scope :with_payment_method_name, lambda { |payment_method_name| - joins(:payments => :payment_method). - where('spree_payment_methods.name IN (?)', payment_method_name). - select('DISTINCT spree_orders.*') - } - - # -- Methods def products_available_from_new_distribution # Check that the line_items in the current order are available from a newly selected distribution @@ -179,6 +173,10 @@ Spree::Order.class_eval do if order_cycle OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self end + + if distributor.present? && customer.present? + distributor.apply_tag_rules_to(self, customer: customer) + end end end @@ -286,17 +284,21 @@ Spree::Order.class_eval do def customer_is_valid? return true unless require_customer? - customer.present? && customer.enterprise_id == distributor_id && customer.email == (user.andand.email || email) + customer.present? && customer.enterprise_id == distributor_id && customer.email == email_for_customer + end + + def email_for_customer + (user.andand.email || email).andand.downcase end def associate_customer - email_for_customer = user.andand.email || email - existing_customer = Customer.of(distributor).find_by_email(email_for_customer) - if existing_customer - self.customer = existing_customer - else - new_customer = Customer.create(enterprise: distributor, email: email_for_customer, user: user) - self.customer = new_customer + return customer if customer.present? + self.customer = Customer.of(distributor).find_by_email(email_for_customer) + end + + def ensure_customer + unless associate_customer + self.customer = Customer.create(enterprise: distributor, email: email_for_customer, user: user) end end end diff --git a/app/models/spree/shipping_method_decorator.rb b/app/models/spree/shipping_method_decorator.rb index b8be603048..f4f1d2c999 100644 --- a/app/models/spree/shipping_method_decorator.rb +++ b/app/models/spree/shipping_method_decorator.rb @@ -1,10 +1,12 @@ Spree::ShippingMethod.class_eval do + acts_as_taggable + has_many :distributor_shipping_methods has_many :distributors, through: :distributor_shipping_methods, class_name: 'Enterprise', foreign_key: 'distributor_id' after_save :touch_distributors attr_accessible :distributor_ids, :description - attr_accessible :require_ship_address + attr_accessible :require_ship_address, :tag_list validates :distributors, presence: { message: "^At least one hub must be selected" } diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 274330d1da..9a992ba910 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -15,6 +15,7 @@ Spree.user_class.class_eval do accepts_nested_attributes_for :enterprise_roles, :allow_destroy => true attr_accessible :enterprise_ids, :enterprise_roles_attributes, :enterprise_limit + after_create :associate_customers after_create :send_signup_confirmation validate :limit_owned_enterprises @@ -41,6 +42,10 @@ Spree.user_class.class_eval do customers.of(enterprise).first end + def associate_customers + Customer.update_all({ user_id: id }, { user_id: nil, email: email }) + end + def send_signup_confirmation Delayed::Job.enqueue ConfirmSignupJob.new(id) end diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb new file mode 100644 index 0000000000..6e6405a589 --- /dev/null +++ b/app/models/tag_rule.rb @@ -0,0 +1,42 @@ +class TagRule < ActiveRecord::Base + attr_accessor :subject, :context + + belongs_to :enterprise + + preference :customer_tags, :string, default: "" + + validates :enterprise, presence: true + + attr_accessible :enterprise, :enterprise_id, :preferred_customer_tags + + def set_context(subject, context) + @subject = subject + @context = context + end + + def apply + if relevant? + if customer_tags_match? + apply! + else + apply_default! if respond_to?(:apply_default!,true) + end + end + end + + private + + def relevant? + return false unless subject_class_matches? + if respond_to?(:additional_requirements_met?, true) + return false unless additional_requirements_met? + end + true + end + + def customer_tags_match? + context_customer_tags = context.andand[:customer].andand.tag_list || [] + preferred_tags = preferred_customer_tags.split(",") + ( context_customer_tags & preferred_tags ).any? + end +end diff --git a/app/models/tag_rule/discount_order.rb b/app/models/tag_rule/discount_order.rb new file mode 100644 index 0000000000..5984814289 --- /dev/null +++ b/app/models/tag_rule/discount_order.rb @@ -0,0 +1,23 @@ +class TagRule::DiscountOrder < TagRule + calculated_adjustments + + private + + # Warning: this should only EVER be called via TagRule#apply + def apply! + create_adjustment(I18n.t("discount"), subject, subject) + end + + def subject_class_matches? + subject.class == Spree::Order + end + + def additional_requirements_met? + return false if already_applied? + true + end + + def already_applied? + subject.adjustments.where(originator_id: id, originator_type: "TagRule").any? + end +end diff --git a/app/models/tag_rule/filter_shipping_methods.rb b/app/models/tag_rule/filter_shipping_methods.rb new file mode 100644 index 0000000000..74438e560e --- /dev/null +++ b/app/models/tag_rule/filter_shipping_methods.rb @@ -0,0 +1,32 @@ +class TagRule::FilterShippingMethods < TagRule + preference :matched_shipping_methods_visibility, :string, default: "visible" + preference :shipping_method_tags, :string, default: "" + + attr_accessible :preferred_matched_shipping_methods_visibility, :preferred_shipping_method_tags + + private + + # Warning: this should only EVER be called via TagRule#apply + def apply! + unless preferred_matched_shipping_methods_visibility == "visible" + subject.reject!{ |sm| tags_match?(sm) } + end + end + + def apply_default! + if preferred_matched_shipping_methods_visibility == "visible" + subject.reject!{ |sm| tags_match?(sm) } + end + end + + def tags_match?(shipping_method) + shipping_method_tags = shipping_method.andand.tag_list || [] + preferred_tags = preferred_shipping_method_tags.split(",") + ( shipping_method_tags & preferred_tags ).any? + end + + def subject_class_matches? + subject.class == Array && + subject.all? { |i| i.class == Spree::ShippingMethod } + end +end diff --git a/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface b/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface index 14f4925206..37754bb40e 100644 --- a/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface +++ b/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface @@ -1,4 +1,4 @@ // insert_bottom "[data-hook='admin_configurations_sidebar_menu']" %li - = link_to 'Accounts & Billing', main_app.edit_admin_accounts_and_billing_settings_path + = link_to t(:accounts_and_billing), main_app.edit_admin_accounts_and_billing_settings_path diff --git a/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface index 190cff1a17..a2f9ed766a 100644 --- a/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface @@ -1,6 +1,9 @@ / replace "div[data-hook='admin_shipping_method_form_fields']" -.alpha.eleven.columns{"data-hook" => "admin_shipping_method_form_fields"} +=admin_inject_shipping_method +.alpha.eleven.columns{ "data-hook" => "admin_shipping_method_form_fields", + "ng-app" => "admin.shippingMethods", + "ng-controller" => "shippingMethodCtrl" } .row .alpha.three.columns = f.label :name, t(:name) @@ -46,6 +49,13 @@   = f.label :pick_up, t(:pick_up) + .row + .alpha.three.columns + = f.label :tags, t(:tags) + .omega.eight.columns + = f.hidden_field :tag_list, "ng-value" => "shippingMethod.tag_list" + %tags-with-translation#something{ object: "shippingMethod" } + .row .alpha.eleven.columns - = render :partial => 'spree/admin/shared/calculator_fields', :locals => { :f => f } \ No newline at end of file + = render :partial => 'spree/admin/shared/calculator_fields', :locals => { :f => f } diff --git a/app/serializers/api/admin/calculator/flat_percent_item_total_serializer.rb b/app/serializers/api/admin/calculator/flat_percent_item_total_serializer.rb new file mode 100644 index 0000000000..7662edb3f6 --- /dev/null +++ b/app/serializers/api/admin/calculator/flat_percent_item_total_serializer.rb @@ -0,0 +1,7 @@ +class Api::Admin::Calculator::FlatPercentItemTotalSerializer < ActiveModel::Serializer + attributes :id, :preferred_flat_percent + + def preferred_flat_percent + object.preferred_flat_percent.to_i + end +end diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 37a96b402f..8f70da1a0d 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -3,8 +3,25 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :producer_profile_only, :email, :long_description, :permalink attributes :preferred_shopfront_message, :preferred_shopfront_closed_message, :preferred_shopfront_taxon_order, :preferred_shopfront_order_cycle_order attributes :preferred_product_selection_from_inventory_only - attributes :owner, :users + attributes :owner, :users, :tag_groups has_one :owner, serializer: Api::Admin::UserSerializer has_many :users, serializer: Api::Admin::UserSerializer + + def tag_groups + tag_groups = [] + object.tag_rules.each do |tag_rule| + tag_group = find_match(tag_groups, tag_rule.preferred_customer_tags.split(",").map{ |t| { text: t } }) + tag_groups << tag_group if tag_group[:rules].empty? + tag_group[:rules] << Api::Admin::TagRuleSerializer.new(tag_rule).serializable_hash + end + tag_groups + end + + def find_match(tag_groups, tags) + tag_groups.each do |tag_group| + return tag_group if tag_group[:tags].length == tags.length && (tag_group[:tags] & tags) == tag_group[:tags] + end + return { tags: tags, rules: [] } + end end diff --git a/app/serializers/api/admin/shipping_method_serializer.rb b/app/serializers/api/admin/shipping_method_serializer.rb new file mode 100644 index 0000000000..9fbb864d09 --- /dev/null +++ b/app/serializers/api/admin/shipping_method_serializer.rb @@ -0,0 +1,7 @@ +class Api::Admin::ShippingMethodSerializer < ActiveModel::Serializer + attributes :id, :name, :tags + + def tags + object.tag_list.map{ |t| { text: t } } + end +end diff --git a/app/serializers/api/admin/tag_rule_serializer.rb b/app/serializers/api/admin/tag_rule_serializer.rb new file mode 100644 index 0000000000..ac333c23c6 --- /dev/null +++ b/app/serializers/api/admin/tag_rule_serializer.rb @@ -0,0 +1,27 @@ +class Api::Admin::TagRuleSerializer < ActiveModel::Serializer + def serializable_hash + rule_specific_serializer.serializable_hash + end + + def rule_specific_serializer + "Api::Admin::#{object.class.to_s}Serializer".constantize.new(object) + end +end + +module Api::Admin::TagRule + class BaseSerializer < ActiveModel::Serializer + attributes :id, :enterprise_id, :type, :preferred_customer_tags + end + + class DiscountOrderSerializer < BaseSerializer + has_one :calculator, serializer: Api::Admin::Calculator::FlatPercentItemTotalSerializer + end + + class FilterShippingMethodsSerializer < BaseSerializer + attributes :preferred_matched_shipping_methods_visibility, :shipping_method_tags + + def shipping_method_tags + object.preferred_shipping_method_tags.split(",") + end + end +end diff --git a/app/views/admin/accounts_and_billing_settings/edit.html.haml b/app/views/admin/accounts_and_billing_settings/edit.html.haml index 2121719e75..72ce7920a9 100644 --- a/app/views/admin/accounts_and_billing_settings/edit.html.haml +++ b/app/views/admin/accounts_and_billing_settings/edit.html.haml @@ -1,7 +1,7 @@ = render :partial => 'spree/admin/shared/configuration_menu' - content_for :page_title do - = t(:accounts_and_billing_settings) + = t(:accounts_and_billing) = render 'spree/shared/error_messages', target: @settings @@ -11,7 +11,7 @@ %legend =t :admin_settings = form_for @settings, as: :settings, url: main_app.admin_accounts_and_billing_settings_path, :method => :put do |f| - .row{ ng: { app: t(:admin_accounts_and_billing) } } + .row{ ng: { app: 'admin.accounts_and_billing_settings' } } .twelve.columns.alpha.omega .field = f.label :accounts_distributor_id, t(:accounts_administration_distributor) diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index f68ad37da1..e33b871df4 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -5,7 +5,7 @@ = admin_inject_shops %div{ ng: { app: 'admin.customers', controller: 'customersCtrl' } } - .row{ ng: { hide: "loaded() && filteredCustomers.length > 0" } } + .row{ ng: { hide: "loaded && customers.length > 0" } } .five.columns.alpha %h3 =t :please_select_hub @@ -13,25 +13,23 @@ %select.select2.fullwidth#shop_id{ 'ng-model' => 'shop.id', name: 'shop_id', 'ng-options' => 'shop.id as shop.name for shop in shops' } .seven.columns.omega   - .row{ 'ng-hide' => '!loaded() || filteredCustomers.length == 0' } + .row{ 'ng-hide' => '!loaded || customers.length == 0' } .controls.sixteen.columns.alpha.omega .five.columns.alpha %input.fullwidth{ :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' } - .five.columns   - -# =render 'admin/shared/bulk_actions_dropdown' - .three.columns   + .eight.columns   = render 'admin/shared/columns_dropdown' - .row{ 'ng-if' => 'shop && !loaded()' } + .row{ 'ng-if' => 'shop.id && !loaded' } .sixteen.columns.alpha#loading %img.spinner{ src: "/assets/spinning-circles.svg" } %h1 =t :loading_customers - .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded() && filteredCustomers.length == 0'} + .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded && filteredCustomers.length == 0'} %h1#no_results =t :no_customers_found - .row{ ng: { show: "loaded() && filteredCustomers.length > 0" } } + .row{ ng: { show: "loaded && filteredCustomers.length > 0" } } %form{ name: "customers" } %table.index#customers %col.email{ width: "20%"} @@ -62,3 +60,11 @@ %td.actions %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' } + + %form{ng: {show: "loaded", submit: 'add(newCustomerEmail)'}} + %h2= t '.add_new_customer' + .row + .five.columns.alpha + %input.fullwidth{type: "text", placeholder: t('.customer_placeholder'), ng: {model: 'newCustomerEmail'}} + .eleven.columns.omega + %input{type: "submit", value: t('.add_customer')} diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 79faeea229..e363078733 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -54,3 +54,7 @@ %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Shop Preferences'" } } %legend Shop Preferences = render 'admin/enterprises/form/shop_preferences', f: f + +%fieldset.alpha.no-border-bottom{ ng: { if: "menu.selected.name=='Tag Rules'" } } + %legend Tag Rules + = render 'admin/enterprises/form/tag_rules', f: f diff --git a/app/views/admin/enterprises/edit.html.haml b/app/views/admin/enterprises/edit.html.haml index 19ae0f3b44..5d3a623f40 100644 --- a/app/views/admin/enterprises/edit.html.haml +++ b/app/views/admin/enterprises/edit.html.haml @@ -7,6 +7,7 @@ - content_for :page_actions do %li= button_link_to "Back to enterprises list", main_app.admin_enterprises_path, icon: 'icon-arrow-left' + = render 'admin/enterprises/form_data' = render 'admin/enterprises/ng_form', action: 'edit' diff --git a/app/views/admin/enterprises/form/_primary_details.html.haml b/app/views/admin/enterprises/form/_primary_details.html.haml index 437a61e866..ccf59c8a12 100644 --- a/app/views/admin/enterprises/form/_primary_details.html.haml +++ b/app/views/admin/enterprises/form/_primary_details.html.haml @@ -22,7 +22,6 @@ %a What's this? .five.columns.omega = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' -   = f.label :is_primary_producer, 'Producer' - if spree_current_user.admin? .row @@ -33,15 +32,12 @@ %a What's this? .two.columns = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' -   = f.label :sells, "None", value: "none" .two.columns = f.radio_button :sells, "own", 'ng-model' => 'Enterprise.sells' -   = f.label :sells, "Own", value: "own" .four.columns.omega = f.radio_button :sells, "any", 'ng-model' => 'Enterprise.sells' -   = f.label :sells, "Any", value: "any" .row .three.columns.alpha @@ -50,12 +46,21 @@ %a What's this? .two.columns = f.radio_button :visible, true -   = f.label :visible, "Visible", :value => "true" .five.columns.omega = f.radio_button :visible, false -   = f.label :visible, "Not Visible", :value => "false" +.row + .three.columns.alpha + %label= t '.shopfront_requires_login' + %div{'ofn-with-tip' => t('.shopfront_requires_login_tip')} + %a= t 'admin.whats_this' + .two.columns + = f.radio_button :require_login, false + = f.label :require_login, t('.shopfront_requires_login_false'), value: :false + .five.columns.omega + = f.radio_button :require_login, true + = f.label :require_login, t('.shopfront_requires_login_true'), value: :true .permalink{ ng: { controller: "permalinkCtrl" } } .row{ ng: { show: "Enterprise.sells == 'own' || Enterprise.sells == 'any'" } } .three.columns.alpha diff --git a/app/views/admin/enterprises/form/_shipping_methods.html.haml b/app/views/admin/enterprises/form/_shipping_methods.html.haml index 445b026c38..5ccc22a720 100644 --- a/app/views/admin/enterprises/form/_shipping_methods.html.haml +++ b/app/views/admin/enterprises/form/_shipping_methods.html.haml @@ -7,7 +7,7 @@ %th %tbody - @shipping_methods.each do |shipping_method| - %tr{ ng: { controller: 'shippingMethodCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } + %tr{ ng: { controller: 'shippingMethodsCtrl', init: "findShippingMethodByID(#{shipping_method.id})" } } %td= shipping_method.name %td= f.check_box :shipping_method_ids, { :multiple => true, 'ng-model' => 'ShippingMethod.selected' }, shipping_method.id, nil %td= link_to "Edit", edit_admin_shipping_method_path(shipping_method) diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml new file mode 100644 index 0000000000..1a50e4f353 --- /dev/null +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -0,0 +1,33 @@ +.row{ ng: { controller: "TagRulesCtrl" } } + .eleven.columns.alpha.omega + .eleven.columns.alpha.omega + .no_tags{ ng: { show: "tagGroups.length == 0" } } + No tags apply to this enterprise yet + .customer_tag{ ng: { repeat: "tagGroup in tagGroups" }, bindonce: true } + .header + %table + %colgroup + %col{width: '35%'} + %col{width: '65%'} + %tr + %td + %h5 + For customers tagged: + %td + %tags-input{ ng: { model: 'tagGroup.tags'}, + min: { tags: "1" }, + on: { tag: { added: "updateTagsRulesFor(tagGroup)", removed: "updateTagsRulesFor(tagGroup)" } } } + + .no_rules{ ng: { show: "tagGroup.rules.length == 0" } } + No rules apply to this tag yet + %table + %tr.tag_rule{ id: "tr_{{rule.id}}", ng: { repeat: "rule in tagGroup.rules" } } + %td + %discount-order{ bo: { if: "rule.type == 'TagRule::DiscountOrder'" } } + %filter-shipping-methods{ bo: { if: "rule.type == 'TagRule::FilterShippingMethods'" } } + %td.actions + %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } + .add_rule.text-center + %input.button.icon-plus{ type: 'button', value: "+ Add A New Rule", "new-tag-rule-dialog" => true } + .add_tage + %input.button.red.icon-plus{ type: 'button', value: "+ Add A New Tag", ng: { click: 'addNewTag()' } } diff --git a/app/views/admin/variant_overrides/_filters.html.haml b/app/views/admin/variant_overrides/_filters.html.haml index 7330c26799..c9f3a27b32 100644 --- a/app/views/admin/variant_overrides/_filters.html.haml +++ b/app/views/admin/variant_overrides/_filters.html.haml @@ -1,4 +1,4 @@ -.filters.sixteen.columns.alpha +.filters.sixteen.columns.alpha.omega .filter.four.columns.alpha %label{ :for => 'query', ng: {class: '{disabled: !hub_id}'} }=t('admin.quick_search') %br diff --git a/app/views/admin/variant_overrides/_show_more.html.haml b/app/views/admin/variant_overrides/_show_more.html.haml index ad943c853e..8d60593ddc 100644 --- a/app/views/admin/variant_overrides/_show_more.html.haml +++ b/app/views/admin/variant_overrides/_show_more.html.haml @@ -1,4 +1,4 @@ -.sixteen.columns.alpha.omega.text-center{ ng: {show: 'productLimit < filteredProducts.length'}} +.text-center %input{ type: 'button', value: 'Show More', ng: { click: 'productLimit = productLimit + 10' } } or %input{ type: 'button', value: "Show All ({{ filteredProducts.length - productLimit }} More)", ng: { click: 'productLimit = filteredProducts.length' } } diff --git a/app/views/checkout/_authentication.html.haml b/app/views/checkout/_authentication.html.haml index 2120ba6549..1c3b06e63b 100644 --- a/app/views/checkout/_authentication.html.haml +++ b/app/views/checkout/_authentication.html.haml @@ -1,11 +1,11 @@ %section{"ng-show" => "!enabled"} .row - .small-12.columns.text-center{"ng-controller" => "AuthenticationCtrl"} + .small-12.columns.text-center %h3.pad-top = t :checkout_headline .row.pad-top - .small-5.columns.text-center{"ng-controller" => "AuthenticationCtrl"} - %button.primary.expand{"ng-click" => "open()"} + .small-5.columns.text-center + %button.primary.expand{"auth" => "login"} = t :label_login .small-2.columns.text-center %p.pad-top= "-#{t :action_or}-" diff --git a/app/views/enterprises/shop.html.haml b/app/views/enterprises/shop.html.haml index f2063494b4..d635f68164 100644 --- a/app/views/enterprises/shop.html.haml +++ b/app/views/enterprises/shop.html.haml @@ -22,6 +22,7 @@ %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", "ofn-change-order-cycle" => true, + "disabled" => require_customer?, "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}", "popover-placement" => "left", "popover" => t(:enterprises_choose), "popover-trigger" => "openTrigger"} @@ -31,7 +32,7 @@ = render partial: 'shop/messages' - .row - = render partial: "shop/products/form" + - unless require_customer? + .row= render partial: "shop/products/form" = render partial: "shared/footer" diff --git a/app/views/home/_hubs.html.haml b/app/views/home/_hubs.html.haml index fea1fc0a7d..e899270efc 100644 --- a/app/views/home/_hubs.html.haml +++ b/app/views/home/_hubs.html.haml @@ -27,3 +27,9 @@ .show-distance-matches{"ng-show" => "nameMatchesFiltered.length > 0 && !distanceMatchesShown"} %a{href: "", "ng-click" => "showDistanceMatches()"} = t :hubs_distance_filter, location: "{{ nameMatchesFiltered[0].name }}" + .more-controls + %a.button{href: "", ng: {click: "showClosedShops()", show: "filterExpression.active"}} + = t '.show_closed_shops' + %a.button{href: "", ng: {click: "hideClosedShops()", show: "!filterExpression.active"}} + = t '.hide_closed_shops' + %a.button{href: main_app.map_path}= t '.show_on_map' diff --git a/app/views/home/_hubs_table.html.haml b/app/views/home/_hubs_table.html.haml index 8842079f56..edf9eb5ec8 100644 --- a/app/views/home/_hubs_table.html.haml +++ b/app/views/home/_hubs_table.html.haml @@ -1,5 +1,5 @@ .active_table - %hub.active_table_node.row{"ng-repeat" => "hub in #{enterprises}Filtered = (#{enterprises} | visible | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+distance', '+orders_close_at'])", + %hub.active_table_node.row{"ng-repeat" => "hub in #{enterprises}Filtered = (#{enterprises} | filter:filterExpression | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+distance', '+orders_close_at'])", "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", "ng-controller" => "HubNodeCtrl", id: "{{hub.hash}}"} diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index fd7f5faf4e..e3e1f44174 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -7,13 +7,7 @@ .row .small-12.medium-8.medium-offset-2.columns.text-center .alert-box - %a.big-alert{href: "http://www.openfoodnetwork.org", target: "_blank"} - %h6 - = t :alert_selling_on_ofn -   - %strong - = t :alert_start_here - %i.ofn-i_054-point-right + = render 'shared/register_call' .row .small-12.medium-4.medium-offset-2.columns.text-center %h6 diff --git a/app/views/shared/_register_call.html.haml b/app/views/shared/_register_call.html.haml new file mode 100644 index 0000000000..8c2d95ed2e --- /dev/null +++ b/app/views/shared/_register_call.html.haml @@ -0,0 +1,7 @@ +%a.alert-cta{href: registration_path, target: "_blank"} + %h6 + = t '.selling_on_ofn' +   + %strong + = t '.register' + %i.ofn-i_054-point-right diff --git a/app/views/shared/_signed_out.html.haml b/app/views/shared/_signed_out.html.haml index 1a7cbcc920..1e6efdc32a 100644 --- a/app/views/shared/_signed_out.html.haml +++ b/app/views/shared/_signed_out.html.haml @@ -1,5 +1,5 @@ -%li#login-link{"ng-controller" => "AuthenticationCtrl"} - %a{"ng-click" => "open()"} +%li#login-link + %a{"auth" => "login"} %i.ofn-i_017-locked %span = t 'label_login' diff --git a/app/views/shared/menu/_alert.html.haml b/app/views/shared/menu/_alert.html.haml index b6ee3acfb1..6512358cc3 100644 --- a/app/views/shared/menu/_alert.html.haml +++ b/app/views/shared/menu/_alert.html.haml @@ -1,10 +1,4 @@ .text-center.page-alert.fixed{ "ofn-page-alert" => true } .alert-box - %a.alert-cta{href: registration_path, target: "_blank"} - %h6 - = t 'alert_selling_on_ofn' -   - %strong - = t 'alert_start_here' - %i.ofn-i_054-point-right + = render 'shared/register_call' %a.close{ ng: { click: "close()" } } × diff --git a/app/views/shop/_messages.html.haml b/app/views/shop/_messages.html.haml index 1e56aba468..ba35354bf3 100644 --- a/app/views/shop/_messages.html.haml +++ b/app/views/shop/_messages.html.haml @@ -1,5 +1,18 @@ -- if @order_cycles and @order_cycles.empty? +- if require_customer? + .row.footer-pad + .small-12.columns + .shopfront_hidden_message + = t '.require_customer_login' + - if spree_current_user.nil? + = t '.require_login_html', + {login: ('' + t('.login') + '').html_safe, + register: ('' + t('.register') + '').html_safe} + - else + = t '.require_customer_html', + {contact: ('' + t('.contact') + '').html_safe, + enterprise: current_distributor.name} +- elsif @order_cycles and @order_cycles.empty? - if current_distributor.preferred_shopfront_closed_message.present? .row .small-12.columns diff --git a/app/views/spree/admin/reports/customers.html.haml b/app/views/spree/admin/reports/customers.html.haml index 70045662f9..3ab25e5c6c 100644 --- a/app/views/spree/admin/reports/customers.html.haml +++ b/app/views/spree/admin/reports/customers.html.haml @@ -33,7 +33,7 @@ %br %table#listing_customers.index %thead - %tr{'data-hook' => "orders_header" } + %tr{'data-hook' => "orders_header"} - @report.header.each do |heading| %th=heading %tbody diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml index 0b32eabe78..808c1ead61 100644 --- a/app/views/spree/admin/reports/order_cycle_management.html.haml +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -33,7 +33,7 @@ %br %table#listing_ocm_orders.index %thead - %tr{'data-hook' => "orders_header" } + %tr{'data-hook' => "orders_header"} - @report.header.each do |heading| %th=heading %tbody diff --git a/app/views/spree/admin/reports/orders_and_distributors.html.haml b/app/views/spree/admin/reports/orders_and_distributors.html.haml index 9d733cc995..aa710b1db6 100644 --- a/app/views/spree/admin/reports/orders_and_distributors.html.haml +++ b/app/views/spree/admin/reports/orders_and_distributors.html.haml @@ -10,7 +10,7 @@ %br %table#listing_orders.index %thead - %tr{'data-hook' => t(:report_customers_header)} + %tr{'data-hook' => 'orders_header'} - @report.header.each do |heading| %th=heading %tbody @@ -21,4 +21,3 @@ - if @report.table.empty? %tr %td{:colspan => "2"}= t(:none) - diff --git a/app/views/spree/admin/shared/_trial_progress_bar.html.haml b/app/views/spree/admin/shared/_trial_progress_bar.html.haml index 999dd9a13f..3deef097fe 100644 --- a/app/views/spree/admin/shared/_trial_progress_bar.html.haml +++ b/app/views/spree/admin/shared/_trial_progress_bar.html.haml @@ -1,7 +1,7 @@ - if enterprise -if shop_trial_in_progress?(enterprise) #trial_progress_bar - = t(:shop_trial_in_progress) + = t(:shop_trial_in_progress, days: remaining_trial_days(enterprise)) -elsif shop_trial_expired?(enterprise) #trial_progress_bar - = t(:shop_trial_expired) \ No newline at end of file + = t(:shop_trial_expired) diff --git a/config/initializers/acts_as_taggable_on.rb b/config/initializers/acts_as_taggable_on.rb new file mode 100644 index 0000000000..08d8aa67fc --- /dev/null +++ b/config/initializers/acts_as_taggable_on.rb @@ -0,0 +1 @@ +ActsAsTaggableOn.force_lowercase = true diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 3e793f88a4..131d718a87 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -10,6 +10,12 @@ en-GB: password: confirmation: you have successfully registered too_short: pick a longer name + # Overridden here due to a bug in spree i18n (Issue #870) + attributes: + spree/order: + payment_state: Payment State + shipment_state: Shipment State + devise: failure: invalid: | diff --git a/config/locales/en.yml b/config/locales/en.yml index 020011ea8d..d9bd758c5a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -15,6 +15,12 @@ # See http://community.openfoodnetwork.org/t/localisation-ofn-in-your-language/397 en: + activerecord: + # Overridden here due to a bug in spree i18n (Issue #870) + attributes: + spree/order: + payment_state: Payment State + shipment_state: Shipment State devise: failure: invalid: | @@ -74,6 +80,10 @@ en: whats_this: What's this? + customers: + index: + add_customer: "Add customer" + customer_placeholder: "customer@example.org" inventory: title: Inventory description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page @@ -105,6 +115,32 @@ en: enterprise: select_outgoing_oc_products_from: Select outgoing OC products from + enterprises: + form: + primary_details: + shopfront_requires_login: "Shopfront requires login?" + shopfront_requires_login_tip: "Choose whether customers must login to view the shopfront." + shopfront_requires_login_false: "Public" + shopfront_requires_login_true: "Require customers to login" + + home: + hubs: + show_closed_shops: "Show closed shops" + hide_closed_shops: "Hide closed shops" + show_on_map: "Show all on the map" + shared: + register_call: + selling_on_ofn: "Interested in getting on the Open Food Network?" + register: "Register here" + shop: + messages: + login: "login" + register: "register" + contact: "contact" + require_customer_login: "This shop is for customers only." + require_login_html: "Please %{login} if you have an account already. Otherwise, %{register} to become a customer." + require_customer_html: "Please %{contact} %{enterprise} to become a customer." + # Printable Invoice Columns invoice_column_item: "Item" invoice_column_qty: "Qty" @@ -145,8 +181,6 @@ en: on_demand: On demand none: None - alert_selling_on_ofn: "Interested in selling food on the Open Food Network?" - alert_start_here: "Start here" label_shops: "Shops" label_map: "Map" label_producers: "Producers" @@ -748,7 +782,7 @@ Please follow the instructions there to make your enterprise visible on the Open shop_variant_quantity_max: "max" contact: "Contact" follow: "Follow" - shop_for_products_html: "Shop for %{enterprise} products at:" #FIXME + shop_for_products_html: "Shop for %{enterprise} products at:" change_shop: "Change shop to:" shop_at: "Shop now at:" price_breakdown: "Full price breakdown" @@ -840,13 +874,12 @@ Please follow the instructions there to make your enterprise visible on the Open go: "Go" hub: "Hub" accounts_administration_distributor: "accounts administration distributor" - admin_accounts_and_billing: "admin.accounts_and_billing_settings" #FIXME + accounts_and_billing: "Accounts & Billing" producer: "Producer" product: "Product" price: "Price" on_hand: "On hand" save_changes: "Save Changes" - update_action: "update()" #FIXME spree_admin_overview_enterprises_header: "My Enterprises" spree_admin_overview_enterprises_footer: "MANAGE MY ENTERPRISES" spree_admin_enterprises_hubs_name: "Name" @@ -882,8 +915,8 @@ Please follow the instructions there to make your enterprise visible on the Open live: "live" manage: "Manage" resend: "Resend" - add_and_manage_products: "Add & manage products" - add_and_manage_order_cycles: "Add & manage order cycles" + add_and_manage_products: "Add & manage products" + add_and_manage_order_cycles: "Add & manage order cycles" manage_order_cycles: "Manage order cycles" manage_products: "Manage products" edit_profile_details: "Edit profile details" @@ -916,14 +949,13 @@ Please follow the instructions there to make your enterprise visible on the Open hub_sidebar_at_least: "At least one hub must be selected" hub_sidebar_blue: "blue" hub_sidebar_red: "red" - shop_trial_in_progress: "Your shopfront trial expires in #{remaining_trial_days(enterprise)}." #FIXME + shop_trial_in_progress: "Your shopfront trial expires in %{days}." shop_trial_expired: "Good news! We have decided to extend shopfront trials until further notice (probably around March 2015)." #FIXME report_customers_distributor: "Distributor" report_customers_supplier: "Supplier" report_customers_cycle: "Order Cycle" report_customers_type: "Report Type" report_customers_csv: "Download as csv" - report_customers_header: "orders header" report_producers: "Producers: " report_type: "Report Type: " report_hubs: "Hubs: " @@ -934,8 +966,6 @@ Please follow the instructions there to make your enterprise visible on the Open report_payment_totals: 'Payment Totals' report_all: 'all' report_order_cycle: "Order Cycle: " - report_product_header: "products_header" - report_order_header: "orders_header" report_entreprises: "Enterprises: " report_users: "Users: " initial_invoice_number: "Initial invoice number:" @@ -944,6 +974,7 @@ Please follow the instructions there to make your enterprise visible on the Open account_code: "Account code:" equals: "Equals" contains: "contains" + discount: "Discount" filter_products: "Filter Products" delete_product_variant: "The last variant cannot be deleted!" progress: "progress" @@ -972,6 +1003,7 @@ Please follow the instructions there to make your enterprise visible on the Open payment_methods: "Payment Methods" enterprise_fees: "Enterprise Fees" inventory_settings: "Inventory Settings" + tag_rules: "Tag Rules" shop_preferences: "Shop Preferences" validation_msg_relationship_already_established: "^That relationship is already established." validation_msg_at_least_one_hub: "^At least one hub must be selected" diff --git a/config/routes.rb b/config/routes.rb index c314747786..8d8d6d27c3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -87,6 +87,8 @@ Openfoodnetwork::Application.routes.draw do resources :producer_properties do post :update_positions, on: :collection end + + resources :tag_rules, only: [:destroy] end resources :enterprise_relationships @@ -113,7 +115,7 @@ Openfoodnetwork::Application.routes.draw do resources :inventory_items, only: [:create, :update] - resources :customers, only: [:index, :update] + resources :customers, only: [:index, :create, :update, :destroy] resource :content @@ -141,6 +143,10 @@ Openfoodnetwork::Application.routes.draw do get :managed, on: :collection get :accessible, on: :collection end + + resource :status do + get :job_queue + end end namespace :open_food_network do diff --git a/config/schedule.rb b/config/schedule.rb index 023382330a..8cc8d7d225 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -4,8 +4,11 @@ require 'whenever' env "MAILTO", "rohan@rohanmitchell.com" + # If we use -e with a file containing specs, rspec interprets it and filters out our examples job_type :run_file, "cd :path; :environment_variable=:environment bundle exec script/rails runner :task :output" +job_type :enqueue_job, "cd :path; :environment_variable=:environment bundle exec script/enqueue :task :priority :output" + every 1.hour do rake 'openfoodnetwork:cache:check_products_integrity' @@ -23,6 +26,10 @@ every 4.hours do rake 'db2fog:backup' end +every 5.minutes do + enqueue_job 'HeartbeatJob', priority: 0 +end + every 1.day, at: '1:00am' do rake 'openfoodnetwork:billing:update_account_invoices' end diff --git a/db/migrate/20160303004210_create_tag_rules.rb b/db/migrate/20160303004210_create_tag_rules.rb new file mode 100644 index 0000000000..157fee1e69 --- /dev/null +++ b/db/migrate/20160303004210_create_tag_rules.rb @@ -0,0 +1,10 @@ +class CreateTagRules < ActiveRecord::Migration + def change + create_table :tag_rules do |t| + t.references :enterprise, null: false, index: true + t.string :type, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20160316051131_add_require_login_to_enterprise.rb b/db/migrate/20160316051131_add_require_login_to_enterprise.rb new file mode 100644 index 0000000000..68de642b62 --- /dev/null +++ b/db/migrate/20160316051131_add_require_login_to_enterprise.rb @@ -0,0 +1,5 @@ +class AddRequireLoginToEnterprise < ActiveRecord::Migration + def change + add_column :enterprises, :require_login, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20160401043927_change_value_type_of_paypal_passwords.rb b/db/migrate/20160401043927_change_value_type_of_paypal_passwords.rb new file mode 100644 index 0000000000..e03ed25f9e --- /dev/null +++ b/db/migrate/20160401043927_change_value_type_of_paypal_passwords.rb @@ -0,0 +1,15 @@ +class ChangeValueTypeOfPaypalPasswords < ActiveRecord::Migration + def up + Spree::Preference + .where("key like ?", "spree/gateway/pay_pal_express/password/%") + .where(value_type: "string") + .update_all(value_type: "password") + end + + def down + Spree::Preference + .where("key like ?", "spree/gateway/pay_pal_express/password/%") + .where(value_type: "password") + .update_all(value_type: "string") + end +end diff --git a/db/schema.rb b/db/schema.rb index cdab93834c..dc54bd8f3d 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 => 20160302044850) do +ActiveRecord::Schema.define(:version => 20160401043927) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -348,6 +348,7 @@ ActiveRecord::Schema.define(:version => 20160302044850) do t.string "permalink", :null => false t.boolean "charges_sales_tax", :default => false, :null => false t.string "email_address" + t.boolean "require_login", :default => false, :null => false end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" @@ -1146,6 +1147,13 @@ ActiveRecord::Schema.define(:version => 20160302044850) do t.integer "state_id" end + create_table "tag_rules", :force => true do |t| + t.integer "enterprise_id", :null => false + t.string "type", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "taggings", :force => true do |t| t.integer "tag_id" t.integer "taggable_id" diff --git a/lib/open_food_network/customers_report.rb b/lib/open_food_network/customers_report.rb index 820bfff7df..d7900a16ac 100644 --- a/lib/open_food_network/customers_report.rb +++ b/lib/open_food_network/customers_report.rb @@ -31,7 +31,7 @@ module OpenFoodNetwork ba.phone, order.distributor.andand.name, [da.andand.address1, da.andand.address2, da.andand.city].join(" "), - order.shipping_method.andand.name + order.shipping_method.andand.name ] end end @@ -78,4 +78,3 @@ module OpenFoodNetwork end end end - diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index ea238f7792..8a9c6ba60f 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -50,7 +50,7 @@ module OpenFoodNetwork order.shipping_method.andand.name, order.payments.first.andand.payment_method.andand.name, order.payments.first.amount, - OpenFoodNetwork::UserBalanceCalculator.new(order.user, order.distributor).balance + OpenFoodNetwork::UserBalanceCalculator.new(order.email, order.distributor).balance ] end @@ -67,23 +67,23 @@ module OpenFoodNetwork order.shipping_method.andand.name, order.payments.first.andand.payment_method.andand.name, order.payments.first.amount, - OpenFoodNetwork::UserBalanceCalculator.new(order.user, order.distributor).balance, + OpenFoodNetwork::UserBalanceCalculator.new(order.email, order.distributor).balance, has_temperature_controlled_items?(order), order.special_instructions ] end def filter_to_payment_method(orders) - if params[:payment_method_name].present? - orders.with_payment_method_name(params[:payment_method_name]) + if params[:payment_method_in].present? + orders.joins(payments: :payment_method).where(spree_payments: { payment_method_id: params[:payment_method_in]}) else orders end end def filter_to_shipping_method(orders) - if params[:shipping_method_name].present? - orders.joins(:shipping_method).where("spree_shipping_methods.name = ?", params[:shipping_method_name]) + if params[:shipping_method_in].present? + orders.joins(:shipping_method).where(shipping_method_id: params[:shipping_method_in]) else orders end diff --git a/lib/open_food_network/user_balance_calculator.rb b/lib/open_food_network/user_balance_calculator.rb index 73926c1d03..32cd00c90c 100644 --- a/lib/open_food_network/user_balance_calculator.rb +++ b/lib/open_food_network/user_balance_calculator.rb @@ -1,32 +1,30 @@ module OpenFoodNetwork class UserBalanceCalculator - def initialize(user, distributor) - @user = user + def initialize(email, distributor) + @email = email @distributor = distributor end def balance - payment_total - order_total + payment_total - completed_order_total end - private - def order_total - orders.sum &:total + def completed_order_total + completed_orders.sum &:total end def payment_total payments.sum &:amount end - - def orders - Spree::Order.where(distributor_id: @distributor, user_id: @user) + def completed_orders + Spree::Order.where(distributor_id: @distributor, email: @email).complete.not_state(:canceled) end def payments - Spree::Payment.where(order_id: orders) + Spree::Payment.where(order_id: completed_orders, state: "completed") end end end diff --git a/script/enqueue b/script/enqueue new file mode 100755 index 0000000000..2071414e4e --- /dev/null +++ b/script/enqueue @@ -0,0 +1,61 @@ +#!/usr/bin/env ruby + +# Push a job onto the Delayed Job queue without booting the Rails stack +# Perfect for calling via cron. +# +# Use like this: +# +# ./script/enqueue +# ./script/enqueue Background::ImportJobs + +require 'erb' +require 'yaml' + +ENV["RAILS_ENV"] ||= "development" + +DATABASE_CONFIG = File.expand_path("../../config/database.yml", __FILE__) + +def psql + raise "Missing database.yml" unless File.exists?(DATABASE_CONFIG) + + file = File.read(DATABASE_CONFIG) + erb = ERB.new(file).result + env = ENV["RAILS_ENV"] + config = YAML.load(erb)[env] + + raise "Missing config for #{env} environment" unless config + + "psql".tap do |s| + s << " --host #{config['host']}" if config['host'] + s << " --user #{config['username']}" if config['username'] + s << " --port #{config['port']}" if config['port'] + s << " #{config['database']}" + end +end + +def enqueue_delayed_job(handler, priority=nil) + time = Time.now.utc.strftime("%Y-%m-%d %H:%M:%S") + priority ||= 50 + + sql = <<-SQL + INSERT INTO delayed_jobs ( + handler, + created_at, + updated_at, + run_at, + priority + ) VALUES ( + '--- !ruby/object:#{handler} {}\n', + '#{time}', + '#{time}', + '#{time}', + #{priority} + ); + SQL + + IO.popen(psql, "w") do |io| + io.write sql + end +end + +enqueue_delayed_job ARGV[0], ARGV[1] diff --git a/spec/controllers/admin/customers_controller_spec.rb b/spec/controllers/admin/customers_controller_spec.rb index bb2e4888c2..f64a8057e8 100644 --- a/spec/controllers/admin/customers_controller_spec.rb +++ b/spec/controllers/admin/customers_controller_spec.rb @@ -94,4 +94,45 @@ describe Admin::CustomersController, type: :controller do end end end + + describe "create" do + let(:enterprise) { create(:distributor_enterprise) } + let(:another_enterprise) { create(:distributor_enterprise) } + + def create_customer(enterprise) + spree_put :create, format: :json, customer: { email: 'new@example.com', enterprise_id: enterprise.id } + end + + context "json" do + context "where I manage the customer's enterprise" do + before do + controller.stub spree_current_user: enterprise.owner + end + + it "allows me to create the customer" do + expect { create_customer enterprise }.to change(Customer, :count).by(1) + end + end + + context "where I don't manage the customer's enterprise" do + before do + controller.stub spree_current_user: another_enterprise.owner + end + + it "prevents me from creating the customer" do + expect { create_customer enterprise }.to change(Customer, :count).by(0) + end + end + + context "where I am the admin user" do + before do + controller.stub spree_current_user: create(:admin_user) + end + + it "allows admins to create the customer" do + expect { create_customer enterprise }.to change(Customer, :count).by(1) + end + end + end + end end diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 8b972a3321..05498010bb 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -181,6 +181,58 @@ module Admin end end end + + describe "tag rules" do + let(:enterprise) { create(:distributor_enterprise) } + let!(:tag_rule) { create(:tag_rule, enterprise: enterprise) } + + before do + login_as_enterprise_user [enterprise] + end + + context "discount order rules" do + it "updates the existing rule with new attributes" do + spree_put :update, { + id: enterprise, + enterprise: { + tag_rules_attributes: { + '0' => { + id: tag_rule, + type: "TagRule::DiscountOrder", + preferred_customer_tags: "some,new,tags", + calculator_type: "Spree::Calculator::FlatPercentItemTotal", + calculator_attributes: { id: tag_rule.calculator.id, preferred_flat_percent: "15" } + } + } + } + } + tag_rule.reload + expect(tag_rule.preferred_customer_tags).to eq "some,new,tags" + expect(tag_rule.calculator.preferred_flat_percent).to eq 15 + end + + it "creates new rules with new attributes" do + spree_put :update, { + id: enterprise, + enterprise: { + tag_rules_attributes: { + '0' => { + id: "", + type: "TagRule::DiscountOrder", + preferred_customer_tags: "tags,are,awesome", + calculator_type: "Spree::Calculator::FlatPercentItemTotal", + calculator_attributes: { id: "", preferred_flat_percent: "24" } + } + } + } + } + expect(tag_rule.reload).to be + new_tag_rule = TagRule::DiscountOrder.last + expect(new_tag_rule.preferred_customer_tags).to eq "tags,are,awesome" + expect(new_tag_rule.calculator.preferred_flat_percent).to eq 24 + end + end + end end context "as owner" do diff --git a/spec/controllers/admin/tag_rules_controller_spec.rb b/spec/controllers/admin/tag_rules_controller_spec.rb new file mode 100644 index 0000000000..fa95650479 --- /dev/null +++ b/spec/controllers/admin/tag_rules_controller_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Admin::TagRulesController, type: :controller do + + describe "destroy" do + context "json" do + let(:format) { :json } + + let(:enterprise) { create(:distributor_enterprise) } + let!(:tag_rule) { create(:tag_rule, enterprise: enterprise) } + let(:params) { { format: format, id: tag_rule.id } } + + context "where I don't manage the tag rule enterprise" do + let(:user) { create(:user) } + + before do + user.owned_enterprises << create(:enterprise) + allow(controller).to receive(:spree_current_user) { user } + end + + it "redirects to unauthorized" do + spree_delete :destroy, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "where I manage the tag rule enterprise" do + before do + allow(controller).to receive(:spree_current_user) { enterprise.owner } + end + + it { expect{ spree_delete :destroy, params }.to change{TagRule.count}.by(-1) } + end + end + end +end diff --git a/spec/controllers/api/statuses_controller_spec.rb b/spec/controllers/api/statuses_controller_spec.rb new file mode 100644 index 0000000000..f2427efb79 --- /dev/null +++ b/spec/controllers/api/statuses_controller_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +module Api + describe StatusesController do + render_views + + describe "job queue status" do + it "returns alive when up to date" do + Spree::Config.last_job_queue_heartbeat_at = Time.now + spree_get :job_queue + response.should be_success + response.body.should == {alive: true}.to_json + end + + it "returns dead otherwise" do + Spree::Config.last_job_queue_heartbeat_at = 10.minutes.ago + spree_get :job_queue + response.should be_success + response.body.should == {alive: false}.to_json + end + + it "returns dead when no heartbeat recorded" do + Spree::Config.last_job_queue_heartbeat_at = nil + spree_get :job_queue + response.should be_success + response.body.should == {alive: false}.to_json + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 8b06b67cad..b4a3a1b573 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -285,6 +285,17 @@ FactoryGirl.define do year { 2000 + rand(100) } month { 1 + rand(12) } end + + factory :filter_shipping_methods_tag_rule, class: TagRule::FilterShippingMethods do + enterprise { FactoryGirl.create :distributor_enterprise } + end + + factory :tag_rule, class: TagRule::DiscountOrder do + enterprise { FactoryGirl.create :distributor_enterprise } + before(:create) do |tr| + tr.calculator = Spree::Calculator::FlatPercentItemTotal.new(calculable: tr) + end + end end diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 92ac89015e..3579421b84 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -82,6 +82,10 @@ feature %q{ page.should have_selector '.available' choose 'Own' + # Require login to view shopfront + expect(page).to have_checked_field "enterprise_require_login_false" + choose "Require customers to login" + within (".side_menu") { click_link "Users" } select2_search user.email, from: 'Owner' @@ -162,6 +166,8 @@ feature %q{ page.should have_field 'enterprise_name', :with => 'Eaterprises' @enterprise.reload expect(@enterprise.owner).to eq user + expect(page).to have_checked_field "enterprise_visible_true" + expect(page).to have_checked_field "enterprise_require_login_true" click_link "Business Details" page.should have_checked_field "enterprise_charges_sales_tax_true" @@ -276,7 +282,6 @@ feature %q{ end end - context "as an Enterprise user", js: true do let(:supplier1) { create(:supplier_enterprise, name: 'First Supplier') } let(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index caf52c08f8..abb5feaf3e 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -30,7 +30,7 @@ feature %q{ payment_method.distributors.should == [@distributors[0]] end - scenario "updating a payment method", retry: 3 do + scenario "updating a payment method" do pm = create(:payment_method, distributors: [@distributors[0]]) login_to_admin_section @@ -42,14 +42,33 @@ feature %q{ check "payment_method_distributor_ids_#{@distributors[1].id}" check "payment_method_distributor_ids_#{@distributors[2].id}" select2_select "PayPal Express", from: "payment_method_type" + expect(page).to have_field 'Login' + fill_in 'payment_method_preferred_login', with: 'testlogin' + fill_in 'payment_method_preferred_password', with: 'secret' + fill_in 'payment_method_preferred_signature', with: 'sig' + click_button 'Update' - flash_message.should eq 'Payment Method has been successfully updated!' + expect(flash_message).to eq 'Payment Method has been successfully updated!' payment_method = Spree::PaymentMethod.find_by_name('New PM Name') expect(payment_method.distributors).to include @distributors[1], @distributors[2] expect(payment_method.distributors).not_to include @distributors[0] expect(payment_method.type).to eq "Spree::Gateway::PayPalExpress" + expect(payment_method.preferences[:login]).to eq 'testlogin' + expect(payment_method.preferences[:password]).to eq 'secret' + expect(payment_method.preferences[:signature]).to eq 'sig' + + fill_in 'payment_method_preferred_login', with: 'otherlogin' + click_button 'Update' + + expect(flash_message).to eq 'Payment Method has been successfully updated!' + expect(page).to have_field 'Password', with: '' + + payment_method = Spree::PaymentMethod.find_by_name('New PM Name') + expect(payment_method.preferences[:login]).to eq 'otherlogin' + expect(payment_method.preferences[:password]).to eq 'secret' + expect(payment_method.preferences[:signature]).to eq 'sig' end end diff --git a/spec/features/admin/shipping_methods_spec.rb b/spec/features/admin/shipping_methods_spec.rb index 9f04b6707b..646b383c21 100644 --- a/spec/features/admin/shipping_methods_spec.rb +++ b/spec/features/admin/shipping_methods_spec.rb @@ -93,12 +93,16 @@ feature 'shipping methods' do fill_in 'shipping_method_name', :with => 'Teleport' check "shipping_method_distributor_ids_#{distributor1.id}" + find(:css, "tags-input .tags input").set "local\n" + click_button 'Create' flash_message.should == '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') shipping_method.distributors.should == [distributor1] + shipping_method.tag_list.should == ["local"] end it "shows me only shipping methods I have access to" do diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb new file mode 100644 index 0000000000..e91215a21d --- /dev/null +++ b/spec/features/admin/tag_rules_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +feature 'Tag Rules', js: true do + include AuthenticationWorkflow + include WebHelper + + let!(:enterprise) { create(:distributor_enterprise) } + + context "creating" do + before do + login_to_admin_section + visit main_app.edit_admin_enterprise_path(enterprise) + end + + it "allows creation of rules of each type" do + click_link "Tag Rules" + + # Creating a new tag + expect(page).to_not have_selector '.customer_tag' + expect(page).to have_content 'No tags apply to this enterprise yet' + click_button '+ Add A New Tag' + find(:css, "tags-input .tags input").set "volunteer\n" + + # New FilterShippingMethods Rule + click_button '+ Add A New Rule' + select2_select 'Show/Hide shipping methods', from: 'rule_type_selector' + click_button "Add Rule" + select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility" + + # New DiscountOrder Rule + # expect(page).to have_content 'No rules apply to this tag yet' + # click_button '+ Add A New Rule' + # select2_select 'Apply a discount to orders', from: 'rule_type_selector' + # click_button "Add Rule" + # fill_in "enterprise_tag_rules_attributes_1_calculator_attributes_preferred_flat_percent", with: 22 + + click_button 'Update' + + # tag_rule = TagRule::DiscountOrder.last + # expect(tag_rule.preferred_customer_tags).to eq "volunteer" + # expect(tag_rule.calculator.preferred_flat_percent).to eq -22 + + tag_rule = TagRule::FilterShippingMethods.last + expect(tag_rule.preferred_customer_tags).to eq "volunteer" + expect(tag_rule.preferred_shipping_method_tags).to eq "volunteer" + expect(tag_rule.preferred_matched_shipping_methods_visibility).to eq "hidden" + end + end + + context "updating" do + let!(:do_tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } + let!(:fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_matched_shipping_methods_visibility: "hidden", preferred_customer_tags: "member" ) } + + before do + login_to_admin_section + visit main_app.edit_admin_enterprise_path(enterprise) + end + + it "saves changes to rules of each type" do + click_link "Tag Rules" + + # Tag group exists + expect(first('.customer_tag .header')).to have_content "For customers tagged:" + expect(first('tags-input .tag-list ti-tag-item')).to have_content "member" + find(:css, "tags-input .tags input").set "volunteer\n" + + # DiscountOrder rule + expect(page).to have_field "enterprise_tag_rules_attributes_0_calculator_attributes_preferred_flat_percent", with: '0' + fill_in "enterprise_tag_rules_attributes_0_calculator_attributes_preferred_flat_percent", with: 45 + + # FilterShippingMethods rule + expect(page).to have_select2 "enterprise_tag_rules_attributes_1_preferred_matched_shipping_methods_visibility", selected: 'NOT VISIBLE' + select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_1_preferred_matched_shipping_methods_visibility" + + click_button 'Update' + + # DiscountOrder rule + expect(do_tag_rule.preferred_customer_tags).to eq "member,volunteer" + expect(do_tag_rule.calculator.preferred_flat_percent).to eq -45 + + # FilterShippingMethods rule + expect(fsm_tag_rule.preferred_customer_tags).to eq "member,volunteer" + expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "member,volunteer" + expect(fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "visible" + end + end + + context "deleting" do + let!(:tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } + + before do + login_to_admin_section + visit main_app.edit_admin_enterprise_path(enterprise) + end + + it "deletes rules from the database" do + click_link "Tag Rules" + + expect(page).to have_selector "#tr_#{tag_rule.id}" + + expect{ + within "#tr_#{tag_rule.id}" do + first("a.delete-tag-rule").click + end + expect(page).to_not have_selector "#tr_#{tag_rule.id}" + }.to change{TagRule.count}.by(-1) + end + end +end diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 1df700a1fc..74145bbe1a 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -28,6 +28,7 @@ feature "As a consumer I want to check out my cart", js: true do describe "with shipping and payment methods" do let(:sm1) { create(:shipping_method, require_ship_address: true, name: "Frogs", description: "yellow", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 0.00)) } let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 4.56)) } + let(:sm3) { create(:shipping_method, require_ship_address: false, name: "Local", tag_list: "local") } let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::PaymentMethod::Check") } let!(:pm2) { create(:payment_method, distributors: [distributor]) } let!(:pm3) do @@ -41,6 +42,7 @@ feature "As a consumer I want to check out my cart", js: true do before do distributor.shipping_methods << sm1 distributor.shipping_methods << sm2 + distributor.shipping_methods << sm3 end context "on the checkout page" do @@ -68,10 +70,11 @@ feature "As a consumer I want to check out my cart", js: true do page.should_not have_content product.tax_category.name end - it "shows all shipping methods, but doesn't show ship address when not needed" do + it "shows all shipping methods" do toggle_shipping page.should have_content "Frogs" page.should have_content "Donkeys" + page.should have_content "Local" end context "when shipping method requires an address" do @@ -84,6 +87,39 @@ feature "As a consumer I want to check out my cart", js: true do find("#ship_address > div.visible").visible?.should be_true end end + + context "using FilterShippingMethods" do + it "shows shipping methods allowed by the rule" do + # No rules in effect + toggle_shipping + page.should have_content "Frogs" + page.should have_content "Donkeys" + page.should have_content "Local" + + create(:filter_shipping_methods_tag_rule, + enterprise: distributor, + preferred_customer_tags: "local", + preferred_shipping_method_tags: "local", + preferred_matched_shipping_methods_visibility: 'visible') + visit checkout_path + checkout_as_guest + + # Rule in effect, disallows access to 'Local' + page.should have_content "Frogs" + page.should have_content "Donkeys" + page.should_not have_content "Local" + + customer = create(:customer, enterprise: distributor, tag_list: "local") + order.update_attribute(:customer_id, customer.id) + visit checkout_path + checkout_as_guest + + # #local Customer can access 'Local' shipping method + page.should have_content "Frogs" + page.should have_content "Donkeys" + page.should have_content "Local" + end + end end context "on the checkout page with payments open" do diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index e81a792881..a8545af3db 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -253,5 +253,78 @@ feature "As a consumer I want to shop with a distributor", js: true do page.should have_content "The next cycle opens in 10 days" end end + + context "when shopping requires a customer" do + let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } + let(:product) { create(:simple_product) } + let(:variant) { create(:variant, product: product) } + + before do + add_product_and_variant_to_order_cycle(exchange, product, variant) + set_order_cycle(order, oc1) + distributor.require_login = true + distributor.save! + end + + context "when not logged in" do + it "tells us to login" do + visit shop_path + expect(page).to have_content "This shop is for customers only." + expect(page).to have_content "Please login" + expect(page).to have_no_content product.name + end + end + + context "when logged in" do + let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } + let(:user) { create(:user, bill_address: address, ship_address: address) } + + before do + quick_login_as user + end + + context "as non-customer" do + it "tells us to contact enterprise" do + visit shop_path + expect(page).to have_content "This shop is for customers only." + expect(page).to have_content "Please contact #{distributor.name}" + expect(page).to have_no_content product.name + end + end + + context "as customer" do + let!(:customer) { create(:customer, user: user, enterprise: distributor) } + + it "shows just products" do + visit shop_path + expect(page).to have_no_content "This shop is for customers only." + expect(page).to have_content product.name + end + end + + context "as a manager" do + let!(:role) { create(:enterprise_role, user: user, enterprise: distributor) } + + it "shows just products" do + visit shop_path + expect(page).to have_no_content "This shop is for customers only." + expect(page).to have_content product.name + end + end + + context "as the owner" do + before do + distributor.owner = user + distributor.save! + end + + it "shows just products" do + visit shop_path + expect(page).to have_no_content "This shop is for customers only." + expect(page).to have_content product.name + end + end + end + end end end diff --git a/spec/features/consumer/shops_spec.rb b/spec/features/consumer/shops_spec.rb index faeff9c37d..4edaa702e0 100644 --- a/spec/features/consumer/shops_spec.rb +++ b/spec/features/consumer/shops_spec.rb @@ -26,9 +26,17 @@ feature 'Shops', js: true do page.should_not have_content invisible_distributor.name end - it "should grey out hubs that are not in an order cycle" do + it "should not show hubs that are not in an order cycle" do create(:simple_product, distributors: [d1, d2]) visit shops_path + page.should have_no_selector 'hub.inactive' + page.should have_no_selector 'hub', text: d2.name + end + + it "should show closed shops after clicking the button" do + create(:simple_product, distributors: [d1, d2]) + visit shops_path + click_link "Show closed shops" page.should have_selector 'hub.inactive' page.should have_selector 'hub.inactive', text: d2.name end diff --git a/spec/helpers/enterprises_helper_spec.rb b/spec/helpers/enterprises_helper_spec.rb new file mode 100644 index 0000000000..1dc32d63b3 --- /dev/null +++ b/spec/helpers/enterprises_helper_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe EnterprisesHelper do + describe "loading available shipping methods" do + + context "when a FilterShippingMethods tag rule is in effect, with preferred visibility of 'visible'" do + let!(:distributor) { create(:distributor_enterprise) } + let!(:allowed_customer) { create(:customer, enterprise: distributor, tag_list: "local") } + let!(:disallowed_customer) { create(:customer, enterprise: distributor, tag_list: "") } + let!(:order) { create(:order, distributor: distributor) } + let!(:tag_rule) { create(:filter_shipping_methods_tag_rule, + enterprise: distributor, + preferred_customer_tags: "local", + preferred_shipping_method_tags: "local-delivery") } + let!(:tagged_sm) { create(:shipping_method, require_ship_address: false, name: "Untagged", tag_list: "local-delivery") } + let!(:untagged_sm) { create(:shipping_method, require_ship_address: false, name: "Tagged", tag_list: "") } + + before do + distributor.shipping_methods = [tagged_sm, untagged_sm] + allow(helper).to receive(:current_order) { order } + end + + context "with a preferred visiblity of 'visible" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'visible') } + + context "when the customer is nil" do + it "applies default action (hide)" do + expect(helper.available_shipping_methods).to include untagged_sm + expect(helper.available_shipping_methods).to_not include tagged_sm + end + end + + context "when the customer's tags match" do + before { order.update_attribute(:customer_id, allowed_customer.id) } + + it "applies the action (show)" do + expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm + end + end + + context "when the customer's tags don't match" do + before { order.update_attribute(:customer_id, disallowed_customer.id) } + + it "applies the default action (hide)" do + expect(helper.available_shipping_methods).to include untagged_sm + expect(helper.available_shipping_methods).to_not include tagged_sm + end + end + end + + context "with a preferred visiblity of 'hidden" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'hidden') } + + context "when the customer is nil" do + it "applies default action (show)" do + expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm + end + end + + context "when the customer's tags match" do + before { order.update_attribute(:customer_id, allowed_customer.id) } + + it "applies the action (hide)" do + expect(helper.available_shipping_methods).to include untagged_sm + expect(helper.available_shipping_methods).to_not include tagged_sm + end + end + + context "when the customer's tags don't match" do + before { order.update_attribute(:customer_id, disallowed_customer.id) } + + it "applies the default action (show)" do + expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm + end + end + end + end + end +end diff --git a/spec/helpers/injection_helper_spec.rb b/spec/helpers/injection_helper_spec.rb index 0eecdeeaf9..3b62fb2660 100644 --- a/spec/helpers/injection_helper_spec.rb +++ b/spec/helpers/injection_helper_spec.rb @@ -27,7 +27,10 @@ describe InjectionHelper do it "injects shipping_methods" do sm = create(:shipping_method) helper.stub(:current_order).and_return order = create(:order) - helper.stub_chain(:current_distributor, :shipping_methods, :uniq).and_return [sm] + shipping_methods = double(:shipping_methods, uniq: [sm]) + current_distributor = double(:distributor, shipping_methods: shipping_methods) + allow(helper).to receive(:current_distributor) { current_distributor } + allow(current_distributor).to receive(:apply_tag_rules_to).with(shipping_methods, {customer: nil} ) helper.inject_available_shipping_methods.should match sm.id.to_s helper.inject_available_shipping_methods.should match sm.compute_amount(order).to_s end 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 22777a6528..215f2834ed 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 @@ -1,25 +1,49 @@ describe "CustomersCtrl", -> - ctrl = null scope = null - Customers = null + http = null beforeEach -> - shops = "list of shops" - module('admin.customers') - inject ($controller, $rootScope, _Customers_) -> + inject ($controller, $rootScope, _CustomerResource_, $httpBackend) -> scope = $rootScope - Customers = _Customers_ - ctrl = $controller 'customersCtrl', {$scope: scope, Customers: Customers, shops: shops} + http = $httpBackend + $controller 'customersCtrl', {$scope: scope, CustomerResource: _CustomerResource_, shops: {}} + this.addMatchers + toAngularEqual: (expected) -> + return angular.equals(this.actual, expected) + + it "has no shop pre-selected", -> + expect(scope.shop).toEqual {} describe "setting the shop on scope", -> + customer = { id: 5, email: 'someone@email.com'} + customers = [customer] + beforeEach -> - spyOn(Customers, "index").andReturn "list of customers" + http.expectGET('/admin/customers.json?enterprise_id=1').respond 200, customers scope.$apply -> scope.shop = {id: 1} + http.flush() - it "calls Customers#index with the correct params", -> - expect(Customers.index).toHaveBeenCalledWith({enterprise_id: 1}) + it "retrievs the list of customers", -> + expect(scope.customers).toAngularEqual customers - it "resets $scope.customers with the result of Customers#index", -> - expect(scope.customers).toEqual "list of customers" + describe "scope.add", -> + it "creates a new customer", -> + email = "customer@example.org" + newCustomer = {id: 6, email: email} + customers.push(newCustomer) + http.expectPOST('/admin/customers.json?email=' + email + '&enterprise_id=1').respond 200, newCustomer + scope.add(email) + http.flush() + expect(scope.customers).toAngularEqual customers + + describe "scope.deleteCustomer", -> + it "deletes a customer", -> + expect(scope.customers.length).toBe 2 + customer = scope.customers[0] + http.expectDELETE('/admin/customers/' + customer.id + '.json').respond 200 + scope.deleteCustomer(customer) + http.flush() + expect(scope.customers.length).toBe 1 + expect(scope.customers[0]).not.toAngularEqual customer diff --git a/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee b/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee deleted file mode 100644 index 7123055d63..0000000000 --- a/spec/javascripts/unit/admin/customers/services/customers_spec.js.coffee +++ /dev/null @@ -1,31 +0,0 @@ -describe "Customers service", -> - Customers = CustomerResource = customers = $httpBackend = null - - beforeEach -> - module 'admin.customers' - - inject ($q, _$httpBackend_, _Customers_, _CustomerResource_) -> - Customers = _Customers_ - CustomerResource = _CustomerResource_ - $httpBackend = _$httpBackend_ - $httpBackend.expectGET('/admin/customers.json?enterprise_id=2').respond 200, [{ id: 5, email: 'someone@email.com'}] - - describe "#index", -> - result = null - - beforeEach -> - expect(Customers.loaded).toBe false - result = Customers.index(enterprise_id: 2) - $httpBackend.flush() - - it "stores returned data in @customers, with ids as keys", -> - # This is super weird and freaking annoying. I think resource results have extra - # properties ($then, $promise) that cause them to not be equal to the reponse object - # provided to the expectGET clause above. - expect(Customers.customers).toEqual [ new CustomerResource({ id: 5, email: 'someone@email.com'}) ] - - it "returns @customers", -> - expect(result).toEqual Customers.customers - - it "sets @loaded to true", -> - expect(Customers.loaded).toBe true diff --git a/spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee b/spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee index 49ea827900..61d002f23a 100644 --- a/spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee +++ b/spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee @@ -8,10 +8,6 @@ describe "InventoryItems service", -> $provide.value 'inventoryItems', inventoryItems null - this.addMatchers - toDeepEqual: (expected) -> - return angular.equals(this.actual, expected) - inject ($q, _$httpBackend_, _InventoryItems_, _InventoryItemResource_) -> InventoryItems = _InventoryItems_ InventoryItemResource = _InventoryItemResource_ diff --git a/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee b/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee new file mode 100644 index 0000000000..1e132ec07d --- /dev/null +++ b/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee @@ -0,0 +1,79 @@ +describe "TagRulesCtrl", -> + ctrl = null + scope = null + enterprise = null + + beforeEach -> + module('admin.tagRules') + enterprise = + id: 45 + tag_groups: [ + { tags: "member", rules: [{ id: 1, preferred_customer_tags: "member" }, { id: 2, preferred_customer_tags: "member" }] }, + { tags: "volunteer", rules: [{ id: 3, preferred_customer_tags: "local" }] } + ] + + inject ($rootScope, $controller) -> + scope = $rootScope + ctrl = $controller 'TagRulesCtrl', {$scope: scope, enterprise: enterprise} + + describe "tagGroup start indices", -> + it "updates on initialization", -> + expect(scope.tagGroups[0].startIndex).toEqual 0 + expect(scope.tagGroups[1].startIndex).toEqual 2 + + describe "adding a new tag group", -> + beforeEach -> + scope.addNewRuleTo(scope.tagGroups[0], "DiscountOrder") + + it "adds a new rule of the specified type to the rules array for the tagGroup", -> + expect(scope.tagGroups[0].rules.length).toEqual 3 + expect(scope.tagGroups[0].rules[2].type).toEqual "TagRule::DiscountOrder" + + it "updates tagGroup start indices", -> + expect(scope.tagGroups[0].startIndex).toEqual 0 + expect(scope.tagGroups[1].startIndex).toEqual 3 + + describe "deleting a tag group", -> + describe "where the rule is not in the rule list for the tagGroup", -> + beforeEach -> + scope.deleteTagRule(scope.tagGroups[0],scope.tagGroups[1].rules[0]) + + it "does not remove any rules", -> + expect(scope.tagGroups[0].rules.length).toEqual 2 + expect(scope.tagGroups[1].rules.length).toEqual 1 + + describe "with an id", -> + rule = null + + beforeEach inject ($httpBackend) -> + rule = scope.tagGroups[0].rules[0] + spyOn(window, "confirm").andReturn(true) + $httpBackend.expectDELETE('/admin/enterprises/45/tag_rules/1.json').respond(status: 204) + scope.deleteTagRule(scope.tagGroups[0], rule) + $httpBackend.flush() + + it "removes the specified rule from the rules list", -> + expect(scope.tagGroups[0].rules.length).toEqual 1 + expect(scope.tagGroups[1].rules.length).toEqual 1 + expect(scope.tagGroups[0].rules.indexOf(rule)).toEqual -1 + + it "updates tagGroup start indices", -> + expect(scope.tagGroups[0].startIndex).toEqual 0 + expect(scope.tagGroups[1].startIndex).toEqual 1 + + describe "without an id", -> + rule = null + + beforeEach inject ($httpBackend) -> + rule = scope.tagGroups[0].rules[0] + rule.id = null + scope.deleteTagRule(scope.tagGroups[0], rule) + + it "removes the specified rule from the rules list", -> + expect(scope.tagGroups[0].rules.length).toEqual 1 + expect(scope.tagGroups[1].rules.length).toEqual 1 + expect(scope.tagGroups[0].rules.indexOf(rule)).toEqual -1 + + it "updates tagGroup start indices", -> + expect(scope.tagGroups[0].startIndex).toEqual 0 + expect(scope.tagGroups[1].startIndex).toEqual 1 diff --git a/spec/jobs/heartbeat_job_spec.rb b/spec/jobs/heartbeat_job_spec.rb new file mode 100644 index 0000000000..3bafecd572 --- /dev/null +++ b/spec/jobs/heartbeat_job_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe HeartbeatJob do + context "with time frozen" do + let(:run_time) { Time.zone.local(2016, 4, 13, 13, 0, 0) } + + before { Spree::Config.last_job_queue_heartbeat_at = nil } + + around do |example| + Timecop.freeze(run_time) { example.run } + end + + it "updates the last_job_queue_heartbeat_at config var" do + run_job + Time.parse(Spree::Config.last_job_queue_heartbeat_at).should == run_time + end + end + + + private + + def run_job + clear_jobs + Delayed::Job.enqueue HeartbeatJob.new + flush_jobs ignore_exceptions: false + end +end diff --git a/spec/lib/open_food_network/order_cycle_management_report_spec.rb b/spec/lib/open_food_network/order_cycle_management_report_spec.rb index e0e7cb40bf..99ebf575af 100644 --- a/spec/lib/open_food_network/order_cycle_management_report_spec.rb +++ b/spec/lib/open_food_network/order_cycle_management_report_spec.rb @@ -86,19 +86,23 @@ module OpenFoodNetwork it "filters to a payment method" do pm2 = create(:payment_method, name: "PM2") - order2 = create(:order) - payment2 = create(:payment, order: order2, payment_method: pm2) + pm3 = create(:payment_method, name: "PM3") + order2 = create(:order, payments: [create(:payment, payment_method: pm2)]) + order3 = create(:order, payments: [create(:payment, payment_method: pm3)]) + # payment2 = create(:payment, order: order2, payment_method: pm2) - subject.stub(:params).and_return(payment_method_name: pm1.name) - subject.filter(orders).should == [order1] + subject.stub(:params).and_return(payment_method_in: [pm1.id, pm3.id] ) + subject.filter(orders).should match_array [order1, order3] end it "filters to a shipping method" do sm2 = create(:shipping_method, name: "ship2") + sm3 = create(:shipping_method, name: "ship3") order2 = create(:order, shipping_method: sm2) + order3 = create(:order, shipping_method: sm3) - subject.stub(:params).and_return(shipping_method_name: sm1.name) - subject.filter(orders).should == [order1] + subject.stub(:params).and_return(shipping_method_in: [sm1.id, sm3.id]) + expect(subject.filter(orders)).to match_array [order1, order3] end it "should do all the filters at once" do diff --git a/spec/lib/open_food_network/user_balance_calculator_spec.rb b/spec/lib/open_food_network/user_balance_calculator_spec.rb index 9588439f5e..7ff095d83a 100644 --- a/spec/lib/open_food_network/user_balance_calculator_spec.rb +++ b/spec/lib/open_food_network/user_balance_calculator_spec.rb @@ -9,34 +9,56 @@ module OpenFoodNetwork let!(:user1) { create(:user) } let!(:hub1) { create(:distributor_enterprise) } - let!(:o1) { create(:order_with_totals_and_distribution, user: user1, distributor: hub1) } #total=10 - let!(:o2) { create(:order_with_totals_and_distribution, user: user1, distributor: hub1) } #total=10 - let!(:p1) { create(:payment, order: o1, amount: 15.00) } - let!(:p2) { create(:payment, order: o2, amount: 10.00) } + let!(:o1) { create(:order_with_totals_and_distribution, + user: user1, distributor: hub1, + completed_at: 1.day.ago) } #total=10 + let!(:o2) { create(:order_with_totals_and_distribution, + user: user1, distributor: hub1, + completed_at: 1.day.ago) } #total=10 + let!(:p1) { create(:payment, order: o1, amount: 15.00, + state: "completed") } + let!(:p2) { create(:payment, order: o2, amount: 2.00, + state: "completed") } - it "finds the user balance for this enterprise" do - UserBalanceCalculator.new(user1, hub1).balance.should == 5 + it "finds the correct balance for this email and enterprise" do + UserBalanceCalculator.new(o1.email, hub1).balance.should == -3 end context "with another hub" do let!(:hub2) { create(:distributor_enterprise) } let!(:o3) { create(:order_with_totals_and_distribution, - user: user1, distributor: hub2) } #total=10 - let!(:p3) { create(:payment, order: o3, amount: 10.00) } + user: user1, distributor: hub2, + completed_at: 1.day.ago) } #total=10 + let!(:p3) { create(:payment, order: o3, amount: 15.00, + state: "completed") } it "does not find the balance for other enterprises" do - UserBalanceCalculator.new(user1, hub2).balance.should == 0 + UserBalanceCalculator.new(o3.email, hub2).balance.should == 5 end end context "with another user" do let!(:user2) { create(:user) } let!(:o4) { create(:order_with_totals_and_distribution, - user: user2, distributor: hub1) } #total=10 - let!(:p3) { create(:payment, order: o4, amount: 20.00) } + user: user2, distributor: hub1, + completed_at: 1.day.ago) } #total=10 + let!(:p3) { create(:payment, order: o4, amount: 20.00, + state: "completed") } it "does not find the balance for other users" do - UserBalanceCalculator.new(user2, hub1).balance.should == 10 + UserBalanceCalculator.new(o4.email, hub1).balance.should == 10 + end + end + + context "with canceled orders" do + let!(:o4) { create(:order_with_totals_and_distribution, + user: user1, distributor: hub1, + completed_at: 1.day.ago, state: "canceled") } #total=10 + let!(:p4) { create(:payment, order: o4, amount: 20.00, + state: "completed") } + + it "does not include canceled orders in the balance" do + UserBalanceCalculator.new(o4.email, hub1).balance.should == -3 end end end diff --git a/spec/models/content_configuration_spec.rb b/spec/models/content_configuration_spec.rb new file mode 100644 index 0000000000..6b7bc8a4e0 --- /dev/null +++ b/spec/models/content_configuration_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe ContentConfiguration do + describe "default logos and home_hero" do + it "sets a default url with existing image" do + expect(image_exist?(ContentConfig.logo.options[:default_url])).to be_true + expect(image_exist?(ContentConfig.logo_mobile_svg.options[:default_url])).to be_true + expect(image_exist?(ContentConfig.home_hero.options[:default_url])).to be_true + expect(image_exist?(ContentConfig.footer_logo.options[:default_url])).to be_true + end + + def image_exist?(default_url) + image_path = default_url.gsub(/\/assets\//,'/assets/images/') + File.exist?(File.join(Rails.root, 'app', image_path)) + end + end +end diff --git a/spec/models/customer_spec.rb b/spec/models/customer_spec.rb index 0e43ce9df5..f2ac14e9b2 100644 --- a/spec/models/customer_spec.rb +++ b/spec/models/customer_spec.rb @@ -6,15 +6,25 @@ describe Customer, type: :model do let!(:user2) { create(:user) } let!(:enterprise) { create(:distributor_enterprise) } + it "associates no user using non-existing email" do + c = Customer.create(enterprise: enterprise, email: 'some-email-not-associated-with-a-user@email.com') + expect(c.user).to be_nil + end + it "associates an existing user using email" do - c1 = Customer.create(enterprise: enterprise, email: 'some-email-not-associated-with-a-user@email.com') - expect(c1.user).to be_nil + non_existing_email = 'some-email-not-associated-with-a-user@email.com' + c1 = Customer.create(enterprise: enterprise, email: non_existing_email, user: user1) + expect(c1.user).to eq user1 + expect(c1.email).to eq non_existing_email + expect(c1.email).to_not eq user1.email - c2 = Customer.create(enterprise: enterprise, email: 'some-email-not-associated-with-a-user@email.com', user: user1) - expect(c2.user).to eq user1 + c2 = Customer.create(enterprise: enterprise, email: user2.email) + expect(c2.user).to eq user2 + end - c3 = Customer.create(enterprise: enterprise, email: user2.email) - expect(c3.user).to eq user2 + it "associates an existing user using email case-insensitive" do + c = Customer.create(enterprise: enterprise, email: user2.email.upcase) + expect(c.user).to eq user2 end end end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 4d681e01cc..ab86be1580 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -108,44 +108,76 @@ describe Spree::Order do subject.update_distribution_charge! end - describe "looking up whether a line item can be provided by an order cycle" do - it "returns true when the variant is provided" do - v = double(:variant) - line_item = double(:line_item, variant: v) - order_cycle = double(:order_cycle, variants: [v]) - subject.stub(:order_cycle) { order_cycle } + context "appying tag rules" do + let(:enterprise) { create(:distributor_enterprise) } + let(:customer) { create(:customer, enterprise: enterprise, tag_list: "tagtagtag") } + let(:tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "tagtagtag") } + let(:order) { create(:order_with_totals_and_distribution, distributor: enterprise, customer: customer) } - subject.send(:provided_by_order_cycle?, line_item).should be_true + before do + tag_rule.calculator.update_attribute(:preferred_flat_percent, -10) end - it "returns false otherwise" do - v = double(:variant) - line_item = double(:line_item, variant: v) - order_cycle = double(:order_cycle, variants: []) - subject.stub(:order_cycle) { order_cycle } - - subject.send(:provided_by_order_cycle?, line_item).should be_false + context "when the rule applies" do + it "applies the rule" do + order.update_distribution_charge! + order.reload + discount = order.adjustments.find_by_label("Discount") + expect(discount).to be_a Spree::Adjustment + expect(discount.amount).to eq (order.item_total / -10).round(2) + end end - it "returns false when there is no order cycle" do - v = double(:variant) - line_item = double(:line_item, variant: v) - subject.stub(:order_cycle) { nil } + context "when the rule does not apply" do + before { tag_rule.update_attribute(:preferred_customer_tags, "tagtag") } - subject.send(:provided_by_order_cycle?, line_item).should be_false + it "does not apply the rule" do + order.update_distribution_charge! + order.reload + discount = order.adjustments.find_by_label("Discount") + expect(discount).to be_nil + end end end + end - it "looks up product distribution enterprise fees for a line item" do - product = double(:product) - variant = double(:variant, product: product) - line_item = double(:line_item, variant: variant) + describe "looking up whether a line item can be provided by an order cycle" do + it "returns true when the variant is provided" do + v = double(:variant) + line_item = double(:line_item, variant: v) + order_cycle = double(:order_cycle, variants: [v]) + subject.stub(:order_cycle) { order_cycle } - product_distribution = double(:product_distribution) - product.should_receive(:product_distribution_for).with(subject.distributor) { product_distribution } - - subject.send(:product_distribution_for, line_item).should == product_distribution + subject.send(:provided_by_order_cycle?, line_item).should be_true end + + it "returns false otherwise" do + v = double(:variant) + line_item = double(:line_item, variant: v) + order_cycle = double(:order_cycle, variants: []) + subject.stub(:order_cycle) { order_cycle } + + subject.send(:provided_by_order_cycle?, line_item).should be_false + end + + it "returns false when there is no order cycle" do + v = double(:variant) + line_item = double(:line_item, variant: v) + subject.stub(:order_cycle) { nil } + + subject.send(:provided_by_order_cycle?, line_item).should be_false + end + end + + it "looks up product distribution enterprise fees for a line item" do + product = double(:product) + variant = double(:variant, product: product) + line_item = double(:line_item, variant: variant) + + product_distribution = double(:product_distribution) + product.should_receive(:product_distribution_for).with(subject.distributor) { product_distribution } + + subject.send(:product_distribution_for, line_item).should == product_distribution end describe "getting the admin and handling charge" do @@ -457,33 +489,6 @@ describe Spree::Order do Spree::Order.not_state(:canceled).should_not include o end end - - describe "with payment method names" do - let!(:o1) { create(:order) } - let!(:o2) { create(:order) } - let!(:pm1) { create(:payment_method, name: 'foo') } - let!(:pm2) { create(:payment_method, name: 'bar') } - let!(:p1) { create(:payment, order: o1, payment_method: pm1) } - let!(:p2) { create(:payment, order: o2, payment_method: pm2) } - - it "returns the order with payment method name when one specified" do - Spree::Order.with_payment_method_name('foo').should == [o1] - end - - it "returns the orders with payment method name when many specified" do - Spree::Order.with_payment_method_name(['foo', 'bar']).should include o1, o2 - end - - it "doesn't return rows with a different payment method name" do - Spree::Order.with_payment_method_name('foobar').should_not include o1 - Spree::Order.with_payment_method_name('foobar').should_not include o2 - end - - it "doesn't return duplicate rows" do - p2 = FactoryGirl.create(:payment, order: o1, payment_method: pm1) - Spree::Order.with_payment_method_name('foo').length.should == 1 - end - end end describe "shipping address prepopulation" do @@ -559,39 +564,76 @@ describe Spree::Order do end describe "associating a customer" do - let(:user) { create(:user) } let(:distributor) { create(:distributor_enterprise) } + let!(:order) { create(:order, distributor: distributor) } - context "when a user has been set on the order" do - let!(:order) { create(:order, distributor: distributor, user: user) } - context "and a customer for order.distributor and order.user.email already exists" do - let!(:customer) { create(:customer, enterprise: distributor, email: user.email) } - it "associates the order with the existing customer" do - order.send(:associate_customer) + context "when an email address is available for the order" do + before { allow(order).to receive(:email_for_customer) { "existing@email.com" }} + + context "and a customer for order.distributor and order#email_for_customer already exists" do + let!(:customer) { create(:customer, enterprise: distributor, email: "existing@email.com" ) } + + it "associates the order with the existing customer, and returns the customer" do + result = order.send(:associate_customer) expect(order.customer).to eq customer + expect(result).to eq customer end end + context "and a customer for order.distributor and order.user.email does not alread exist" do let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') } - it "creates a new customer" do - expect{order.send(:associate_customer)}.to change{Customer.count}.by 1 + + it "does not set the customer and returns nil" do + result = order.send(:associate_customer) + expect(order.customer).to be_nil + expect(result).to be_nil end end end - context "when a user has not been set on the order" do - let!(:order) { create(:order, distributor: distributor, user: nil) } - context "and a customer for order.distributor and order.email already exists" do - let!(:customer) { create(:customer, enterprise: distributor, email: order.email) } - it "creates a new customer" do - order.send(:associate_customer) + context "when an email address is not available for the order" do + let!(:customer) { create(:customer, enterprise: distributor) } + before { allow(order).to receive(:email_for_customer) { nil }} + + it "does not set the customer and returns nil" do + result = order.send(:associate_customer) + expect(order.customer).to be_nil + expect(result).to be_nil + end + end + end + + describe "ensuring a customer is linked" do + let(:distributor) { create(:distributor_enterprise) } + let!(:order) { create(:order, distributor: distributor) } + + context "when a customer has already been linked to the order" do + let!(:customer) { create(:customer, enterprise: distributor, email: "existing@email.com" ) } + before { order.update_attribute(:customer_id, customer.id) } + + it "does nothing" do + order.send(:ensure_customer) + expect(order.customer).to eq customer + end + end + + context "when a customer not been linked to the order" do + context "but one matching order#email_for_customer already exists" do + let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') } + before { allow(order).to receive(:email_for_customer) { 'some-other-email@email.com' } } + + it "links the customer customer to the order" do + expect(order.customer).to be_nil + expect{order.send(:ensure_customer)}.to_not change{Customer.count} expect(order.customer).to eq customer end end - context "and a customer for order.distributor and order.email does not alread exist" do - let!(:customer) { create(:customer, enterprise: distributor, email: 'some-other-email@email.com') } + + context "and order#email_for_customer does not match any existing customers" do it "creates a new customer" do - expect{order.send(:associate_customer)}.to change{Customer.count}.by 1 + expect(order.customer).to be_nil + expect{order.send(:ensure_customer)}.to change{Customer.count}.by 1 + expect(order.customer).to be_a Customer end end end diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index 9911c16a51..3e572ecfb1 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -17,11 +17,11 @@ describe Spree.user_class do it "enforces the limit on the number of enterprise owned" do expect(u2.owned_enterprises(:reload)).to eq [] u2.owned_enterprises << e1 - expect(u2.save!).to_not raise_error - expect { + expect { u2.save! }.to_not raise_error + expect do u2.owned_enterprises << e2 u2.save! - }.to raise_error ActiveRecord::RecordInvalid, "Validation failed: #{u2.email} is not permitted to own any more enterprises (limit is 1)." + end.to raise_error ActiveRecord::RecordInvalid, "Validation failed: #{u2.email} is not permitted to own any more enterprises (limit is 1)." end end @@ -53,6 +53,23 @@ describe Spree.user_class do create(:user) end.to enqueue_job ConfirmSignupJob end + + it "should not create a customer" do + expect do + create(:user) + end.to change(Customer, :count).by(0) + end + + describe "when a customer record exists" do + let!(:customer) { create(:customer, user: nil) } + + it "should not create a customer" do + expect(customer.user).to be nil + user = create(:user, email: customer.email) + customer.reload + expect(customer.user).to eq user + end + end end describe "known_users" do @@ -65,9 +82,9 @@ describe Spree.user_class do it "returns a list of users which manage shared enterprises" do expect(u1.known_users).to include u1, u2 expect(u1.known_users).to_not include u3 - expect(u2.known_users).to include u1,u2 + expect(u2.known_users).to include u1, u2 expect(u2.known_users).to_not include u3 - expect(u3.known_users).to_not include u1,u2,u3 + expect(u3.known_users).to_not include u1, u2, u3 end end @@ -85,14 +102,14 @@ describe Spree.user_class do let!(:u2) { create(:user) } let!(:distributor1) { create(:distributor_enterprise) } let!(:distributor2) { create(:distributor_enterprise) } - let!(:d1o1) { create(:completed_order_with_totals, distributor: distributor1, user_id: u1.id)} - let!(:d1o2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u1.id)} - let!(:d1_order_for_u2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u2.id)} - let!(:d1o3) { create(:order, state: 'cart', distributor: distributor1, user_id: u1.id)} - let!(:d2o1) { create(:completed_order_with_totals, distributor: distributor2, user_id: u2.id)} + let!(:d1o1) { create(:completed_order_with_totals, distributor: distributor1, user_id: u1.id) } + let!(:d1o2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u1.id) } + let!(:d1_order_for_u2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u2.id) } + let!(:d1o3) { create(:order, state: 'cart', distributor: distributor1, user_id: u1.id) } + let!(:d2o1) { create(:completed_order_with_totals, distributor: distributor2, user_id: u2.id) } - let!(:completed_payment) { create(:payment, order: d1o1, state: 'completed')} - let!(:payment) { create(:payment, order: d1o2, state: 'checkout')} + let!(:completed_payment) { create(:payment, order: d1o1, state: 'completed') } + let!(:payment) { create(:payment, order: d1o2, state: 'checkout') } it "returns enterprises that the user has ordered from" do expect(u1.enterprises_ordered_from).to eq [distributor1.id] diff --git a/spec/models/tag_rule/discount_order_spec.rb b/spec/models/tag_rule/discount_order_spec.rb new file mode 100644 index 0000000000..dab901dfbf --- /dev/null +++ b/spec/models/tag_rule/discount_order_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe TagRule::DiscountOrder, type: :model do + let!(:tag_rule) { create(:tag_rule) } + + describe "determining relevance based on additional requirements" do + let(:subject) { double(:subject) } + + before do + tag_rule.set_context(subject,{}) + allow(tag_rule).to receive(:customer_tags_match?) { true } + allow(subject).to receive(:class) { Spree::Order } + end + + context "when already_applied? returns false" do + before { expect(tag_rule).to receive(:already_applied?) { false } } + + it "returns true" do + expect(tag_rule.send(:relevant?)).to be true + end + end + + context "when already_applied? returns true" do + before { expect(tag_rule).to receive(:already_applied?) { true } } + + it "returns false immediately" do + expect(tag_rule.send(:relevant?)).to be false + end + end + end + + describe "determining whether a the rule has already been applied to an order" do + let!(:order) { create(:order) } + let!(:adjustment) { order.adjustments.create({:amount => 12.34, :source => order, :originator => tag_rule, :label => 'discount' }, :without_protection => true) } + + before do + tag_rule.set_context(order, nil) + end + + context "where adjustments originating from the rule already exist" do + it { expect(tag_rule.send(:already_applied?)).to be true} + end + + context "where existing adjustments originate from other rules" do + before { adjustment.update_attribute(:originator_id,create(:tag_rule).id) } + it { expect(tag_rule.send(:already_applied?)).to be false} + end + end + + describe "applying the rule" do + # Assume that all validation is done by the TagRule base class + + let!(:line_item) { create(:line_item, price: 100.00) } + let!(:order) { line_item.order } + + before do + order.update_distribution_charge! + tag_rule.calculator.update_attribute(:preferred_flat_percent, -10.00) + tag_rule.set_context(order, nil) + end + + context "in a simple scenario" do + let(:adjustment) { order.reload.adjustments.where(originator_id: tag_rule, originator_type: "TagRule").first } + + it "creates a new adjustment on the order" do + tag_rule.send(:apply!) + expect(adjustment).to be_a Spree::Adjustment + expect(adjustment.amount).to eq -10.00 + expect(adjustment.label).to eq "Discount" + expect(order.adjustment_total).to eq -10.00 + expect(order.total).to eq 90.00 + end + end + + context "when shipping charges apply" do + let!(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatRate.new( preferred_amount: 25.00 ) ) } + before do + shipping_method.create_adjustment("Shipping", order, order, true) + end + + let(:adjustment) { order.reload.adjustments.where(originator_id: tag_rule, originator_type: "TagRule").first } + + it "the adjustment is made on line item total, ie. ignores the shipping amount" do + tag_rule.send(:apply!) + expect(adjustment).to be_a Spree::Adjustment + expect(adjustment.amount).to eq -10.00 + expect(adjustment.label).to eq "Discount" + expect(order.adjustment_total).to eq 15.00 + expect(order.total).to eq 115.00 + end + end + end +end diff --git a/spec/models/tag_rule/filter_shipping_methods_spec.rb b/spec/models/tag_rule/filter_shipping_methods_spec.rb new file mode 100644 index 0000000000..539aa3c6ca --- /dev/null +++ b/spec/models/tag_rule/filter_shipping_methods_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe TagRule::DiscountOrder, type: :model do + let!(:tag_rule) { create(:filter_shipping_methods_tag_rule) } + + describe "determining whether tags match for a given shipping method" do + context "when the shipping method is nil" do + + it "returns false" do + expect(tag_rule.send(:tags_match?, nil)).to be false + end + end + + context "when the shipping method is not nil" do + let(:shipping_method) { create(:shipping_method, tag_list: ["member","local","volunteer"]) } + + context "when the rule has no preferred shipping method tags specified" do + before { allow(tag_rule).to receive(:preferred_shipping_method_tags) { "" } } + it { expect(tag_rule.send(:tags_match?, shipping_method)).to be false } + end + + context "when the rule has preferred customer tags specified that match ANY of the customer tags" do + before { allow(tag_rule).to receive(:preferred_shipping_method_tags) { "wholesale,some_tag,member" } } + it { expect(tag_rule.send(:tags_match?, shipping_method)).to be true } + end + + context "when the rule has preferred customer tags specified that match NONE of the customer tags" do + before { allow(tag_rule).to receive(:preferred_shipping_method_tags) { "wholesale,some_tag,some_other_tag" } } + it { expect(tag_rule.send(:tags_match?, shipping_method)).to be false } + end + end + end + + describe "applying the rule" do + # Assume that all validation is done by the TagRule base class + + let(:sm1) { create(:shipping_method, tag_list: ["tag1", "something", "somethingelse"]) } + let(:sm2) { create(:shipping_method, tag_list: ["tag2"]) } + let(:sm3) { create(:shipping_method, tag_list: ["tag3"]) } + let!(:shipping_methods) { [sm1, sm2, sm3] } + + before do + tag_rule.update_attribute(:preferred_shipping_method_tags, "tag2") + tag_rule.set_context(shipping_methods, nil) + end + + context "apply!" do + context "when showing matching shipping methods" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "visible") } + it "does nothing" do + tag_rule.send(:apply!) + expect(shipping_methods).to eq [sm1, sm2, sm3] + end + end + + context "when hiding matching shipping methods" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "hidden") } + it "removes matching shipping methods from the list" do + tag_rule.send(:apply!) + expect(shipping_methods).to eq [sm1, sm3] + end + end + end + + context "apply_default!" do + context "when showing matching shipping methods" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "visible") } + it "remove matching shipping methods from the list" do + tag_rule.send(:apply_default!) + expect(shipping_methods).to eq [sm1, sm3] + end + end + + context "when hiding matching shipping methods" do + before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "hidden") } + it "does nothing" do + tag_rule.send(:apply_default!) + expect(shipping_methods).to eq [sm1, sm2, sm3] + end + end + end + end +end diff --git a/spec/models/tag_rule_spec.rb b/spec/models/tag_rule_spec.rb new file mode 100644 index 0000000000..549c2f88aa --- /dev/null +++ b/spec/models/tag_rule_spec.rb @@ -0,0 +1,201 @@ +require 'spec_helper' + +describe TagRule, type: :model do + let!(:tag_rule) { create(:tag_rule) } + + describe "validations" do + it "requires a enterprise" do + expect(tag_rule).to validate_presence_of :enterprise + end + end + + describe 'setting the context' do + let(:subject) { double(:subject) } + let(:context) { double(:context) } + it "stores the subject and context provided as instance variables on the model" do + tag_rule.set_context(subject, context) + expect(tag_rule.subject).to eq subject + expect(tag_rule.context).to eq context + expect(tag_rule.instance_variable_get(:@subject)).to eq subject + expect(tag_rule.instance_variable_get(:@context)).to eq context + end + end + + describe "determining relevance based on subject and context" do + context "when the subject is nil" do + it "returns false" do + expect(tag_rule.send(:relevant?)).to be false + end + end + + context "when the subject is not nil" do + let(:subject) { double(:subject) } + + before do + tag_rule.set_context(subject,{}) + allow(tag_rule).to receive(:customer_tags_match?) { :customer_tags_match_result } + allow(tag_rule).to receive(:subject_class) { Spree::Order} + end + + + context "when the subject class matches tag_rule#subject_class" do + before do + allow(subject).to receive(:class) { Spree::Order } + end + + context "when the rule does not repond to #additional_requirements_met?" do + before { allow(tag_rule).to receive(:respond_to?).with(:additional_requirements_met?, true) { false } } + + it "returns true" do + expect(tag_rule.send(:relevant?)).to be true + end + end + + context "when the rule reponds to #additional_requirements_met?" do + before { allow(tag_rule).to receive(:respond_to?).with(:additional_requirements_met?, true) { true } } + + context "and #additional_requirements_met? returns a truthy value" do + before { allow(tag_rule).to receive(:additional_requirements_met?) { "smeg" } } + + it "returns true immediately" do + expect(tag_rule.send(:relevant?)).to be true + end + end + + context "and #additional_requirements_met? returns true" do + before { allow(tag_rule).to receive(:additional_requirements_met?) { true } } + + it "returns true immediately" do + expect(tag_rule.send(:relevant?)).to be true + end + end + + context "and #additional_requirements_met? returns false" do + before { allow(tag_rule).to receive(:additional_requirements_met?) { false } } + + it "returns false immediately" do + expect(tag_rule.send(:relevant?)).to be false + end + end + end + end + + context "when the subject class does not match tag_rule#subject_class" do + before do + allow(subject).to receive(:class) { Spree::LineItem } + end + + it "returns false immediately" do + expect(tag_rule.send(:relevant?)).to be false + expect(tag_rule).to_not have_received :customer_tags_match? + end + end + end + + describe "determining whether specified customer tags match the given context" do + context "when the context is nil" do + before { tag_rule.set_context(nil, nil) } + it "returns false" do + expect(tag_rule.send(:customer_tags_match?)).to be false + end + end + + context "when the context has no customer specified" do + let(:context) { { something_that_is_not_a_customer: double(:something) } } + + before { tag_rule.set_context(nil, context) } + + it "returns false" do + expect(tag_rule.send(:customer_tags_match?)).to be false + end + end + + context "when the context has a customer specified" do + let(:context) { { customer: double(:customer, tag_list: ["member","local","volunteer"] ) } } + + before { tag_rule.set_context(nil, context) } + + context "when the rule has no preferred customer tags specified" do + before do + allow(tag_rule).to receive(:preferred_customer_tags) { "" } + end + + it "returns false" do + expect(tag_rule.send(:customer_tags_match?)).to be false + end + end + + context "when the rule has preferred customer tags specified that match ANY of the customer tags" do + before do + allow(tag_rule).to receive(:preferred_customer_tags) { "wholesale,some_tag,member" } + end + + it "returns false" do + expect(tag_rule.send(:customer_tags_match?)).to be true + end + end + + context "when the rule has preferred customer tags specified that match NONE of the customer tags" do + before do + allow(tag_rule).to receive(:preferred_customer_tags) { "wholesale,some_tag,some_other_tag" } + end + + it "returns false" do + expect(tag_rule.send(:customer_tags_match?)).to be false + end + end + end + end + + describe "applying a tag rule to a subject" do + before { allow(tag_rule).to receive(:apply!) } + + context "when the rule is deemed to be relevant" do + before { allow(tag_rule).to receive(:relevant?) { true } } + + context "and customer_tags_match? returns true" do + before { expect(tag_rule).to receive(:customer_tags_match?) { true } } + + it "applies the rule" do + tag_rule.apply + expect(tag_rule).to have_received(:apply!) + end + end + + context "when customer_tags_match? returns false" do + before { expect(tag_rule).to receive(:customer_tags_match?) { false } } + before { allow(tag_rule).to receive(:apply_default!) } + + context "and the rule responds to #apply_default!" do + before { allow(tag_rule).to receive(:respond_to?).with(:apply_default!, true) { true } } + + it "applies the default action" do + tag_rule.apply + expect(tag_rule).to_not have_received(:apply!) + expect(tag_rule).to have_received(:apply_default!) + end + end + + context "and the rule does not respond to #apply_default!" do + before { allow(tag_rule).to receive(:respond_to?).with(:apply_default!, true) { false } } + + it "does not apply the rule or the default action" do + tag_rule.apply + expect(tag_rule).to_not have_received(:apply!) + expect(tag_rule).to_not have_received(:apply_default!) + end + end + end + end + + context "when the rule is deemed not to be relevant" do + before { allow(tag_rule).to receive(:relevant?) { false } } + + it "does not apply the rule" do + tag_rule.apply + expect(tag_rule).to_not have_received(:apply!) + end + end + end + end +end