mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-21 15:54:22 +01:00
(feat) notify_admin_order_is_paid
This commit is contained in:
parent
f92a4a741b
commit
ed072184bf
@ -3,7 +3,13 @@
|
|||||||
- Report user's prepaid packs in the dashboard
|
- Report user's prepaid packs in the dashboard
|
||||||
- Export external ID and private notes in the members excel export
|
- Export external ID and private notes in the members excel export
|
||||||
- Ability to buy a new prepaid pack from the user's dashboard
|
- Ability to buy a new prepaid pack from the user's dashboard
|
||||||
- Improved public calendar loading time
|
- Improved calendars loading time
|
||||||
|
- Admin notification when an order was placed
|
||||||
|
- Management of notifications preferences for admins
|
||||||
|
- Display custom banners in machines/trainings/events lists
|
||||||
|
- Filter projects by status
|
||||||
|
- Maximum validity period for trainings authorizations
|
||||||
|
- Automatically cancel trainings with insufficient attendees
|
||||||
- Fix a bug: event image updates are not reflected unless the browser's cache is purged
|
- Fix a bug: event image updates are not reflected unless the browser's cache is purged
|
||||||
- Fix a bug: schedules jobs are not launched at the right time
|
- Fix a bug: schedules jobs are not launched at the right time
|
||||||
- Fix a bug: unable to update the title of a training
|
- Fix a bug: unable to update the title of a training
|
||||||
|
@ -14,7 +14,7 @@ import { MemberSelect } from '../user/member-select';
|
|||||||
import { User } from '../../models/user';
|
import { User } from '../../models/user';
|
||||||
import { FormInput } from '../form/form-input';
|
import { FormInput } from '../form/form-input';
|
||||||
import OrderAPI from '../../api/order';
|
import OrderAPI from '../../api/order';
|
||||||
import { Order, OrderIndexFilter, OrderSortOption } from '../../models/order';
|
import { Order, OrderIndexFilter, OrderSortOption, OrderState } from '../../models/order';
|
||||||
import { FabPagination } from '../base/fab-pagination';
|
import { FabPagination } from '../base/fab-pagination';
|
||||||
import { CaretDoubleUp, X } from 'phosphor-react';
|
import { CaretDoubleUp, X } from 'phosphor-react';
|
||||||
import { ChecklistOption, SelectOption } from '../../models/select';
|
import { ChecklistOption, SelectOption } from '../../models/select';
|
||||||
@ -50,7 +50,7 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onError }) => {
|
|||||||
const [pageCount, setPageCount] = useState<number>(0);
|
const [pageCount, setPageCount] = useState<number>(0);
|
||||||
const [totalCount, setTotalCount] = useState<number>(0);
|
const [totalCount, setTotalCount] = useState<number>(0);
|
||||||
const [reference, setReference] = useState<string>(filters.reference);
|
const [reference, setReference] = useState<string>(filters.reference);
|
||||||
const [states, setStates] = useState<Array<string>>(filters.states);
|
const [states, setStates] = useState<Array<OrderState>>(filters.states);
|
||||||
const [user, setUser] = useState<{ id: number, name?: string }>(filters.user);
|
const [user, setUser] = useState<{ id: number, name?: string }>(filters.user);
|
||||||
const [periodFrom, setPeriodFrom] = useState<string>(filters.period_from);
|
const [periodFrom, setPeriodFrom] = useState<string>(filters.period_from);
|
||||||
const [periodTo, setPeriodTo] = useState<string>(filters.period_to);
|
const [periodTo, setPeriodTo] = useState<string>(filters.period_to);
|
||||||
@ -64,7 +64,7 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onError }) => {
|
|||||||
}).catch(onError);
|
}).catch(onError);
|
||||||
}, [filters]);
|
}, [filters]);
|
||||||
|
|
||||||
const statusOptions: ChecklistOption<string>[] = [
|
const statusOptions: ChecklistOption<OrderState>[] = [
|
||||||
{ value: 'cart', label: t('app.admin.store.orders.state.cart') },
|
{ value: 'cart', label: t('app.admin.store.orders.state.cart') },
|
||||||
{ value: 'paid', label: t('app.admin.store.orders.state.paid') },
|
{ value: 'paid', label: t('app.admin.store.orders.state.paid') },
|
||||||
{ value: 'payment_failed', label: t('app.admin.store.orders.state.payment_failed') },
|
{ value: 'payment_failed', label: t('app.admin.store.orders.state.payment_failed') },
|
||||||
@ -108,7 +108,7 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onError }) => {
|
|||||||
/**
|
/**
|
||||||
* Clear filter by type
|
* Clear filter by type
|
||||||
*/
|
*/
|
||||||
const removeFilter = (filterType: string, state?: string) => {
|
const removeFilter = (filterType: string, state?: OrderState) => {
|
||||||
return () => {
|
return () => {
|
||||||
setFilters(draft => {
|
setFilters(draft => {
|
||||||
draft.page = 1;
|
draft.page = 1;
|
||||||
@ -185,7 +185,7 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onError }) => {
|
|||||||
/**
|
/**
|
||||||
* Filter: by status
|
* Filter: by status
|
||||||
*/
|
*/
|
||||||
const handleSelectStatus = (s: ChecklistOption<string>, checked: boolean) => {
|
const handleSelectStatus = (s: ChecklistOption<OrderState>, checked: boolean) => {
|
||||||
const list = [...states];
|
const list = [...states];
|
||||||
checked
|
checked
|
||||||
? list.push(s.value)
|
? list.push(s.value)
|
||||||
|
@ -79,7 +79,8 @@ export const notificationTypeNames = [
|
|||||||
'notify_user_order_is_canceled',
|
'notify_user_order_is_canceled',
|
||||||
'notify_user_order_is_refunded',
|
'notify_user_order_is_refunded',
|
||||||
'notify_admin_low_stock_threshold',
|
'notify_admin_low_stock_threshold',
|
||||||
'notify_admin_training_auto_cancelled'
|
'notify_admin_training_auto_cancelled',
|
||||||
|
'notify_admin_order_is_paid'
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type NotificationTypeName = typeof notificationTypeNames[number];
|
export type NotificationTypeName = typeof notificationTypeNames[number];
|
||||||
|
@ -8,6 +8,8 @@ import type { CartItemReservationType, CartItemType } from './cart_item';
|
|||||||
|
|
||||||
export type OrderableType = 'Product' | CartItemType;
|
export type OrderableType = 'Product' | CartItemType;
|
||||||
|
|
||||||
|
export type OrderState = 'cart'|'paid'|'payment_failed'|'refunded'|'in_progress'|'ready'|'canceled'|'delivered';
|
||||||
|
|
||||||
export interface OrderItem {
|
export interface OrderItem {
|
||||||
id: number,
|
id: number,
|
||||||
orderable_type: OrderableType,
|
orderable_type: OrderableType,
|
||||||
@ -51,7 +53,7 @@ export interface Order {
|
|||||||
},
|
},
|
||||||
operator_profile_id?: number,
|
operator_profile_id?: number,
|
||||||
reference?: string,
|
reference?: string,
|
||||||
state?: string,
|
state?: OrderState,
|
||||||
total?: number,
|
total?: number,
|
||||||
coupon?: Coupon,
|
coupon?: Coupon,
|
||||||
created_at?: TDateISO,
|
created_at?: TDateISO,
|
||||||
@ -82,7 +84,7 @@ export interface OrderIndexFilter extends ApiFilter {
|
|||||||
},
|
},
|
||||||
page?: number,
|
page?: number,
|
||||||
sort?: OrderSortOption
|
sort?: OrderSortOption
|
||||||
states?: Array<string>,
|
states?: Array<OrderState>,
|
||||||
period_from?: string,
|
period_from?: string,
|
||||||
period_to?: string
|
period_to?: string
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# NotificationType defines the different types of Notification.
|
# NotificationType defines the different types of Notification.
|
||||||
# When recording a new notification type in db, you might also want to add it in:
|
# To add a new notification type in db, you must add it in:
|
||||||
# test/fixtures/notification_types.yml
|
# - db/seeds/notification_types.rb
|
||||||
# app/frontend/src/javascript/models/notification-type.ts
|
# - app/views/api/notifications/_XXXXXX.json.jbuilder
|
||||||
# config/locales/app.logged.en.yml
|
# - app/views/notifications_mailer/XXXXXX.html.erb
|
||||||
|
# - app/frontend/src/javascript/models/notification-type.ts
|
||||||
|
# - config/locales/app.logged.en.yml
|
||||||
|
# - test/fixtures/notification_types.yml
|
||||||
# If you change the name of a category, or create a new one, please add it in:
|
# If you change the name of a category, or create a new one, please add it in:
|
||||||
# app/frontend/src/javascript/models/notification-preference.ts
|
# - app/frontend/src/javascript/models/notification-type.ts
|
||||||
class NotificationType < ApplicationRecord
|
class NotificationType < ApplicationRecord
|
||||||
has_many :notifications, dependent: :destroy
|
has_many :notifications, dependent: :destroy
|
||||||
has_many :notification_preferences, dependent: :destroy
|
has_many :notification_preferences, dependent: :destroy
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Provides methods for cancel an order
|
# Provides a method to cancel an order
|
||||||
class Orders::OrderCanceledService
|
class Orders::OrderCanceledService
|
||||||
def call(order, current_user)
|
def call(order, current_user)
|
||||||
raise ::UpdateOrderStateError if %w[cart canceled refunded delivered].include?(order.state)
|
raise ::UpdateOrderStateError if %w[cart canceled refunded delivered].include?(order.state)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Provides methods for set order to delivered state
|
# Provides a method to set the order state to delivered
|
||||||
class Orders::OrderDeliveredService
|
class Orders::OrderDeliveredService
|
||||||
def call(order, current_user)
|
def call(order, current_user)
|
||||||
raise ::UpdateOrderStateError if %w[cart payment_failed canceled refunded delivered].include?(order.state)
|
raise ::UpdateOrderStateError if %w[cart payment_failed canceled refunded delivered].include?(order.state)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Provides methods for set order to ready state
|
# Provides a method to set the order state to ready
|
||||||
class Orders::OrderReadyService
|
class Orders::OrderReadyService
|
||||||
def call(order, current_user, note = '')
|
def call(order, current_user, note = '')
|
||||||
raise ::UpdateOrderStateError if %w[cart payment_failed ready canceled refunded delivered].include?(order.state)
|
raise ::UpdateOrderStateError if %w[cart payment_failed ready canceled refunded delivered].include?(order.state)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Provides methods for refund an order
|
# Provides a method to refund an order
|
||||||
class Orders::OrderRefundedService
|
class Orders::OrderRefundedService
|
||||||
def call(order, current_user)
|
def call(order, current_user)
|
||||||
raise ::UpdateOrderStateError if %w[cart payment_error refunded delivered].include?(order.state)
|
raise ::UpdateOrderStateError if %w[cart payment_error refunded delivered].include?(order.state)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Provides methods for set in progress state to order
|
# Provides a method to set the order state to "in progress"
|
||||||
class Orders::SetInProgressService
|
class Orders::SetInProgressService
|
||||||
def call(order, current_user)
|
def call(order, current_user)
|
||||||
raise ::UpdateOrderStateError if %w[cart payment_failed in_progress canceled refunded delivered].include?(order.state)
|
raise ::UpdateOrderStateError if %w[cart payment_failed in_progress canceled refunded delivered].include?(order.state)
|
||||||
|
@ -28,16 +28,22 @@ module Payments::PaymentConcern
|
|||||||
payment_method
|
payment_method
|
||||||
end
|
end
|
||||||
order.state = 'paid'
|
order.state = 'paid'
|
||||||
|
# we change the order creation date to the date of payment to be more consistent for admins when filtering on orders
|
||||||
order.created_at = Time.current
|
order.created_at = Time.current
|
||||||
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', operator_profile_id: order.operator_profile_id)
|
activity = order.order_activities.create(activity_type: 'paid', operator_profile_id: order.operator_profile_id)
|
||||||
order.order_items.each do |item|
|
order.order_items.each do |item|
|
||||||
ProductService.update_stock(item.orderable,
|
ProductService.update_stock(item.orderable,
|
||||||
[{ stock_type: 'external', reason: 'sold', quantity: item.quantity, order_item_id: item.id }]).save
|
[{ stock_type: 'external', reason: 'sold', quantity: item.quantity, order_item_id: item.id }]).save
|
||||||
end
|
end
|
||||||
create_invoice(order, coupon, payment_id, payment_type) if order.save
|
if order.save
|
||||||
|
create_invoice(order, coupon, payment_id, payment_type)
|
||||||
|
NotificationCenter.call type: 'notify_admin_order_is_paid',
|
||||||
|
receiver: User.admins_and_managers,
|
||||||
|
attached_object: activity
|
||||||
|
end
|
||||||
order.reload
|
order.reload
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.title notification.notification_type
|
||||||
|
json.description t('.order_paid_html', { ID: notification.attached_object.order_id })
|
@ -1 +1,3 @@
|
|||||||
|
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||||
|
|
||||||
<p><%= t('.body.objects_sync') %></p>
|
<p><%= t('.body.objects_sync') %></p>
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= t('.body.order_placed', REFERENCE: @attached_object.order.reference, USER: @attached_object.order.statistic_profile.user.profile.full_name) %>
|
||||||
|
<%= link_to t('.body.view_details'), "#{root_url}#!/admin/store/orders/#{@attached_object.order_id}" %>
|
||||||
|
</p>
|
@ -319,3 +319,4 @@ en:
|
|||||||
notify_admin_payment_schedule_gateway_canceled: "A payment schedule has been canceled by the payment gateway"
|
notify_admin_payment_schedule_gateway_canceled: "A payment schedule has been canceled by the payment gateway"
|
||||||
notify_project_collaborator_to_valid: "You are invited to collaborate on a project"
|
notify_project_collaborator_to_valid: "You are invited to collaborate on a project"
|
||||||
notify_project_author_when_collaborator_valid: "A collaborator has accepted your invitation to join your project"
|
notify_project_author_when_collaborator_valid: "A collaborator has accepted your invitation to join your project"
|
||||||
|
notify_admin_order_is_paid: "A new order has been placed"
|
||||||
|
@ -412,6 +412,8 @@ en:
|
|||||||
user_NAME_changed_ROLE_html: "User <strong><em>%{NAME}</strong></em> is now %{ROLE}."
|
user_NAME_changed_ROLE_html: "User <strong><em>%{NAME}</strong></em> is now %{ROLE}."
|
||||||
notify_admin_objects_stripe_sync:
|
notify_admin_objects_stripe_sync:
|
||||||
all_objects_sync: "All data were successfully synchronized on Stripe."
|
all_objects_sync: "All data were successfully synchronized on Stripe."
|
||||||
|
notify_admin_order_is_paid:
|
||||||
|
order_paid_html: "A new order has been placed. <a href='/#!/admin/store/orders/%{ID}'>View details</a>."
|
||||||
notify_user_when_payment_schedule_ready:
|
notify_user_when_payment_schedule_ready:
|
||||||
your_schedule_is_ready_html: "Your payment schedule #%{REFERENCE}, of %{AMOUNT}, is ready. <a href='api/payment_schedules/%{SCHEDULE_ID}/download' target='_blank'>Click here to download</a>."
|
your_schedule_is_ready_html: "Your payment schedule #%{REFERENCE}, of %{AMOUNT}, is ready. <a href='api/payment_schedules/%{SCHEDULE_ID}/download' target='_blank'>Click here to download</a>."
|
||||||
notify_admin_payment_schedule_error:
|
notify_admin_payment_schedule_error:
|
||||||
|
@ -317,6 +317,11 @@ en:
|
|||||||
subject: "Stripe synchronization"
|
subject: "Stripe synchronization"
|
||||||
body:
|
body:
|
||||||
objects_sync: "All members, coupons, machines, trainings, spaces and plans were successfully synchronized on Stripe."
|
objects_sync: "All members, coupons, machines, trainings, spaces and plans were successfully synchronized on Stripe."
|
||||||
|
notify_admin_order_is_paid:
|
||||||
|
subject: "New order"
|
||||||
|
body:
|
||||||
|
order_placed: "A new order (%{REFERENCE}) has been placed and paid by %{USER}."
|
||||||
|
view_details: ""
|
||||||
notify_member_payment_schedule_ready:
|
notify_member_payment_schedule_ready:
|
||||||
subject: "Your payment schedule"
|
subject: "Your payment schedule"
|
||||||
body:
|
body:
|
||||||
|
@ -15,3 +15,11 @@ unless NotificationType.find_by(name: 'notify_member_training_invalidated')
|
|||||||
is_configurable: false
|
is_configurable: false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
unless NotificationType.find_by(name: 'notify_admin_order_is_paid')
|
||||||
|
NotificationType.create!(
|
||||||
|
name: 'notify_admin_order_is_paid',
|
||||||
|
category: 'shop',
|
||||||
|
is_configurable: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
8
test/fixtures/notification_types.yml
vendored
8
test/fixtures/notification_types.yml
vendored
@ -541,3 +541,11 @@ notification_type_68:
|
|||||||
is_configurable: true
|
is_configurable: true
|
||||||
created_at: 2023-02-02 08:25:33.443888000 Z
|
created_at: 2023-02-02 08:25:33.443888000 Z
|
||||||
updated_at: 2023-02-02 08:25:33.443888000 Z
|
updated_at: 2023-02-02 08:25:33.443888000 Z
|
||||||
|
|
||||||
|
notification_type_69:
|
||||||
|
id: 69
|
||||||
|
name: notify_admin_order_is_paid
|
||||||
|
category: shop
|
||||||
|
is_configurable: true
|
||||||
|
created_at: 2023-02-16 10:42:39.143888000 Z
|
||||||
|
updated_at: 2023-02-16 10:42:39.143888000 Z
|
||||||
|
@ -105,9 +105,15 @@ class Store::UserPayOrderTest < ActionDispatch::IntegrationTest
|
|||||||
assert_not invoice.total.blank?
|
assert_not invoice.total.blank?
|
||||||
assert invoice.check_footprint
|
assert invoice.check_footprint
|
||||||
|
|
||||||
# notification
|
# invoice notification
|
||||||
assert_not_empty Notification.where(attached_object: invoice)
|
assert_not_empty Notification.where(attached_object: invoice)
|
||||||
|
|
||||||
|
# order notification
|
||||||
|
assert_not_nil Notification.find_by(
|
||||||
|
notification_type_id: NotificationType.find_by(name: 'notify_admin_order_is_paid'),
|
||||||
|
attached_object: @cart1.order_activities.last
|
||||||
|
)
|
||||||
|
|
||||||
assert_equal @cart1.state, 'paid'
|
assert_equal @cart1.state, 'paid'
|
||||||
assert_equal @cart1.payment_method, 'card'
|
assert_equal @cart1.payment_method, 'card'
|
||||||
assert_equal @cart1.paid_total, 400
|
assert_equal @cart1.paid_total, 400
|
||||||
|
Loading…
x
Reference in New Issue
Block a user