diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 1b1c16d1ab..a2eb2323cb 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, Orders, SortOptions, $window, $filter) -> +angular.module("admin.orders").controller "ordersCtrl", ($scope, $timeout, RequestMonitor, Orders, SortOptions, $window, $filter) -> $scope.RequestMonitor = RequestMonitor $scope.pagination = Orders.pagination $scope.orders = Orders.all @@ -13,6 +13,7 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $scope.selected = false $scope.select_all = false $scope.poll = 0 + $scope.rowStatus = {} $scope.initialise = -> $scope.per_page = 15 @@ -69,6 +70,23 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, $scope.fetchResults() , true + $scope.capturePayment = (order) -> + $scope.rowAction('capture', order) + + $scope.shipOrder = (order) -> + $scope.rowAction('ship', order) + + $scope.rowAction = (action, order) -> + $scope.rowStatus[order.id] = "loading" + + Orders[action](order).$promise.then (data) -> + $scope.rowStatus[order.id] = "success" + $timeout(-> + $scope.rowStatus[order.id] = null + , 1500) + , (error) -> + $scope.rowStatus[order.id] = "error" + $scope.changePage = (newPage) -> $scope.page = newPage $scope.fetchResults(newPage) diff --git a/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee b/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee index 9bf7ad5838..534833d1c9 100644 --- a/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee +++ b/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee @@ -5,4 +5,14 @@ angular.module("admin.resources").factory 'OrderResource', ($resource) -> method: 'GET' 'update': method: 'PUT' + 'capture': + url: '/api/orders/:id/capture.json' + method: 'PUT' + params: + id: '@id' + 'ship': + url: '/api/orders/:id/ship.json' + method: 'PUT' + params: + id: '@id' }) diff --git a/app/assets/javascripts/admin/resources/services/orders.js.coffee b/app/assets/javascripts/admin/resources/services/orders.js.coffee index 3d266aa48d..1a001e9f7f 100644 --- a/app/assets/javascripts/admin/resources/services/orders.js.coffee +++ b/app/assets/javascripts/admin/resources/services/orders.js.coffee @@ -44,5 +44,19 @@ angular.module("admin.resources").factory 'Orders', ($q, OrderResource, RequestM changed.push attr unless attr is "$$hashKey" changed + capture: (order) -> + @processAction('capture', order) + + ship: (order) -> + @processAction('ship', order) + + processAction: (action, order) -> + OrderResource[action] {id: order.number}, (data) => + if data.id + angular.merge(order, data) + data + , (response) => + response.data + resetAttribute: (order, attribute) -> order[attribute] = @pristineByID[order.id][attribute] diff --git a/app/assets/stylesheets/admin/components/table_loading.css.scss b/app/assets/stylesheets/admin/components/table_loading.css.scss new file mode 100644 index 0000000000..7aededd14b --- /dev/null +++ b/app/assets/stylesheets/admin/components/table_loading.css.scss @@ -0,0 +1,32 @@ +@import '../variables'; + +.row-loading { + opacity: .5; +} + +.row-loading-icons { + margin-left: 3em; + position: absolute; + + .spinner { + border: 0; + width: 2.3em; + } + + i { + font-size: 2.3em; + opacity: .75; + + &::before { + vertical-align: top; + } + + &.success { + color: $spree-green; + } + + &.error { + color: $warning-red; + } + } +} diff --git a/app/controllers/api/orders_controller.rb b/app/controllers/api/orders_controller.rb index 9b7bf9b1d6..3be0910f10 100644 --- a/app/controllers/api/orders_controller.rb +++ b/app/controllers/api/orders_controller.rb @@ -16,8 +16,38 @@ module Api } end + def ship + authorize! :admin, order + + if order.ship + render json: order.reload, serializer: Api::Admin::OrderSerializer, status: :ok + else + render json: { error: I18n.t('api.orders.failed_to_update') }, status: :unprocessable_entity + end + end + + def capture + authorize! :admin, order + + pending_payment = order.pending_payments.first + + return payment_capture_failed unless order.payment_required? && pending_payment + + if pending_payment.capture! + render json: order.reload, serializer: Api::Admin::OrderSerializer, status: :ok + else + payment_capture_failed + end + rescue Spree::Core::GatewayError => e + error_during_processing(e) + end + private + def payment_capture_failed + render json: { error: t(:payment_processing_failed) }, status: :unprocessable_entity + end + def serialized_orders(orders) ActiveModel::ArraySerializer.new( orders, diff --git a/app/serializers/api/admin/order_serializer.rb b/app/serializers/api/admin/order_serializer.rb index 342490d90e..d6888994d3 100644 --- a/app/serializers/api/admin/order_serializer.rb +++ b/app/serializers/api/admin/order_serializer.rb @@ -1,8 +1,8 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer attributes :id, :number, :user_id, :full_name, :email, :phone, :completed_at, :display_total, :edit_path, :state, :payment_state, :shipment_state, - :payments_path, :ship_path, :ready_to_ship, :created_at, - :distributor_name, :special_instructions, :payment_capture_path, + :payments_path, :ready_to_ship, :ready_to_capture, :created_at, + :distributor_name, :special_instructions, :item_total, :adjustment_total, :payment_total, :total has_one :distributor, serializer: Api::Admin::IdSerializer @@ -28,15 +28,9 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer spree_routes_helper.admin_order_payments_path(object) end - def ship_path - spree_routes_helper.fire_admin_order_path(object, e: 'ship') - end - - def payment_capture_path + def ready_to_capture pending_payment = object.pending_payments.first - return '' unless object.payment_required? && pending_payment - - spree_routes_helper.fire_admin_order_payment_path(object, pending_payment.id, e: 'capture') + object.payment_required? && pending_payment end def ready_to_ship diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index b693582db0..80fe18217d 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -47,7 +47,7 @@ %th.actions %tbody - %tr{ng: {repeat: 'order in orders track by order.id', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "'state-{{order.state}}'"} + %tr{ng: {repeat: 'order in orders track by order.id', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "{'state-{{order.state}}': true, 'row-loading': rowStatus[order.id] == 'loading'}"} %td.align-center %input{type: 'checkbox', 'ng-model' => 'checkboxes[order.id]', 'ng-change' => 'toggleSelection(order.id)'} %td.align-center @@ -78,11 +78,15 @@ %td.align-center %span{'ng-bind-html' => 'order.display_total'} %td.actions + %div.row-loading-icons + %img.spinner{src: "/assets/spinning-circles.svg", ng: {show: 'rowStatus[order.id] == "loading"'} } + %i.success.icon-ok-sign{ng: {show: 'rowStatus[order.id] == "success"'} } + %i.error.icon-remove-sign.with-tip{ng: {show: 'rowStatus[order.id] == "error"'}, 'ofn-with-tip' => t('.order_not_updated')} %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t('.edit')} %div{'ng-if' => 'order.ready_to_ship'} - %a.icon-road.icon_link.with-tip.no-text{'ng-href' => '{{order.ship_path}}', 'data-action' => 'ship', 'data-confirm' => t(:are_you_sure), 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.ship')} - %div{'ng-if' => 'order.payment_capture_path'} - %a.icon-capture.icon_link.no-text{'ng-href' => '{{order.payment_capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.capture')} + %button.icon-road.icon_link.with-tip.no-text{'ng-click' => 'shipOrder(order)', 'data-confirm' => t(:are_you_sure), rel: 'nofollow', 'ofn-with-tip' => t('.ship')} + %div{'ng-if' => 'order.ready_to_capture'} + %button.icon-capture.icon_link.no-text{'ng-click' => 'capturePayment(order)', rel: 'nofollow', 'ofn-with-tip' => t('.capture')} .orders-loading{'ng-show' => 'RequestMonitor.loading'} .row diff --git a/config/locales/en.yml b/config/locales/en.yml index 702f95df75..fb0ec30859 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1169,6 +1169,8 @@ en: destroy_attachment_does_not_exist: "Logo does not exist" enterprise_promo_image: destroy_attachment_does_not_exist: "Promo image does not exist" + orders: + failed_to_update: "Failed to update order" # Frontend views # @@ -3069,6 +3071,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using capture: "Capture" ship: "Ship" edit: "Edit" + order_not_updated: "The order could not be updated" note: "Note" first: "First" last: "Last" diff --git a/config/routes/api.rb b/config/routes/api.rb index 1861e084a2..59e256acac 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -16,6 +16,11 @@ Openfoodnetwork::Application.routes.draw do resources :variants, :only => [:index] resources :orders, only: [:index, :show] do + member do + put :capture + put :ship + end + resources :shipments, :only => [:create, :update] do member do put :ready diff --git a/spec/controllers/api/orders_controller_spec.rb b/spec/controllers/api/orders_controller_spec.rb index 05f9743303..5e463fe7a2 100644 --- a/spec/controllers/api/orders_controller_spec.rb +++ b/spec/controllers/api/orders_controller_spec.rb @@ -255,15 +255,58 @@ module Api expect(json_response[:line_items].first[:variant][:product_name]). to eq order.line_items.first.variant.product.name end end + end - def expect_order - expect(response.status).to eq 200 - expect(json_response[:number]).to eq order.number + describe "#capture and #ship actions" do + let(:user) { create(:user) } + let(:product) { create(:simple_product) } + let(:distributor) { create(:distributor_enterprise, owner: user) } + let(:order_cycle) { + create(:simple_order_cycle, + distributors: [distributor], variants: [product.variants.first]) + } + let!(:order) { + create(:order_with_totals_and_distribution, + user: user, distributor: distributor, order_cycle: order_cycle, + state: 'complete', payment_state: 'balance_due') + } + + before do + order.finalize! + create(:check_payment, order: order, amount: order.total) + allow(controller).to receive(:spree_current_user) { order.distributor.owner } + end + + describe "#capture" do + it "captures payments and returns an updated order object" do + put :capture, id: order.number + + expect(order.reload.pending_payments.empty?).to be true + expect_order + end + end + + describe "#ship" do + before do + order.payments.first.capture! + end + + it "marks orders as shipped and returns an updated order object" do + put :ship, id: order.number + + expect(order.reload.shipments.any?(&:shipped?)).to be true + expect_order + end end end private + def expect_order + expect(response.status).to eq 200 + expect(json_response[:number]).to eq order.number + end + def serialized_orders(orders) serialized_orders = ActiveModel::ArraySerializer.new( orders, @@ -283,8 +326,8 @@ module Api [ :id, :number, :full_name, :email, :phone, :completed_at, :display_total, :edit_path, :state, :payment_state, :shipment_state, - :payments_path, :ship_path, :ready_to_ship, :created_at, - :distributor_name, :special_instructions, :payment_capture_path + :payments_path, :ready_to_ship, :ready_to_capture, :created_at, + :distributor_name, :special_instructions ] end diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index 3776079136..09bec96ccf 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -187,9 +187,10 @@ feature ' expect(page).to have_current_path spree.admin_orders_path # click the 'capture' link for the order - page.find("[data-action=capture][href*=#{@order.number}]").click + page.find("[data-powertip=Capture]").click - expect(page).to have_content "Payment Updated" + expect(page).to have_css "i.success" + expect(page).to have_css "button.icon-road" # check the order was captured expect(@order.reload.payment_state).to eq "paid" @@ -198,6 +199,17 @@ feature ' expect(page).to have_current_path spree.admin_orders_path end + scenario "ship order from the orders index page" do + @order.payments.first.capture! + quick_login_as_admin + visit spree.admin_orders_path + + page.find("[data-powertip=Ship]").click + + expect(page).to have_css "i.success" + expect(@order.reload.shipments.any?(&:shipped?)).to be true + end + context "as an enterprise manager" do let(:coordinator1) { create(:distributor_enterprise) } let(:coordinator2) { create(:distributor_enterprise) }