OrdersService: add checkBeforeCreate
This commit is contained in:
parent
81dfdec7a5
commit
429f5dcab2
@ -7,7 +7,7 @@ import { useIsMaster } from '@/hooks/api/customers';
|
||||
import { useOrderMutation, useOrderQuery } from '@/hooks/api/orders';
|
||||
import { usePushWithData } from '@/hooks/url';
|
||||
import { Enum_Order_State } from '@repo/graphql/types';
|
||||
import { isBeforeToday } from '@repo/utils/datetime-format';
|
||||
import { isBeforeNow } from '@repo/utils/datetime-format';
|
||||
|
||||
export function OrderButtons({ documentId }: Readonly<OrderComponentProps>) {
|
||||
const push = usePushWithData();
|
||||
@ -50,7 +50,7 @@ export function OrderButtons({ documentId }: Readonly<OrderComponentProps>) {
|
||||
push('/orders/add', order);
|
||||
}
|
||||
|
||||
const isOrderStale = order?.datetime_start && isBeforeToday(order?.datetime_start);
|
||||
const isOrderStale = order?.datetime_start && isBeforeNow(order?.datetime_start);
|
||||
|
||||
const canCancel = !isOrderStale && (isCreated || (isMaster && isCancelling) || isApproved);
|
||||
const canComplete = isMaster && isApproved;
|
||||
|
||||
@ -7,7 +7,7 @@ import { useCustomerQuery } from '@/hooks/api/customers';
|
||||
import { useMasterSlotsQuery } from '@/hooks/api/slots';
|
||||
import { useDateTimeStore } from '@/stores/datetime';
|
||||
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
|
||||
import { getDateUTCRange, isTodayOrAfter } from '@repo/utils/datetime-format';
|
||||
import { getDateUTCRange, isNowOrAfter } from '@repo/utils/datetime-format';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
export function DaySlotsList() {
|
||||
@ -26,7 +26,7 @@ export function DaySlotsList() {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
const isSelectedDateTodayOrAfter = selectedDate && isTodayOrAfter(selectedDate);
|
||||
const isSelectedDateTodayOrAfter = selectedDate && isNowOrAfter(selectedDate);
|
||||
|
||||
if (!slots?.length) {
|
||||
return (
|
||||
|
||||
@ -5,7 +5,7 @@ import FloatingActionPanel from '../shared/action-panel';
|
||||
import { type SlotComponentProps } from './types';
|
||||
import { useSlotDelete, useSlotMutation, useSlotQuery } from '@/hooks/api/slots';
|
||||
import { Enum_Slot_State } from '@repo/graphql/types';
|
||||
import { isBeforeToday } from '@repo/utils/datetime-format';
|
||||
import { isBeforeNow } from '@repo/utils/datetime-format';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export function SlotButtons({ documentId }: Readonly<SlotComponentProps>) {
|
||||
@ -42,7 +42,7 @@ export function SlotButtons({ documentId }: Readonly<SlotComponentProps>) {
|
||||
|
||||
const hasOrders = Boolean(slot?.orders.length);
|
||||
|
||||
const isSlotStale = slot?.datetime_start && isBeforeToday(slot?.datetime_start);
|
||||
const isSlotStale = slot?.datetime_start && isBeforeNow(slot?.datetime_start);
|
||||
|
||||
const canDelete = !isSlotStale && !hasOrders;
|
||||
const canToggle = !isSlotStale;
|
||||
|
||||
@ -20,8 +20,8 @@ export function useCustomerContacts() {
|
||||
refetch: refetchMasters,
|
||||
} = useMastersQuery();
|
||||
|
||||
const clients = clientsData?.customers?.at(0)?.clients || [];
|
||||
const masters = mastersData?.customers?.at(0)?.masters || [];
|
||||
const clients = clientsData?.clients || [];
|
||||
const masters = mastersData?.masters || [];
|
||||
|
||||
const isLoading = isLoadingClients || isLoadingMasters;
|
||||
|
||||
|
||||
@ -43,7 +43,9 @@ export class CustomersService extends BaseService {
|
||||
},
|
||||
});
|
||||
|
||||
return result.data;
|
||||
const customer = result.data.customers.at(0);
|
||||
|
||||
return customer;
|
||||
}
|
||||
|
||||
async getCustomer(variables: VariablesOf<typeof GQL.GetCustomerDocument>) {
|
||||
@ -69,7 +71,9 @@ export class CustomersService extends BaseService {
|
||||
},
|
||||
});
|
||||
|
||||
return result.data;
|
||||
const customer = result.data.customers.at(0);
|
||||
|
||||
return customer;
|
||||
}
|
||||
|
||||
async updateCustomer(
|
||||
|
||||
@ -1,25 +1,43 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { getClientWithToken } from '../apollo/client';
|
||||
import * as GQL from '../types';
|
||||
import { BaseService } from './base';
|
||||
import { CustomersService } from './customers';
|
||||
import { ServicesService } from './services';
|
||||
import { SlotsService } from './slots';
|
||||
import { type VariablesOf } from '@graphql-typed-document-node/core';
|
||||
import { isCustomerMaster } from '@repo/utils/customer';
|
||||
import { getMinutes } from '@repo/utils/datetime-format';
|
||||
import { getMinutes, isBeforeNow } from '@repo/utils/datetime-format';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const ERRORS = {
|
||||
INVALID_SERVICE_DURATION: 'Invalid service duration',
|
||||
MISSING_CLIENT: 'Missing client',
|
||||
MISSING_ORDER: 'Order not found',
|
||||
MISSING_SERVICE_ID: 'Missing service id',
|
||||
MISSING_SERVICES: 'Missing services',
|
||||
MISSING_SLOT: 'Missing slot id',
|
||||
MISSING_START_TIME: 'Missing time start',
|
||||
NO_PERMISSION: 'No permission',
|
||||
INACTIVE_CLIENT: 'Клиент не активен',
|
||||
INACTIVE_MASTER: 'Мастер не активен',
|
||||
INVALID_MASTER: 'Некорректный мастер',
|
||||
INVALID_SERVICE_DURATION: 'Неверная длительность услуги',
|
||||
INVALID_TIME: 'Некорректное время',
|
||||
MISSING_CLIENT: 'Не указан клиент',
|
||||
MISSING_ORDER: 'Заказ не найден',
|
||||
MISSING_SERVICE_ID: 'Не указан идентификатор услуги',
|
||||
MISSING_SERVICES: 'Отсутствуют услуги',
|
||||
MISSING_SLOT: 'Не указан слот',
|
||||
MISSING_START_TIME: 'Не указано время начала',
|
||||
MISSING_TIME: 'Не указано время',
|
||||
NO_MASTER_SELF_BOOK: 'Нельзя записать к самому себе',
|
||||
NO_PERMISSION: 'Нет доступа',
|
||||
NOT_FOUND_CLIENT: 'Клиент не найден',
|
||||
NOT_FOUND_MASTER: 'Мастер не найден',
|
||||
ORDER_IN_PAST: 'Нельзя создать запись на время в прошлом',
|
||||
ORDER_OUT_OF_SLOT: 'Время заказа выходит за пределы слота',
|
||||
OVERLAPPING_TIME: 'Время пересекается с другими заказами',
|
||||
SLOT_CLOSED: 'Слот закрыт',
|
||||
};
|
||||
|
||||
export class OrdersService extends BaseService {
|
||||
async createOrder(variables: VariablesOf<typeof GQL.CreateOrderDocument>) {
|
||||
await this.checkBeforeCreate(variables);
|
||||
|
||||
const { customer } = await this._getUser();
|
||||
|
||||
// Проверки на существование обязательных полей для предотвращения ошибок типов
|
||||
@ -86,6 +104,8 @@ export class OrdersService extends BaseService {
|
||||
}
|
||||
|
||||
async updateOrder(variables: VariablesOf<typeof GQL.UpdateOrderDocument>) {
|
||||
await this.checkUpdatePermission(variables);
|
||||
|
||||
const { customer } = await this._getUser();
|
||||
|
||||
const { query } = await getClientWithToken();
|
||||
@ -127,4 +147,147 @@ export class OrdersService extends BaseService {
|
||||
|
||||
return mutationResult.data;
|
||||
}
|
||||
|
||||
private async checkBeforeCreate(variables: VariablesOf<typeof GQL.CreateOrderDocument>) {
|
||||
const {
|
||||
client: clientId,
|
||||
datetime_end,
|
||||
datetime_start,
|
||||
services,
|
||||
slot: slotId,
|
||||
} = variables.input;
|
||||
|
||||
// Проверка наличия обязательных полей
|
||||
if (!slotId) throw new Error(ERRORS.MISSING_SLOT);
|
||||
if (!clientId) throw new Error(ERRORS.MISSING_CLIENT);
|
||||
if (!services?.length) throw new Error(ERRORS.MISSING_SERVICES);
|
||||
|
||||
// Проверка корректности времени заказа.
|
||||
if (!datetime_start || !datetime_end) {
|
||||
throw new Error(ERRORS.MISSING_TIME);
|
||||
}
|
||||
|
||||
if (new Date(datetime_end) <= new Date(datetime_start)) {
|
||||
throw new Error(ERRORS.INVALID_TIME);
|
||||
}
|
||||
|
||||
// Проверка, что заказ не создается на время в прошлом
|
||||
if (isBeforeNow(datetime_start, 'minute')) {
|
||||
throw new Error(ERRORS.ORDER_IN_PAST);
|
||||
}
|
||||
|
||||
const slotService = new SlotsService(this._user);
|
||||
// Получаем слот
|
||||
const { slot } = await slotService.getSlot({ documentId: slotId });
|
||||
|
||||
if (!slot) throw new Error(ERRORS.MISSING_SLOT);
|
||||
|
||||
// Проверка, что заказ укладывается в рамки слота
|
||||
if (
|
||||
new Date(datetime_start) < new Date(slot.datetime_start) ||
|
||||
new Date(datetime_end) > new Date(slot.datetime_end)
|
||||
) {
|
||||
throw new Error(ERRORS.ORDER_OUT_OF_SLOT);
|
||||
}
|
||||
|
||||
// 1. Слот не должен быть закрыт
|
||||
if (slot.state === GQL.Enum_Slot_State.Closed) {
|
||||
throw new Error(ERRORS.SLOT_CLOSED);
|
||||
}
|
||||
|
||||
const customerService = new CustomersService(this._user);
|
||||
// Получаем клиента
|
||||
const { customer: clientEntity } = await customerService.getCustomer({ documentId: clientId });
|
||||
|
||||
if (!clientEntity) throw new Error(ERRORS.NOT_FOUND_CLIENT);
|
||||
|
||||
// Проверка активности клиента
|
||||
if (!clientEntity?.active) {
|
||||
throw new Error(ERRORS.INACTIVE_CLIENT);
|
||||
}
|
||||
|
||||
// Получаем мастера слота
|
||||
const slotMaster = slot.master;
|
||||
if (!slotMaster) throw new Error(ERRORS.INVALID_MASTER);
|
||||
if (!slotMaster.active || slotMaster.role !== GQL.Enum_Customer_Role.Master) {
|
||||
throw new Error(ERRORS.INACTIVE_MASTER);
|
||||
}
|
||||
|
||||
// 2. Проверка ролей и связей
|
||||
const isClientMaster = clientEntity.role === GQL.Enum_Customer_Role.Master;
|
||||
const slotMasterId = slot.master?.documentId;
|
||||
|
||||
if (!slotMasterId) {
|
||||
throw new Error(ERRORS.NOT_FOUND_MASTER);
|
||||
}
|
||||
|
||||
if (isClientMaster) {
|
||||
// Мастер может записывать только себя
|
||||
if (slotMasterId !== clientId) {
|
||||
throw new Error(ERRORS.INVALID_MASTER);
|
||||
}
|
||||
} else {
|
||||
// Клиент не должен быть мастером слота
|
||||
if (slotMasterId === clientId) {
|
||||
throw new Error(ERRORS.NO_MASTER_SELF_BOOK);
|
||||
}
|
||||
|
||||
const clientMasters = await customerService.getMasters({ documentId: clientId });
|
||||
|
||||
const isLinkedToSlotMaster = clientMasters?.masters.some(
|
||||
(master) => master?.documentId === slotMasterId,
|
||||
);
|
||||
|
||||
// Клиент должен быть привязан к мастеру слота
|
||||
if (!isLinkedToSlotMaster) {
|
||||
throw new Error(ERRORS.INVALID_MASTER);
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка пересечений заказов по времени.
|
||||
|
||||
const { orders: overlappingOrders } = await this.getOrders({
|
||||
filters: {
|
||||
datetime_end: { gt: datetime_start },
|
||||
datetime_start: { lt: datetime_end },
|
||||
slot: {
|
||||
documentId: { eq: slotId },
|
||||
},
|
||||
state: {
|
||||
notIn: [GQL.Enum_Order_State.Cancelled],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (overlappingOrders?.length) {
|
||||
throw new Error(ERRORS.OVERLAPPING_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkUpdatePermission(variables: VariablesOf<typeof GQL.UpdateOrderDocument>) {
|
||||
const { customer } = await this._getUser();
|
||||
|
||||
const { order } = await this.getOrder({ documentId: variables.documentId });
|
||||
|
||||
if (!order) throw new Error(ERRORS.MISSING_ORDER);
|
||||
|
||||
const isOrderClient = order.client?.documentId === customer.documentId;
|
||||
const isOrderMaster = order.slot?.master?.documentId === customer.documentId;
|
||||
|
||||
if (!isOrderClient && !isOrderMaster) {
|
||||
throw new Error(ERRORS.NO_PERMISSION);
|
||||
}
|
||||
|
||||
if (isOrderClient && variables?.data && Object.keys(variables.data).length > 1) {
|
||||
throw new Error(ERRORS.NO_PERMISSION);
|
||||
}
|
||||
|
||||
if (
|
||||
isOrderClient &&
|
||||
variables?.data?.state &&
|
||||
variables.data.state !== GQL.Enum_Order_State.Cancelling
|
||||
) {
|
||||
throw new Error(ERRORS.NO_PERMISSION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import * as GQL from '../types';
|
||||
import { BaseService } from './base';
|
||||
import { ServicesService } from './services';
|
||||
import { type VariablesOf } from '@graphql-typed-document-node/core';
|
||||
import { getMinutes, isBeforeToday } from '@repo/utils/datetime-format';
|
||||
import { getMinutes, isBeforeNow } from '@repo/utils/datetime-format';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const ERRORS = {
|
||||
@ -190,7 +190,7 @@ export class SlotsService extends BaseService {
|
||||
}
|
||||
|
||||
// Проверка, что слот не создаётся в прошлом
|
||||
if (datetime_start && isBeforeToday(datetime_start)) {
|
||||
if (datetime_start && isBeforeNow(datetime_start)) {
|
||||
throw new Error(ERRORS.NO_PAST_SLOT);
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
/* eslint-disable import/no-unassigned-import */
|
||||
import { type OpUnitType } from 'dayjs';
|
||||
import dayjs, { type ConfigType } from 'dayjs';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
@ -91,15 +92,22 @@ export function getTimeZoneLabel(tz: string = DEFAULT_TZ): string {
|
||||
return `GMT${offset}`;
|
||||
}
|
||||
|
||||
export function isBeforeToday(date: Date | string) {
|
||||
/**
|
||||
* This indicates whether the date is before the other supplied date-time.
|
||||
* ```
|
||||
* // Default unit - 'day':
|
||||
* isBeforeNow(date, 'day')
|
||||
* ```
|
||||
*/
|
||||
export function isBeforeNow(date: Date | string, unit: OpUnitType = 'day') {
|
||||
const now = dayjs().tz(DEFAULT_TZ);
|
||||
const inputDate = dayjs(date).tz(DEFAULT_TZ);
|
||||
|
||||
return inputDate.isBefore(now, 'day');
|
||||
return inputDate.isBefore(now, unit);
|
||||
}
|
||||
|
||||
export function isTodayOrAfter(date: Date | string) {
|
||||
return !isBeforeToday(date);
|
||||
export function isNowOrAfter(date: Date | string) {
|
||||
return !isBeforeNow(date);
|
||||
}
|
||||
|
||||
export function sumTime(datetime: DateTime, durationMinutes: number) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user