Issues/76 (#6)
* refactor: consolidate order lifecycle validation logic and remove slot lifecycle - Streamlined the order lifecycle by consolidating validation logic and removing redundant error handling. - Deleted the slot lifecycle file to simplify the codebase, as its functionality is no longer needed. - This change enhances maintainability and reduces complexity in the order management system. * feat: enhance Telegram notification heading for order updates - Updated the heading emoji logic to display a checkered flag for completed orders and a pencil for updates, improving clarity in notifications. - Added specific handling for completed records in the notification heading to differentiate between updated and completed statuses.
This commit is contained in:
parent
b0aa644435
commit
9f1b13192f
@ -1,24 +1,6 @@
|
||||
import { bot, extractId, dayjs } from '../../../../utils';
|
||||
import { DEFAULT_TZ } from '../../../../constants';
|
||||
|
||||
const ERR_MISSING_TIME = 'Не указано время';
|
||||
const ERR_INVALID_TIME = 'Некорректное время';
|
||||
const ERR_OVERLAPPING_TIME = 'Время пересекается с другими заказами';
|
||||
const ERR_INACTIVE_CLIENT = 'Клиент не активен';
|
||||
const ERR_INACTIVE_MASTER = 'Мастер не активен';
|
||||
const ERR_SLOT_CLOSED = 'Слот закрыт';
|
||||
const ERR_INVALID_CLIENT = 'Некорректный клиент';
|
||||
const ERR_INVALID_MASTER = 'Некорректный мастер';
|
||||
const ERR_MISSING_CLIENT = 'Не указан клиент';
|
||||
const ERR_MISSING_SLOT = 'Не указан слот';
|
||||
const ERR_MISSING_SERVICE = 'Не указан сервис';
|
||||
const ERR_ORDER_OUT_OF_SLOT = 'Время заказа выходит за пределы слота';
|
||||
const ERR_EXISTING_ORDER_OR_SLOT_NOT_FOUND =
|
||||
'Существующий заказ или слот не найден';
|
||||
const ERR_CANNOT_COMPLETE_BEFORE_START =
|
||||
'Нельзя завершить запись до её наступления';
|
||||
const ERR_ORDER_IN_PAST = 'Нельзя создать запись на время в прошлом';
|
||||
|
||||
const STATE_MAP: Record<State, string> = {
|
||||
approved: 'Подтверждено',
|
||||
cancelled: 'Отменено',
|
||||
@ -87,13 +69,20 @@ async function sendTelegramNotification(orderEntity: Order, isUpdate = false) {
|
||||
const emojiForState = EMOJI_MAP[state] || '';
|
||||
const stateLabel = STATE_MAP[state] || state;
|
||||
|
||||
// Эмодзи в заголовке: карандаш при обновлении, иначе эмодзи статуса
|
||||
const headingEmoji = isUpdate ? '✏️' : emojiForState;
|
||||
// Эмодзи в заголовке: карандаш при обновлении, флаг для завершенных, иначе эмодзи статуса
|
||||
const headingEmoji = isUpdate
|
||||
? (state === 'completed' ? '🏁' : '✏️')
|
||||
: emojiForState;
|
||||
|
||||
let heading = '';
|
||||
|
||||
if (isUpdate) {
|
||||
heading = `${headingEmoji} <b>Запись изменена</b>`;
|
||||
// Специальная обработка для завершенных записей
|
||||
if (state === 'completed') {
|
||||
heading = `${headingEmoji} <b>Запись завершена</b>`;
|
||||
} else {
|
||||
heading = `${headingEmoji} <b>Запись изменена</b>`;
|
||||
}
|
||||
} else {
|
||||
const isApproved = state === 'approved';
|
||||
const creationText = isApproved
|
||||
@ -170,201 +159,4 @@ export default {
|
||||
|
||||
await sendTelegramNotification(updatedEntity, true);
|
||||
},
|
||||
|
||||
async beforeCreate(event) {
|
||||
const { data } = event.params;
|
||||
const { datetime_start, datetime_end, client, services } = data;
|
||||
|
||||
const isUpdate = !!data?.publishedAt;
|
||||
if (isUpdate) return;
|
||||
|
||||
const clientId = extractId(client);
|
||||
const slotId = extractId(data.slot);
|
||||
|
||||
// Проверка наличия обязательных полей
|
||||
if (!slotId) throw new Error(ERR_MISSING_SLOT);
|
||||
if (!clientId) throw new Error(ERR_MISSING_CLIENT);
|
||||
if (!extractId(services)) throw new Error(ERR_MISSING_SERVICE);
|
||||
|
||||
// Проверка корректности времени заказа.
|
||||
if (!datetime_start || !datetime_end) {
|
||||
throw new Error(ERR_MISSING_TIME);
|
||||
}
|
||||
if (new Date(datetime_end) <= new Date(datetime_start)) {
|
||||
throw new Error(ERR_INVALID_TIME);
|
||||
}
|
||||
|
||||
// Проверка, что заказ не создается на время в прошлом
|
||||
const now = dayjs().tz(DEFAULT_TZ);
|
||||
const orderStart = dayjs(datetime_start).tz(DEFAULT_TZ);
|
||||
|
||||
if (orderStart.isBefore(now, 'minute')) {
|
||||
throw new Error(ERR_ORDER_IN_PAST);
|
||||
}
|
||||
|
||||
// Получаем слот
|
||||
const slot = await strapi.db.query('api::slot.slot').findOne({
|
||||
where: { id: slotId },
|
||||
populate: ['master'],
|
||||
});
|
||||
if (!slot) throw new Error(ERR_MISSING_SLOT);
|
||||
|
||||
// Проверка, что заказ укладывается в рамки слота
|
||||
if (
|
||||
new Date(datetime_start) < new Date(slot.datetime_start) ||
|
||||
new Date(datetime_end) > new Date(slot.datetime_end)
|
||||
) {
|
||||
throw new Error(ERR_ORDER_OUT_OF_SLOT);
|
||||
}
|
||||
|
||||
// 1. Слот не должен быть закрыт
|
||||
if (slot.state === 'closed') {
|
||||
throw new Error(ERR_SLOT_CLOSED);
|
||||
}
|
||||
|
||||
// Получаем клиента
|
||||
const clientEntity = await strapi.db
|
||||
.query('api::customer.customer')
|
||||
.findOne({
|
||||
where: { id: clientId },
|
||||
populate: { masters: true },
|
||||
});
|
||||
if (!clientEntity) throw new Error(ERR_MISSING_CLIENT);
|
||||
|
||||
// Проверка активности клиента
|
||||
if (!clientEntity.active) {
|
||||
throw new Error(ERR_INACTIVE_CLIENT);
|
||||
}
|
||||
|
||||
// Получаем мастера слота
|
||||
const slotMaster = slot.master;
|
||||
if (!slotMaster) throw new Error(ERR_INVALID_MASTER);
|
||||
if (!slotMaster.active || slotMaster.role !== 'master') {
|
||||
throw new Error(ERR_INACTIVE_MASTER);
|
||||
}
|
||||
|
||||
// 2. Проверка ролей и связей
|
||||
const isClientMaster = clientEntity.role === 'master';
|
||||
const slotMasterId = slotMaster.id;
|
||||
|
||||
if (!isClientMaster) {
|
||||
// Клиент не должен быть мастером слота
|
||||
if (clientEntity.id === slotMasterId) {
|
||||
throw new Error(ERR_INVALID_CLIENT);
|
||||
}
|
||||
// Клиент должен быть в списке клиентов мастера
|
||||
const masters = clientEntity.masters?.map(m => m.id) || [];
|
||||
if (!masters.includes(slotMasterId)) {
|
||||
throw new Error(ERR_INVALID_MASTER);
|
||||
}
|
||||
} else {
|
||||
// Мастер не может записать другого мастера
|
||||
if (slotMasterId !== clientEntity.id) {
|
||||
throw new Error(ERR_INVALID_MASTER);
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка пересечений заказов по времени.
|
||||
const overlappingEntities = await strapi.db
|
||||
.query('api::order.order')
|
||||
.findMany({
|
||||
where: {
|
||||
documentId: { $ne: data.documentId },
|
||||
datetime_start: { $lt: datetime_end },
|
||||
datetime_end: { $gt: datetime_start },
|
||||
slot: {
|
||||
id: { $eq: slotId },
|
||||
},
|
||||
state: {
|
||||
$notIn: ['cancelled'],
|
||||
},
|
||||
},
|
||||
populate: ['slot'],
|
||||
});
|
||||
|
||||
if (overlappingEntities.length > 0) {
|
||||
throw new Error(ERR_OVERLAPPING_TIME);
|
||||
}
|
||||
},
|
||||
|
||||
async beforeUpdate(event) {
|
||||
const { data, where } = event.params;
|
||||
const { id: entityId } = where;
|
||||
const { datetime_start, datetime_end, state } = data;
|
||||
|
||||
const existingOrder = await strapi.db.query('api::order.order').findOne({
|
||||
where: { id: entityId },
|
||||
select: ['documentId', 'datetime_start', 'datetime_end'],
|
||||
populate: ['slot', 'client'],
|
||||
});
|
||||
|
||||
if (state && !datetime_start && !datetime_end) {
|
||||
if (state === 'completed') {
|
||||
const clientId = extractId(existingOrder.client);
|
||||
|
||||
const lastOrder = await strapi.db.query('api::order.order').findMany({
|
||||
where: {
|
||||
client: {
|
||||
id: clientId,
|
||||
},
|
||||
state: 'completed',
|
||||
},
|
||||
orderBy: { order_number: 'desc' },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const lastOrderNumber =
|
||||
lastOrder.length > 0 ? lastOrder[0].order_number : 0;
|
||||
data.order_number = lastOrderNumber + 1;
|
||||
}
|
||||
|
||||
const now = dayjs().tz(DEFAULT_TZ);
|
||||
const orderStart = dayjs(existingOrder.datetime_start).tz(DEFAULT_TZ);
|
||||
|
||||
if (state === 'completed' && now.isBefore(orderStart, 'minute')) {
|
||||
throw new Error(ERR_CANNOT_COMPLETE_BEFORE_START);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
existingOrder.datetime_start === datetime_start &&
|
||||
existingOrder.datetime_end === datetime_end
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!datetime_start || !datetime_end) {
|
||||
throw new Error(ERR_INVALID_TIME);
|
||||
}
|
||||
|
||||
if (new Date(datetime_end) <= new Date(datetime_start)) {
|
||||
throw new Error(ERR_INVALID_TIME);
|
||||
}
|
||||
|
||||
if (!existingOrder || !existingOrder.slot) {
|
||||
throw new Error(ERR_EXISTING_ORDER_OR_SLOT_NOT_FOUND);
|
||||
}
|
||||
|
||||
const slotId = existingOrder.slot.documentId;
|
||||
|
||||
const overlappingEntities = await strapi.db
|
||||
.query('api::order.order')
|
||||
.findMany({
|
||||
where: {
|
||||
id: { $ne: entityId },
|
||||
datetime_start: { $lt: datetime_end },
|
||||
datetime_end: { $gt: datetime_start },
|
||||
slot: {
|
||||
documentId: { $eq: slotId },
|
||||
},
|
||||
},
|
||||
populate: ['slot'],
|
||||
});
|
||||
|
||||
if (overlappingEntities.length > 0) {
|
||||
throw new Error(ERR_OVERLAPPING_TIME);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
import { DEFAULT_TZ } from '../../../../constants';
|
||||
import { extractId, dayjs } from '../../../../utils';
|
||||
|
||||
const ERR_INVALID_TIME = 'Некорректное время';
|
||||
const ERR_OVERLAPPING_TIME = 'Время пересекается с другими слотами';
|
||||
const ERR_FORBIDDEN_SLOT_STATUS =
|
||||
'Нельзя менять время слота, если есть связанные заказы';
|
||||
|
||||
const FORBIDDEN_ORDER_STATES = [
|
||||
'scheduled',
|
||||
'approved',
|
||||
'completed',
|
||||
'cancelling',
|
||||
];
|
||||
|
||||
const ERR_INACTIVE_MASTER = 'Мастер не активен';
|
||||
const ERR_INVALID_MASTER = 'Некорректный мастер';
|
||||
const ERR_PAST_SLOT = 'Нельзя создать слот в прошлом';
|
||||
const ERR_SLOT_HAS_ORDERS = 'Нельзя удалить слот с активными заказами';
|
||||
const ERR_RECORD_NOT_FOUND = 'Запись не найдена';
|
||||
|
||||
export default {
|
||||
async beforeCreate(event) {
|
||||
const { data } = event.params;
|
||||
const { master, datetime_start, datetime_end } = data;
|
||||
|
||||
const isUpdate = !!data?.publishedAt;
|
||||
if (isUpdate) return;
|
||||
|
||||
// Проверка, что мастер существует и активен
|
||||
const masterId = extractId(master);
|
||||
const masterEntity = await strapi.db
|
||||
.query('api::customer.customer')
|
||||
.findOne({
|
||||
where: { id: masterId },
|
||||
});
|
||||
if (!masterEntity) throw new Error(ERR_INVALID_MASTER);
|
||||
if (!masterEntity.active || masterEntity.role !== 'master') {
|
||||
throw new Error(ERR_INACTIVE_MASTER);
|
||||
}
|
||||
|
||||
// Проверка, что слот не создаётся в прошлом
|
||||
if (datetime_start) {
|
||||
const now = dayjs().tz(DEFAULT_TZ);
|
||||
const slotStart = dayjs(datetime_start).tz(DEFAULT_TZ);
|
||||
if (slotStart.isBefore(now, 'day')) {
|
||||
throw new Error(ERR_PAST_SLOT);
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка валидности времени
|
||||
if (!datetime_start || !datetime_end) {
|
||||
throw new Error(ERR_INVALID_TIME);
|
||||
}
|
||||
if (new Date(datetime_end) <= new Date(datetime_start)) {
|
||||
throw new Error(ERR_INVALID_TIME);
|
||||
}
|
||||
const overlappingEntities = await strapi.db
|
||||
.query('api::slot.slot')
|
||||
.findMany({
|
||||
where: {
|
||||
documentId: { $ne: data.documentId },
|
||||
datetime_start: { $lt: datetime_end },
|
||||
datetime_end: { $gt: datetime_start },
|
||||
master: masterId,
|
||||
},
|
||||
});
|
||||
if (overlappingEntities.length > 0) {
|
||||
throw new Error(ERR_OVERLAPPING_TIME);
|
||||
}
|
||||
},
|
||||
async beforeUpdate(event) {
|
||||
const { data, where } = event.params;
|
||||
const { id: entityId } = where;
|
||||
|
||||
// Если меняется хотя бы одно из полей времени
|
||||
const isTimeChange = 'datetime_start' in data || 'datetime_end' in data;
|
||||
|
||||
if (isTimeChange) {
|
||||
let datetime_start = data.datetime_start;
|
||||
let datetime_end = data.datetime_end;
|
||||
|
||||
// Подтянуть недостающие значения из существующего слота
|
||||
const existingSlot = await strapi.db.query('api::slot.slot').findOne({
|
||||
where: { id: entityId },
|
||||
select: ['datetime_start', 'datetime_end'],
|
||||
});
|
||||
if (!datetime_start) datetime_start = existingSlot?.datetime_start;
|
||||
if (!datetime_end) datetime_end = existingSlot?.datetime_end;
|
||||
|
||||
// Проверка: оба времени должны быть определены
|
||||
if (!datetime_start || !datetime_end) {
|
||||
throw new Error(ERR_INVALID_TIME);
|
||||
}
|
||||
|
||||
// Проверка валидности времени
|
||||
if (new Date(datetime_end) <= new Date(datetime_start)) {
|
||||
throw new Error(ERR_INVALID_TIME);
|
||||
}
|
||||
|
||||
const existingEntity = await strapi.db.query('api::slot.slot').findOne({
|
||||
where: { id: entityId },
|
||||
select: ['documentId'],
|
||||
populate: ['orders'],
|
||||
});
|
||||
|
||||
const orders = existingEntity?.orders;
|
||||
|
||||
if (
|
||||
orders?.length > 0 &&
|
||||
orders?.some(order => FORBIDDEN_ORDER_STATES.includes(order.state))
|
||||
) {
|
||||
throw new Error(ERR_FORBIDDEN_SLOT_STATUS);
|
||||
}
|
||||
|
||||
if (!existingEntity) {
|
||||
throw new Error(ERR_RECORD_NOT_FOUND);
|
||||
}
|
||||
|
||||
const { documentId } = existingEntity;
|
||||
const overlappingEntities = await strapi.db
|
||||
.query('api::slot.slot')
|
||||
.findMany({
|
||||
where: {
|
||||
id: { $ne: entityId },
|
||||
documentId: { $ne: documentId },
|
||||
datetime_start: { $lt: datetime_end },
|
||||
datetime_end: { $gt: datetime_start },
|
||||
},
|
||||
});
|
||||
if (overlappingEntities.length > 0) {
|
||||
throw new Error(ERR_OVERLAPPING_TIME);
|
||||
}
|
||||
}
|
||||
},
|
||||
// async beforeDelete(event) {
|
||||
// const { where } = event.params;
|
||||
// const slotId = where.id;
|
||||
// const slot = await strapi.db.query('api::slot.slot').findOne({
|
||||
// where: { id: slotId },
|
||||
// populate: ['orders'],
|
||||
// });
|
||||
// if (slot?.orders?.length) {
|
||||
// throw new Error(ERR_SLOT_HAS_ORDERS);
|
||||
// }
|
||||
// },
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user