mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-25 20:46:48 +00:00
Merge pull request #4580 from Matt-Yorkley/order_capture
Use asynchronous requests for order capture and ship actions
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) }
|
||||
|
||||
Reference in New Issue
Block a user