vchikalkin 64dfec1355 refactor(order-form): update service handling to support multiple services
- Renamed `ServiceSelect` to `ServicesSelect` for clarity.
- Updated state management to handle multiple service IDs instead of a single service ID.
- Adjusted related components (`DateSelect`, `TimeSelect`, `SubmitButton`, and `NextButton`) to accommodate the new services structure.
- Removed the deprecated `service-select.tsx` file and refactored related logic in the order store and API to support multiple services.
- Enhanced error handling in the slots service to validate multiple services correctly.
2025-08-19 19:14:14 +03:00

1560 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { getClientWithToken } from '../apollo/client';
import * as GQL from '../types';
import { ERRORS as BASE_ERRORS } from './base';
import { ServicesService } from './services';
import { ERRORS, SlotsService } from './slots';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
if (!dayjs.prototype.duration) {
dayjs.extend(duration);
}
vi.mock('../apollo/client');
vi.mock('./services');
vi.mock('../config/env', () => {
return {
env: {
BOT_TOKEN: 'test',
LOGIN_GRAPHQL: 'test',
PASSWORD_GRAPHQL: 'test',
URL_GRAPHQL: 'test',
},
};
});
const mockGetClientWithToken = vi.mocked(getClientWithToken);
const mockServicesService = vi.mocked(ServicesService);
describe('SlotsService', () => {
/**
* @type {SlotsService}
*/
let slotsService;
const mockUser = { telegramId: 123_456_789 };
const mockCustomer = {
documentId: 'customer-123',
firstName: 'John',
lastName: 'Doe',
telegramId: 123_456_789,
};
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: mockCustomer,
orders: [],
state: GQL.Enum_Slot_State.Open,
};
const mockGetCustomerResult = {
data: {
customers: [mockCustomer],
},
};
const mockGetSlotResult = {
data: {
slot: mockSlot,
},
};
beforeEach(() => {
slotsService = new SlotsService(mockUser);
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('getAvailableTimeSlots', () => {
const mockVariables = {
filters: {
datetime_start: now.toISOString(),
},
};
const mockContext = {
services: ['service-123', 'service-456'],
};
const mockService1 = {
active: true,
documentId: 'service-123',
duration: '01:00:00', // 1 час
master: mockCustomer,
name: 'Test Service 1',
};
const mockService2 = {
active: true,
documentId: 'service-456',
duration: '00:30:00', // 30 минут
master: mockCustomer,
name: 'Test Service 2',
};
const mockGetServiceResult1 = {
service: mockService1,
};
const mockGetServiceResult2 = {
service: mockService2,
};
const mockSlotWithOrders = {
datetime_end: now.add(2, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'slot-123',
master: mockCustomer,
orders: [
{
datetime_end: now.add(1, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'order-123',
state: GQL.Enum_Order_State.Scheduled,
},
],
state: GQL.Enum_Slot_State.Open,
};
const mockSlotWithoutOrders = {
datetime_end: now.add(5, 'hour').toISOString(),
datetime_start: now.add(3, 'hour').toISOString(),
documentId: 'slot-124',
master: mockCustomer,
orders: [],
state: GQL.Enum_Slot_State.Open,
};
const mockGetSlotsOrdersResult = {
data: {
slots: [mockSlotWithOrders, mockSlotWithoutOrders],
},
};
it('should successfully get available time slots', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetSlotsOrdersDocument) {
return Promise.resolve(mockGetSlotsOrdersResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
query: mockQuery,
});
// Мокаем ServicesService.getService для двух сервисов
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1) // Первый вызов для service-123
.mockResolvedValueOnce(mockGetServiceResult2); // Второй вызов для service-456
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext);
expect(result).toEqual({
slots: [mockSlotWithOrders, mockSlotWithoutOrders],
times: [
{
slotId: 'slot-124',
time: now.add(3, 'hour').toISOString(), // 18:00
},
{
slotId: 'slot-124',
time: now.add(3, 'hour').add(15, 'minute').toISOString(), // 18:15
},
{
slotId: 'slot-124',
time: now.add(3, 'hour').add(30, 'minute').toISOString(), // 18:30
},
],
});
// Проверяем, что getService был вызван для каждого сервиса
expect(mockGetService).toHaveBeenCalledTimes(2);
expect(mockGetService).toHaveBeenNthCalledWith(1, { documentId: 'service-123' });
expect(mockGetService).toHaveBeenNthCalledWith(2, { documentId: 'service-456' });
});
it('should throw error when datetime_start is missing', async () => {
const variablesWithoutStart = {
filters: {},
};
const result = slotsService.getAvailableTimeSlots(variablesWithoutStart, mockContext);
await expect(result).rejects.toThrow(ERRORS.MISSING_DATETIME_START);
});
it('should throw error when services array is missing', async () => {
const contextWithoutServices = {
services: [],
};
const result = slotsService.getAvailableTimeSlots(mockVariables, contextWithoutServices);
await expect(result).rejects.toThrow(ERRORS.MISSING_SERVICES_IDS);
});
it('should throw error when services array is null', async () => {
const contextWithNullServices = {
services: null,
};
const result = slotsService.getAvailableTimeSlots(mockVariables, contextWithNullServices);
await expect(result).rejects.toThrow(ERRORS.MISSING_SERVICES_IDS);
});
it('should throw error when service is not found', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetSlotsOrdersDocument) {
return Promise.resolve(mockGetSlotsOrdersResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
query: mockQuery,
});
// Мокаем ServicesService.getService возвращающий null для первого сервиса
const mockGetService = vi.fn().mockResolvedValue({
service: null,
});
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = slotsService.getAvailableTimeSlots(mockVariables, mockContext);
await expect(result).rejects.toThrow(ERRORS.NOT_FOUND_SERVICE);
});
it('should filter out times that conflict with orders', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetSlotsOrdersDocument) {
return Promise.resolve(mockGetSlotsOrdersResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
query: mockQuery,
});
// Мокаем ServicesService.getService для двух сервисов
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1)
.mockResolvedValueOnce(mockGetServiceResult2);
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext);
// Проверяем, что время конфликтующее с заказом не включено
const conflictingTime = now.toISOString();
const hasConflictingTime = result.times.some((time) => time.time === conflictingTime);
expect(hasConflictingTime).toBe(false);
});
it('should include times from cancelled orders', async () => {
// Устанавливаем текущее время на 5 минут раньше начала слота
const currentTime = now.subtract(5, 'minute');
vi.setSystemTime(currentTime.toDate());
const slotWithCancelledOrder = {
datetime_end: now.add(2, 'hour').toISOString(), // 12:00
datetime_start: now.toISOString(), // 10:00
documentId: 'slot-cancelled',
master: mockCustomer,
orders: [
{
datetime_end: now.add(1, 'hour').toISOString(), // 11:00
datetime_start: now.toISOString(), // 10:00
documentId: 'order-123',
state: GQL.Enum_Order_State.Cancelled,
},
],
state: GQL.Enum_Slot_State.Open,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetSlotsOrdersDocument) {
return Promise.resolve({
data: { slots: [slotWithCancelledOrder] },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
query: mockQuery,
});
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1)
.mockResolvedValueOnce(mockGetServiceResult2);
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext);
// Слот: 10:0012:00, общая длительность сервисов = 1.5 часа (90 мин)
// datetimeEnd для генерации = 12:00 - 90 мин = 10:30
// Шаг = 15 мин
// Доступные времена: 10:00, 10:15, 10:30 (но 10:00 не пройдет проверку isAfter(now, 'minute'))
// Текущее время = 9:55, поэтому 10:00 > 9:55 на 5 минут > 1 минуту
const expectedTimes = [
now.toISOString(), // 10:00
now.add(15, 'minute').toISOString(), // 10:15
now.add(30, 'minute').toISOString(), // 10:30
];
const actualTimes = result.times
.filter((time) => time.slotId === slotWithCancelledOrder.documentId)
.map((time) => time.time);
expect(actualTimes).toEqual(expectedTimes);
// Восстанавливаем исходное время
vi.setSystemTime(now.toDate());
});
it('should filter out times in the past', async () => {
const pastSlot = {
datetime_end: now.subtract(1, 'hour').toISOString(),
datetime_start: now.subtract(2, 'hour').toISOString(),
documentId: 'slot-past',
master: mockCustomer,
orders: [],
state: GQL.Enum_Slot_State.Open,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetSlotsOrdersDocument) {
return Promise.resolve({
data: { slots: [pastSlot] },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
query: mockQuery,
});
// Мокаем ServicesService.getService для двух сервисов
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1)
.mockResolvedValueOnce(mockGetServiceResult2);
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext);
// Проверяем, что время в прошлом не включено
expect(result.times).toHaveLength(0);
});
it('should generate times with 15-minute intervals', async () => {
const longSlot = {
datetime_end: now.add(3, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'slot-long',
master: mockCustomer,
orders: [],
state: GQL.Enum_Slot_State.Open,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetSlotsOrdersDocument) {
return Promise.resolve({
data: { slots: [longSlot] },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
query: mockQuery,
});
// Мокаем ServicesService.getService для двух сервисов
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1)
.mockResolvedValueOnce(mockGetServiceResult2);
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext);
// Проверяем, что времена генерируются с интервалом 15 минут
expect(result.times.length).toBeGreaterThan(1);
for (let index = 1; index < result.times.length; index++) {
const currentTime = dayjs(result.times[index].time);
const previousTime = dayjs(result.times[index - 1].time);
const diff = currentTime.diff(previousTime, 'minute');
expect(diff).toBe(15);
}
});
it('should skip slots without datetime_start or datetime_end', async () => {
const incompleteSlot = {
datetime_end: now.add(1, 'hour').toISOString(),
datetime_start: null, // отсутствует datetime_start
documentId: 'slot-incomplete',
master: mockCustomer,
orders: [],
state: GQL.Enum_Slot_State.Open,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetSlotsOrdersDocument) {
return Promise.resolve({
data: { slots: [incompleteSlot] },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
query: mockQuery,
});
// Мокаем ServicesService.getService для двух сервисов
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1)
.mockResolvedValueOnce(mockGetServiceResult2);
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext);
// Проверяем, что слот без datetime_start пропущен
expect(result.times).toHaveLength(0);
});
it('should handle GraphQL errors', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetSlotsOrdersDocument) {
return Promise.resolve({
error: { message: 'GraphQL error' },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
query: mockQuery,
});
const result = slotsService.getAvailableTimeSlots(mockVariables, mockContext);
await expect(result).rejects.toThrow('GraphQL error');
});
it('should calculate total service duration correctly', async () => {
const serviceWithDuration1 = {
...mockService1,
duration: '00:30:00', // 30 минут
};
const serviceWithDuration2 = {
...mockService2,
duration: '00:45:00', // 45 минут
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetSlotsOrdersDocument) {
return Promise.resolve({
data: { slots: [mockSlotWithoutOrders] },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
query: mockQuery,
});
// Мокаем ServicesService.getService с разными длительностями
const mockGetService = vi
.fn()
.mockResolvedValueOnce({ service: serviceWithDuration1 })
.mockResolvedValueOnce({ service: serviceWithDuration2 });
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext);
// Проверяем, что времена генерируются с учетом общей длительности услуг (30 + 45 = 75 минут)
expect(result.times.length).toBeGreaterThan(0);
// Последнее время должно быть не позже чем datetime_end минус общая длительность услуг
const lastTime = dayjs(result.times[result.times.length - 1].time);
const slotEnd = dayjs(mockSlotWithoutOrders.datetime_end);
const totalServiceDuration = dayjs.duration('01:15:00'); // 75 минут
const maxTime = slotEnd.subtract(totalServiceDuration);
expect(lastTime.valueOf()).toBeLessThanOrEqual(maxTime.valueOf());
});
});
describe('createSlot', () => {
const mockVariables = {
input: {
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
state: GQL.Enum_Slot_State.Open,
},
};
const mockMutationResult = {
data: {
createSlot: mockSlot,
},
errors: undefined,
};
const mockMasterCustomer = {
...mockCustomer,
active: true,
role: 'master',
};
const mockGetMasterCustomerResult = {
data: {
customers: [mockMasterCustomer],
},
};
it('should successfully create slot when master is active', async () => {
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetMasterCustomerResult);
}
if (query === GQL.GetSlotsDocument) {
return Promise.resolve({ data: { slots: [] } }); // нет пересекающихся слотов
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
const result = slotsService.createSlot({
input: {
...mockVariables.input,
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
},
});
await expect(result).resolves.toBe(mockMutationResult.data);
});
it('should throw error when master is not found', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve({
data: { customers: [] }, // мастер не найден
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.createSlot(mockVariables);
await expect(result).rejects.toThrow(BASE_ERRORS.CUSTOMER_NOT_FOUND);
});
it('should throw error when master is not active', async () => {
const inactiveMaster = {
...mockMasterCustomer,
active: false,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve({
data: { customers: [inactiveMaster] },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.createSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.INACTIVE_MASTER);
});
it('should throw error when master role is not master', async () => {
const nonMasterCustomer = {
...mockMasterCustomer,
role: 'customer',
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve({
data: { customers: [nonMasterCustomer] },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.createSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.INACTIVE_MASTER);
});
it('should throw error when datetime_start is missing', async () => {
const variablesWithoutStart = {
input: {
datetime_end: now.add(6, 'hour').toISOString(),
state: GQL.Enum_Slot_State.Open,
},
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetMasterCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.createSlot(variablesWithoutStart);
await expect(result).rejects.toThrow(ERRORS.MISSING_DATETIME_START);
});
it('should throw error when datetime_end is missing', async () => {
const variablesWithoutEnd = {
input: {
datetime_start: now.toISOString(),
state: GQL.Enum_Slot_State.Open,
},
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetMasterCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.createSlot(variablesWithoutEnd);
await expect(result).rejects.toThrow(ERRORS.MISSING_DATETIME_END);
});
it('should throw error when datetime_end is before datetime_start', async () => {
const variablesWithInvalidTime = {
input: {
datetime_end: now.toISOString(),
datetime_start: now.add(6, 'hour').toISOString(),
state: GQL.Enum_Slot_State.Open,
},
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetMasterCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.createSlot(variablesWithInvalidTime);
await expect(result).rejects.toThrow(ERRORS.INVALID_TIME);
});
it('should throw error when datetime_end equals datetime_start', async () => {
const variablesWithEqualTime = {
input: {
datetime_end: now.toISOString(),
datetime_start: now.toISOString(),
state: GQL.Enum_Slot_State.Open,
},
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetMasterCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.createSlot(variablesWithEqualTime);
await expect(result).rejects.toThrow(ERRORS.INVALID_TIME);
});
it('should throw error when slot is created in the past', async () => {
const yesterday = dayjs().subtract(1, 'day');
const variablesWithPastTime = {
input: {
datetime_end: yesterday.add(1, 'hour').toISOString(),
datetime_start: yesterday.toISOString(),
state: GQL.Enum_Slot_State.Open,
},
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetMasterCustomerResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.createSlot(variablesWithPastTime);
await expect(result).rejects.toThrow(ERRORS.PAST_SLOT);
});
it('should throw error when time overlaps with other slots', async () => {
const overlappingSlot = {
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'slot-456',
master: mockMasterCustomer,
orders: [],
state: GQL.Enum_Slot_State.Open,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetMasterCustomerResult);
}
if (query === GQL.GetSlotsDocument) {
return Promise.resolve({
data: { slots: [overlappingSlot] },
}); // есть пересекающиеся слоты
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.createSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.OVERLAPPING_TIME);
});
it('should allow creation when time does not overlap with other slots', async () => {
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetMasterCustomerResult);
}
if (query === GQL.GetSlotsDocument) {
return Promise.resolve({
data: { slots: [] },
}); // нет пересекающихся слотов
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
const result = slotsService.createSlot(mockVariables);
await expect(result).resolves.toBe(mockMutationResult.data);
});
it('should include master documentId in mutation variables', async () => {
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetMasterCustomerResult);
}
if (query === GQL.GetSlotsDocument) {
return Promise.resolve({ data: { slots: [] } });
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
await slotsService.createSlot(mockVariables);
expect(mockMutate).toHaveBeenCalledWith({
mutation: GQL.CreateSlotDocument,
variables: {
...mockVariables,
input: {
...mockVariables.input,
master: mockMasterCustomer.documentId,
},
},
});
});
});
describe('updateSlot', () => {
const mockVariables = {
data: {
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
state: GQL.Enum_Slot_State.Open,
},
documentId: 'slot-123',
};
const mockMutationResult = {
data: {
updateSlot: mockSlot,
},
errors: undefined,
};
it('should successfully update slot when user has permission', async () => {
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve(mockGetSlotResult);
}
if (query === GQL.GetSlotsDocument) {
return Promise.resolve({ data: { slots: [] } });
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).resolves.toBe(mockMutationResult.data);
});
it('should successfully update slot when time is not changing', async () => {
const variablesWithoutTime = {
data: {
state: GQL.Enum_Slot_State.Closed,
},
documentId: 'slot-123',
};
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve(mockGetSlotResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
const result = slotsService.updateSlot(variablesWithoutTime);
await expect(result).resolves.toBe(mockMutationResult.data);
});
it('should throw error when user does not have permission', async () => {
const unrelatedCustomer = {
...mockCustomer,
documentId: 'different-customer-123',
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve({
data: { customers: [unrelatedCustomer] },
});
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: mockSlot }, // slot принадлежит другому пользователю
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION);
});
it('should throw error when slot does not exist', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: null }, // slot не найден
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.SLOT_NOT_FOUND);
});
it('should throw error when customer is not found', async () => {
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve({
data: { customers: [] }, // пользователь не найден
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).rejects.toThrow(BASE_ERRORS.CUSTOMER_NOT_FOUND);
});
it('should throw error when datetime_start is missing', async () => {
const variablesWithMissingStart = {
data: {
datetime_end: now.add(6, 'hour').toISOString(),
},
documentId: 'slot-123',
};
const slotWithoutStart = {
...mockSlot,
datetime_start: null,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: slotWithoutStart },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(variablesWithMissingStart);
await expect(result).rejects.toThrow(ERRORS.MISSING_DATETIME_START);
});
it('should throw error when datetime_end is missing', async () => {
const variablesWithMissingEnd = {
data: {
datetime_start: now.toISOString(),
},
documentId: 'slot-123',
};
const slotWithoutEnd = {
...mockSlot,
datetime_end: null,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: slotWithoutEnd },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(variablesWithMissingEnd);
await expect(result).rejects.toThrow(ERRORS.MISSING_DATETIME_END);
});
it('should throw error when datetime_end is before datetime_start', async () => {
const variablesWithInvalidTime = {
data: {
datetime_end: now.toISOString(),
datetime_start: now.add(6, 'hour').toISOString(),
},
documentId: 'slot-123',
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve(mockGetSlotResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(variablesWithInvalidTime);
await expect(result).rejects.toThrow(ERRORS.INVALID_TIME);
});
it('should throw error when datetime_end equals datetime_start', async () => {
const variablesWithEqualTime = {
data: {
datetime_end: now.toISOString(),
datetime_start: now.toISOString(),
},
documentId: 'slot-123',
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve(mockGetSlotResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(variablesWithEqualTime);
await expect(result).rejects.toThrow(ERRORS.INVALID_TIME);
});
it('should throw error when slot has scheduled orders', async () => {
const slotWithScheduledOrders = {
...mockSlot,
orders: [
{
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'order-123',
state: GQL.Enum_Order_State.Scheduled,
},
],
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: slotWithScheduledOrders },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.HAS_ORDERS);
});
it('should throw error when slot has approved orders', async () => {
const slotWithApprovedOrders = {
...mockSlot,
orders: [
{
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'order-123',
state: GQL.Enum_Order_State.Approved,
},
],
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: slotWithApprovedOrders },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.HAS_ORDERS);
});
it('should throw error when slot has completed orders', async () => {
const slotWithCompletedOrders = {
...mockSlot,
orders: [
{
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'order-123',
state: GQL.Enum_Order_State.Completed,
},
],
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: slotWithCompletedOrders },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.HAS_ORDERS);
});
it('should throw error when slot has cancelled orders', async () => {
const slotWithCancelledOrders = {
...mockSlot,
orders: [
{
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'order-123',
state: GQL.Enum_Order_State.Cancelled,
},
],
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: slotWithCancelledOrders },
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.HAS_ORDERS);
});
it('should allow update when slot has non-forbidden order states', async () => {
const slotWithNonForbiddenOrders = {
...mockSlot,
orders: [
{
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'order-123',
state: GQL.Enum_Order_State.Draft, // не запрещенное состояние
},
],
};
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: slotWithNonForbiddenOrders },
});
}
if (query === GQL.GetSlotsDocument) {
return Promise.resolve({ data: { slots: [] } }); // нет пересекающихся слотов
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).resolves.toBe(mockMutationResult.data);
});
it('should throw error when time overlaps with other slots', async () => {
const overlappingSlot = {
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'slot-456',
master: mockCustomer,
orders: [],
state: GQL.Enum_Slot_State.Open,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve(mockGetSlotResult);
}
if (query === GQL.GetSlotsDocument) {
return Promise.resolve({
data: { slots: [overlappingSlot] },
}); // есть пересекающиеся слоты
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.OVERLAPPING_TIME);
});
it('should allow update when time does not overlap with other slots', async () => {
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve(mockGetSlotResult);
}
if (query === GQL.GetSlotsDocument) {
return Promise.resolve({
data: { slots: [] },
}); // нет пересекающихся слотов
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
const result = slotsService.updateSlot(mockVariables);
await expect(result).resolves.toBe(mockMutationResult.data);
});
});
describe('deleteSlot', () => {
const mockVariables = {
documentId: 'slot-123',
};
const mockMutationResult = {
data: {
deleteSlot: {
documentId: 'slot-123',
},
},
errors: undefined,
};
it('should successfully delete slot when no orders', async () => {
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve(mockGetSlotResult);
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: mockMutate,
query: mockQuery,
});
const result = slotsService.deleteSlot(mockVariables);
await expect(result).resolves.toBe(mockMutationResult.data);
});
it('should throw error when slot has orders', async () => {
const slotWithOrders = {
...mockSlot,
orders: [
{
datetime_end: now.add(6, 'hour').toISOString(),
datetime_start: now.toISOString(),
documentId: 'order-123',
state: GQL.Enum_Order_State.Scheduled,
},
],
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve(mockGetCustomerResult);
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: slotWithOrders }, // slot с заказами
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.deleteSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.HAS_ORDERS);
});
it('should throw error when user does not have permission', async () => {
const unrelatedCustomer = {
...mockCustomer,
documentId: 'different-customer-123',
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetCustomerDocument) {
return Promise.resolve({
data: { customers: [unrelatedCustomer] },
});
}
if (query === GQL.GetSlotDocument) {
return Promise.resolve({
data: { slot: mockSlot }, // slot принадлежит другому пользователю
});
}
return Promise.resolve({ data: {} });
});
mockGetClientWithToken.mockResolvedValue({
mutate: vi.fn(),
query: mockQuery,
});
const result = slotsService.deleteSlot(mockVariables);
await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION);
});
});
});