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 = { service: { documentId: { eq: 'service-123' }, }, }; const mockService = { active: true, documentId: 'service-123', duration: '01:00:00', // 1 час master: mockCustomer, name: 'Test Service', }; const mockGetServiceResult = { service: mockService, }; 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().mockResolvedValue(mockGetServiceResult); mockServicesService.mockImplementation(() => ({ getService: mockGetService, })); const result = slotsService.getAvailableTimeSlots(mockVariables, mockContext); expect(result).resolves.toEqual({ slots: [mockSlotWithOrders, mockSlotWithoutOrders], times: [ { slotId: 'slot-123', time: now.add(1, 'hour').toISOString(), // 11:00 }, { slotId: 'slot-124', time: now.add(3, 'hour').toISOString(), // 13:00 }, { slotId: 'slot-124', time: now.add(3, 'hour').add(15, 'minute').toISOString(), // 13:15 }, { slotId: 'slot-124', time: now.add(3, 'hour').add(30, 'minute').toISOString(), // 13:30 }, { slotId: 'slot-124', time: now.add(3, 'hour').add(45, 'minute').toISOString(), // 13:45 }, { slotId: 'slot-124', time: now.add(4, 'hour').toISOString(), // 14:00 }, ], }); }); 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 service documentId is missing', async () => { const contextWithoutService = { service: {}, }; const result = slotsService.getAvailableTimeSlots(mockVariables, contextWithoutService); await expect(result).rejects.toThrow(ERRORS.MISSING_SERVICE_ID); }); 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({ data: { service: null }, }); mockServicesService.mockImplementation(() => ({ getService: mockGetService, })); const result = slotsService.getAvailableTimeSlots(mockVariables, mockContext); await expect(result).rejects.toThrow(ERRORS.SERVICE_NOT_FOUND); }); 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().mockResolvedValue(mockGetServiceResult); 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 () => { vi.setSystemTime(now.toDate()); // синхронизируем "текущее" время const slotWithCancelledOrder = { ...mockSlotWithOrders, orders: [ { datetime_end: now.add(1, 'hour').toISOString(), datetime_start: now.toISOString(), documentId: 'order-123', state: GQL.Enum_Order_State.Cancelled, }, ], }; 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().mockResolvedValue(mockGetServiceResult); mockServicesService.mockImplementation(() => ({ getService: mockGetService, })); const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext); // Слот: 10:00–12:00, сервис = 1 час, шаг = 15 мин // Все возможные начала интервалов: от 10:00 до 11:00 включительно (всего 5) const expectedTimes = [ // now.toISOString(), // 10:00 now.add(15, 'minute').toISOString(), // 10:15 now.add(30, 'minute').toISOString(), // 10:30 now.add(45, 'minute').toISOString(), // 10:45 now.add(1, 'hour').toISOString(), // 11:00 ]; const actualTimes = result.times .filter((time) => time.slotId === slotWithCancelledOrder.documentId) .map((time) => time.time); expect(actualTimes).toEqual(expectedTimes); }); 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().mockResolvedValue(mockGetServiceResult); 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().mockResolvedValue(mockGetServiceResult); 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().mockResolvedValue(mockGetServiceResult); 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 service duration correctly', async () => { const serviceWithDuration = { ...mockService, duration: '00:30:00', // 30 минут }; 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().mockResolvedValue({ service: serviceWithDuration, }); mockServicesService.mockImplementation(() => ({ getService: mockGetService, })); const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext); // Проверяем, что времена генерируются с учетом длительности услуги 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 serviceDuration = dayjs.duration('00:30:00'); const maxTime = slotEnd.subtract(serviceDuration); 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); }); }); });