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
|
def update
|
||||||
authorize @order
|
authorize @order
|
||||||
|
|
||||||
if @order.update(order_parameters)
|
@order = ::Orders::OrderService.update_state(@order, current_user, order_params[:state], order_params[:note])
|
||||||
render status: :ok
|
render :show
|
||||||
else
|
|
||||||
render json: @order.errors.full_messages, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@ -35,6 +32,6 @@ class API::OrdersController < API::ApiController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def order_params
|
def order_params
|
||||||
params.require(:order).permit(:state)
|
params.require(:order).permit(:state, :note)
|
||||||
end
|
end
|
||||||
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 { Order, OrderIndexFilter, OrderIndex } from '../models/order';
|
||||||
import ApiLib from '../lib/api';
|
import ApiLib from '../lib/api';
|
||||||
|
|
||||||
export default class ProductAPI {
|
export default class OrderAPI {
|
||||||
static async index (filters?: OrderIndexFilter): Promise<OrderIndex> {
|
static async index (filters?: OrderIndexFilter): Promise<OrderIndex> {
|
||||||
const res: AxiosResponse<OrderIndex> = await apiClient.get(`/api/orders${ApiLib.filtersToQuery(filters)}`);
|
const res: AxiosResponse<OrderIndex> = await apiClient.get(`/api/orders${ApiLib.filtersToQuery(filters)}`);
|
||||||
return res?.data;
|
return res?.data;
|
||||||
@ -13,4 +13,9 @@ export default class ProductAPI {
|
|||||||
const res: AxiosResponse<Order> = await apiClient.get(`/api/orders/${id}`);
|
const res: AxiosResponse<Order> = await apiClient.get(`/api/orders/${id}`);
|
||||||
return res?.data;
|
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
|
* Option format, expected by react-select
|
||||||
* @see https://github.com/JedWatson/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
|
* This component shows an order details
|
||||||
@ -35,11 +35,12 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderId, currentUser, onEr
|
|||||||
const { t } = useTranslation('shared');
|
const { t } = useTranslation('shared');
|
||||||
|
|
||||||
const [order, setOrder] = useState<Order>();
|
const [order, setOrder] = useState<Order>();
|
||||||
|
const [currentAction, setCurrentAction] = useState<selectOption>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
OrderAPI.get(orderId).then(data => {
|
OrderAPI.get(orderId).then(data => {
|
||||||
setOrder(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
|
* Creates sorting options to the react-select format
|
||||||
*/
|
*/
|
||||||
const buildOptions = (): Array<selectOption> => {
|
const buildOptions = (): Array<selectOption> => {
|
||||||
return [
|
let actions = [];
|
||||||
{ value: 0, label: t('app.shared.store.show_order.state.error') },
|
switch (order.state) {
|
||||||
{ value: 1, label: t('app.shared.store.show_order.state.canceled') },
|
case 'paid':
|
||||||
{ value: 2, label: t('app.shared.store.show_order.state.pending') },
|
actions = actions.concat(['in_progress', 'ready', 'canceled', 'refunded']);
|
||||||
{ value: 3, label: t('app.shared.store.show_order.state.under_preparation') },
|
break;
|
||||||
{ value: 4, label: t('app.shared.store.show_order.state.paid') },
|
case 'payment_failed':
|
||||||
{ value: 5, label: t('app.shared.store.show_order.state.ready') },
|
actions = actions.concat(['canceled']);
|
||||||
{ value: 6, label: t('app.shared.store.show_order.state.collected') },
|
break;
|
||||||
{ value: 7, label: t('app.shared.store.show_order.state.refunded') }
|
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
|
* Callback after selecting an action
|
||||||
*/
|
*/
|
||||||
const handleAction = (action: selectOption) => {
|
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
|
// Styles the React-select component
|
||||||
@ -127,6 +148,7 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderId, currentUser, onEr
|
|||||||
<Select
|
<Select
|
||||||
options={buildOptions()}
|
options={buildOptions()}
|
||||||
onChange={option => handleAction(option)}
|
onChange={option => handleAction(option)}
|
||||||
|
value={currentAction}
|
||||||
styles={customStyles}
|
styles={customStyles}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -55,8 +55,12 @@ export default class OrderLib {
|
|||||||
switch (order.state) {
|
switch (order.state) {
|
||||||
case 'cart':
|
case 'cart':
|
||||||
return 'cart';
|
return 'cart';
|
||||||
|
case 'paid':
|
||||||
|
return 'paid';
|
||||||
case 'payment_failed':
|
case 'payment_failed':
|
||||||
return 'error';
|
return 'error';
|
||||||
|
case 'ready':
|
||||||
|
return 'ready';
|
||||||
case 'canceled':
|
case 'canceled':
|
||||||
return 'canceled';
|
return 'canceled';
|
||||||
case 'in_progress':
|
case 'in_progress':
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
.fab-state-label {
|
.fab-state-label {
|
||||||
--status-color: var(--success);
|
--status-color: var(--success);
|
||||||
&.cart { --status-color: var(--secondary-dark); }
|
&.cart { --status-color: var(--secondary-dark); }
|
||||||
|
&.paid { --status-color: var(--success-light); }
|
||||||
|
&.ready { --status-color: var(--success); }
|
||||||
&.error { --status-color: var(--alert); }
|
&.error { --status-color: var(--alert); }
|
||||||
&.canceled { --status-color: var(--alert-light); }
|
&.canceled { --status-color: var(--alert-light); }
|
||||||
&.pending { --status-color: var(--information); }
|
&.pending { --status-color: var(--information); }
|
||||||
|
@ -103,6 +103,8 @@
|
|||||||
.fab-state-label {
|
.fab-state-label {
|
||||||
--status-color: var(--success);
|
--status-color: var(--success);
|
||||||
&.cart { --status-color: var(--secondary-dark); }
|
&.cart { --status-color: var(--secondary-dark); }
|
||||||
|
&.paid { --status-color: var(--success-light); }
|
||||||
|
&.ready { --status-color: var(--success); }
|
||||||
&.error { --status-color: var(--alert); }
|
&.error { --status-color: var(--alert); }
|
||||||
&.canceled { --status-color: var(--alert-light); }
|
&.canceled { --status-color: var(--alert-light); }
|
||||||
&.pending { --status-color: var(--information); }
|
&.pending { --status-color: var(--information); }
|
||||||
|
@ -69,6 +69,8 @@ class NotificationType
|
|||||||
notify_user_is_invalidated
|
notify_user_is_invalidated
|
||||||
notify_user_proof_of_identity_refusal
|
notify_user_proof_of_identity_refusal
|
||||||
notify_admin_user_proof_of_identity_refusal
|
notify_admin_user_proof_of_identity_refusal
|
||||||
|
notify_user_order_is_ready
|
||||||
|
notify_user_order_is_canceled
|
||||||
]
|
]
|
||||||
# deprecated:
|
# deprecated:
|
||||||
# - notify_member_subscribed_plan_is_changed
|
# - notify_member_subscribed_plan_is_changed
|
||||||
|
@ -8,6 +8,7 @@ class Order < PaymentDocument
|
|||||||
belongs_to :invoice
|
belongs_to :invoice
|
||||||
has_many :order_items, dependent: :destroy
|
has_many :order_items, dependent: :destroy
|
||||||
has_one :payment_gateway_object, as: :item
|
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
|
ALL_STATES = %w[cart paid payment_failed refunded in_progress ready canceled return].freeze
|
||||||
enum state: ALL_STATES.zip(ALL_STATES).to_h
|
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
|
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')
|
def in_stock?(order, stock_type = 'external')
|
||||||
order.order_items.each do |item|
|
order.order_items.each do |item|
|
||||||
return false if item.orderable.stock[stock_type] < item.quantity
|
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
|
if payment_id && payment_type
|
||||||
order.payment_gateway_object = PaymentGatewayObject.new(gateway_object_id: payment_id, gateway_object_type: payment_type)
|
order.payment_gateway_object = PaymentGatewayObject.new(gateway_object_id: payment_id, gateway_object_type: payment_type)
|
||||||
end
|
end
|
||||||
|
order.order_activities.create(activity_type: 'paid')
|
||||||
order.order_items.each do |item|
|
order.order_items.each do |item|
|
||||||
ProductService.update_stock(item.orderable, 'external', 'sold', -item.quantity, item.id)
|
ProductService.update_stock(item.orderable, 'external', 'sold', -item.quantity, item.id)
|
||||||
end
|
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"
|
refusal: "Your proof of identity are not accepted"
|
||||||
notify_admin_user_proof_of_identity_refusal:
|
notify_admin_user_proof_of_identity_refusal:
|
||||||
refusal: "Member's proof of identity <strong><em>%{NAME}</strong></em> refused."
|
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 tools for admins
|
||||||
statistics:
|
statistics:
|
||||||
subscriptions: "Subscriptions"
|
subscriptions: "Subscriptions"
|
||||||
|
@ -374,3 +374,11 @@ en:
|
|||||||
user_proof_of_identity_files_refusal: "Member %{NAME}'s supporting documents were rejected by %{OPERATOR}:"
|
user_proof_of_identity_files_refusal: "Member %{NAME}'s supporting documents were rejected by %{OPERATOR}:"
|
||||||
shared:
|
shared:
|
||||||
hello: "Hello %{user_name}"
|
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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "fuzzystrmatch"
|
enable_extension "fuzzystrmatch"
|
||||||
@ -445,6 +445,17 @@ ActiveRecord::Schema.define(version: 2022_09_14_145334) do
|
|||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
end
|
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|
|
create_table "order_items", force: :cascade do |t|
|
||||||
t.bigint "order_id"
|
t.bigint "order_id"
|
||||||
t.string "orderable_type"
|
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", "statistic_profiles"
|
||||||
add_foreign_key "invoices", "wallet_transactions"
|
add_foreign_key "invoices", "wallet_transactions"
|
||||||
add_foreign_key "invoicing_profiles", "users"
|
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 "order_items", "orders"
|
||||||
add_foreign_key "orders", "coupons"
|
add_foreign_key "orders", "coupons"
|
||||||
add_foreign_key "orders", "invoices"
|
add_foreign_key "orders", "invoices"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user