From 9f1b13192f789e677eadc3d177f7fab80daaabe2 Mon Sep 17 00:00:00 2001 From: Vlad Chikalkin Date: Mon, 11 Aug 2025 16:25:56 +0300 Subject: [PATCH] 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. --- .../order/content-types/order/lifecycles.ts | 228 +----------------- src/api/slot/content-types/slot/lifecycles.ts | 147 ----------- 2 files changed, 10 insertions(+), 365 deletions(-) delete mode 100644 src/api/slot/content-types/slot/lifecycles.ts diff --git a/src/api/order/content-types/order/lifecycles.ts b/src/api/order/content-types/order/lifecycles.ts index 370062c..2ac7689 100644 --- a/src/api/order/content-types/order/lifecycles.ts +++ b/src/api/order/content-types/order/lifecycles.ts @@ -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 = { 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} Запись изменена`; + // Специальная обработка для завершенных записей + if (state === 'completed') { + heading = `${headingEmoji} Запись завершена`; + } else { + heading = `${headingEmoji} Запись изменена`; + } } 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); - } - }, }; diff --git a/src/api/slot/content-types/slot/lifecycles.ts b/src/api/slot/content-types/slot/lifecycles.ts deleted file mode 100644 index 3a5c18a..0000000 --- a/src/api/slot/content-types/slot/lifecycles.ts +++ /dev/null @@ -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); - // } - // }, -};