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 { SubscriptionsService } from './subscriptions'; 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('./subscriptions'); vi.mock('../config/env', () => { return { env: { LOGIN_GRAPHQL: 'test', PASSWORD_GRAPHQL: 'test', URL_GRAPHQL: 'test', }, }; }); const mockGetClientWithToken = vi.mocked(getClientWithToken); const mockServicesService = vi.mocked(ServicesService); const mockSubscriptionsService = vi.mocked(SubscriptionsService); 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(); // Глобальный мок для checkIsBanned vi.spyOn(slotsService, 'checkIsBanned').mockResolvedValue({ customer: mockCustomer, }); // Глобальный мок для SubscriptionsService mockSubscriptionsService.mockImplementation(() => ({ getSubscription: vi.fn().mockResolvedValue({ maxOrdersPerMonth: 10, remainingOrdersCount: 5, subscription: { autoRenew: false, documentId: 'subscription-123', expiresAt: now.add(30, 'day').toISOString(), isActive: true, }, }), getSubscriptionSettings: vi.fn().mockResolvedValue({ subscriptionSetting: { documentId: 'subscription-setting-123', maxOrdersPerMonth: 10, referralBonusDays: 3, referralRewardDays: 7, }, }), })); }); 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:00–12: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 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); }); }); });