add orders.test.js

This commit is contained in:
vchikalkin 2025-08-06 15:06:01 +03:00
parent 429f5dcab2
commit f7c21d5c01
2 changed files with 808 additions and 8 deletions

View File

@ -0,0 +1,796 @@
import { getClientWithToken } from '../apollo/client';
import * as GQL from '../types';
import { CustomersService } from './customers';
import { ERRORS, OrdersService } from './orders';
import { ServicesService } from './services';
import { SlotsService } from './slots';
import dayjs from 'dayjs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('../apollo/client');
vi.mock('./customers');
vi.mock('./services');
vi.mock('./slots');
vi.mock('../config/env', () => {
return {
env: {
BOT_TOKEN: 'test',
LOGIN_GRAPHQL: 'test',
PASSWORD_GRAPHQL: 'test',
URL_GRAPHQL: 'test',
},
};
});
const mockGetClientWithToken = vi.mocked(getClientWithToken);
const mockCustomersService = vi.mocked(CustomersService);
const mockServicesService = vi.mocked(ServicesService);
const mockSlotsService = vi.mocked(SlotsService);
describe('OrdersService', () => {
/**
* @type {OrdersService}
*/
let ordersService;
const mockUser = { telegramId: 123_456_789 };
const mockCustomer = {
active: true,
documentId: 'customer-123',
firstName: 'John',
lastName: 'Doe',
role: GQL.Enum_Customer_Role.Customer,
telegramId: 123_456_789,
};
const mockMaster = {
active: true,
documentId: 'master-123',
firstName: 'Jane',
lastName: 'Master',
role: GQL.Enum_Customer_Role.Master,
telegramId: 987_654_321,
};
const now = dayjs().minute(0).second(0).millisecond(0);
vi.setSystemTime(now.toDate());
const mockSlot = {
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'slot-123',
master: mockMaster,
orders: [],
state: GQL.Enum_Slot_State.Open,
};
const mockService = {
active: true,
documentId: 'service-123',
duration: '01:00:00', // 1 час
master: mockMaster,
name: 'Test Service',
};
const mockOrder = {
client: mockCustomer,
datetime_end: now.add(1, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'order-123',
services: [mockService],
slot: mockSlot,
state: GQL.Enum_Order_State.Created,
};
const mockGetCustomerResult = {
data: {
customers: [mockCustomer],
},
};
beforeEach(() => {
ordersService = new OrdersService(mockUser);
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('createOrder', () => {
const mockVariables = {
input: {
client: 'customer-123',
datetime_end: now.add(1, 'hour').toISOString(),
datetime_start: now.toISOString(),
services: ['service-123'],
slot: 'slot-123',
},
};
const mockMutationResult = {
data: {
createOrder: mockOrder,
},
errors: undefined,
};
it('should successfully create order for customer', async () => {
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetOrdersDocument) {
return Promise.resolve({ data: { orders: [] } }); // нет пересекающихся заказов
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
// Мокаем ServicesService.getService
const mockGetService = vi.fn().mockResolvedValue({
service: mockService,
});
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
// Мокаем SlotsService.getSlot
const mockGetSlot = vi.fn().mockResolvedValue({
slot: mockSlot,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
// Мокаем CustomersService.getCustomer
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: mockCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [mockMaster],
}),
}));
const result = ordersService.createOrder(mockVariables);
await expect(result).resolves.toBe(mockMutationResult.data);
});
it('should successfully create approved order for master', async () => {
const masterCustomer = {
...mockCustomer,
role: GQL.Enum_Customer_Role.Master,
};
const masterSlot = {
...mockSlot,
master: masterCustomer,
};
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve({
data: { customers: [masterCustomer] },
});
}
if (query === GQL.GetOrdersDocument) {
return Promise.resolve({ data: { orders: [] } });
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
const mockGetService = vi.fn().mockResolvedValue({
service: mockService,
});
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const mockGetSlot = vi.fn().mockResolvedValue({
slot: masterSlot,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: masterCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [masterCustomer],
}),
}));
const result = ordersService.createOrder({
...mockVariables,
input: {
...mockVariables.input,
client: masterCustomer.documentId,
},
});
await expect(result).resolves.toBe(mockMutationResult.data);
});
it('should throw error when slot is missing', async () => {
const variablesWithoutSlot = {
input: {
client: 'customer-123',
datetime_start: now.toISOString(),
services: ['service-123'],
},
};
const result = ordersService.createOrder(variablesWithoutSlot);
await expect(result).rejects.toThrow(ERRORS.MISSING_SLOT);
});
it('should throw error when services are missing', async () => {
const variablesWithoutServices = {
input: {
client: 'customer-123',
datetime_start: now.toISOString(),
slot: 'slot-123',
},
};
const result = ordersService.createOrder(variablesWithoutServices);
await expect(result).rejects.toThrow(ERRORS.MISSING_SERVICES);
});
it('should throw error when datetime_start is missing', async () => {
const variablesWithoutStart = {
input: {
client: 'customer-123',
services: ['service-123'],
slot: 'slot-123',
},
};
const result = ordersService.createOrder(variablesWithoutStart);
await expect(result).rejects.toThrow(ERRORS.MISSING_START_TIME);
});
it('should throw error when client is missing', async () => {
const variablesWithoutClient = {
input: {
datetime_start: now.toISOString(),
services: ['service-123'],
slot: 'slot-123',
},
};
const result = ordersService.createOrder(variablesWithoutClient);
await expect(result).rejects.toThrow(ERRORS.MISSING_CLIENT);
});
it('should throw error when order time is in the past', async () => {
const pastTime = now.subtract(1, 'hour');
const variablesWithPastTime = {
input: {
client: 'customer-123',
datetime_end: pastTime.add(1, 'hour').toISOString(),
datetime_start: pastTime.toISOString(),
services: ['service-123'],
slot: 'slot-123',
},
};
const result = ordersService.createOrder(variablesWithPastTime);
await expect(result).rejects.toThrow(ERRORS.NO_ORDER_IN_PAST);
});
it('should throw error when order time is invalid', async () => {
const variablesWithInvalidTime = {
input: {
client: 'customer-123',
datetime_end: now.toISOString(), // равно datetime_start
datetime_start: now.toISOString(),
services: ['service-123'],
slot: 'slot-123',
},
};
const result = ordersService.createOrder(variablesWithInvalidTime);
await expect(result).rejects.toThrow(ERRORS.INVALID_TIME);
});
it('should throw error when slot is not found', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const mockGetSlot = vi.fn().mockResolvedValue({
slot: null, // слот не найден
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const result = ordersService.createOrder(mockVariables);
await expect(result).rejects.toThrow(ERRORS.MISSING_SLOT);
});
it('should throw error when order is out of slot time', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const mockGetSlot = vi.fn().mockResolvedValue({
slot: mockSlot,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: mockCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [mockMaster],
}),
}));
const variablesWithOutOfSlotTime = {
input: {
client: 'customer-123',
datetime_end: now.add(8, 'hour').toISOString(),
datetime_start: now.add(7, 'hour').toISOString(), // после окончания слота
services: ['service-123'],
slot: 'slot-123',
},
};
const result = ordersService.createOrder(variablesWithOutOfSlotTime);
await expect(result).rejects.toThrow(ERRORS.NO_ORDER_OUT_OF_SLOT);
});
it('should throw error when slot is closed', async () => {
const closedSlot = {
...mockSlot,
state: GQL.Enum_Slot_State.Closed,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const mockGetSlot = vi.fn().mockResolvedValue({
slot: closedSlot,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: mockCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [mockMaster],
}),
}));
const result = ordersService.createOrder(mockVariables);
await expect(result).rejects.toThrow(ERRORS.SLOT_CLOSED);
});
it('should throw error when client is not found', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const mockGetSlot = vi.fn().mockResolvedValue({
slot: mockSlot,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: null, // клиент не найден
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [mockMaster],
}),
}));
const result = ordersService.createOrder(mockVariables);
await expect(result).rejects.toThrow(ERRORS.NOT_FOUND_CLIENT);
});
it('should throw error when client is inactive', async () => {
const inactiveCustomer = {
...mockCustomer,
active: false,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const mockGetSlot = vi.fn().mockResolvedValue({
slot: mockSlot,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: inactiveCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [mockMaster],
}),
}));
const result = ordersService.createOrder(mockVariables);
await expect(result).rejects.toThrow(ERRORS.INACTIVE_CLIENT);
});
it('should throw error when master is inactive', async () => {
const inactiveMaster = {
...mockMaster,
active: false,
};
const slotWithInactiveMaster = {
...mockSlot,
master: inactiveMaster,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const mockGetSlot = vi.fn().mockResolvedValue({
slot: slotWithInactiveMaster,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: mockCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [inactiveMaster],
}),
}));
const result = ordersService.createOrder(mockVariables);
await expect(result).rejects.toThrow(ERRORS.INACTIVE_MASTER);
});
it('should throw error when customer tries to book themselves as master', async () => {
const activeCustomerAsMaster = {
...mockCustomer,
active: true,
role: GQL.Enum_Customer_Role.Master,
};
const slotWithCustomerAsMaster = {
...mockSlot,
master: activeCustomerAsMaster,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const mockGetSlot = vi.fn().mockResolvedValue({
slot: slotWithCustomerAsMaster,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: mockCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [activeCustomerAsMaster],
}),
}));
const result = ordersService.createOrder(mockVariables);
await expect(result).rejects.toThrow(ERRORS.NO_MASTER_SELF_BOOK);
});
it('should throw error when customer is not linked to master', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const mockGetSlot = vi.fn().mockResolvedValue({
slot: mockSlot,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: mockCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [], // клиент не связан с мастером
}),
}));
const result = ordersService.createOrder(mockVariables);
await expect(result).rejects.toThrow(ERRORS.INVALID_MASTER);
});
it('should throw error when time overlaps with other orders', async () => {
const overlappingOrder = {
...mockOrder,
documentId: 'order-456',
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetOrdersDocument) {
return Promise.resolve({
data: { orders: [overlappingOrder] },
}); // есть пересекающиеся заказы
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const mockGetSlot = vi.fn().mockResolvedValue({
slot: mockSlot,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: mockCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [mockMaster],
}),
}));
const result = ordersService.createOrder(mockVariables);
await expect(result).rejects.toThrow(ERRORS.OVERLAPPING_TIME);
});
it('should throw error when service duration is invalid', async () => {
const serviceWithoutDuration = {
...mockService,
duration: null,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const mockGetSlot = vi.fn().mockResolvedValue({
slot: mockSlot,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: mockCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [mockMaster],
}),
}));
const mockGetService = vi.fn().mockResolvedValue({
service: serviceWithoutDuration,
});
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = ordersService.createOrder(mockVariables);
await expect(result).rejects.toThrow(ERRORS.INVALID_SERVICE_DURATION);
});
it('should calculate datetime_end based on service duration', async () => {
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetOrdersDocument) {
return Promise.resolve({ data: { orders: [] } });
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
const mockGetService = vi.fn().mockResolvedValue({
service: mockService,
});
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const mockGetSlot = vi.fn().mockResolvedValue({
slot: mockSlot,
});
mockSlotsService.mockImplementation(() => ({
getSlot: mockGetSlot,
}));
const mockGetCustomer = vi.fn().mockResolvedValue({
customer: mockCustomer,
});
mockCustomersService.mockImplementation(() => ({
getCustomer: mockGetCustomer,
getMasters: vi.fn().mockResolvedValue({
masters: [mockMaster],
}),
}));
await ordersService.createOrder(mockVariables);
expect(mockMutate).toHaveBeenCalledWith({
mutation: GQL.CreateOrderDocument,
variables: {
...mockVariables,
input: {
...mockVariables.input,
datetime_end: now.add(1, 'hour').toISOString(), // 1 час от начала
state: GQL.Enum_Order_State.Created,
},
},
});
});
});
});

View File

@ -11,13 +11,14 @@ import { isCustomerMaster } from '@repo/utils/customer';
import { getMinutes, isBeforeNow } from '@repo/utils/datetime-format';
import dayjs from 'dayjs';
const ERRORS = {
export const ERRORS = {
INACTIVE_CLIENT: 'Клиент не активен',
INACTIVE_MASTER: 'Мастер не активен',
INVALID_MASTER: 'Некорректный мастер',
INVALID_SERVICE_DURATION: 'Неверная длительность услуги',
INVALID_TIME: 'Некорректное время',
MISSING_CLIENT: 'Не указан клиент',
MISSING_END_TIME: 'Не указано время окончания',
MISSING_ORDER: 'Заказ не найден',
MISSING_SERVICE_ID: 'Не указан идентификатор услуги',
MISSING_SERVICES: 'Отсутствуют услуги',
@ -25,11 +26,11 @@ const ERRORS = {
MISSING_START_TIME: 'Не указано время начала',
MISSING_TIME: 'Не указано время',
NO_MASTER_SELF_BOOK: 'Нельзя записать к самому себе',
NO_ORDER_IN_PAST: 'Нельзя создать запись на время в прошлом',
NO_ORDER_OUT_OF_SLOT: 'Время заказа выходит за пределы слота',
NO_PERMISSION: 'Нет доступа',
NOT_FOUND_CLIENT: 'Клиент не найден',
NOT_FOUND_MASTER: 'Мастер не найден',
ORDER_IN_PAST: 'Нельзя создать запись на время в прошлом',
ORDER_OUT_OF_SLOT: 'Время заказа выходит за пределы слота',
OVERLAPPING_TIME: 'Время пересекается с другими заказами',
SLOT_CLOSED: 'Слот закрыт',
};
@ -44,7 +45,6 @@ export class OrdersService extends BaseService {
if (!variables.input.slot) throw new Error(ERRORS.MISSING_SLOT);
if (!variables.input.services?.length) throw new Error(ERRORS.MISSING_SERVICES);
if (!variables.input.services[0]) throw new Error(ERRORS.MISSING_SERVICE_ID);
if (!variables.input.datetime_start) throw new Error(ERRORS.MISSING_START_TIME);
if (!variables.input.client) throw new Error(ERRORS.MISSING_CLIENT);
const servicesService = new ServicesService(this._user);
@ -163,8 +163,12 @@ export class OrdersService extends BaseService {
if (!services?.length) throw new Error(ERRORS.MISSING_SERVICES);
// Проверка корректности времени заказа.
if (!datetime_start || !datetime_end) {
throw new Error(ERRORS.MISSING_TIME);
if (!datetime_start) {
throw new Error(ERRORS.MISSING_START_TIME);
}
if (!datetime_end) {
throw new Error(ERRORS.MISSING_END_TIME);
}
if (new Date(datetime_end) <= new Date(datetime_start)) {
@ -173,7 +177,7 @@ export class OrdersService extends BaseService {
// Проверка, что заказ не создается на время в прошлом
if (isBeforeNow(datetime_start, 'minute')) {
throw new Error(ERRORS.ORDER_IN_PAST);
throw new Error(ERRORS.NO_ORDER_IN_PAST);
}
const slotService = new SlotsService(this._user);
@ -187,7 +191,7 @@ export class OrdersService extends BaseService {
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);
throw new Error(ERRORS.NO_ORDER_OUT_OF_SLOT);
}
// 1. Слот не должен быть закрыт