Merge pull request #4580 from Matt-Yorkley/order_capture

Use asynchronous requests for order capture and ship actions
This commit is contained in:
Luis Ramos
2020-01-13 17:14:40 +00:00
committed by GitHub
11 changed files with 187 additions and 22 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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