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 d2e6d58562..bfccfd3f4b 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, Columns, pendingChanges, shops) -> +angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, TagsResource, $q, Columns, pendingChanges, shops) -> $scope.shop = {} $scope.shops = shops $scope.submitAll = pendingChanges.submitAll @@ -12,6 +12,16 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerR if $scope.shop.id? $scope.customers = index {enterprise_id: $scope.shop.id} + $scope.findTags = (query) -> + defer = $q.defer() + params = + enterprise_id: $scope.shop.id + TagsResource.index params, (data) => + filtered = data.filter (tag) -> + tag.text.toLowerCase().indexOf(query.toLowerCase()) != -1 + defer.resolve filtered + defer.promise + $scope.add = (email) -> params = enterprise_id: $scope.shop.id diff --git a/app/assets/javascripts/admin/customers/services/tags_resource.js.coffee b/app/assets/javascripts/admin/customers/services/tags_resource.js.coffee new file mode 100644 index 0000000000..ad89511e32 --- /dev/null +++ b/app/assets/javascripts/admin/customers/services/tags_resource.js.coffee @@ -0,0 +1,9 @@ +angular.module("admin.customers").factory 'TagsResource', ($resource) -> + $resource('/admin/tags.json', {}, { + 'index': + method: 'GET' + isArray: true + cache: true + params: + enterprise_id: '@enterprise_id' + }) diff --git a/app/assets/javascripts/admin/directives/track_master.js.coffee b/app/assets/javascripts/admin/directives/track_master.js.coffee index ce26a4aa32..be84e0b204 100644 --- a/app/assets/javascripts/admin/directives/track_master.js.coffee +++ b/app/assets/javascripts/admin/directives/track_master.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").directive "ofnTrackMaster", ["DirtyProducts", (DirtyProducts) -> +angular.module("ofn.admin").directive "ofnTrackMaster", (DirtyProducts) -> require: "ngModel" link: (scope, element, attrs, ngModel) -> ngModel.$parsers.push (viewValue) -> @@ -6,4 +6,3 @@ angular.module("ofn.admin").directive "ofnTrackMaster", ["DirtyProducts", (Dirty DirtyProducts.addMasterProperty scope.product.id, scope.product.master.id, attrs.ofnTrackMaster, viewValue scope.displayDirtyProducts() viewValue - ] \ No newline at end of file diff --git a/app/assets/javascripts/admin/filters/translate.js.coffee b/app/assets/javascripts/admin/filters/translate.js.coffee deleted file mode 100644 index 20becc147a..0000000000 --- a/app/assets/javascripts/admin/filters/translate.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('ofn.admin').filter "translate", -> - (key, options) -> - t(key, options) - -angular.module('ofn.admin').filter "t", -> - (key, options) -> - t(key, options) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee index fd426eb455..227c6a3045 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee @@ -1,5 +1,5 @@ angular.module('admin.orderCycles') - .controller 'AdminEditOrderCycleCtrl', ($scope, $filter, $location, OrderCycle, Enterprise, EnterpriseFee, StatusMessage) -> + .controller 'AdminEditOrderCycleCtrl', ($scope, $filter, $location, $window, OrderCycle, Enterprise, EnterpriseFee, StatusMessage) -> order_cycle_id = $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1] $scope.enterprises = Enterprise.index(order_cycle_id: order_cycle_id) $scope.supplier_enterprises = Enterprise.producer_enterprises @@ -12,6 +12,9 @@ angular.module('admin.orderCycles') $scope.StatusMessage = StatusMessage + $scope.$watch 'order_cycle_form.$dirty', (newValue) -> + StatusMessage.display 'notice', 'You have unsaved changes' if newValue + $scope.loaded = -> Enterprise.loaded && EnterpriseFee.loaded && OrderCycle.loaded @@ -60,6 +63,7 @@ angular.module('admin.orderCycles') $scope.removeExchange = ($event, exchange) -> $event.preventDefault() OrderCycle.removeExchange(exchange) + $scope.order_cycle_form.$dirty = true $scope.addCoordinatorFee = ($event) -> $event.preventDefault() @@ -81,4 +85,9 @@ angular.module('admin.orderCycles') OrderCycle.removeDistributionOfVariant(variant_id) $scope.submit = (destination) -> + StatusMessage.display 'progress', "Saving..." OrderCycle.update(destination) + $scope.order_cycle_form.$setPristine() + + $scope.cancel = (destination) -> + $window.location = destination diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee index bf00bb3df1..99c2e2649c 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee @@ -9,6 +9,9 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl", $scope.order_cycle = OrderCycle.load $scope.orderCycleId(), (order_cycle) => $scope.init() + $scope.$watch 'order_cycle_form.$dirty', (newValue) -> + StatusMessage.display 'notice', 'You have unsaved changes' if newValue + $scope.loaded = -> Enterprise.loaded && EnterpriseFee.loaded && OrderCycle.loaded @@ -35,5 +38,6 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl", OrderCycle.removeCoordinatorFee(index) $scope.submit = (destination) -> + StatusMessage.display 'progress', "Saving..." OrderCycle.mirrorIncomingToOutgoingProducts() OrderCycle.update(destination) 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 232eee7045..863163a9ef 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.shippingMethods", ["ngTagsInput", 'admin.utils']) +angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils', 'templates']) diff --git a/app/assets/javascripts/admin/utils/directives/save_bar.js.coffee b/app/assets/javascripts/admin/utils/directives/save_bar.js.coffee index 13e4f84bc6..0999679394 100644 --- a/app/assets/javascripts/admin/utils/directives/save_bar.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/save_bar.js.coffee @@ -1,8 +1,8 @@ angular.module("admin.utils").directive "saveBar", (StatusMessage) -> restrict: "E" scope: - save: "&" form: "=" + buttons: "=" templateUrl: "admin/save_bar.html" link: (scope, element, attrs) -> scope.StatusMessage = StatusMessage 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 index 6ce7953608..7a6ae9afff 100644 --- a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee @@ -1,10 +1,11 @@ angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) -> restrict: "E" - template: "" + templateUrl: "admin/tags_input.html" scope: object: "=" tagsAttr: "@?" tagListAttr: "@?" + findTags: "&" link: (scope, element, attrs) -> $timeout -> scope.tagsAttr ||= "tags" diff --git a/app/assets/javascripts/admin/utils/filters/translate.js.coffee b/app/assets/javascripts/admin/utils/filters/translate.js.coffee new file mode 100644 index 0000000000..1a0602a59d --- /dev/null +++ b/app/assets/javascripts/admin/utils/filters/translate.js.coffee @@ -0,0 +1,7 @@ +angular.module("admin.utils").filter "translate", -> + (key, options) -> + t(key, options) + +angular.module("admin.utils").filter "t", -> + (key, options) -> + t(key, options) diff --git a/app/assets/javascripts/darkswarm/directives/map_search.js.coffee b/app/assets/javascripts/darkswarm/directives/map_search.js.coffee index f67407ffb3..e67ce529a5 100644 --- a/app/assets/javascripts/darkswarm/directives/map_search.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/map_search.js.coffee @@ -7,6 +7,22 @@ Darkswarm.directive 'mapSearch', ($timeout)-> link: (scope, elem, attrs, ctrl)-> $timeout => map = ctrl.getMap() + + # Use OSM tiles server + map.mapTypes.set 'OSM', new (google.maps.ImageMapType)( + getTileUrl: (coord, zoom) -> + # "Wrap" x (logitude) at 180th meridian properly + # NB: Don't touch coord.x because coord param is by reference, and changing its x property breakes something in Google's lib + tilesPerGlobe = 1 << zoom + x = coord.x % tilesPerGlobe + if x < 0 + x = tilesPerGlobe + x + # Wrap y (latitude) in a like manner if you want to enable vertical infinite scroll + 'http://tile.openstreetmap.org/' + zoom + '/' + x + '/' + coord.y + '.png' + tileSize: new (google.maps.Size)(256, 256) + name: 'OpenStreetMap' + maxZoom: 18) + input = (document.getElementById("pac-input")) map.controls[google.maps.ControlPosition.TOP_LEFT].push input searchBox = new google.maps.places.SearchBox((input)) @@ -21,7 +37,7 @@ Darkswarm.directive 'mapSearch', ($timeout)-> #map.setCenter place.geometry.location map.fitBounds place.geometry.viewport #map.fitBounds bounds - + # Bias the SearchBox results towards places that are within the bounds of the # current map's viewport. google.maps.event.addListener map, "bounds_changed", -> diff --git a/app/assets/javascripts/darkswarm/directives/on_hand.js.coffee b/app/assets/javascripts/darkswarm/directives/on_hand.js.coffee new file mode 100644 index 0000000000..610b11d3ca --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/on_hand.js.coffee @@ -0,0 +1,19 @@ +Darkswarm.directive "ofnOnHand", -> + restrict: 'A' + require: "ngModel" + + link: (scope, elem, attr, ngModel) -> + # In cases where this field gets its value from the HTML element rather than the model, + # initialise the model with the HTML value. + if scope.$eval(attr.ngModel) == undefined + ngModel.$setViewValue elem.val() + + ngModel.$parsers.push (viewValue) -> + on_hand = parseInt(attr.ofnOnHand) + if parseInt(viewValue) > on_hand + alert t('insufficient_stock', {on_hand: on_hand}) + viewValue = on_hand + ngModel.$setViewValue viewValue + ngModel.$render() + + viewValue diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index f1c9421a15..9ee03e26b3 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)-> +Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $rootScope, storage)-> # Handles syncing of current cart/order state to server new class Cart dirty: false @@ -28,15 +28,39 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)-> update: => @update_running = true + $http.post('/orders/populate', @data()).success (data, status)=> @saved() @update_running = false + + @compareAndNotifyStockLevels data.stock_levels + @popQueue() if @update_enqueued .error (response, status)=> @scheduleRetry(status) @update_running = false + compareAndNotifyStockLevels: (stockLevels) => + scope = $rootScope.$new(true) + scope.variants = [] + + # TODO: These changes to quantity/max_quantity trigger another cart update, which + # is unnecessary. + + for li in @line_items_present() + if stockLevels[li.variant.id]? + li.variant.count_on_hand = stockLevels[li.variant.id].on_hand + if li.quantity > li.variant.count_on_hand + li.quantity = li.variant.count_on_hand + scope.variants.push li.variant + if li.variant.count_on_hand == 0 && li.max_quantity > li.variant.count_on_hand + li.max_quantity = li.variant.count_on_hand + scope.variants.push(li.variant) unless li.variant in scope.variants + + if scope.variants.length > 0 + $modal.open(templateUrl: "out_of_stock.html", scope: scope, windowClass: 'out-of-stock-modal') + popQueue: => @update_enqueued = false @scheduleUpdate() diff --git a/app/assets/javascripts/darkswarm/services/checkout.js.coffee b/app/assets/javascripts/darkswarm/services/checkout.js.coffee index 652ecd02f9..34cae22fcc 100644 --- a/app/assets/javascripts/darkswarm/services/checkout.js.coffee +++ b/app/assets/javascripts/darkswarm/services/checkout.js.coffee @@ -10,9 +10,12 @@ Darkswarm.factory 'Checkout', (CurrentOrder, ShippingMethods, PaymentMethods, $h $http.put('/checkout', {order: @preprocess()}).success (data, status)=> Navigation.go data.path .error (response, status)=> - Loading.clear() - @errors = response.errors - RailsFlashLoader.loadFlash(response.flash) + if response.path + Navigation.go response.path + else + Loading.clear() + @errors = response.errors + RailsFlashLoader.loadFlash(response.flash) # Rails wants our Spree::Address data to be provided with _attributes preprocess: -> diff --git a/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee b/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee index 9c5a375d8c..1979affeab 100644 --- a/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee +++ b/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee @@ -1,11 +1,14 @@ Darkswarm.factory "MapConfiguration", -> new class MapConfiguration options: - center: + center: latitude: -37.4713077 longitude: 144.7851531 zoom: 12 - additional_options: {} - #mapTypeId: 'satellite' + additional_options: + # mapTypeId: 'satellite' + mapTypeId: 'OSM' + mapTypeControl: false + streetViewControl: false styles: [{"featureType":"landscape","stylers":[{"saturation":-100},{"lightness":65},{"visibility":"on"}]},{"featureType":"poi","stylers":[{"saturation":-100},{"lightness":51},{"visibility":"simplified"}]},{"featureType":"road.highway","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"road.arterial","stylers":[{"saturation":-100},{"lightness":30},{"visibility":"on"}]},{"featureType":"road.local","stylers":[{"saturation":-100},{"lightness":40},{"visibility":"on"}]},{"featureType":"transit","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"administrative.province","stylers":[{"visibility":"off"}]},{"featureType":"water","elementType":"labels","stylers":[{"visibility":"on"},{"lightness":-25},{"saturation":-100}]},{"featureType":"water","elementType":"geometry","stylers":[{"hue":"#ffff00"},{"lightness":-25},{"saturation":-97}]},{"featureType":"road","elementType": "labels.icon","stylers":[{"visibility":"off"}]}] diff --git a/app/assets/javascripts/templates/admin/save_bar.html.haml b/app/assets/javascripts/templates/admin/save_bar.html.haml index 452e81f6e3..08b7cbdf3d 100644 --- a/app/assets/javascripts/templates/admin/save_bar.html.haml +++ b/app/assets/javascripts/templates/admin/save_bar.html.haml @@ -1,6 +1,7 @@ #save-bar.animate-show{ ng: { show: 'form.$dirty || StatusMessage.active()' } } - .twelve.columns.alpha - %h5#status-message{ ng: { style: 'StatusMessage.statusMessage.style' } } - {{ StatusMessage.statusMessage.text || " " }} - .four.columns.omega.text-right - %input.red{type: "button", value: "Save Changes", ng: { disabled: '!form.$dirty', click: "save()" } } + .container + .eight.columns.alpha + %h5#status-message{ ng: { style: 'StatusMessage.statusMessage.style' } } + {{ StatusMessage.statusMessage.text || " " }} + .eight.columns.omega.text-right + %input{"ng-repeat" => "button in buttons", type: "button", value: "{{button.text}}", ng: { class: "button.class", click: "button.action(button.param)" } } diff --git a/app/assets/javascripts/templates/admin/tag.html.haml b/app/assets/javascripts/templates/admin/tag.html.haml new file mode 100644 index 0000000000..c4afcfa5ad --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag.html.haml @@ -0,0 +1,8 @@ +.tag-template + %div + %span.tag-with-rules{ ng: { if: "data.rules" }, "ofn-with-tip" => "{{ 'admin.tag_has_rules' | t:{num: data.rules} }}" } + {{$getDisplayText()}} + %span{ ng: { if: "!data.rules" } } + {{$getDisplayText()}} + %a.remove-button{ ng: {click: "$removeTag()"} } + ✖ diff --git a/app/assets/javascripts/templates/admin/tag_autocomplete.html.haml b/app/assets/javascripts/templates/admin/tag_autocomplete.html.haml new file mode 100644 index 0000000000..49ff00ff27 --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_autocomplete.html.haml @@ -0,0 +1,11 @@ +.autocomplete-template + %span.tag-with-rules{ ng: { if: "data.rules" } } + {{$getDisplayText()}} + %span.tag-with-rules{ ng: { if: "data.rules == 1" } } + — + = t 'admin.has_one_rule' + %span.tag-with-rules{ ng: { if: "data.rules > 1" } } + — + = t 'admin.has_n_rules', { num: '{{data.rules}}' } + %span{ ng: { if: "!data.rules" } } + {{$getDisplayText()}} diff --git a/app/assets/javascripts/templates/admin/tags_input.html.haml b/app/assets/javascripts/templates/admin/tags_input.html.haml new file mode 100644 index 0000000000..2dd75ce5ac --- /dev/null +++ b/app/assets/javascripts/templates/admin/tags_input.html.haml @@ -0,0 +1,7 @@ +%tags-input{ template: 'admin/tag.html', ng: { model: 'object[tagsAttr]' } } + %auto-complete{source: "findTags({query: $query})", + template: "admin/tag_autocomplete.html", + "min-length" => "0", + "load-on-focus" => "true", + "load-on-empty" => "true", + "max-results-to-show" => "32"} diff --git a/app/assets/javascripts/templates/out_of_stock.html.haml b/app/assets/javascripts/templates/out_of_stock.html.haml new file mode 100644 index 0000000000..d894583fcf --- /dev/null +++ b/app/assets/javascripts/templates/out_of_stock.html.haml @@ -0,0 +1,13 @@ +%a.close-reveal-modal{"ng-click" => "$close()"} + %i.ofn-i_009-close + +%h3 Reduced stock available + +%p While you've been shopping, the stock levels for one or more of the products in your cart have reduced. Here's what's changed: + +%p{'ng-repeat' => "v in variants"} + %em {{ v.name_to_display }} - {{ v.unit_to_display }} + %span{'ng-if' => "v.count_on_hand == 0"} + is now out of stock. + %span{'ng-if' => "v.count_on_hand > 0"} + now only has {{ v.count_on_hand }} remaining. diff --git a/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml b/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml new file mode 100644 index 0000000000..1153d93b0f --- /dev/null +++ b/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml @@ -0,0 +1,12 @@ +.small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"} + + %input{type: :number, + integer: true, + value: nil, + min: 0, + placeholder: "0", + "ofn-disable-scroll" => true, + "ng-model" => "variant.line_item.quantity", + "ofn-on-hand" => "{{variant.on_demand && 9999 || variant.count_on_hand }}", + "ng-disabled" => "!variant.on_demand && variant.count_on_hand == 0", + name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} diff --git a/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml b/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml new file mode 100644 index 0000000000..6601f0c019 --- /dev/null +++ b/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml @@ -0,0 +1,23 @@ +.small-5.medium-3.large-3.columns.text-right{"bo-if" => "variant.product.group_buy"} + %span.bulk-input-container + %span.bulk-input + %input.bulk.first{type: :number, + value: nil, + integer: true, + min: 0, + "ng-model" => "variant.line_item.quantity", + placeholder: "{{'shop_variant_quantity_min' | t}}", + "ofn-disable-scroll" => true, + "ofn-on-hand" => "{{variant.on_demand && 9999 || variant.count_on_hand }}", + name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} + %span.bulk-input + %input.bulk.second{type: :number, + "ng-disabled" => "!variant.line_item.quantity", + integer: true, + min: 0, + "ng-model" => "variant.line_item.max_quantity", + placeholder: "{{'shop_variant_quantity_max' | t}}", + "ofn-disable-scroll" => true, + min: "{{variant.line_item.quantity}}", + name: "variant_attributes[{{variant.id}}][max_quantity]", + id: "variants_{{variant.id}}_max"} diff --git a/app/assets/javascripts/templates/shop_variant.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml index 00e4f70d7c..2c9eb932d5 100644 --- a/app/assets/javascripts/templates/shop_variant.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -1,61 +1,28 @@ .variants.row .small-12.medium-4.large-4.columns.variant-name - .table-cell + .table-cell .inline {{ variant.name_to_display }} .bulk-buy.inline{"bo-if" => "variant.product.group_buy"} %i.ofn-i_056-bulk>< %em>< \ {{'bulk' | t}} - -# WITHOUT GROUP BUY - .small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"} - %input{type: :number, - integer: true, - value: nil, - min: 0, - placeholder: "0", - "ofn-disable-scroll" => true, - "ng-model" => "variant.line_item.quantity", - max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", - name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} - + %ng-include{src: "'partials/shop_variant_no_group_buy.html'"} + %ng-include{src: "'partials/shop_variant_with_group_buy.html'"} - -# WITH GROUP BUY - .small-5.medium-3.large-3.columns.text-right{"bo-if" => "variant.product.group_buy"} - %span.bulk-input-container - %span.bulk-input - %input.bulk.first{type: :number, - value: nil, - integer: true, - min: 0, - "ng-model" => "variant.line_item.quantity", - placeholder: "{{'shop_variant_quantity_min' | t}}", - "ofn-disable-scroll" => true, - max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", - name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} - %span.bulk-input - %input.bulk.second{type: :number, - "ng-disabled" => "!variant.line_item.quantity", - integer: true, - min: 0, - "ng-model" => "variant.line_item.max_quantity", - placeholder: "{{'shop_variant_quantity_max' | t}}", - "ofn-disable-scroll" => true, - max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", - name: "variant_attributes[{{variant.id}}][max_quantity]"} .small-3.medium-1.large-1.columns.variant-unit - .table-cell + .table-cell %em {{ variant.unit_to_display }} .small-4.medium-2.large-2.columns.variant-price .table-cell.price - %i.ofn-i_009-close + %i.ofn-i_009-close {{ variant.price_with_fees | localizeCurrency }} -# Now in a template in app/assets/javascripts/templates ! - %price-breakdown{"price-breakdown" => "_", variant: "variant", + %price-breakdown{"price-breakdown" => "_", variant: "variant", "price-breakdown-append-to-body" => "true", "price-breakdown-placement" => "left", "price-breakdown-animation" => true} @@ -63,4 +30,4 @@ .small-12.medium-2.large-2.columns.total-price.text-right .table-cell %strong{"ng-class" => "{filled: variant.totalPrice()}"} - {{ variant.totalPrice() | localizeCurrency }} + {{ variant.totalPrice() | localizeCurrency }} diff --git a/app/assets/stylesheets/admin/components/save_bar.sass b/app/assets/stylesheets/admin/components/save_bar.sass index c6b1236490..87dcce82f9 100644 --- a/app/assets/stylesheets/admin/components/save_bar.sass +++ b/app/assets/stylesheets/admin/components/save_bar.sass @@ -1,9 +1,13 @@ #save-bar position: fixed + width: 100% bottom: 0px - padding: 8px 10px + left: 0 + padding: 8px 8px font-weight: bold - background-color: #fff + background-color: #eff5fc color: #5498da h5 color: #5498da + input + margin-right: 5px diff --git a/app/assets/stylesheets/admin/customers.css.scss b/app/assets/stylesheets/admin/customers.css.scss new file mode 100644 index 0000000000..e3c427649c --- /dev/null +++ b/app/assets/stylesheets/admin/customers.css.scss @@ -0,0 +1,3 @@ +.tag-with-rules { + color: black; +} diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 17d43ff3a5..1fc95face8 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -37,6 +37,7 @@ text-angular .ta-editor { input.red { background-color: #DA5354; + margin-right: 5px; } input.search { diff --git a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass index c67b030292..c0e4fb27aa 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass @@ -11,20 +11,20 @@ padding-bottom: 0em display: table line-height: 1.1 - // outline: 1px solid red + // outline: 1px solid red - @media all and (max-width: 768px) - font-size: 0.875rem + @media all and (max-width: 768px) + font-size: 0.875rem + + @media all and (max-width: 640px) + font-size: 0.75rem - @media all and (max-width: 640px) - font-size: 0.75rem - .table-cell display: table-cell vertical-align: middle height: 37px - // ROW VARIANTS + // ROW VARIANTS .row.variants margin-left: 0 margin-right: 0 @@ -35,7 +35,10 @@ background-color: #f9f9f9 &:hover, &:focus, &:active background-color: $clr-brick-ultra-light - + + &.out-of-stock + opacity: 0.2 + // Variant name .variant-name padding-left: 7.9375rem @@ -52,7 +55,7 @@ height: 27px // Variant unit - .variant-unit + .variant-unit padding-left: 0rem padding-right: 0rem color: #888 @@ -88,18 +91,18 @@ margin-left: 0 margin-right: 0 background: #fff - + .columns padding-top: 1em padding-bottom: 1em line-height: 1 - + @media all and (max-width: 768px) padding-top: 0.65rem padding-bottom: 0.65rem .summary-header - padding-left: 7.9375rem + padding-left: 7.9375rem @media all and (max-width: 768px) padding-left: 4.9375rem @media all and (max-width: 640px) @@ -118,4 +121,3 @@ color: $clr-brick i font-size: 0.8em - diff --git a/app/assets/stylesheets/darkswarm/account.css.sass b/app/assets/stylesheets/darkswarm/account.css.sass index f582545b74..0f3de5af57 100644 --- a/app/assets/stylesheets/darkswarm/account.css.sass +++ b/app/assets/stylesheets/darkswarm/account.css.sass @@ -32,6 +32,13 @@ .debit color: $clr-brick + .invalid + color: $ofn-grey + .credit + color: $ofn-grey + .debit + color: $ofn-grey + .distributor-balance.paid visibility: hidden diff --git a/app/assets/stylesheets/darkswarm/map.css.sass b/app/assets/stylesheets/darkswarm/map.css.sass index a831e9448c..e2a25d61c9 100644 --- a/app/assets/stylesheets/darkswarm/map.css.sass +++ b/app/assets/stylesheets/darkswarm/map.css.sass @@ -22,7 +22,29 @@ background: rgba(255,255,255,0.85) width: 50% margin-top: 1.2rem + margin-left: 1rem @media all and (max-width: 768px) width: 80% &:active, &:focus, &.active background: rgba(255,255,255, 1) + +.map-footer + position: fixed + z-index: 2 + width: 100% + height: 23px + left: 80px + right: 0 + bottom: 6px + margin: 0 + padding: 6px + font-size: 14px + font-weight: bold + text-shadow: 2px 2px #aaa + color: #fff + + a, a:hover, a:active, a:focus + color: #fff + + @media all and (max-width: 1025px) + left: 0px \ No newline at end of file diff --git a/app/assets/stylesheets/mail/email.css.sass b/app/assets/stylesheets/mail/email.css.sass index 4256357923..e9eabd4edb 100644 --- a/app/assets/stylesheets/mail/email.css.sass +++ b/app/assets/stylesheets/mail/email.css.sass @@ -74,6 +74,9 @@ table.order-summary padding-left: 5px padding-right: 5px +.text-right + text-align: right + .social .soc-btn padding: 3px 7px font-size: 12px diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index bd865c8130..24c26661e5 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -26,6 +26,23 @@ module Admin end end + # copy of Spree::Admin::ResourceController without flash notice + def destroy + invoke_callbacks(:destroy, :before) + if @object.destroy + invoke_callbacks(:destroy, :after) + respond_with(@object) do |format| + format.html { redirect_to location_after_destroy } + format.js { render partial: "spree/admin/shared/destroy" } + end + else + invoke_callbacks(:destroy, :fails) + respond_with(@object) do |format| + format.html { redirect_to location_after_destroy } + end + end + end + private def collection diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb new file mode 100644 index 0000000000..3ab5685ffe --- /dev/null +++ b/app/controllers/admin/tags_controller.rb @@ -0,0 +1,28 @@ +module Admin + class TagsController < Spree::Admin::BaseController + respond_to :json + + def index + respond_to do |format| + format.json do + serialiser = ActiveModel::ArraySerializer.new(tags_of_enterprise) + render json: serialiser.to_json + end + end + end + + private + + def enterprise + Enterprise.managed_by(spree_current_user).find_by_id(params[:enterprise_id]) + end + + def tags_of_enterprise + return [] unless enterprise + tag_rule_map = enterprise.rules_per_tag + tag_rule_map.keys.map do |tag| + { text: tag, rules: tag_rule_map[tag] } + end + end + end +end diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 1cffc05734..c6d1291477 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -151,8 +151,15 @@ class CheckoutController < Spree::CheckoutController # Overriding Spree's methods def raise_insufficient_quantity - flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) - redirect_to main_app.shop_path + respond_to do |format| + format.html do + redirect_to cart_path + end + + format.json do + render json: {path: cart_path}, status: 400 + end + end end def redirect_to_paypal_express_form_if_needed diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 8a875c76bf..fe2eda8bfd 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -5,6 +5,8 @@ class EnterprisesController < BaseController # These prepended filters are in the reverse order of execution prepend_before_filter :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop + before_filter :check_stock_levels, only: :shop + before_filter :clean_permalink, only: :check_permalink respond_to :js, only: :permalink_checker @@ -21,17 +23,24 @@ class EnterprisesController < BaseController end end + private def clean_permalink params[:permalink] = params[:permalink].parameterize end + def check_stock_levels + if current_order(true).insufficient_stock_lines.present? + redirect_to spree.cart_path + end + end + def reset_order distributor = Enterprise.is_distributor.find_by_permalink(params[:id]) || Enterprise.is_distributor.find(params[:id]) order = current_order(true) - if order.distributor and order.distributor != distributor + if order.distributor && order.distributor != distributor order.empty! order.set_order_cycle! nil end diff --git a/app/controllers/spree/admin/general_settings_controller_decorator.rb b/app/controllers/spree/admin/general_settings_controller_decorator.rb new file mode 100644 index 0000000000..603f74bf63 --- /dev/null +++ b/app/controllers/spree/admin/general_settings_controller_decorator.rb @@ -0,0 +1,15 @@ +module Spree + module Admin + GeneralSettingsController.class_eval do + end + + + module GeneralSettingsEditPreferences + def edit + super + @preferences_general << :bugherd_api_key + end + end + GeneralSettingsController.send(:prepend, GeneralSettingsEditPreferences) + end +end diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index c6d325d0f8..2c0ff8562c 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -1,9 +1,9 @@ require 'spree/core/controller_helpers/order_decorator' Spree::OrdersController.class_eval do - after_filter :populate_variant_attributes, :only => :populate - before_filter :update_distribution, :only => :update - before_filter :filter_order_params, :only => :update + after_filter :populate_variant_attributes, only: :populate + before_filter :update_distribution, only: :update + before_filter :filter_order_params, only: :update prepend_before_filter :require_order_cycle, only: :edit prepend_before_filter :require_distributor_chosen, only: :edit @@ -12,16 +12,58 @@ Spree::OrdersController.class_eval do include OrderCyclesHelper layout 'darkswarm' + # Patching to redirect to shop if order is empty def edit @order = current_order(true) + @insufficient_stock_lines = @order.insufficient_stock_lines + if @order.line_items.empty? redirect_to main_app.shop_path else associate_user + + if @order.insufficient_stock_lines.present? + flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) + end end end + + def update + @insufficient_stock_lines = [] + @order = current_order + unless @order + flash[:error] = t(:order_not_found) + redirect_to root_path and return + end + + if @order.update_attributes(params[:order]) + @order.line_items = @order.line_items.select {|li| li.quantity > 0 } + @order.restart_checkout_flow + + render :edit and return unless apply_coupon_code + + fire_event('spree.order.contents_changed') + respond_with(@order) do |format| + format.html do + if params.has_key?(:checkout) + @order.next_transition.run_callbacks if @order.cart? + redirect_to checkout_state_path(@order.checkout_steps.first) + else + redirect_to cart_path + end + end + end + else + # Show order with original values, not newly entered ones + @insufficient_stock_lines = @order.insufficient_stock_lines + @order.line_items(true) + respond_with(@order) + end + end + + def populate # Without intervention, the Spree::Adjustment#update_adjustable callback is called many times # during cart population, for both taxation and enterprise fees. This operation triggers a @@ -30,19 +72,54 @@ Spree::OrdersController.class_eval do Spree::Adjustment.without_callbacks do populator = Spree::OrderPopulator.new(current_order(true), current_currency) + if populator.populate(params.slice(:products, :variants, :quantity), true) fire_event('spree.cart.add') fire_event('spree.order.contents_changed') + current_order.cap_quantity_at_stock! current_order.update! - render json: true, status: 200 + variant_ids = variant_ids_in(populator.variants_h) + + render json: {error: false, stock_levels: stock_levels(current_order, variant_ids)}, + status: 200 + else - render json: false, status: 402 + render json: {error: true}, status: 412 end end end + + # Report the stock levels in the order for all variant ids requested + def stock_levels(order, variant_ids) + stock_levels = li_stock_levels(order) + + li_variant_ids = stock_levels.keys + (variant_ids - li_variant_ids).each do |variant_id| + stock_levels[variant_id] = {quantity: 0, max_quantity: 0, + on_hand: Spree::Variant.find(variant_id).on_hand} + end + + stock_levels + end + + def variant_ids_in(variants_h) + variants_h.map { |v| v[:variant_id].to_i } + end + + def li_stock_levels(order) + Hash[ + order.line_items.map do |li| + [li.variant.id, + {quantity: li.quantity, + max_quantity: li.max_quantity, + on_hand: wrap_json_infinity(li.variant.on_hand)}] + end + ] + end + def update_distribution @order = current_order(true) @@ -121,4 +198,9 @@ Spree::OrdersController.class_eval do end end + # Rails to_json encodes Float::INFINITY as Infinity, which is not valid JSON + # Return it as a large integer (max 32 bit signed int) + def wrap_json_infinity(n) + n == Float::INFINITY ? 2147483647 : n + end end diff --git a/app/mailers/enterprise_mailer.rb b/app/mailers/enterprise_mailer.rb index 248e31f109..dfd3f21636 100644 --- a/app/mailers/enterprise_mailer.rb +++ b/app/mailers/enterprise_mailer.rb @@ -4,19 +4,26 @@ class EnterpriseMailer < Spree::BaseMailer def welcome(enterprise) @enterprise = enterprise - mail(:to => enterprise.email, :from => from_address, - :subject => "#{enterprise.name} is now on #{Spree::Config[:site_name]}") + subject = t('enterprise_mailer.welcome.subject', + enterprise: @enterprise.name, + sitename: Spree::Config[:site_name]) + mail(:to => enterprise.email, + :from => from_address, + :subject => subject) end - def confirmation_instructions(record, token, opts={}) + def confirmation_instructions(record, token) @token = token find_enterprise(record) - mail(subject: "Please confirm your email for #{@enterprise.name}", - to: ( @enterprise.unconfirmed_email || @enterprise.email ), - from: from_address) + subject = t('enterprise_mailer.confirmation_instructions.subject', + enterprise: @enterprise.name) + mail(to: (@enterprise.unconfirmed_email || @enterprise.email), + from: from_address, + subject: subject) end private + def find_enterprise(enterprise) @enterprise = enterprise.is_a?(Enterprise) ? enterprise : Enterprise.find(enterprise) end diff --git a/app/mailers/producer_mailer.rb b/app/mailers/producer_mailer.rb index b9fb52fb41..30fad48f58 100644 --- a/app/mailers/producer_mailer.rb +++ b/app/mailers/producer_mailer.rb @@ -4,8 +4,11 @@ class ProducerMailer < Spree::BaseMailer @producer = producer @coordinator = order_cycle.coordinator @order_cycle = order_cycle - @line_items = aggregated_line_items_from(@order_cycle, @producer) + line_items = line_items_from(@order_cycle, @producer) + @grouped_line_items = line_items.group_by(&:product_and_full_name) @receival_instructions = @order_cycle.receival_instructions_for @producer + @total = total_from_line_items(line_items) + @tax_total = tax_total_from_line_items(line_items) subject = "[#{Spree::Config.site_name}] Order cycle report for #{producer.name}" @@ -25,10 +28,6 @@ class ProducerMailer < Spree::BaseMailer line_items_from(order_cycle, producer).any? end - def aggregated_line_items_from(order_cycle, producer) - aggregate_line_items line_items_from(order_cycle, producer) - end - def line_items_from(order_cycle, producer) Spree::LineItem. joins(:order => :order_cycle, :variant => :product). @@ -37,16 +36,11 @@ class ProducerMailer < Spree::BaseMailer merge(Spree::Order.complete) end - def aggregate_line_items(line_items) - # Arrange the items in a hash to group quantities - line_items.inject({}) do |lis, li| - if lis.key? li.variant - lis[li.variant].quantity += li.quantity - else - lis[li.variant] = li - end + def total_from_line_items(line_items) + Spree::Money.new line_items.sum(&:total) + end - lis - end + def tax_total_from_line_items(line_items) + Spree::Money.new line_items.sum(&:included_tax) end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 9f839f9d08..b7a5e3189a 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -351,6 +351,20 @@ class Enterprise < ActiveRecord::Base end end + def rules_per_tag + tag_rule_map = {} + tag_rules.each do |rule| + rule.preferred_customer_tags.split(",").each do |tag| + if tag_rule_map[tag] + tag_rule_map[tag] += 1 + else + tag_rule_map[tag] = 1 + end + end + end + tag_rule_map + end + protected def devise_mailer diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 867d3c9e2b..e250d1341d 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -101,11 +101,6 @@ 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) @@ -221,7 +216,9 @@ class AbilityDecorator # Reports page can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], :report - can [:admin, :index, :update], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id) + can [:create], Customer + can [:admin, :index, :update, :destroy], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id) + can [:admin, :index], :tag end diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb index 2d69036a17..570c7acc1e 100644 --- a/app/models/spree/app_configuration_decorator.rb +++ b/app/models/spree/app_configuration_decorator.rb @@ -26,4 +26,6 @@ Spree::AppConfiguration.class_eval do # Monitoring preference :last_job_queue_heartbeat_at, :string, default: nil + # External services + preference :bugherd_api_key, :string, default: nil end diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index 9db9ae1269..8a89d8fdef 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -43,6 +43,11 @@ Spree::LineItem.class_eval do where('spree_adjustments.id IS NULL') + def cap_quantity_at_stock! + update_attributes!(quantity: variant.on_hand) if quantity > variant.on_hand + end + + def has_tax? adjustments.included_tax.any? end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index f0e9324185..a06a34bcd0 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -99,7 +99,7 @@ Spree::Order.class_eval do def remove_variant(variant) line_items(:reload) current_item = find_line_item_by_variant(variant) - current_item.destroy + current_item.andand.destroy end @@ -144,6 +144,11 @@ Spree::Order.class_eval do current_item end + def cap_quantity_at_stock! + line_items.each &:cap_quantity_at_stock! + end + + def set_distributor!(distributor) self.distributor = distributor self.order_cycle = nil unless self.order_cycle.andand.has_distributor? distributor diff --git a/app/models/spree/order_populator_decorator.rb b/app/models/spree/order_populator_decorator.rb index 1ca0f9efc2..db03f1f2e2 100644 --- a/app/models/spree/order_populator_decorator.rb +++ b/app/models/spree/order_populator_decorator.rb @@ -1,6 +1,8 @@ require 'open_food_network/scope_variant_to_hub' Spree::OrderPopulator.class_eval do + attr_reader :variants_h + def populate(from_hash, overwrite = false) @distributor, @order_cycle = distributor_and_order_cycle # Refactor: We may not need this validation - we can't change distribution here, so @@ -11,8 +13,7 @@ Spree::OrderPopulator.class_eval do if valid? @order.with_lock do - variants = read_products_hash(from_hash) + - read_variants_hash(from_hash) + variants = read_variants from_hash variants.each do |v| if varies_from_cart(v) @@ -31,6 +32,11 @@ Spree::OrderPopulator.class_eval do valid? end + def read_variants(data) + @variants_h = read_products_hash(data) + + read_variants_hash(data) + end + def read_products_hash(data) (data[:products] || []).map do |product_id, variant_id| {variant_id: variant_id, quantity: data[:quantity]} @@ -49,17 +55,34 @@ Spree::OrderPopulator.class_eval do def attempt_cart_add(variant_id, quantity, max_quantity = nil) quantity = quantity.to_i + max_quantity = max_quantity.to_i if max_quantity variant = Spree::Variant.find(variant_id) OpenFoodNetwork::ScopeVariantToHub.new(@distributor).scope(variant) - if quantity > 0 - if check_stock_levels(variant, quantity) && - check_order_cycle_provided_for(variant) && - check_variant_available_under_distribution(variant) - @order.add_variant(variant, quantity, max_quantity, currency) + if quantity > 0 && + check_order_cycle_provided_for(variant) && + check_variant_available_under_distribution(variant) + + quantity_to_add, max_quantity_to_add = quantities_to_add(variant, quantity, max_quantity) + + if quantity_to_add > 0 + @order.add_variant(variant, quantity_to_add, max_quantity_to_add, currency) + else + @order.remove_variant variant end end end + def quantities_to_add(variant, quantity, max_quantity) + # If not enough stock is available, add as much as we can to the cart + on_hand = variant.on_hand + on_hand = [quantity, max_quantity].compact.max if Spree::Config.allow_backorders + quantity_to_add = [quantity, on_hand].min + max_quantity_to_add = max_quantity # max_quantity is not capped + + [quantity_to_add, max_quantity_to_add] + end + + def cart_remove(variant_id) variant = Spree::Variant.find(variant_id) @order.remove_variant(variant) diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index be60307d52..afd32bd294 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -15,7 +15,6 @@ 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 @@ -42,10 +41,6 @@ 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 @@ -56,11 +51,16 @@ Spree.user_class.class_eval do # Returns Enterprise IDs for distributors that the user has shopped at def enterprises_ordered_from - orders.where(state: :complete).map(&:distributor_id).uniq + enterprise_ids = orders.where(state: :complete).map(&:distributor_id).uniq + # Exclude the accounts distributor + if Spree::Config.accounts_distributor_id + enterprise_ids = enterprise_ids.keep_if { |a| a != Spree::Config.accounts_distributor_id } + end + enterprise_ids end # Returns orders and their associated payments for all distributors that have been ordered from - def compelete_orders_by_distributor + def complete_orders_by_distributor Enterprise .includes(distributed_orders: { payments: :payment_method }) .where(enterprises: { id: enterprises_ordered_from }, @@ -70,8 +70,8 @@ Spree.user_class.class_eval do def orders_by_distributor # Remove uncompleted payments as these will not be reflected in order balance - data_array = compelete_orders_by_distributor.to_a - remove_uncompleted_payments(data_array) + data_array = complete_orders_by_distributor.to_a + remove_payments_in_checkout(data_array) data_array.sort! { |a, b| b.distributed_orders.length <=> a.distributed_orders.length } end @@ -83,10 +83,10 @@ Spree.user_class.class_eval do end end - def remove_uncompleted_payments(enterprises) + def remove_payments_in_checkout(enterprises) enterprises.each do |enterprise| enterprise.distributed_orders.each do |order| - order.payments.keep_if { |payment| payment.state == "completed" } + order.payments.keep_if { |payment| payment.state != "checkout" } end end end diff --git a/app/serializers/api/admin/customer_serializer.rb b/app/serializers/api/admin/customer_serializer.rb index 3cb9518a9f..052f49a917 100644 --- a/app/serializers/api/admin/customer_serializer.rb +++ b/app/serializers/api/admin/customer_serializer.rb @@ -6,6 +6,10 @@ class Api::Admin::CustomerSerializer < ActiveModel::Serializer end def tags - object.tag_list.map{ |t| { text: t } } + tag_rule_map = object.enterprise.rules_per_tag + object.tag_list.map do |tag| + { text: tag, rules: tag_rule_map[tag] } + end end + end diff --git a/app/serializers/api/payment_serializer.rb b/app/serializers/api/payment_serializer.rb index d48f75a07f..8465998db6 100644 --- a/app/serializers/api/payment_serializer.rb +++ b/app/serializers/api/payment_serializer.rb @@ -1,6 +1,6 @@ module Api class PaymentSerializer < ActiveModel::Serializer - attributes :amount, :updated_at, :payment_method + attributes :amount, :updated_at, :payment_method, :state def payment_method object.payment_method.name end diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index e33b871df4..0a348d4a0a 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -56,7 +56,7 @@ %input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" } %td.tags{ 'ng-show' => 'columns.tags.visible' } .tag_watcher{ 'obj-for-update' => "customer", "attr-for-update" => "tag_list"} - %tags_with_translation{ object: 'customer' } + %tags_with_translation{ object: 'customer', 'find-tags' => 'findTags(query)' } %td.actions %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' } diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index c447d35d58..ed2d3acbc2 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -52,14 +52,10 @@ .actions - if @order_cycle.new_record? = f.submit 'Create', 'ng-click' => "submit('#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()' - - else - = f.submit 'Update', 'ng-click' => "submit(null)", 'ng-disabled' => '!loaded()' - = f.submit 'Update and Close', 'ng-click' => "submit('#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()' + %span{'ng-show' => 'loaded()'} - or = link_to 'Cancel', main_app.admin_order_cycles_path %span{'ng-hide' => 'loaded()'} Loading... - = render 'spree/admin/shared/status_message' - unless Rails.env.production? diff --git a/app/views/admin/order_cycles/_simple_form.html.haml b/app/views/admin/order_cycles/_simple_form.html.haml index 4dc64a012b..9364d080ff 100644 --- a/app/views/admin/order_cycles/_simple_form.html.haml +++ b/app/views/admin/order_cycles/_simple_form.html.haml @@ -23,12 +23,7 @@ .actions - if @order_cycle.new_record? = f.submit 'Create', 'ng-click' => "submit('#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()' - - else - = f.submit 'Update', 'ng-click' => "submit(null)", 'ng-disabled' => '!loaded()' - = f.submit 'Update and Close', 'ng-click' => "submit('#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()' %span{'ng-show' => 'loaded()'} - or = link_to 'Cancel', main_app.admin_order_cycles_path %span{'ng-hide' => 'loaded()'} Loading... - = render 'spree/admin/shared/status_message' diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index f3b0bdb721..9f85261224 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -27,7 +27,10 @@ - ng_controller = order_cycles_simple_form ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl' -= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller} do |f| += form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form'} do |f| + + %save-bar{ buttons: "[{ text: 'Update', action: submit, param: null, class: 'red' }, { text: 'Update and Close', action: submit, param: '#{main_app.admin_order_cycles_path}', class: 'red' }, { text: 'Cancel', action: cancel, param: '#{main_app.admin_order_cycles_path}', class: '' }]", form: "order_cycle_form" } + - if order_cycles_simple_form = render 'simple_form', f: f - else diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index c17e4c189a..f255d86e0b 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -1,5 +1,5 @@ %form{ name: 'variant_overrides_form', ng: { show: "views.inventory.visible" } } - %save-bar{ save: "update()", form: "variant_overrides_form" } + %save-bar{ form: "variant_overrides_form", buttons: "[{ text: 'Save Changes', action: update, class: 'red' }]" } %table.index.bulk#variant-overrides %col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } } %col.product{ width: "20%", ng: { show: 'columns.product.visible' } } diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 74588176b9..b5222e5f86 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -40,6 +40,7 @@ .map-container %map{"ng-if" => "(active(\'\') && (mapShowed = true)) || mapShowed"} %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} + %map-search %markers{models: "mapMarkers", fit: "true", coords: "'self'", icon: "'icon'", click: "'reveal'"} diff --git a/app/views/layouts/_bugherd_script.html.haml b/app/views/layouts/_bugherd_script.html.haml index ad6fe585f5..8a04160cfd 100644 --- a/app/views/layouts/_bugherd_script.html.haml +++ b/app/views/layouts/_bugherd_script.html.haml @@ -1,18 +1,8 @@ -- if Rails.env.staging? or Rails.env.production? +- if (Rails.env.staging? || Rails.env.production?) && Spree::Config.bugherd_api_key.present? :javascript (function (d, t) { var bh = d.createElement(t), s = d.getElementsByTagName(t)[0]; bh.type = 'text/javascript'; - bh.src = '//www.bugherd.com/sidebarv2.js?apikey=4ftxjbgwx7y6ssykayr04w'; + bh.src = '//www.bugherd.com/sidebarv2.js?apikey=#{Spree::Config.bugherd_api_key}'; s.parentNode.insertBefore(bh, s); })(document, 'script'); - - --#- elsif Rails.env.production? - -#:javascript - -#(function (d, t) { - -#var bh = d.createElement(t), s = d.getElementsByTagName(t)[0]; - -#bh.type = 'text/javascript'; - -#bh.src = '//www.bugherd.com/sidebarv2.js?apikey=xro3uv55objies58o2wrua'; - -#s.parentNode.insertBefore(bh, s); - -#})(document, 'script'); diff --git a/app/views/map/index.html.haml b/app/views/map/index.html.haml index a4d012282b..e4a22e540c 100644 --- a/app/views/map/index.html.haml +++ b/app/views/map/index.html.haml @@ -9,3 +9,6 @@ %map-search %markers{models: "OfnMap.enterprises", fit: "true", coords: "'self'", icon: "'icon'", click: "'reveal'"} + +.map-footer + %a{:href => "http://www.openstreetmap.org/copyright"} © OpenStreetMap contributors diff --git a/app/views/producer_mailer/order_cycle_report.html.haml b/app/views/producer_mailer/order_cycle_report.html.haml new file mode 100644 index 0000000000..b69dde8e66 --- /dev/null +++ b/app/views/producer_mailer/order_cycle_report.html.haml @@ -0,0 +1,74 @@ +%p + = t :producer_mail_greeting + #{" " + @producer.name}, +%p + = t :producer_mail_text_before + - if @receival_instructions + %p + %b + =t :producer_mail_delivery_instructions + = @receival_instructions +%p + = t :producer_mail_order_text + %table.order-summary + %thead + %tr + %th + = t :sku + %th + = t :supplier + %th + = t :product + %th.text-right + = t :quantity + %th.text-right + = t :price + %th.text-right + = t :subtotal + %th.text-right + = t :included_tax + %tbody + - @grouped_line_items.each_pair do |product_and_full_name, line_items| + %tr + %td + #{line_items.first.variant.sku} + %td + #{raw(line_items.first.product.supplier.name)} + %td + #{raw(product_and_full_name)} + %td.text-right + #{line_items.sum(&:quantity)} + %td.text-right + #{line_items.first.single_money} + %td.text-right + #{Spree::Money.new(line_items.sum(&:total), currency: line_items.first.currency) } + %td.tax.text-right + #{Spree::Money.new(line_items.sum(&:included_tax), currency: line_items.first.currency) } + %tr.total-row + %td + %td + %td + %td + %td + %td.text-right + #{@total} + %td.text-right + #{@tax_total} +%p + = t :producer_mail_text_after +%p + #{t(:producer_mail_signoff)}, + %em + %p + #{@coordinator.name} + %p + %br + #{@coordinator.address.address1} + %br + #{@coordinator.address.city} + %br + #{@coordinator.address.zipcode} + %p + #{@coordinator.phone} + %p + #{@coordinator.email} diff --git a/app/views/producer_mailer/order_cycle_report.text.haml b/app/views/producer_mailer/order_cycle_report.text.haml index 93748395ce..b5b644bf01 100644 --- a/app/views/producer_mailer/order_cycle_report.text.haml +++ b/app/views/producer_mailer/order_cycle_report.text.haml @@ -1,21 +1,26 @@ -Dear #{@producer.name}, +#{t :producer_mail_greeting} #{@producer.name}, \ -We now have all the consumer orders for the next food drop. += t :producer_mail_text_before \ - if @receival_instructions - Stock pickup/delivery instructions: + = t :producer_mail_delivery_instructions = @receival_instructions \ Orders summary ================ \ -Here is a summary of the orders for your products: += t :producer_mail_order_text \ -- @line_items.each_pair do |variant, line_item| - #{variant.sku} - #{raw(variant.product.supplier.name)} - #{raw(variant.product_and_full_name)} (QTY: #{line_item.quantity}) @ #{line_item.single_money} = #{line_item.display_amount} +- @grouped_line_items.each_pair do |product_and_full_name, line_items| + #{line_items.first.variant.sku} - #{raw(line_items.first.product.supplier.name)} - #{raw(product_and_full_name)} (QTY: #{line_items.sum(&:quantity)}) @ #{line_items.first.single_money} = #{Spree::Money.new(line_items.sum(&:total), currency: line_items.first.currency)} \ -Thanks and best wishes, +\ +Total: #{@total} +\ += t :producer_mail_text_after + +#{t :producer_mail_signoff}, #{@coordinator.name} #{@coordinator.address.address1}, #{@coordinator.address.city}, #{@coordinator.address.zipcode} #{@coordinator.phone} diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 18868e5c77..f2be498451 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -32,9 +32,9 @@ %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | properties: activeProperties) track by product.id ", "id" => "product-{{ product.id }}"} - = render partial: "shop/products/summary" + = render "shop/products/summary" %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"} - %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}"} + %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}", "ng-class" => "{'out-of-stock': !variant.on_demand && variant.count_on_hand == 0}"} %product{"ng-show" => "Products.loading"} .row.summary diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml index 6d36ac81ce..8671b00cbf 100644 --- a/app/views/shop/products/_summary.html.haml +++ b/app/views/shop/products/_summary.html.haml @@ -13,8 +13,9 @@ %em = t :products_from %span - %enterprise-modal - %i.ofn-i_036-producers{"bo-text" => "enterprise.name"} + %enterprise-modal + %i.ofn-i_036-producers + %span{"bo-bind" => "enterprise.name"} .small-2.medium-2.large-1.columns.text-center .taxon-flag %render-svg{path: "{{product.primary_taxon.icon}}"} diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 43983e87ea..d8228b5c83 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -10,7 +10,7 @@ = render :partial => 'spree/admin/shared/order_sub_menu' %div{ ng: { controller: 'LineItemsCtrl' } } - %save-bar{ save: "submit()", form: "bulk_order_form" } + %save-bar{ form: "bulk_order_form", buttons: "[{ text: 'Save Changes', action: submit, class: 'red' }]" } .filters{ :class => "sixteen columns alpha" } .date_filter{ :class => "two columns alpha" } %label{ :for => 'start_date_filter' } diff --git a/app/views/spree/orders/_line_item.html.haml b/app/views/spree/orders/_line_item.html.haml index 6014b617a9..5976332a18 100644 --- a/app/views/spree/orders/_line_item.html.haml +++ b/app/views/spree/orders/_line_item.html.haml @@ -17,7 +17,7 @@ = render 'spree/shared/line_item_name', line_item: line_item - - if @order.insufficient_stock_lines.include? line_item + - if @insufficient_stock_lines.include? line_item %span.out-of-stock = variant.in_stock? ? t(:insufficient_stock, :on_hand => variant.on_hand) : t(:out_of_stock) %br/ @@ -30,7 +30,7 @@ -# "price-breakdown-placement" => "left", -# "price-breakdown-animation" => true} %td.text-center.cart-item-quantity{"data-hook" => "cart_item_quantity"} - = item_form.number_field :quantity, :min => 0, :class => "line_item_quantity", :size => 5 + = item_form.number_field :quantity, :min => 0, "ofn-on-hand" => variant.on_hand, "ng-model" => "line_item_#{line_item.id}", :class => "line_item_quantity", :size => 5 %td.cart-item-total.text-right{"data-hook" => "cart_item_total"} = line_item.display_amount_with_adjustments.to_html unless line_item.quantity.nil? diff --git a/app/views/spree/users/_fat.html.haml b/app/views/spree/users/_fat.html.haml index c10cf8254a..5c87077b4c 100644 --- a/app/views/spree/users/_fat.html.haml +++ b/app/views/spree/users/_fat.html.haml @@ -19,10 +19,12 @@ %td.order5.text-right{"ng-class" => "{'credit' : order.total < 0, 'debit' : order.total > 0, 'paid' : order.total == 0}","bo-text" => "order.total | localizeCurrency"} %td.order6.text-right.show-for-large-up{"ng-class" => "{'credit' : order.outstanding_balance < 0, 'debit' : order.outstanding_balance > 0, 'paid' : order.outstanding_balance == 0}", "bo-text" => "order.outstanding_balance | localizeCurrency"} %td.order7.text-right{"ng-class" => "{'credit' : order.running_balance < 0, 'debit' : order.running_balance > 0, 'paid' : order.running_balance == 0}", "bo-text" => "order.running_balance | localizeCurrency"} - %tr.payment-row{"ng-repeat" => "payment in order.payments"} - %td.order1= t :payment + %tr.payment-row{"ng-repeat" => "payment in order.payments", "ng-class" => "{'invalid': payment.state != 'completed'}"} + %td.order1{"bo-text" => "payment.payment_method"} %td.order2{"bo-text" => "payment.updated_at"} - %td.order3.show-for-large-up{"bo-text" => "payment.payment_method"} + %td.order3.show-for-large-up + %i{"ng-class" => "{'ofn-i_012-warning': payment.state == 'invalid' || payment.state == 'void' || payment.state == 'failed'}"} + %span{"bo-text" => "'spree.payment_states.' + payment.state | t | capitalize"} %td.order4.show-for-large-up %td.order5.text-right{"ng-class" => "{'credit' : payment.amount > 0, 'debit' : payment.amount < 0, 'paid' : payment.amount == 0}","bo-text" => "payment.amount | localizeCurrency"} %td.order6.show-for-large-up diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 58441e9934..e22e91fa0f 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -58,6 +58,13 @@ en-GB: sort_order_cycles_on_shopfront_by: "Sort Order Cycles On Shopfront By" + # To customise text in emails. + producer_mail_greeting: "Dear" + producer_mail_text_before: "We now have all the consumer orders for the next food drop." + producer_mail_order_text: "Here is a summary of the orders for your products:" + producer_mail_delivery_instructions: "Stock pickup/delivery instructions:" + producer_mail_text_after: "Please confirm that you have got this email. Please send me an invoice for this amount so that we can send you payment. If you need to phone me on the day, please use the number below." + producer_mail_signoff: "Thanks and best wishes" admin: # General form elements @@ -614,6 +621,10 @@ See the %{link} to find out more about %{sitename}'s features and to start using products_distributor: Distributor products_distributor_info: When you select a distributor for your order, their address and pickup times will be displayed here. + shop_trial_length: "Shop Trial Length (Days)" + shop_trial_expires_in: "Your shopfront trial expires in" + shop_trial_expired_notice: "Open Food Network UK is currently free while we prepare for our soft launch in April, 2016." + # keys used in javascript password: Password remember_me: Remember Me @@ -1024,6 +1035,7 @@ Please follow the instructions there to make your enterprise visible on the Open pending: pending processing: processing void: void + invalid: invalid order_state: address: address adjustments: adjustments @@ -1037,7 +1049,3 @@ Please follow the instructions there to make your enterprise visible on the Open resumed: resumed returned: returned skrill: skrill - shop_trial_length: "Shop Trial Length (Days)" - shop_trial_length: "Shop Trial Length (Days)" - shop_trial_expires_in: "Your shopfront trial expires in" - shop_trial_expired_notice: "Open Food Network UK is currently free while we prepare for our soft launch in April, 2016." diff --git a/config/locales/en.yml b/config/locales/en.yml index 34c4f4d4e9..e895522903 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -32,6 +32,11 @@ en: not_confirmed: Your email address could not be confirmed. Perhaps you have already completed this step? confirmation_sent: "Confirmation email sent!" confirmation_not_sent: "Could not send a confirmation email." + enterprise_mailer: + confirmation_instructions: + subject: "Please confirm the email address for %{enterprise}" + welcome: + subject: "%{enterprise} is now on %{sitename}" home: "OFN" title: Open Food Network welcome_to: 'Welcome to ' @@ -80,6 +85,10 @@ en: whats_this: What's this? + tag_has_rules: "Existing rules for this tag: %{num}" + has_one_rule: "has one rule" + has_n_rules: "has %{num} rules" + customers: index: add_customer: "Add customer" @@ -401,6 +410,13 @@ See the %{link} to find out more about %{sitename}'s features and to start using If you are a producer or food enterprise, we are excited to have you as a part of the network." email_signup_help_html: "We welcome all your questions and feedback; you can use the Send Feedback button on the site or email us at" + producer_mail_greeting: "Dear" + producer_mail_text_before: "We now have all the consumer orders for the next food drop." + producer_mail_order_text: "Here is a summary of the orders for your products:" + producer_mail_delivery_instructions: "Stock pickup/delivery instructions:" + producer_mail_text_after: "" + producer_mail_signoff: "Thanks and best wishes" + shopping_oc_closed: Orders are closed shopping_oc_closed_description: "Please wait until the next cycle opens (or contact us directly to see if we can accept any late orders)" shopping_oc_last_closed: "The last cycle closed %{distance_of_time} ago" @@ -618,6 +634,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using products_distributor: Distributor products_distributor_info: When you select a distributor for your order, their address and pickup times will be displayed here. + shop_trial_length: "Shop Trial Length (Days)" + shop_trial_expires_in: "Your shopfront trial expires in" + shop_trial_expired_notice: "Good news! We have decided to extend shopfront trials until further notice." + + # keys used in javascript password: Password remember_me: Remember Me @@ -1028,6 +1049,7 @@ Please follow the instructions there to make your enterprise visible on the Open pending: pending processing: processing void: void + invalid: invalid order_state: address: address adjustments: adjustments @@ -1041,6 +1063,3 @@ Please follow the instructions there to make your enterprise visible on the Open resumed: resumed returned: returned skrill: skrill - shop_trial_length: "Shop Trial Length (Days)" - shop_trial_expires_in: "Your shopfront trial expires in" - shop_trial_expired_notice: "Good news! We have decided to extend shopfront trials until further notice (probably around March 2015)." diff --git a/config/routes.rb b/config/routes.rb index 8d8d6d27c3..e240f901a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -117,6 +117,8 @@ Openfoodnetwork::Application.routes.draw do resources :customers, only: [:index, :create, :update, :destroy] + resources :tags, only: [:index], format: :json + resource :content resource :accounts_and_billing_settings, only: [:edit, :update] do diff --git a/spec/controllers/checkout_controller_spec.rb b/spec/controllers/checkout_controller_spec.rb index ddacd0fdeb..1f5e40a841 100644 --- a/spec/controllers/checkout_controller_spec.rb +++ b/spec/controllers/checkout_controller_spec.rb @@ -34,13 +34,13 @@ describe CheckoutController do flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later." end - it "redirects to the shop when no line items are present" do + it "redirects to the cart when some items are out of stock" do controller.stub(:current_distributor).and_return(distributor) controller.stub(:current_order_cycle).and_return(order_cycle) controller.stub(:current_order).and_return(order) order.stub_chain(:insufficient_stock_lines, :present?).and_return true get :edit - response.should redirect_to shop_path + response.should redirect_to spree.cart_path end it "renders when both distributor and order cycle is selected" do diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb index b3cb6b5e32..0727488841 100644 --- a/spec/controllers/enterprises_controller_spec.rb +++ b/spec/controllers/enterprises_controller_spec.rb @@ -2,13 +2,14 @@ require 'spec_helper' describe EnterprisesController do describe "shopping for a distributor" do + let(:order) { controller.current_order(true) } before(:each) do @current_distributor = create(:distributor_enterprise, with_payment_and_shipping: true) @distributor = create(:distributor_enterprise, with_payment_and_shipping: true) @order_cycle1 = create(:simple_order_cycle, distributors: [@distributor], orders_open_at: 2.days.ago, orders_close_at: 3.days.from_now ) @order_cycle2 = create(:simple_order_cycle, distributors: [@distributor], orders_open_at: 3.days.ago, orders_close_at: 4.days.from_now ) - controller.current_order(true).distributor = @current_distributor + order.set_distributor! @current_distributor end it "sets the shop as the distributor on the order when shopping for the distributor" do @@ -52,6 +53,27 @@ describe EnterprisesController do controller.current_order.line_items.size.should == 1 end + describe "when an out of stock item is in the cart" do + let(:variant) { create(:variant, on_demand: false, on_hand: 10) } + let(:line_item) { create(:line_item, variant: variant) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [@distributor], variants: [variant]) } + + before do + order.set_distribution! @current_distributor, order_cycle + order.line_items << line_item + + Spree::Config.set allow_backorders: false + variant.on_hand = 0 + variant.save! + end + + it "redirects to the cart" do + spree_get :shop, {id: @current_distributor} + + response.should redirect_to spree.cart_path + end + end + it "sets order cycle if only one is available at the chosen distributor" do @order_cycle2.destroy diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 73231bc86c..a4ddab3ad3 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -69,17 +69,6 @@ describe ShopController do end - describe "producers/suppliers" do - let(:supplier) { create(:supplier_enterprise) } - let(:product) { create(:product, supplier: supplier) } - let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) } - - before do - exchange = order_cycle.exchanges.to_enterprises(distributor).outgoing.first - exchange.variants << product.master - end - end - describe "returning products" do let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) } let(:exchange) { order_cycle.exchanges.to_enterprises(distributor).outgoing.first } diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index 73c72f3386..74e9efaaf9 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -15,6 +15,7 @@ describe Spree::OrdersController do controller.stub(:current_order_cycle).and_return(order_cycle) controller.stub(:current_order).and_return order order.stub_chain(:line_items, :empty?).and_return true + order.stub(:insufficient_stock_lines).and_return [] session[:access_token] = order.token spree_get :edit response.should redirect_to shop_path @@ -42,6 +43,88 @@ describe Spree::OrdersController do flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later." end + describe "when an item has insufficient stock" do + let(:order) { subject.current_order(true) } + let(:oc) { create(:simple_order_cycle, distributors: [d], variants: [variant]) } + let(:d) { create(:distributor_enterprise, shipping_methods: [create(:shipping_method)], payment_methods: [create(:payment_method)]) } + let(:variant) { create(:variant, on_demand: false, on_hand: 5) } + let(:line_item) { order.line_items.last } + + before do + Spree::Config.allow_backorders = false + order.set_distribution! d, oc + order.add_variant variant, 5 + variant.update_attributes! on_hand: 3 + end + + it "displays a flash message when we view the cart" do + spree_get :edit + expect(response.status).to eq 200 + flash[:error].should == "An item in your cart has become unavailable." + end + end + + describe "returning stock levels in JSON on success" do + let(:product) { create(:simple_product) } + + it "returns stock levels as JSON" do + controller.stub(:variant_ids_in) { [123] } + controller.stub(:stock_levels) { 'my_stock_levels' } + Spree::OrderPopulator.stub(:new).and_return(populator = double()) + populator.stub(:populate) { true } + populator.stub(:variants_h) { {} } + + xhr :post, :populate, use_route: :spree, format: :json + + data = JSON.parse(response.body) + data['stock_levels'].should == 'my_stock_levels' + end + + describe "generating stock levels" do + let!(:order) { create(:order) } + let!(:li) { create(:line_item, order: order, variant: v, quantity: 2, max_quantity: 3) } + let!(:v) { create(:variant, count_on_hand: 4) } + let!(:v2) { create(:variant, count_on_hand: 2) } + + before do + order.reload + controller.stub(:current_order) { order } + end + + it "returns a hash with variant id, quantity, max_quantity and stock on hand" do + controller.stock_levels(order, [v.id]).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} + end + + it "includes all line items, even when the variant_id is not specified" do + controller.stock_levels(order, []).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} + end + + it "includes an empty quantity entry for variants that aren't in the order" do + controller.stock_levels(order, [v.id, v2.id]).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}, + v2.id => {quantity: 0, max_quantity: 0, on_hand: 2}} + end + + describe "encoding Infinity" do + let!(:v) { create(:variant, on_demand: true, count_on_hand: 0) } + + it "encodes Infinity as a large, finite integer" do + controller.stock_levels(order, [v.id]).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 2147483647}} + end + end + end + + it "extracts variant ids from the populator" do + variants_h = [{:variant_id=>"900", :quantity=>2, :max_quantity=>nil}, + {:variant_id=>"940", :quantity=>3, :max_quantity=>3}] + + controller.variant_ids_in(variants_h).should == [900, 940] + end + end + context "adding a group buy product to the cart" do it "sets a variant attribute for the max quantity" do distributor_product = create(:distributor_enterprise) @@ -59,7 +142,8 @@ describe Spree::OrdersController do it "returns HTTP success when successful" do Spree::OrderPopulator.stub(:new).and_return(populator = double()) - populator.stub(:populate).and_return true + populator.stub(:populate) { true } + populator.stub(:variants_h) { {} } xhr :post, :populate, use_route: :spree, format: :json response.status.should == 200 end @@ -68,7 +152,7 @@ describe Spree::OrdersController do Spree::OrderPopulator.stub(:new).and_return(populator = double()) populator.stub(:populate).and_return false xhr :post, :populate, use_route: :spree, format: :json - response.status.should == 402 + response.status.should == 412 end it "tells populator to overwrite" do @@ -78,11 +162,11 @@ describe Spree::OrdersController do end end - context "removing line items from cart" do + describe "removing line items from cart" do describe "when I pass params that includes a line item no longer in our cart" do it "should silently ignore the missing line item" do order = subject.current_order(true) - li = order.add_variant(create(:simple_product, on_hand: 110).master) + li = order.add_variant(create(:simple_product, on_hand: 110).variants.first) spree_get :update, order: { line_items_attributes: { "0" => {quantity: "0", id: "9999"}, "1" => {quantity: "99", id: li.id} diff --git a/spec/features/admin/external_services_spec.rb b/spec/features/admin/external_services_spec.rb new file mode 100644 index 0000000000..ed81c8e6fa --- /dev/null +++ b/spec/features/admin/external_services_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +feature 'External services' do + include AuthenticationWorkflow + + describe "bugherd" do + before do + Spree::Config.bugherd_api_key = nil + login_to_admin_section + end + + it "lets me set an API key" do + visit spree.edit_admin_general_settings_path + + fill_in 'bugherd_api_key', with: 'abc123' + click_button 'Update' + + page.should have_content 'General Settings has been successfully updated!' + expect(Spree::Config.bugherd_api_key).to eq 'abc123' + end + end +end diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index aafaa5ee77..cd5ef0a07e 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -267,6 +267,9 @@ feature %q{ scenario "updating an order cycle", js: true do + # Make the page long enough to avoid the save bar overlaying the form + page.driver.resize(1280, 3600) + # Given an order cycle with all the settings oc = create(:order_cycle) initial_variants = oc.variants.sort_by &:id @@ -359,6 +362,8 @@ feature %q{ select 'Distributor fee 2', from: 'order_cycle_outgoing_exchange_2_enterprise_fees_0_enterprise_fee_id' # And I click Update + expect(page).to have_selector "#save-bar" + save_screenshot('abc.png') click_button 'Update and Close' # Then my order cycle should have been updated @@ -607,10 +612,6 @@ feature %q{ page.all('tr.supplier').count.should == 3 page.all('tr.distributor').count.should == 3 - # When I save, then those exchanges should remain - click_button 'Update' - page.should have_content "Your order cycle has been updated." - oc.reload oc.suppliers.should match_array [supplier_managed, supplier_permitted, supplier_unmanaged] oc.coordinator.should == distributor_managed @@ -618,6 +619,9 @@ feature %q{ end scenario "editing an order cycle" do + # Make the page long enough to avoid the save bar overlaying the form + page.driver.resize(1280, 3600) + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: distributor_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) visit edit_admin_order_cycle_path(oc) @@ -627,6 +631,7 @@ feature %q{ page.find("tr.supplier-#{supplier_permitted.id} a.remove-exchange").click page.find("tr.distributor-#{distributor_managed.id} a.remove-exchange").click page.find("tr.distributor-#{distributor_permitted.id} a.remove-exchange").click + click_button 'Update' # Then the exchanges should be removed @@ -698,10 +703,6 @@ feature %q{ # I should be able to see but not toggle v2, because I don't have permission expect(page).to have_field "order_cycle_outgoing_exchange_0_variants_#{v2.id}", disabled: true - # When I save, any exchanges that I can't manage remain - click_button 'Update' - page.should have_content "Your order cycle has been updated." - oc.reload oc.suppliers.should match_array [supplier_managed, supplier_permitted, supplier_unmanaged] oc.coordinator.should == distributor_managed @@ -751,10 +752,6 @@ feature %q{ # I should be able to see but not toggle v2, because I don't have permission expect(page).to have_field "order_cycle_incoming_exchange_0_variants_#{v2.id}", disabled: true - # When I save, any exchange that I can't manage remains - click_button 'Update' - page.should have_content "Your order cycle has been updated." - oc.reload oc.suppliers.should match_array [supplier_managed, supplier_permitted, supplier_unmanaged] oc.coordinator.should == distributor_managed @@ -868,6 +865,9 @@ feature %q{ end scenario "updating an order cycle" do + # Make the page long enough to avoid the save bar overlaying the form + page.driver.resize(1280, 3600) + # Given an order cycle with pickup time and instructions fee1 = create(:enterprise_fee, name: 'my fee', enterprise: enterprise) fee2 = create(:enterprise_fee, name: 'that fee', enterprise: enterprise) @@ -902,6 +902,7 @@ feature %q{ # When I update, or update and close, both work click_button 'Update' page.should have_content 'Your order cycle has been updated.' + click_button 'Update and Close' # Then my order cycle should have been updated diff --git a/spec/features/consumer/account_spec.rb b/spec/features/consumer/account_spec.rb index b072a24357..58d4e1acc6 100644 --- a/spec/features/consumer/account_spec.rb +++ b/spec/features/consumer/account_spec.rb @@ -13,6 +13,8 @@ feature %q{ let!(:distributor2) { create(:distributor_enterprise) } let!(:distributor_credit) { create(:distributor_enterprise) } let!(:distributor_without_orders) { create(:distributor_enterprise) } + let!(:accounts_distributor) {create :distributor_enterprise} + let!(:order_account_invoice) { create(:order, distributor: accounts_distributor, state: 'complete', user: user) } let!(:d1o1) { create(:completed_order_with_totals, distributor_id: distributor1.id, user_id: user.id, total: 10000)} let!(:d1o2) { create(:order_without_full_payment, distributor_id: distributor1.id, user_id: user.id, total: 5000)} let!(:d2o1) { create(:completed_order_with_totals, distributor_id: distributor2.id, user_id: user.id)} @@ -21,19 +23,24 @@ feature %q{ before do + Spree::Config.accounts_distributor_id = accounts_distributor.id credit_order.update! login_as user visit "/account" end it "shows all hubs that have been ordered from with balance or credit" do + # Single test to avoid re-rendering page expect(page).to have_content distributor1.name expect(page).to have_content distributor2.name expect(page).not_to have_content distributor_without_orders.name + # Exclude the special Accounts & Billing distributor + expect(page).not_to have_content accounts_distributor.name expect(page).to have_content distributor1.name + " " + "Balance due" expect(page).to have_content distributor_credit.name + " Credit" end + it "reveals table of orders for distributors when clicked" do expand_active_table_node distributor1.name expect(page).to have_link "Order " + d1o1.number, href:"/orders/#{d1o1.number}" diff --git a/spec/features/consumer/external_services_spec.rb b/spec/features/consumer/external_services_spec.rb new file mode 100644 index 0000000000..28be1f8cb6 --- /dev/null +++ b/spec/features/consumer/external_services_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +feature 'External services' do + include AuthenticationWorkflow + include WebHelper + + describe "bugherd" do + describe "limiting inclusion by environment" do + before { Spree::Config.bugherd_api_key = 'abc123' } + + it "is not included in test" do + visit root_path + expect(script_content(with: 'bugherd')).to be_nil + end + + it "is not included in dev" do + Rails.env.stub(:development?) { true } + visit root_path + expect(script_content(with: 'bugherd')).to be_nil + end + + it "is included in staging" do + Rails.env.stub(:staging?) { true } + visit root_path + expect(script_content(with: 'bugherd')).not_to be_nil + end + + it "is included in production" do + Rails.env.stub(:production?) { true } + visit root_path + expect(script_content(with: 'bugherd')).not_to be_nil + end + end + + context "in an environment where BugHerd is displayed" do + before { Rails.env.stub(:staging?) { true } } + + context "when there is no API key set" do + before { Spree::Config.bugherd_api_key = nil } + + it "does not include the BugHerd script" do + visit root_path + expect(script_content(with: 'bugherd')).to be_nil + end + end + + context "when an API key is set" do + before { Spree::Config.bugherd_api_key = 'abc123' } + + it "includes the BugHerd script, with the correct API key" do + visit root_path + expect(script_content(with: 'bugherd')).to include 'abc123' + end + end + end + end +end diff --git a/spec/features/consumer/shopping/cart_spec.rb b/spec/features/consumer/shopping/cart_spec.rb index 58c570735d..ed573d0460 100644 --- a/spec/features/consumer/shopping/cart_spec.rb +++ b/spec/features/consumer/shopping/cart_spec.rb @@ -7,25 +7,53 @@ feature "full-page cart", js: true do include UIComponentHelper describe "viewing the cart" do + let!(:zone) { create(:zone_with_member) } + let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } + let(:supplier) { create(:supplier_enterprise) } + let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) } + let(:enterprise_fee) { create(:enterprise_fee, amount: 11.00, tax_category: product.tax_category) } + let(:product) { create(:taxed_product, supplier: supplier, zone: zone, price: 110.00, tax_rate_amount: 0.1) } + let(:variant) { product.variants.first } + let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } + + before do + add_enterprise_fee enterprise_fee + set_order order + add_product_to_cart + visit spree.cart_path + end + describe "tax" do - let!(:zone) { create(:zone_with_member) } - let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } - let(:supplier) { create(:supplier_enterprise) } - let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) } - let(:enterprise_fee) { create(:enterprise_fee, amount: 11.00, tax_category: product.tax_category) } - let(:product) { create(:taxed_product, supplier: supplier, zone: zone, price: 110.00, tax_rate_amount: 0.1) } - let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } - - before do - add_enterprise_fee enterprise_fee - set_order order - add_product_to_cart - visit spree.cart_path - end - it "shows the total tax for the order, including product tax and tax on fees" do page.should have_selector '.tax-total', text: '11.00' # 10 + 1 end end + + describe "updating quantities with insufficient stock available" do + let(:li) { order.line_items(true).last } + + before do + variant.update_attributes! on_hand: 2 + end + + it "prevents me from entering an invalid value" do + visit spree.cart_path + + accept_alert 'Insufficient stock available, only 2 remaining' do + fill_in "order_line_items_attributes_0_quantity", with: '4' + end + + page.should have_field "order_line_items_attributes_0_quantity", with: '2' + end + + it "shows the quantities saved, not those submitted" do + fill_in "order_line_items_attributes_0_quantity", with: '4' + + click_button 'Update' + + page.should have_field "order[line_items_attributes][0][quantity]", with: '1' + page.should have_content "Insufficient stock available, only 2 remaining" + end + end end end diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index a19776f7a9..0b0683def0 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -9,7 +9,7 @@ feature "As a consumer I want to check out my cart", js: true do let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } let(:supplier) { create(:supplier_enterprise) } - let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) } + let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) } let(:product) { create(:simple_product, supplier: supplier) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } @@ -23,7 +23,7 @@ feature "As a consumer I want to check out my cart", js: true do it "does not not render the login form when logged in" do quick_login_as user - visit checkout_path + visit checkout_path within "section[role='main']" do page.should_not have_content "Login" page.should have_checkout_details @@ -31,7 +31,7 @@ feature "As a consumer I want to check out my cart", js: true do end it "renders the login buttons when logged out" do - visit checkout_path + visit checkout_path within "section[role='main']" do page.should have_content "Login" click_button "Login" @@ -53,9 +53,8 @@ feature "As a consumer I want to check out my cart", js: true do end it "allows user to checkout as guest" do - visit checkout_path + visit checkout_path checkout_as_guest - page.should have_checkout_details + page.should have_checkout_details end end - diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 74145bbe1a..9e9d830a50 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -11,9 +11,10 @@ feature "As a consumer I want to check out my cart", js: true do let!(:zone) { create(:zone_with_member) } let(:distributor) { create(:distributor_enterprise, charges_sales_tax: true) } let(:supplier) { create(:supplier_enterprise) } - let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) } + let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [variant]) } let(:enterprise_fee) { create(:enterprise_fee, amount: 1.23, tax_category: product.tax_category) } let(:product) { create(:taxed_product, supplier: supplier, price: 10, zone: zone, tax_rate_amount: 0.1) } + let(:variant) { product.variants.first } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } before do @@ -45,6 +46,22 @@ feature "As a consumer I want to check out my cart", js: true do distributor.shipping_methods << sm3 end + describe "when I have an out of stock product in my cart" do + before do + Spree::Config.set allow_backorders: false + variant.on_hand = 0 + variant.save! + end + + it "returns me to the cart with an error message" do + visit checkout_path + + page.should_not have_selector 'closing', text: "Checkout now" + page.should have_selector 'closing', text: "Your shopping cart" + page.should have_content "An item in your cart has become unavailable" + end + end + context "on the checkout page" do before do visit checkout_path @@ -213,6 +230,18 @@ feature "As a consumer I want to check out my cart", js: true do page.should have_content "Your order has been processed successfully" end + it "takes us to the cart page with an error when a product becomes out of stock just before we purchase", js: true do + Spree::Config.set allow_backorders: false + variant.on_hand = 0 + variant.save! + + place_order + + page.should_not have_content "Your order has been processed successfully" + page.should have_selector 'closing', text: "Your shopping cart" + page.should have_content "Out of Stock" + end + context "when we are charged a shipping fee" do before { choose sm2.name } diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index a8545af3db..dceecd43f8 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -182,7 +182,7 @@ feature "As a consumer I want to shop with a distributor", js: true do describe "with variants on the product" do let(:variant) { create(:variant, product: product, on_hand: 10 ) } before do - add_product_and_variant_to_order_cycle(exchange, product, variant) + add_variant_to_order_cycle(exchange, variant) set_order_cycle(order, oc1) visit shop_path end @@ -215,9 +215,11 @@ feature "As a consumer I want to shop with a distributor", js: true do let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } let(:product) { create(:simple_product) } let(:variant) { create(:variant, product: product) } + let(:variant2) { create(:variant, product: product) } before do - add_product_and_variant_to_order_cycle(exchange, product, variant) + add_variant_to_order_cycle(exchange, variant) + add_variant_to_order_cycle(exchange, variant2) set_order_cycle(order, oc1) visit shop_path end @@ -235,6 +237,111 @@ feature "As a consumer I want to shop with a distributor", js: true do Spree::LineItem.where(id: li).should be_empty end + + it "alerts us when we enter a quantity greater than the stock available" do + variant.update_attributes on_hand: 5 + visit shop_path + + accept_alert 'Insufficient stock available, only 5 remaining' do + fill_in "variants[#{variant.id}]", with: '10' + end + + page.should have_field "variants[#{variant.id}]", with: '5' + end + + describe "when a product goes out of stock just before it's added to the cart" do + it "stops the attempt, shows an error message and refreshes the products asynchronously" do + variant.update_attributes! on_hand: 0 + + # -- Messaging + fill_in "variants[#{variant.id}]", with: '1' + wait_until { !cart_dirty } + + within(".out-of-stock-modal") do + page.should have_content "stock levels for one or more of the products in your cart have reduced" + page.should have_content "#{product.name} - #{variant.unit_to_display} is now out of stock." + end + + # -- Page updates + # Update amount in cart + page.should have_field "variants[#{variant.id}]", with: '0', disabled: true + page.should have_field "variants[#{variant2.id}]", with: '' + + # Update amount available in product list + # If amount falls to zero, variant should be greyed out and input disabled + page.should have_selector "#variant-#{variant.id}.out-of-stock" + page.should have_selector "#variants_#{variant.id}[ofn-on-hand='0']" + page.should have_selector "#variants_#{variant.id}[disabled='disabled']" + end + + context "group buy products" do + let(:product) { create(:simple_product, group_buy: true) } + + it "does the same" do + # -- Place in cart so we can set max_quantity, then make out of stock + fill_in "variants[#{variant.id}]", with: '1' + wait_until { !cart_dirty } + variant.update_attributes! on_hand: 0 + + # -- Messaging + fill_in "variant_attributes[#{variant.id}][max_quantity]", with: '1' + wait_until { !cart_dirty } + + within(".out-of-stock-modal") do + page.should have_content "stock levels for one or more of the products in your cart have reduced" + page.should have_content "#{product.name} - #{variant.unit_to_display} is now out of stock." + end + + # -- Page updates + # Update amount in cart + page.should have_field "variant_attributes[#{variant.id}][max_quantity]", with: '0', disabled: true + + # Update amount available in product list + # If amount falls to zero, variant should be greyed out and input disabled + page.should have_selector "#variant-#{variant.id}.out-of-stock" + page.should have_selector "#variants_#{variant.id}_max[disabled='disabled']" + end + end + + context "when the update is for another product" do + it "updates quantity" do + fill_in "variants[#{variant.id}]", with: '2' + wait_until { !cart_dirty } + + variant.update_attributes! on_hand: 1 + + fill_in "variants[#{variant2.id}]", with: '1' + wait_until { !cart_dirty } + + within(".out-of-stock-modal") do + page.should have_content "stock levels for one or more of the products in your cart have reduced" + page.should have_content "#{product.name} - #{variant.unit_to_display} now only has 1 remaining" + end + end + + context "group buy products" do + let(:product) { create(:simple_product, group_buy: true) } + + it "does not update max_quantity" do + fill_in "variants[#{variant.id}]", with: '2' + fill_in "variant_attributes[#{variant.id}][max_quantity]", with: '3' + wait_until { !cart_dirty } + variant.update_attributes! on_hand: 1 + + fill_in "variants[#{variant2.id}]", with: '1' + wait_until { !cart_dirty } + + within(".out-of-stock-modal") do + page.should have_content "stock levels for one or more of the products in your cart have reduced" + page.should have_content "#{product.name} - #{variant.unit_to_display} now only has 1 remaining" + end + + page.should have_field "variants[#{variant.id}]", with: '1' + page.should have_field "variant_attributes[#{variant.id}][max_quantity]", with: '3' + end + end + end + end end context "when no order cycles are available" do @@ -260,7 +367,7 @@ feature "As a consumer I want to shop with a distributor", js: true do let(:variant) { create(:variant, product: product) } before do - add_product_and_variant_to_order_cycle(exchange, product, variant) + add_variant_to_order_cycle(exchange, variant) set_order_cycle(order, oc1) distributor.require_login = true distributor.save! 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 215f2834ed..6981ef4e4f 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 @@ -47,3 +47,32 @@ describe "CustomersCtrl", -> http.flush() expect(scope.customers.length).toBe 1 expect(scope.customers[0]).not.toAngularEqual customer + + describe "scope.findTags", -> + tags = [ + { text: 'one' } + { text: 'two' } + { text: 'three' } + ] + beforeEach -> + http.expectGET('/admin/tags.json?enterprise_id=1').respond 200, tags + + it "retrieves the tag list", -> + promise = scope.findTags('') + result = null + promise.then (data) -> + result = data + http.flush() + expect(result).toAngularEqual tags + + it "filters the tag list", -> + filtered_tags = [ + { text: 'two' } + { text: 'three' } + ] + promise = scope.findTags('t') + result = null + promise.then (data) -> + result = data + http.flush() + expect(result).toAngularEqual filtered_tags diff --git a/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee b/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee index dbad5a9f05..f5ccd25aac 100644 --- a/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee +++ b/spec/javascripts/unit/admin/order_cycles/controllers/simple_edit.js.coffee @@ -10,7 +10,8 @@ describe "AdminSimpleEditOrderCycleCtrl", -> outgoing_exchange = {} beforeEach -> - scope = {} + scope = + $watch: jasmine.createSpy('$watch') location = absUrl: -> 'example.com/admin/order_cycles/27/edit' diff --git a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee index 518a524010..d985904f81 100644 --- a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee @@ -126,6 +126,85 @@ describe 'Cart service', -> $httpBackend.flush() expect(Cart.scheduleRetry).toHaveBeenCalled() + describe "verifying stock levels after update", -> + describe "when an item is out of stock", -> + it "reduces the quantity in the cart", -> + li = {variant: {id: 1}, quantity: 5} + stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.quantity).toEqual 0 + expect(li.max_quantity).toBeUndefined() + + it "reduces the max_quantity in the cart", -> + li = {variant: {id: 1}, quantity: 5, max_quantity: 6} + stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.max_quantity).toEqual 0 + + it "resets the count on hand available", -> + li = {variant: {id: 1, count_on_hand: 10}, quantity: 5} + stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.variant.count_on_hand).toEqual 0 + + describe "when the quantity available is less than that requested", -> + it "reduces the quantity in the cart", -> + li = {variant: {id: 1}, quantity: 6} + stockLevels = {1: {quantity: 5, on_hand: 5}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.quantity).toEqual 5 + expect(li.max_quantity).toBeUndefined() + + it "does not reduce the max_quantity in the cart", -> + li = {variant: {id: 1}, quantity: 6, max_quantity: 7} + stockLevels = {1: {quantity: 5, max_quantity: 5, on_hand: 5}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.max_quantity).toEqual 7 + + it "resets the count on hand available", -> + li = {variant: {id: 1}, quantity: 6} + stockLevels = {1: {quantity: 5, on_hand: 6}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.variant.count_on_hand).toEqual 6 + + describe "when the client-side quantity has been increased during the request", -> + it "does not reset the quantity", -> + li = {variant: {id: 1}, quantity: 6} + stockLevels = {1: {quantity: 5, on_hand: 6}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.quantity).toEqual 6 + expect(li.max_quantity).toBeUndefined() + + it "does not reset the max_quantity", -> + li = {variant: {id: 1}, quantity: 5, max_quantity: 7} + stockLevels = {1: {quantity: 5, max_quantity: 6, on_hand: 7}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.quantity).toEqual 5 + expect(li.max_quantity).toEqual 7 + + describe "when the client-side quantity has been changed from 0 to 1 during the request", -> + it "does not reset the quantity", -> + li = {variant: {id: 1}, quantity: 1} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels {} + expect(li.quantity).toEqual 1 + expect(li.max_quantity).toBeUndefined() + + it "does not reset the max_quantity", -> + li = {variant: {id: 1}, quantity: 1, max_quantity: 1} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels {} + expect(li.quantity).toEqual 1 + expect(li.max_quantity).toEqual 1 + it "pops the queue", -> Cart.update_enqueued = true spyOn(Cart, 'scheduleUpdate') diff --git a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee index e3ba3f51de..718504a377 100644 --- a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee @@ -5,7 +5,7 @@ describe 'Checkout service', -> Navigation = null flash = null scope = null - FlashLoaderMock = + FlashLoaderMock = loadFlash: (arg)-> paymentMethods = [{ id: 99 @@ -41,10 +41,10 @@ describe 'Checkout service', -> module 'Darkswarm' module ($provide)-> - $provide.value "RailsFlashLoader", FlashLoaderMock - $provide.value "currentOrder", orderData - $provide.value "shippingMethods", shippingMethods - $provide.value "paymentMethods", paymentMethods + $provide.value "RailsFlashLoader", FlashLoaderMock + $provide.value "currentOrder", orderData + $provide.value "shippingMethods", shippingMethods + $provide.value "paymentMethods", paymentMethods null inject ($injector, _$httpBackend_, $rootScope)-> @@ -80,26 +80,33 @@ describe 'Checkout service', -> it 'Gets the current payment method', -> expect(Checkout.paymentMethod()).toEqual null Checkout.order.payment_method_id = 99 - expect(Checkout.paymentMethod()).toEqual paymentMethods[0] + expect(Checkout.paymentMethod()).toEqual paymentMethods[0] it "Posts the Checkout to the server", -> $httpBackend.expectPUT("/checkout", {order: Checkout.preprocess()}).respond 200, {path: "test"} Checkout.submit() $httpBackend.flush() - it "sends flash messages to the flash service", -> - spyOn(FlashLoaderMock, "loadFlash") # Stubbing out writes to window.location - $httpBackend.expectPUT("/checkout").respond 400, {flash: {error: "frogs"}} - Checkout.submit() + describe "when there is an error", -> + it "redirects when a redirect is given", -> + $httpBackend.expectPUT("/checkout").respond 400, {path: 'path'} + Checkout.submit() + $httpBackend.flush() + expect(Navigation.go).toHaveBeenCalledWith 'path' - $httpBackend.flush() - expect(FlashLoaderMock.loadFlash).toHaveBeenCalledWith {error: "frogs"} + it "sends flash messages to the flash service", -> + spyOn(FlashLoaderMock, "loadFlash") # Stubbing out writes to window.location + $httpBackend.expectPUT("/checkout").respond 400, {flash: {error: "frogs"}} + Checkout.submit() - it "puts errors into the scope", -> - $httpBackend.expectPUT("/checkout").respond 400, {errors: {error: "frogs"}} - Checkout.submit() - $httpBackend.flush() - expect(Checkout.errors).toEqual {error: "frogs"} + $httpBackend.flush() + expect(FlashLoaderMock.loadFlash).toHaveBeenCalledWith {error: "frogs"} + + it "puts errors into the scope", -> + $httpBackend.expectPUT("/checkout").respond 400, {errors: {error: "frogs"}} + Checkout.submit() + $httpBackend.flush() + expect(Checkout.errors).toEqual {error: "frogs"} describe "data preprocessing", -> beforeEach -> diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 973b348383..b66acfcb1d 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -169,7 +169,9 @@ describe 'OrderCycle controllers', -> EnterpriseFee = null beforeEach -> - scope = {} + scope = + order_cycle_form: jasmine.createSpyObj('order_cycle_form', ['$dirty', '$setPristine']) + $watch: jasmine.createSpy('$watch') event = preventDefault: jasmine.createSpy('preventDefault') location = @@ -292,6 +294,7 @@ describe 'OrderCycle controllers', -> scope.removeExchange(event, 'exchange') expect(event.preventDefault).toHaveBeenCalled() expect(OrderCycle.removeExchange).toHaveBeenCalledWith('exchange') + expect(scope.order_cycle_form.$dirty).toEqual true it 'Adds coordinator fees', -> scope.addCoordinatorFee(event) @@ -320,6 +323,7 @@ describe 'OrderCycle controllers', -> it 'Submits the order cycle via OrderCycle update', -> scope.submit('/admin/order_cycles') expect(OrderCycle.update).toHaveBeenCalledWith('/admin/order_cycles') + expect(scope.order_cycle_form.$setPristine.calls.length).toEqual 1 describe 'OrderCycle services', -> diff --git a/spec/mailers/enterprise_mailer_spec.rb b/spec/mailers/enterprise_mailer_spec.rb index eaa21b54ae..2399d4146a 100644 --- a/spec/mailers/enterprise_mailer_spec.rb +++ b/spec/mailers/enterprise_mailer_spec.rb @@ -12,7 +12,7 @@ describe EnterpriseMailer do EnterpriseMailer.confirmation_instructions(enterprise, 'token').deliver ActionMailer::Base.deliveries.count.should == 1 mail = ActionMailer::Base.deliveries.first - expect(mail.subject).to eq "Please confirm your email for #{enterprise.name}" + expect(mail.subject).to eq "Please confirm the email address for #{enterprise.name}" expect(mail.to).to include enterprise.email expect(mail.reply_to).to be_nil end @@ -28,7 +28,7 @@ describe EnterpriseMailer do EnterpriseMailer.confirmation_instructions(enterprise, 'token').deliver ActionMailer::Base.deliveries.count.should == 1 mail = ActionMailer::Base.deliveries.first - expect(mail.subject).to eq "Please confirm your email for #{enterprise.name}" + expect(mail.subject).to eq "Please confirm the email address for #{enterprise.name}" expect(mail.to).to include enterprise.unconfirmed_email end end diff --git a/spec/mailers/producer_mailer_spec.rb b/spec/mailers/producer_mailer_spec.rb index 0c7e82b89d..b878c27895 100644 --- a/spec/mailers/producer_mailer_spec.rb +++ b/spec/mailers/producer_mailer_spec.rb @@ -2,29 +2,34 @@ require 'spec_helper' require 'yaml' describe ProducerMailer do + let!(:zone) { create(:zone_with_member) } + let!(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Spree::Calculator::DefaultTax.new, zone: zone, amount: 0.1) } + let!(:tax_category) { create(:tax_category, tax_rates: [tax_rate]) } let(:s1) { create(:supplier_enterprise) } let(:s2) { create(:supplier_enterprise) } let(:s3) { create(:supplier_enterprise) } - let(:d1) { create(:distributor_enterprise) } + let(:d1) { create(:distributor_enterprise, charges_sales_tax: true) } let(:d2) { create(:distributor_enterprise) } - let(:p1) { create(:product, price: 12.34, supplier: s1) } + let(:p1) { create(:product, price: 12.34, supplier: s1, tax_category: tax_category) } let(:p2) { create(:product, price: 23.45, supplier: s2) } let(:p3) { create(:product, price: 34.56, supplier: s1) } + let(:p4) { create(:product, price: 45.67, supplier: s1) } let(:order_cycle) { create(:simple_order_cycle) } let!(:incoming_exchange) { order_cycle.exchanges.create! sender: s1, receiver: d1, incoming: true, receival_instructions: 'Outside shed.' } let!(:order) do order = create(:order, distributor: d1, order_cycle: order_cycle, state: 'complete') - order.line_items << create(:line_item, variant: p1.master) - order.line_items << create(:line_item, variant: p1.master) - order.line_items << create(:line_item, variant: p2.master) + order.line_items << create(:line_item, quantity: 1, variant: p1.variants.first) + order.line_items << create(:line_item, quantity: 2, variant: p1.variants.first) + order.line_items << create(:line_item, quantity: 3, variant: p2.variants.first) + order.line_items << create(:line_item, quantity: 2, variant: p4.variants.first) order.finalize! order.save order end let!(:order_incomplete) do order = create(:order, distributor: d1, order_cycle: order_cycle, state: 'payment') - order.line_items << create(:line_item, variant: p3.master) + order.line_items << create(:line_item, variant: p3.variants.first) order.save order end @@ -44,7 +49,7 @@ describe ProducerMailer do end it "includes receival instructions" do - mail.body.should include 'Outside shed.' + mail.body.encoded.should include 'Outside shed.' end it "cc's the enterprise" do @@ -53,13 +58,28 @@ describe ProducerMailer do it "contains an aggregated list of produce" do body_lines_including(mail, p1.name).each do |line| - line.should include 'QTY: 2' - line.should include '@ $10.00 = $20.00' + line.should include 'QTY: 3' + line.should include '@ $10.00 = $30.00' end + body_as_html(mail).find("table.order-summary tr", text: p1.name) + .should have_selector("td", text: "$30.00") + end + + it "displays tax totals for each product" do + # Tax for p1 line items + body_as_html(mail).find("table.order-summary tr", text: p1.name) + .should have_selector("td", text: "$30.00") end it "does not include incomplete orders" do - mail.body.should_not include p3.name + mail.body.encoded.should_not include p3.name + end + + it "includes the total" do + # puts mail.text_part.body.encoded + mail.body.encoded.should include 'Total: $50.00' + body_as_html(mail).find("tr.total-row") + .should have_selector("td", text: "$50.00") end it "sends no mail when the producer has no orders" do @@ -74,4 +94,8 @@ describe ProducerMailer do def body_lines_including(mail, s) mail.body.to_s.lines.select { |line| line.include? s } end + + def body_as_html(mail) + Capybara.string(mail.html_part.body.encoded) + end end diff --git a/spec/models/spree/line_item_spec.rb b/spec/models/spree/line_item_spec.rb index 6bb18fafa3..111108f7b4 100644 --- a/spec/models/spree/line_item_spec.rb +++ b/spec/models/spree/line_item_spec.rb @@ -42,6 +42,41 @@ module Spree end end + describe "capping quantity at stock level" do + let!(:v) { create(:variant, on_demand: false, on_hand: 10) } + let!(:li) { create(:line_item, variant: v, quantity: 10, max_quantity: 10) } + + before do + v.update_attributes! on_hand: 5 + end + + it "caps quantity" do + li.cap_quantity_at_stock! + li.reload.quantity.should == 5 + end + + it "does not cap max_quantity" do + li.cap_quantity_at_stock! + li.reload.max_quantity.should == 10 + end + + it "works for products without max_quantity" do + li.update_column :max_quantity, nil + li.cap_quantity_at_stock! + li.reload + li.quantity.should == 5 + li.max_quantity.should be_nil + end + + it "does nothing for on_demand items" do + v.update_attributes! on_demand: true + li.cap_quantity_at_stock! + li.reload + li.quantity.should == 10 + li.max_quantity.should == 10 + end + end + describe "calculating price with adjustments" do it "does not return fractional cents" do li = LineItem.new diff --git a/spec/models/spree/order_populator_spec.rb b/spec/models/spree/order_populator_spec.rb index 7ba59fb47a..f0bbc81f55 100644 --- a/spec/models/spree/order_populator_spec.rb +++ b/spec/models/spree/order_populator_spec.rb @@ -149,13 +149,15 @@ module Spree end describe "attempt_cart_add" do - it "performs additional validations" do - variant = double(:variant) - quantity = 123 + let(:variant) { double(:variant, on_hand: 250) } + let(:quantity) { 123 } + + before do Spree::Variant.stub(:find).and_return(variant) VariantOverride.stub(:for).and_return(nil) + end - op.should_receive(:check_stock_levels).with(variant, quantity).and_return(true) + it "performs additional validations" do op.should_receive(:check_order_cycle_provided_for).with(variant).and_return(true) op.should_receive(:check_variant_available_under_distribution).with(variant). and_return(true) @@ -163,8 +165,76 @@ module Spree op.attempt_cart_add(333, quantity.to_s) end + + it "filters quantities through #quantities_to_add" do + op.should_receive(:quantities_to_add).with(variant, 123, 123). + and_return([5, 5]) + + op.stub(:check_order_cycle_provided_for) { true } + op.stub(:check_variant_available_under_distribution) { true } + + order.should_receive(:add_variant).with(variant, 5, 5, currency) + + op.attempt_cart_add(333, quantity.to_s, quantity.to_s) + end + + it "removes variants which have become out of stock" do + op.should_receive(:quantities_to_add).with(variant, 123, 123). + and_return([0, 0]) + + op.stub(:check_order_cycle_provided_for) { true } + op.stub(:check_variant_available_under_distribution) { true } + + order.should_receive(:remove_variant).with(variant) + order.should_receive(:add_variant).never + + op.attempt_cart_add(333, quantity.to_s, quantity.to_s) + end end + describe "quantities_to_add" do + let(:v) { double(:variant, on_hand: 10) } + + context "when backorders are not allowed" do + before { Spree::Config.allow_backorders = false } + + context "when max_quantity is not provided" do + it "returns full amount when available" do + op.quantities_to_add(v, 5, nil).should == [5, nil] + end + + it "returns a limited amount when not entirely available" do + op.quantities_to_add(v, 15, nil).should == [10, nil] + end + end + + context "when max_quantity is provided" do + it "returns full amount when available" do + op.quantities_to_add(v, 5, 6).should == [5, 6] + end + + it "also returns the full amount when not entirely available" do + op.quantities_to_add(v, 15, 16).should == [10, 16] + end + end + end + + context "when backorders are allowed" do + around do |example| + Spree::Config.allow_backorders = true + example.run + Spree::Config.allow_backorders = false + end + + it "does not limit quantity" do + op.quantities_to_add(v, 15, nil).should == [15, nil] + end + + it "does not limit max_quantity" do + op.quantities_to_add(v, 15, 16).should == [15, 16] + end + end + end describe "validations" do describe "determining if distributor can supply products in cart" do diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index ab86be1580..4db0541d6c 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -366,6 +366,7 @@ describe Spree::Order do let(:order) { create(:order) } let(:v1) { create(:variant) } let(:v2) { create(:variant) } + let(:v3) { create(:variant) } before do order.add_variant v1 @@ -376,6 +377,12 @@ describe Spree::Order do order.remove_variant v1 order.line_items(:reload).map(&:variant).should == [v2] end + + it "does nothing when there is no matching line item" do + expect do + order.remove_variant v3 + end.to change(order.line_items(:reload), :count).by(0) + end end describe "emptying the order" do diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index 923bc93183..441757bf38 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -53,23 +53,6 @@ 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 @@ -107,11 +90,17 @@ describe Spree.user_class do 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!(:accounts_distributor) {create :distributor_enterprise} + let!(:order_account_invoice) { create(:order, distributor: accounts_distributor, state: 'complete', user: u1) } let!(:completed_payment) { create(:payment, order: d1o1, state: 'completed') } - let!(:payment) { create(:payment, order: d1o2, state: 'invalid') } + let!(:payment) { create(:payment, order: d1o2, state: 'checkout') } - it "returns enterprises that the user has ordered from" do + before do + Spree::Config.accounts_distributor_id = accounts_distributor.id + end + + it "returns enterprises that the user has ordered from, excluding accounts distributor" do expect(u1.enterprises_ordered_from).to eq [distributor1.id] end @@ -131,8 +120,8 @@ describe Spree.user_class do expect(u1.orders_by_distributor.first.distributed_orders).not_to include d1o3 end - it "doesn't return uncompleted payments" do - expect(u1.orders_by_distributor.first.distributed_orders.map(&:payments).flatten).not_to include payment + it "doesn't return payments that are still at checkout stage" do + expect(u1.orders_by_distributor.first.distributed_orders.map{|o| o.payments}.flatten).not_to include payment end end end diff --git a/spec/serializers/admin/customer_serializer_spec.rb b/spec/serializers/admin/customer_serializer_spec.rb new file mode 100644 index 0000000000..d63f00d4aa --- /dev/null +++ b/spec/serializers/admin/customer_serializer_spec.rb @@ -0,0 +1,14 @@ +describe Api::Admin::CustomerSerializer do + let(:customer) { create(:customer, tag_list: "one, two, three") } + let!(:tag_rule) { create(:tag_rule, enterprise: customer.enterprise, preferred_customer_tags: "two") } + + it "serializes a customer" do + serializer = Api::Admin::CustomerSerializer.new customer + result = JSON.parse(serializer.to_json) + expect(result['email']).to eq customer.email + tags = result['tags'] + expect(tags.length).to eq 3 + expect(tags[0]).to eq({ "text" => 'one', "rules" => nil }) + expect(tags[1]).to eq({ "text" => 'two', "rules" => 1 }) + end +end diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index a0e6ab846c..279ead2630 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -18,7 +18,7 @@ module ShopWorkflow def add_product_to_cart populator = Spree::OrderPopulator.new(order, order.currency) - populator.populate(variants: {product.master.id => 1}) + populator.populate(variants: {product.variants.first.id => 1}) # Recalculate fee totals order.update_distribution_charge! @@ -28,15 +28,10 @@ module ShopWorkflow find("dd a", text: name).trigger "click" end - def add_product_to_order_cycle(exchange, product) - exchange.variants << product.master + def add_variant_to_order_cycle(exchange, variant) + exchange.variants << variant end - def add_product_and_variant_to_order_cycle(exchange, product, variant) - exchange.variants << product.master - exchange.variants << variant - end - def set_order_cycle(order, order_cycle) order.update_attribute(:order_cycle, order_cycle) end diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index 30ce677c6c..70c803709e 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -115,6 +115,24 @@ module WebHelper DirtyFormDialog.new(page) end + # Fetch the content of a script block + # eg. script_content with: 'my-script.com' + # Returns nil if not found + # Raises an exception if multiple matching blocks are found + def script_content(opts={}) + elems = page.all('script', visible: false) + + elems = elems.to_a.select { |e| e.text(:all).include? opts[:with] } if opts[:with] + + if elems.none? + nil + elsif elems.many? + raise "Multiple results returned for script_content" + else + elems.first.text(:all) + end + end + # http://www.elabs.se/blog/53-why-wait_until-was-removed-from-capybara # Do not use this without good reason. Capybara's built-in waiting is very effective. def wait_until(secs=nil)