mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
(wip) change order state by admin
This commit is contained in:
parent
947c69c4ed
commit
f015e23a85
@ -15,11 +15,8 @@ class API::OrdersController < API::ApiController
|
||||
def update
|
||||
authorize @order
|
||||
|
||||
if @order.update(order_parameters)
|
||||
render status: :ok
|
||||
else
|
||||
render json: @order.errors.full_messages, status: :unprocessable_entity
|
||||
end
|
||||
@order = ::Orders::OrderService.update_state(@order, current_user, order_params[:state], order_params[:note])
|
||||
render :show
|
||||
end
|
||||
|
||||
def destroy
|
||||
@ -35,6 +32,6 @@ class API::OrdersController < API::ApiController
|
||||
end
|
||||
|
||||
def order_params
|
||||
params.require(:order).permit(:state)
|
||||
params.require(:order).permit(:state, :note)
|
||||
end
|
||||
end
|
||||
|
3
app/exceptions/update_order_state_error.rb
Normal file
3
app/exceptions/update_order_state_error.rb
Normal file
@ -0,0 +1,3 @@
|
||||
# Raised when update order state error
|
||||
class UpdateOrderStateError < StandardError
|
||||
end
|
@ -3,7 +3,7 @@ import { AxiosResponse } from 'axios';
|
||||
import { Order, OrderIndexFilter, OrderIndex } from '../models/order';
|
||||
import ApiLib from '../lib/api';
|
||||
|
||||
export default class ProductAPI {
|
||||
export default class OrderAPI {
|
||||
static async index (filters?: OrderIndexFilter): Promise<OrderIndex> {
|
||||
const res: AxiosResponse<OrderIndex> = await apiClient.get(`/api/orders${ApiLib.filtersToQuery(filters)}`);
|
||||
return res?.data;
|
||||
@ -13,4 +13,9 @@ export default class ProductAPI {
|
||||
const res: AxiosResponse<Order> = await apiClient.get(`/api/orders/${id}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async updateState (order: Order, state: string, note?: string): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.patch(`/api/orders/${order.id}`, { order: { state, note } });
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ interface ShowOrderProps {
|
||||
* Option format, expected by react-select
|
||||
* @see https://github.com/JedWatson/react-select
|
||||
*/
|
||||
type selectOption = { value: number, label: string };
|
||||
type selectOption = { value: string, label: string };
|
||||
|
||||
/**
|
||||
* This component shows an order details
|
||||
@ -35,11 +35,12 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderId, currentUser, onEr
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [order, setOrder] = useState<Order>();
|
||||
const [currentAction, setCurrentAction] = useState<selectOption>();
|
||||
|
||||
useEffect(() => {
|
||||
OrderAPI.get(orderId).then(data => {
|
||||
setOrder(data);
|
||||
});
|
||||
}).catch(onError);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
@ -53,23 +54,43 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderId, currentUser, onEr
|
||||
* Creates sorting options to the react-select format
|
||||
*/
|
||||
const buildOptions = (): Array<selectOption> => {
|
||||
return [
|
||||
{ value: 0, label: t('app.shared.store.show_order.state.error') },
|
||||
{ value: 1, label: t('app.shared.store.show_order.state.canceled') },
|
||||
{ value: 2, label: t('app.shared.store.show_order.state.pending') },
|
||||
{ value: 3, label: t('app.shared.store.show_order.state.under_preparation') },
|
||||
{ value: 4, label: t('app.shared.store.show_order.state.paid') },
|
||||
{ value: 5, label: t('app.shared.store.show_order.state.ready') },
|
||||
{ value: 6, label: t('app.shared.store.show_order.state.collected') },
|
||||
{ value: 7, label: t('app.shared.store.show_order.state.refunded') }
|
||||
];
|
||||
let actions = [];
|
||||
switch (order.state) {
|
||||
case 'paid':
|
||||
actions = actions.concat(['in_progress', 'ready', 'canceled', 'refunded']);
|
||||
break;
|
||||
case 'payment_failed':
|
||||
actions = actions.concat(['canceled']);
|
||||
break;
|
||||
case 'in_progress':
|
||||
actions = actions.concat(['ready', 'canceled', 'refunded']);
|
||||
break;
|
||||
case 'ready':
|
||||
actions = actions.concat(['canceled', 'refunded']);
|
||||
break;
|
||||
case 'canceled':
|
||||
actions = actions.concat(['refunded']);
|
||||
break;
|
||||
default:
|
||||
actions = [];
|
||||
}
|
||||
return actions.map(action => {
|
||||
return { value: action, label: t(`app.shared.store.show_order.state.${action}`) };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback after selecting an action
|
||||
*/
|
||||
const handleAction = (action: selectOption) => {
|
||||
console.log('Action:', action);
|
||||
setCurrentAction(action);
|
||||
OrderAPI.updateState(order, action.value, action.value).then(data => {
|
||||
setOrder(data);
|
||||
setCurrentAction(null);
|
||||
}).catch((e) => {
|
||||
onError(e);
|
||||
setCurrentAction(null);
|
||||
});
|
||||
};
|
||||
|
||||
// Styles the React-select component
|
||||
@ -127,6 +148,7 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderId, currentUser, onEr
|
||||
<Select
|
||||
options={buildOptions()}
|
||||
onChange={option => handleAction(option)}
|
||||
value={currentAction}
|
||||
styles={customStyles}
|
||||
/>
|
||||
}
|
||||
|
@ -55,8 +55,12 @@ export default class OrderLib {
|
||||
switch (order.state) {
|
||||
case 'cart':
|
||||
return 'cart';
|
||||
case 'paid':
|
||||
return 'paid';
|
||||
case 'payment_failed':
|
||||
return 'error';
|
||||
case 'ready':
|
||||
return 'ready';
|
||||
case 'canceled':
|
||||
return 'canceled';
|
||||
case 'in_progress':
|
||||
|
@ -18,6 +18,8 @@
|
||||
.fab-state-label {
|
||||
--status-color: var(--success);
|
||||
&.cart { --status-color: var(--secondary-dark); }
|
||||
&.paid { --status-color: var(--success-light); }
|
||||
&.ready { --status-color: var(--success); }
|
||||
&.error { --status-color: var(--alert); }
|
||||
&.canceled { --status-color: var(--alert-light); }
|
||||
&.pending { --status-color: var(--information); }
|
||||
|
@ -103,6 +103,8 @@
|
||||
.fab-state-label {
|
||||
--status-color: var(--success);
|
||||
&.cart { --status-color: var(--secondary-dark); }
|
||||
&.paid { --status-color: var(--success-light); }
|
||||
&.ready { --status-color: var(--success); }
|
||||
&.error { --status-color: var(--alert); }
|
||||
&.canceled { --status-color: var(--alert-light); }
|
||||
&.pending { --status-color: var(--information); }
|
||||
|
@ -69,6 +69,8 @@ class NotificationType
|
||||
notify_user_is_invalidated
|
||||
notify_user_proof_of_identity_refusal
|
||||
notify_admin_user_proof_of_identity_refusal
|
||||
notify_user_order_is_ready
|
||||
notify_user_order_is_canceled
|
||||
]
|
||||
# deprecated:
|
||||
# - notify_member_subscribed_plan_is_changed
|
||||
|
@ -8,6 +8,7 @@ class Order < PaymentDocument
|
||||
belongs_to :invoice
|
||||
has_many :order_items, dependent: :destroy
|
||||
has_one :payment_gateway_object, as: :item
|
||||
has_many :order_activities, dependent: :destroy
|
||||
|
||||
ALL_STATES = %w[cart paid payment_failed refunded in_progress ready canceled return].freeze
|
||||
enum state: ALL_STATES.zip(ALL_STATES).to_h
|
||||
|
11
app/models/order_activity.rb
Normal file
11
app/models/order_activity.rb
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# OrderActivity is a model for hold activity of order
|
||||
class OrderActivity < ApplicationRecord
|
||||
belongs_to :order
|
||||
|
||||
TYPES = %w[paid payment_failed refunded in_progress ready canceled return note].freeze
|
||||
enum activity_type: TYPES.zip(TYPES).to_h
|
||||
|
||||
validates :activity_type, presence: true
|
||||
end
|
18
app/services/orders/cancel_order_service.rb
Normal file
18
app/services/orders/cancel_order_service.rb
Normal file
@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides methods for cancel an order
|
||||
class Orders::CancelOrderService
|
||||
def call(order, current_user)
|
||||
raise ::UpdateOrderStateError if %w[cart payment_failed canceled refunded].include?(order.state)
|
||||
|
||||
order.state = 'canceled'
|
||||
ActiveRecord::Base.transaction do
|
||||
activity = order.order_activities.create(activity_type: 'canceled', operator_profile_id: current_user.invoicing_profile.id)
|
||||
order.save
|
||||
NotificationCenter.call type: 'notify_user_order_is_canceled',
|
||||
receiver: order.statistic_profile.user,
|
||||
attached_object: activity
|
||||
end
|
||||
order.reload
|
||||
end
|
||||
end
|
18
app/services/orders/order_ready_service.rb
Normal file
18
app/services/orders/order_ready_service.rb
Normal file
@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides methods for set order to ready state
|
||||
class Orders::OrderReadyService
|
||||
def call(order, current_user, note = '')
|
||||
raise ::UpdateOrderStateError if %w[cart payment_failed ready canceled refunded].include?(order.state)
|
||||
|
||||
order.state = 'ready'
|
||||
ActiveRecord::Base.transaction do
|
||||
activity = order.order_activities.create(activity_type: 'ready', operator_profile_id: current_user.invoicing_profile.id, note: note)
|
||||
order.save
|
||||
NotificationCenter.call type: 'notify_user_order_is_ready',
|
||||
receiver: order.statistic_profile.user,
|
||||
attached_object: activity
|
||||
end
|
||||
order.reload
|
||||
end
|
||||
end
|
@ -43,6 +43,12 @@ class Orders::OrderService
|
||||
}
|
||||
end
|
||||
|
||||
def self.update_state(order, current_user, state, note = nil)
|
||||
return ::Orders::SetInProgressService.new.call(order, current_user) if state == 'in_progress'
|
||||
return ::Orders::OrderReadyService.new.call(order, current_user, note) if state == 'ready'
|
||||
return ::Orders::CancelOrderService.new.call(order, current_user) if state == 'canceled'
|
||||
end
|
||||
|
||||
def in_stock?(order, stock_type = 'external')
|
||||
order.order_items.each do |item|
|
||||
return false if item.orderable.stock[stock_type] < item.quantity
|
||||
|
0
app/services/orders/refund_order_service.rb
Normal file
0
app/services/orders/refund_order_service.rb
Normal file
13
app/services/orders/set_in_progress_service.rb
Normal file
13
app/services/orders/set_in_progress_service.rb
Normal file
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides methods for set in progress state to order
|
||||
class Orders::SetInProgressService
|
||||
def call(order, current_user)
|
||||
raise ::UpdateOrderStateError if %w[cart payment_failed in_progress canceled refunded].include?(order.state)
|
||||
|
||||
order.state = 'in_progress'
|
||||
order.order_activities.push(OrderActivity.new(activity_type: 'in_progress', operator_profile_id: current_user.invoicing_profile.id))
|
||||
order.save
|
||||
order.reload
|
||||
end
|
||||
end
|
@ -31,6 +31,7 @@ module Payments::PaymentConcern
|
||||
if payment_id && payment_type
|
||||
order.payment_gateway_object = PaymentGatewayObject.new(gateway_object_id: payment_id, gateway_object_type: payment_type)
|
||||
end
|
||||
order.order_activities.create(activity_type: 'paid')
|
||||
order.order_items.each do |item|
|
||||
ProductService.update_stock(item.orderable, 'external', 'sold', -item.quantity, item.id)
|
||||
end
|
||||
|
@ -0,0 +1,2 @@
|
||||
json.title notification.notification_type
|
||||
json.description t('.order_canceled', REFERENCE: notification.attached_object.order.reference)
|
@ -0,0 +1,2 @@
|
||||
json.title notification.notification_type
|
||||
json.description t('.order_ready', REFERENCE: notification.attached_object.order.reference)
|
@ -0,0 +1,5 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p>
|
||||
<%= t('.body.notify_user_order_is_canceled', REFERENCE: @attached_object.order.reference) %>
|
||||
</p>
|
@ -0,0 +1,8 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p>
|
||||
<%= t('.body.notify_user_order_is_ready', REFERENCE: @attached_object.order.reference) %>
|
||||
</p>
|
||||
<p>
|
||||
<%= @attached_object.note %>
|
||||
</p>
|
@ -407,6 +407,10 @@ en:
|
||||
refusal: "Your proof of identity are not accepted"
|
||||
notify_admin_user_proof_of_identity_refusal:
|
||||
refusal: "Member's proof of identity <strong><em>%{NAME}</strong></em> refused."
|
||||
notify_user_order_is_ready:
|
||||
order_ready: "Your command %{REFERENCE} is ready"
|
||||
notify_user_order_is_canceled:
|
||||
order_canceled: "Your command %{REFERENCE} is canceled"
|
||||
#statistics tools for admins
|
||||
statistics:
|
||||
subscriptions: "Subscriptions"
|
||||
|
@ -374,3 +374,11 @@ en:
|
||||
user_proof_of_identity_files_refusal: "Member %{NAME}'s supporting documents were rejected by %{OPERATOR}:"
|
||||
shared:
|
||||
hello: "Hello %{user_name}"
|
||||
notify_user_order_is_ready:
|
||||
subject: "Your command is ready"
|
||||
body:
|
||||
notify_user_order_is_ready: "Your command %{REFERENCE} is ready:"
|
||||
notify_user_order_is_canceled:
|
||||
subject: "Your command is canceled"
|
||||
body:
|
||||
notify_user_order_is_canceled: "Your command %{REFERENCE} is canceled:"
|
||||
|
12
db/migrate/20220915133100_create_order_activities.rb
Normal file
12
db/migrate/20220915133100_create_order_activities.rb
Normal file
@ -0,0 +1,12 @@
|
||||
class CreateOrderActivities < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :order_activities do |t|
|
||||
t.belongs_to :order, foreign_key: true
|
||||
t.references :operator_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
t.string :activity_type
|
||||
t.text :note
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
15
db/schema.rb
15
db/schema.rb
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2022_09_14_145334) do
|
||||
ActiveRecord::Schema.define(version: 2022_09_15_133100) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "fuzzystrmatch"
|
||||
@ -445,6 +445,17 @@ ActiveRecord::Schema.define(version: 2022_09_14_145334) do
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "order_activities", force: :cascade do |t|
|
||||
t.bigint "order_id"
|
||||
t.bigint "operator_profile_id"
|
||||
t.string "activity_type"
|
||||
t.text "note"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["operator_profile_id"], name: "index_order_activities_on_operator_profile_id"
|
||||
t.index ["order_id"], name: "index_order_activities_on_order_id"
|
||||
end
|
||||
|
||||
create_table "order_items", force: :cascade do |t|
|
||||
t.bigint "order_id"
|
||||
t.string "orderable_type"
|
||||
@ -1170,6 +1181,8 @@ ActiveRecord::Schema.define(version: 2022_09_14_145334) do
|
||||
add_foreign_key "invoices", "statistic_profiles"
|
||||
add_foreign_key "invoices", "wallet_transactions"
|
||||
add_foreign_key "invoicing_profiles", "users"
|
||||
add_foreign_key "order_activities", "invoicing_profiles", column: "operator_profile_id"
|
||||
add_foreign_key "order_activities", "orders"
|
||||
add_foreign_key "order_items", "orders"
|
||||
add_foreign_key "orders", "coupons"
|
||||
add_foreign_key "orders", "invoices"
|
||||
|
Loading…
x
Reference in New Issue
Block a user