1
0
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:
Du Peng 2022-09-15 20:19:19 +02:00
parent 947c69c4ed
commit f015e23a85
24 changed files with 180 additions and 21 deletions

View File

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

View File

@ -0,0 +1,3 @@
# Raised when update order state error
class UpdateOrderStateError < StandardError
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View 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

View File

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

View 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

View File

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

View File

@ -0,0 +1,2 @@
json.title notification.notification_type
json.description t('.order_canceled', REFERENCE: notification.attached_object.order.reference)

View File

@ -0,0 +1,2 @@
json.title notification.notification_type
json.description t('.order_ready', REFERENCE: notification.attached_object.order.reference)

View File

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

View File

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

View File

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

View File

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

View 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

View File

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