Merge remote-tracking branch 'origin/master' into uk/trial-length

This commit is contained in:
Rob Harrington
2016-05-20 12:22:47 +10:00
91 changed files with 1412 additions and 306 deletions

View File

@@ -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

View File

@@ -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'
})

View File

@@ -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
]

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -1 +1 @@
angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils'])
angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils', 'templates'])

View File

@@ -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

View File

@@ -1,10 +1,11 @@
angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) ->
restrict: "E"
template: "<tags-input ng-model='object[tagsAttr]'>"
templateUrl: "admin/tags_input.html"
scope:
object: "="
tagsAttr: "@?"
tagListAttr: "@?"
findTags: "&"
link: (scope, element, attrs) ->
$timeout ->
scope.tagsAttr ||= "tags"

View File

@@ -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)

View File

@@ -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", ->

View File

@@ -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

View File

@@ -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()

View File

@@ -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: ->

View File

@@ -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"}]}]

View File

@@ -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 || "&nbsp;" }}
.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 || "&nbsp;" }}
.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)" } }

View File

@@ -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()"} }
&#10006;

View File

@@ -0,0 +1,11 @@
.autocomplete-template
%span.tag-with-rules{ ng: { if: "data.rules" } }
{{$getDisplayText()}}
%span.tag-with-rules{ ng: { if: "data.rules == 1" } }
&mdash;
= t 'admin.has_one_rule'
%span.tag-with-rules{ ng: { if: "data.rules > 1" } }
&mdash;
= t 'admin.has_n_rules', { num: '{{data.rules}}' }
%span{ ng: { if: "!data.rules" } }
{{$getDisplayText()}}

View File

@@ -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"}

View File

@@ -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.

View File

@@ -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}}"}

View File

@@ -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"}

View File

@@ -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 }}

View File

@@ -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

View File

@@ -0,0 +1,3 @@
.tag-with-rules {
color: black;
}

View File

@@ -37,6 +37,7 @@ text-angular .ta-editor {
input.red {
background-color: #DA5354;
margin-right: 5px;
}
input.search {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()' }

View File

@@ -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?

View File

@@ -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'

View File

@@ -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

View File

@@ -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' } }

View File

@@ -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'"}

View File

@@ -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');

View File

@@ -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"} &copy; OpenStreetMap contributors

View File

@@ -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}

View File

@@ -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}

View File

@@ -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

View File

@@ -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}}"}

View File

@@ -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' }

View File

@@ -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?

View File

@@ -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

View File

@@ -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."

View File

@@ -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 <em>Send Feedback</em> 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)."

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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}"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }

View File

@@ -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!

View File

@@ -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

View File

@@ -10,7 +10,8 @@ describe "AdminSimpleEditOrderCycleCtrl", ->
outgoing_exchange = {}
beforeEach ->
scope = {}
scope =
$watch: jasmine.createSpy('$watch')
location =
absUrl: ->
'example.com/admin/order_cycles/27/edit'

View File

@@ -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')

View File

@@ -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 ->

View File

@@ -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', ->

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)