diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index 763e92b400..9e1c027531 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -1,43 +1,28 @@ -angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, TagRuleResource, $q, Columns, pendingChanges, shops) -> - $scope.shop = {} +angular.module("admin.customers").controller "customersCtrl", ($scope, $q, Customers, TagRuleResource, CurrentShop, RequestMonitor, Columns, pendingChanges, shops) -> $scope.shops = shops + $scope.CurrentShop = CurrentShop + $scope.RequestMonitor = RequestMonitor $scope.submitAll = pendingChanges.submitAll + $scope.add = Customers.add + $scope.deleteCustomer = Customers.remove + $scope.customerLimit = 20 $scope.columns = Columns.setColumns email: { name: "Email", visible: true } code: { name: "Code", visible: true } tags: { name: "Tags", visible: true } - $scope.$watch "shop.id", -> - if $scope.shop.id? - $scope.customers = index {enterprise_id: $scope.shop.id} + $scope.$watch "CurrentShop.shop", -> + if $scope.CurrentShop.shop.id? + Customers.index({enterprise_id: $scope.CurrentShop.shop.id}).then (data) -> + $scope.customers = data $scope.findTags = (query) -> defer = $q.defer() params = - enterprise_id: $scope.shop.id + enterprise_id: $scope.CurrentShop.shop.id TagRuleResource.mapByTag 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 - email: email - CustomerResource.create params, (customer) => - if customer.id - $scope.customers.push customer - $scope.quickSearch = customer.email - - $scope.deleteCustomer = (customer) -> - params = id: customer.id - CustomerResource.destroy params, -> - i = $scope.customers.indexOf customer - $scope.customers.splice i, 1 unless i < 0 - - index = (params) -> - $scope.loaded = false - CustomerResource.index params, => - $scope.loaded = true diff --git a/app/assets/javascripts/admin/customers/customers.js.coffee b/app/assets/javascripts/admin/customers/customers.js.coffee index 33be58c9ac..fe8ae1de5b 100644 --- a/app/assets/javascripts/admin/customers/customers.js.coffee +++ b/app/assets/javascripts/admin/customers/customers.js.coffee @@ -1 +1 @@ -angular.module("admin.customers", ['ngResource', 'admin.tagRules', 'admin.indexUtils', 'admin.utils', 'admin.dropdown']) \ No newline at end of file +angular.module("admin.customers", ['ngResource', 'admin.tagRules', 'admin.indexUtils', 'admin.utils', 'admin.dropdown']) diff --git a/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee b/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee new file mode 100644 index 0000000000..02276875b9 --- /dev/null +++ b/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee @@ -0,0 +1,46 @@ +angular.module("admin.customers").directive 'newCustomerDialog', ($compile, $injector, $templateCache, $window, CurrentShop, Customers) -> + restrict: 'A' + scope: true + link: (scope, element, attr) -> + scope.CurrentShop = CurrentShop + scope.submitted = null + scope.email = "" + scope.errors = [] + + scope.addCustomer = (valid) -> + scope.submitted = scope.email + scope.errors = [] + if valid + Customers.add(scope.email).$promise.then (data) -> + if data.id + scope.email = "" + scope.submitted = null + template.dialog('close') + , (response) -> + if response.data.errors + scope.errors.push(error) for error in response.data.errors + else + scope.errors.push("Sorry! Could not create '#{scope.email}'") + return + + # Compile modal template + template = $compile($templateCache.get('admin/new_customer_dialog.html'))(scope) + + # Set Dialog options + template.dialog + show: { effect: "fade", duration: 400 } + hide: { effect: "fade", duration: 300 } + autoOpen: false + resizable: false + width: $window.innerWidth * 0.4; + modal: true + open: (event, ui) -> + $('.ui-widget-overlay').bind 'click', -> + $(this).siblings('.ui-dialog').find('.ui-dialog-content').dialog('close') + + # Link opening of dialog to click event on element + element.bind 'click', (e) -> + if CurrentShop.shop.id + template.dialog('open') + else + alert('Please select a shop first') diff --git a/app/assets/javascripts/admin/customers/services/current_enterprise.js.coffee b/app/assets/javascripts/admin/customers/services/current_enterprise.js.coffee new file mode 100644 index 0000000000..9df7e09895 --- /dev/null +++ b/app/assets/javascripts/admin/customers/services/current_enterprise.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.customers").factory "CurrentShop", -> + new class CurrentShop + shop: {} diff --git a/app/assets/javascripts/admin/customers/services/customers.js.coffee b/app/assets/javascripts/admin/customers/services/customers.js.coffee new file mode 100644 index 0000000000..a9f4f0102f --- /dev/null +++ b/app/assets/javascripts/admin/customers/services/customers.js.coffee @@ -0,0 +1,21 @@ +angular.module("admin.customers").factory "Customers", ($q, RequestMonitor, CustomerResource, CurrentShop) -> + new class Customers + customers: [] + + add: (email) -> + params = + enterprise_id: CurrentShop.shop.id + email: email + CustomerResource.create params, (customer) => + @customers.unshift customer if customer.id + + remove: (customer) -> + params = id: customer.id + CustomerResource.destroy params, => + i = @customers.indexOf customer + @customers.splice i, 1 unless i < 0 + + index: (params) -> + request = CustomerResource.index(params, (data) => @customers = data) + RequestMonitor.load(request.$promise) + request.$promise diff --git a/app/assets/javascripts/admin/index_utils/directives/show_more.js.coffee b/app/assets/javascripts/admin/index_utils/directives/show_more.js.coffee new file mode 100644 index 0000000000..a6e4efa333 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/show_more.js.coffee @@ -0,0 +1,12 @@ +angular.module("admin.indexUtils").component 'showMore', + templateUrl: 'admin/show_more.html' + bindings: + data: "=" + limit: "=" + increment: "=" + +# For now, this component is not being used. +# Something about binding "data" to a variable on the parent scope that is continually refreshed by +# being assigned within an ng-repeat means that we get $digest iteration errors. Seems to be solved +# by using the new "as" syntax for ng-repeat to assign and alias the outcome of the filters, but this +# has the limitation of not being able to be limited AFTER the assignment has been made, which we need diff --git a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee index 5e5b5cadf2..1ea74e614b 100644 --- a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee +++ b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee @@ -1 +1 @@ -angular.module("admin.indexUtils", ['ngResource', 'ngSanitize', 'templates']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file +angular.module("admin.indexUtils", ['ngResource', 'ngSanitize', 'templates', 'admin.utils']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file diff --git a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee index 2f40a7faef..15626b1479 100644 --- a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee @@ -1,10 +1,12 @@ -angular.module("admin.indexUtils").factory "pendingChanges", (resources) -> +angular.module("admin.indexUtils").factory "pendingChanges", ($q, resources, StatusMessage) -> new class pendingChanges pendingChanges: {} + errors: [] add: (id, attr, change) => @pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}") @pendingChanges["#{id}"]["#{attr}"] = change + StatusMessage.display('notice', "You have made #{@changeCount(@pendingChanges)} unsaved changes") removeAll: => @pendingChanges = {} @@ -14,11 +16,19 @@ angular.module("admin.indexUtils").factory "pendingChanges", (resources) -> delete @pendingChanges["#{id}"]["#{attr}"] delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 - submitAll: => + submitAll: (form=null) => all = [] + @errors = [] + StatusMessage.display('progress', "Saving...") for id, objectChanges of @pendingChanges for attrName, change of objectChanges all.push @submit(change) + $q.all(all).then => + if @errors.length == 0 + StatusMessage.display('success', "All changes saved successfully") + form.$setPristine() if form? + else + StatusMessage.display('failure', "Oh no! I was unable to save your changes") all submit: (change) -> @@ -26,7 +36,8 @@ angular.module("admin.indexUtils").factory "pendingChanges", (resources) -> @remove change.object.id, change.attr change.scope.reset( data["#{change.attr}"] ) change.scope.success() - , (error) -> + , (error) => + @errors.push error change.scope.error() changeCount: (objectChanges) -> diff --git a/app/assets/javascripts/templates/admin/new_customer_dialog.html.haml b/app/assets/javascripts/templates/admin/new_customer_dialog.html.haml new file mode 100644 index 0000000000..e30cfcd607 --- /dev/null +++ b/app/assets/javascripts/templates/admin/new_customer_dialog.html.haml @@ -0,0 +1,15 @@ +#new-customer-dialog + .text-normal.margin-bottom-30.text-center + = t('admin.customers.index.add_a_new_customer_for', shop_name: "{{ CurrentShop.shop.name }}:") + + %form{ name: 'new_customer_form', novalidate: true } + + .text-center.margin-bottom-30 + %input.fullwidth{ type: 'email', name: 'email', required: true, placeholder: t('admin.customers.index.customer_placeholder'), ng: { model: "email" } } + %div{ ng: { show: "email == submitted" } } + .error{ ng: { show: "(new_customer_form.email.$error.email || new_customer_form.email.$error.required)" } } + = t('admin.customers.index.valid_email_error') + .error{ ng: { repeat: "error in errors", bind: "error" } } + + .text-center + %input.button.red.icon-plus{ type: 'submit', value: t('admin.customers.index.add_customer'), ng: { click: 'addCustomer(new_customer_form.email.$valid)' } } diff --git a/app/assets/javascripts/templates/admin/show_more.html.haml b/app/assets/javascripts/templates/admin/show_more.html.haml new file mode 100644 index 0000000000..f9a008993c --- /dev/null +++ b/app/assets/javascripts/templates/admin/show_more.html.haml @@ -0,0 +1,4 @@ +%div{ ng: { show: "data.length > limit" } } + %input{ type: 'button', value: 'Show More', ng: { click: 'limit = limit + increment' } } + or + %input{ type: 'button', value: "Show All ({{ data.length - limit }} More)", ng: { click: 'limit = data.length' } } diff --git a/app/assets/stylesheets/admin/components/save_bar.sass b/app/assets/stylesheets/admin/components/save_bar.sass index 87dcce82f9..f453c5ddfc 100644 --- a/app/assets/stylesheets/admin/components/save_bar.sass +++ b/app/assets/stylesheets/admin/components/save_bar.sass @@ -1,6 +1,7 @@ #save-bar position: fixed width: 100% + z-index: 100 bottom: 0px left: 0 padding: 8px 8px diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 1fc95face8..293549d0f1 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -28,6 +28,9 @@ text-angular .ta-editor { left: 275px; } +span.error, div.error { + color: #DA5354; +} /* Fix conflict between Spree and elRTE's styles */ .el-rte .toolbar { diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index 761ccbc014..23cb9d8fff 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -13,10 +13,6 @@ input.show-dirty { } } -span.error { - color: #DA5354; -} - input, div { &.update-error { border: solid 1px #DA5354; diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index 9d3bb18017..fe975aa038 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -16,9 +16,12 @@ module Admin def create @customer = Customer.new(params[:customer]) if user_can_create_customer? - @customer.save - tag_rule_mapping = TagRule.mapping_for(Enterprise.where(id: @customer.enterprise)) - render_as_json @customer, tag_rule_mapping: tag_rule_mapping + if @customer.save + tag_rule_mapping = TagRule.mapping_for(Enterprise.where(id: @customer.enterprise)) + render_as_json @customer, tag_rule_mapping: tag_rule_mapping + else + render json: { errors: @customer.errors.full_messages }, status: 400 + end else redirect_to '/unauthorized' end diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index dc38304f8e..ef3ef7e7f0 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -2,35 +2,43 @@ %h1.page-title =t :customers +- content_for :app_wrapper_attrs do + = "ng-app='admin.customers'" + +- content_for :page_actions do + %li + %a.button.icon-plus#new-customer{ href: "#", "new-customer-dialog" => true } + = t('admin.customers.index.new_customer') + = admin_inject_shops -%div{ ng: { app: 'admin.customers', controller: 'customersCtrl' } } - .row{ ng: { hide: "loaded && customers.length > 0" } } +%div{ ng: { controller: 'customersCtrl' } } + .row{ ng: { hide: "!RequestMonitor.loading && customers.length > 0" } } .five.columns.alpha %h3 =t :please_select_hub .four.columns - %select.select2.fullwidth#shop_id{ 'ng-model' => 'shop.id', name: 'shop_id', 'ng-options' => 'shop.id as shop.name for shop in shops' } + %select.select2.fullwidth#shop_id{ 'ng-model' => 'CurrentShop.shop', name: 'shop_id', 'ng-options' => 'shop as shop.name for shop in shops' } .seven.columns.omega   - .row{ 'ng-hide' => '!loaded || customers.length == 0' } + .row{ 'ng-hide' => 'RequestMonitor.loading || !CurrentShop.shop.id || customers.length == 0' } .controls.sixteen.columns.alpha.omega .five.columns.alpha %input.fullwidth{ :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' } .eight.columns   = render 'admin/shared/columns_dropdown' - .row{ 'ng-if' => 'shop.id && !loaded' } + .row{ 'ng-if' => 'CurrentShop.shop.id && RequestMonitor.loading' } .sixteen.columns.alpha#loading %img.spinner{ src: "/assets/spinning-circles.svg" } %h1 =t :loading_customers - .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded && filteredCustomers.length == 0'} + .row{ :class => "sixteen columns alpha", 'ng-show' => '!RequestMonitor.loading && filteredCustomers.length == 0'} %h1#no_results =t :no_customers_found - - .row{ ng: { show: "loaded && filteredCustomers.length > 0" } } + .row.margin-bottom-50{ ng: { show: "!RequestMonitor.loading && filteredCustomers.length > 0" } } %form{ name: "customers_form" } + %save-bar{ save: "submitAll(customers_form)", form: "customers_form" } %table.index#customers %col.email{ width: "20%"} %col.code{ width: "20%"} @@ -48,10 +56,10 @@ %th.actions Ask?  %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } - %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" } + %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse ) | limitTo:customerLimit track by customer.id", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" } -# %td.bulk -# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } - %td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }} + %td.email{ 'ng-show' => 'columns.email.visible', "ng-bind" => '::customer.email' } %td.code{ 'ng-show' => 'columns.code.visible' } %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' } @@ -59,12 +67,9 @@ %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()' } - %form{ng: {show: "loaded", submit: 'add(newCustomerEmail)'}} - %h2= t '.add_new_customer' - .row - .five.columns.alpha - %input.fullwidth{type: "text", placeholder: t('.customer_placeholder'), ng: {model: 'newCustomerEmail'}} - .eleven.columns.omega - %input{type: "submit", value: t('.add_customer')} + -# %show-more.text-center{ data: "filteredCustomers", limit: "customerLimit", increment: "20" } + %div.text-center{ ng: { show: "filteredCustomers.length > customerLimit" } } + %input{ type: 'button', value: 'Show More', ng: { click: 'customerLimit = customerLimit + 20' } } + or + %input{ type: 'button', value: "Show All ({{ filteredCustomers.length - customerLimit }} More)", ng: { click: 'customerLimit = filteredCustomers.length' } } diff --git a/app/views/admin/variant_overrides/_show_more.html.haml b/app/views/admin/variant_overrides/_show_more.html.haml index 8d60593ddc..21e927e443 100644 --- a/app/views/admin/variant_overrides/_show_more.html.haml +++ b/app/views/admin/variant_overrides/_show_more.html.haml @@ -1,4 +1,5 @@ -.text-center +-# %show-more.text-center{ data: "filteredProducts", limit: "productLimit", increment: "10" } +.text-center{ ng: { show: "filteredProducts.length > productLimit" } } %input{ type: 'button', value: 'Show More', ng: { click: 'productLimit = productLimit + 10' } } or %input{ type: 'button', value: "Show All ({{ filteredProducts.length - productLimit }} More)", ng: { click: 'productLimit = filteredProducts.length' } } diff --git a/config/locales/en.yml b/config/locales/en.yml index 79f6e471b1..02d66a5354 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -91,8 +91,11 @@ en: customers: index: - add_customer: "Add customer" + add_customer: "Add Customer" + new_customer: "New Customer" customer_placeholder: "customer@example.org" + valid_email_error: Please enter a valid email address + add_a_new_customer_for: Add a new customer for %{shop_name} inventory: title: Inventory description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page diff --git a/spec/features/admin/customers_spec.rb b/spec/features/admin/customers_spec.rb index 5d2ffedb4e..964608510d 100644 --- a/spec/features/admin/customers_spec.rb +++ b/spec/features/admin/customers_spec.rb @@ -9,7 +9,7 @@ feature 'Customers' do let(:managed_distributor) { create(:distributor_enterprise, owner: user) } let(:unmanaged_distributor) { create(:distributor_enterprise) } - describe "using the customers index" do + describe "using the customers index", js: true do let!(:customer1) { create(:customer, enterprise: managed_distributor) } let!(:customer2) { create(:customer, enterprise: managed_distributor) } let!(:customer3) { create(:customer, enterprise: unmanaged_distributor) } @@ -19,7 +19,7 @@ feature 'Customers' do visit admin_customers_path end - it "passes the smoke test", js: true do + it "passes the smoke test" do # Prompts for a hub for a list of my managed enterprises expect(page).to have_select2 "shop_id", with_options: [managed_distributor.name], without_options: [unmanaged_distributor.name] @@ -45,7 +45,7 @@ feature 'Customers' do expect(page).to_not have_content customer1.email end - it "allows updating of attributes", js: true do + it "allows updating of attributes" do select2_select managed_distributor.name, from: "shop_id" within "tr#c_#{customer1.id}" do @@ -56,7 +56,7 @@ feature 'Customers' do find(:css, "tags-input .tags input").set "awesome\n" expect(page).to have_css ".tag_watcher.update-pending" end - click_button "Update" + click_button "Save Changes" # Every says it updated expect(page).to have_css "input#code.update-success" @@ -66,6 +66,46 @@ feature 'Customers' do expect(customer1.reload.code).to eq "new-customer-code" expect(customer1.tag_list).to eq ["awesome"] end + + describe "creating a new customer" do + context "when no shop has been selected" do + it "asks the user to select a shop" do + accept_alert 'Please select a shop first' do + click_link('New Customer') + end + end + end + + context "when a shop is selected" do + before do + select2_select managed_distributor.name, from: "shop_id" + end + + it "creates customers when the email provided is valid" do + # When an invalid email is used + expect{ + click_link('New Customer') + fill_in 'email', with: "not_an_email" + click_button 'Add Customer' + expect(page).to have_selector "#new-customer-dialog .error", text: "Please enter a valid email address" + }.to_not change{Customer.of(managed_distributor).count} + + # When an existing email is used + expect{ + fill_in 'email', with: customer1.email + click_button 'Add Customer' + expect(page).to have_selector "#new-customer-dialog .error", text: "Email is associated with an existing customer" + }.to_not change{Customer.of(managed_distributor).count} + + # When a new valid email is used + expect{ + fill_in 'email', with: "new@email.com" + click_button 'Add Customer' + expect(page).not_to have_selector "#new-customer-dialog" + }.to change{Customer.of(managed_distributor).count}.from(2).to(3) + end + end + end end end end diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee index b0991c7df1..796dc93ebf 100644 --- a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -14,7 +14,7 @@ describe "CustomersCtrl", -> { pass: angular.equals(actual, expected) } it "has no shop pre-selected", -> - expect(scope.shop).toEqual {} + expect(scope.CurrentShop.shop).toEqual {} describe "setting the shop on scope", -> customer = { id: 5, email: 'someone@email.com'} @@ -23,7 +23,7 @@ describe "CustomersCtrl", -> beforeEach -> http.expectGET('/admin/customers.json?enterprise_id=1').respond 200, customers scope.$apply -> - scope.shop = {id: 1} + scope.CurrentShop.shop = {id: 1} http.flush() it "retrievs the list of customers", -> @@ -33,7 +33,7 @@ describe "CustomersCtrl", -> it "creates a new customer", -> email = "customer@example.org" newCustomer = {id: 6, email: email} - customers.push(newCustomer) + customers.unshift(newCustomer) http.expectPOST('/admin/customers.json?email=' + email + '&enterprise_id=1').respond 200, newCustomer scope.add(email) http.flush()