* feat(profile): add subscription information to profile page - Integrated `SubscriptionInfoBar` component into the profile page for displaying subscription details. - Updated GraphQL types to include subscription-related fields and filters. - Enhanced the profile data management by adding subscription handling capabilities. - Added a new utility function `getRemainingDays` to calculate remaining days until a specified date. * refactor(tests): remove BOT_TOKEN from environment mocks in order and slots tests - Eliminated the hardcoded BOT_TOKEN from the environment mock in both orders.test.js and slots.test.js to streamline test configurations and improve security practices. * feat(env): add BOT_URL to environment variables and update related configurations - Introduced BOT_URL to the environment schema for enhanced configuration management. - Updated turbo.json to include BOT_URL in the environment variables list. - Modified subscription-bar.tsx to improve user messaging for subscription availability. * feat(pro-page): use next/link * feat(pro-page): enhance subscription messaging and add benefits section - Updated subscription status messaging for clarity and conciseness. - Improved button styling based on trial availability. - Added a new benefits section for non-active subscribers, highlighting key features of the Pro subscription. * fix(pro-page): adjust hero section layout for improved visual consistency - Reduced margin in the hero section to enhance alignment and overall aesthetics of the Pro page. * feat(subscriptions): add trial subscription functionality - Implemented `createTrialSubscription` action in the API for initiating trial subscriptions. - Enhanced the Pro page to include a `TryFreeButton` for users to activate their trial. - Updated GraphQL operations and types to support trial subscription features. - Improved subscription messaging and user experience across relevant components. * refactor(pro-page): streamline ProPage layout and improve bottom navigation visibility - Consolidated the main container for the ProPage to enhance layout consistency. - Updated the BottomNav component to conditionally hide on the Pro page, improving navigation clarity for users. * feat(subscriptions): add trial period validation for subscriptions - Implemented a check to verify if a user has already utilized their trial period before allowing access to subscription services. - Enhanced error handling to provide a clear message when a trial period has been previously used, improving user experience and subscription management. * style(pro-page, subscription-bar): enhance dark mode support and improve styling consistency - Updated gradient backgrounds in ProPage and SubscriptionInfoBar to support dark mode variations. - Refactored class names for better conditional styling based on subscription activity. - Improved text color handling for better readability in both active and inactive states. * feat(orders, subscriptions): implement banned user checks and improve remaining orders calculation - Added `checkIsBanned` method calls in the `createOrder`, `getOrder`, `getOrders`, and `updateOrder` methods of the `OrdersService` to prevent actions from banned users. - Updated the calculation of `remainingOrdersCount` in the `SubscriptionsService` to ensure it does not go below zero, enhancing subscription management accuracy. * feat(subscriptions): enhance error handling with centralized error messages - Introduced a centralized `ERRORS` object in the `subscriptions.ts` file to standardize error messages related to trial subscriptions. - Updated error handling in the `createSubscription` method to utilize the new error messages, improving maintainability and clarity for subscription-related errors. * feat(orders): implement order limit checks for clients and masters - Added order limit validation in the `OrdersService` to check if a master has reached their monthly order limit. - Introduced new error messages for exceeding order limits, enhancing user feedback for both clients and masters. - Integrated `SubscriptionsService` to manage subscription status and remaining order counts effectively. * order-card: fix order_number badge overlays navigation bar * fix(docker-compose): update healthcheck endpoint to include API path * feat(profile): add MasterServicesList component to display services for profile masters - Introduced the MasterServicesList component to show services associated with a master profile. - Updated ProfilePage to conditionally render MasterServicesList based on user role. - Refactored services fetching logic into a new useMasterServices hook for better reusability. * feat(profile): conditionally render SubscriptionInfoBar based on user role - Updated ProfilePage to check if the user is a master and conditionally render the SubscriptionInfoBar component. - Refactored customer fetching logic to include a utility function for determining user role. * fix tests * fix(typo): rename updateSlot to updateOrder for clarity * refactor(contact): remove customer master role checks and simplify contact addition - Updated the `addContact` function to allow all users to add contacts, removing the previous restriction that only masters could do so. - Deleted the `become-master` feature and related utility functions, streamlining the codebase. - Adjusted command settings to reflect the removal of the master role functionality. - Refactored components and hooks to eliminate dependencies on the master role, enhancing user experience and simplifying logic. * refactor(contacts): rename masters to invited and update related functionality - Changed the terminology from "masters" to "invited" and "invitedBy" across the codebase for clarity and consistency. - Updated the `addContact` function to reflect the new naming convention. - Refactored API actions and server methods to support the new invited structure. - Adjusted components and hooks to utilize the updated invited data, enhancing user experience and simplifying logic. * feat(profile): enhance user role checks in subscription and links components - Added conditional rendering in SubscriptionInfoBar and LinksCard to hide components for users with the Client role. - Updated ProfileDataCard to use Enum_Customer_Role for role management. - Improved error handling in OrdersService to differentiate between master and client order limit errors. * refactor(contacts): update grid components and improve customer role handling - Renamed InvitedByGrid to MastersGrid and InvitedGrid to ClientsGrid for clarity. - Enhanced customer role checks by using documentId for identifying the current user. - Updated the contacts passed to grid components to reflect the new naming and role structure. - Adjusted titles in grid components for better user experience. * feat(order): enhance order initialization logic with additional client selection step - Added a new step for client selection in the order initialization process when only a masterId is present. - Disabled cognitive complexity checks for improved code maintainability. * feat(contacts): add showServices prop to ContactRow for conditional rendering - Updated ContactsList to pass showServices prop to ContactRow. - Modified ContactRow to conditionally render services based on the showServices prop, enhancing the display of contact information. * feat(contacts): add DataNotFound component for empty states in contacts and services grids - Integrated DataNotFound component to display a message when no contacts or services are found in the respective grids. - Enhanced loading state handling in ServicesSelect and ScheduleCalendar components to improve user experience during data fetching. * feat(contacts): enhance contact display and improve user experience - Updated ContactsList to include a description prop in ContactRow for better service representation. - Renamed header in OrderContacts from "Контакты" to "Участники" for clarity. - Replaced Avatar components with UserAvatar in various components for consistent user representation. - Filtered active contacts in MastersGrid and ClientsGrid to improve data handling. - Adjusted customer query logic to ensure proper handling of telegramId. * feat(customers): add getCustomers API and enhance customer queries - Introduced getCustomers action and corresponding server method to fetch customer data with pagination and sorting. - Updated hooks to support infinite querying of customers, improving data handling in components. - Refactored ContactsList and related components to utilize the new customer fetching logic, enhancing user experience. - Adjusted filter labels in dropdowns for better clarity and user understanding. * refactor(contacts): consolidate customer queries and enhance contact handling - Replaced use of useCustomersInfiniteQuery with a new useContactsInfiniteQuery hook for improved data fetching. - Simplified ContactsList and MastersGrid components by removing unnecessary customer documentId filters. - Deleted outdated contact-related hooks and queries to streamline the codebase. - Enhanced loading state management across components for better user experience. * fix(avatar): update UserAvatar sizes for consistency across components - Changed UserAvatar size from 'xl' to 'lg' in PersonCard for better alignment with design. - Adjusted UserAvatar size from 'sm' to 'xs' in OrderCard to ensure uniformity in avatar presentation. - Updated sizeClasses in UserAvatar component to reflect the new 'xs' size, enhancing responsiveness. * fix(auth): ensure telegramId is a string for consistent handling - Updated the signIn calls in both Auth and useAuth functions to convert telegramId to a string. - Modified the JWT callback to store telegramId as a string in the token. - Enhanced session handling to correctly assign telegramId from the token to the session user. - Added type definitions for telegramId in next-auth to ensure proper type handling. * fix(auth): handle unregistered users in authentication flow - Updated the authentication logic in both Auth and useAuth functions to redirect unregistered users to the '/unregistered' page. - Enhanced error handling in the authOptions to check for user registration status using the Telegram ID. - Improved the matcher configuration in middleware to exclude the '/unregistered' route from authentication checks. * feat(subscriptions): add SubscriptionRewardFields and update related types - Introduced SubscriptionRewardFields fragment to encapsulate reward-related data for subscriptions. - Updated CustomerFiltersInput and SubscriptionHistoryFiltersInput to include subscription_rewards for enhanced filtering capabilities. - Added SubscriptionRewardFiltersInput and SubscriptionRewardInput types to support reward management in subscriptions. - Modified existing fragments and queries to reflect the new structure and ensure consistency across the codebase. * test payment * feat(subscriptions): refactor subscription handling and update related queries - Renamed `hasUserTrialSubscription` to `usedTrialSubscription` for clarity in the SubscriptionsService. - Updated subscription-related queries and fragments to use `active` instead of `isActive` for consistency. - Enhanced the ProPage component to utilize the new subscription checks and improve trial usage logic. - Removed unused subscription history query to streamline the codebase. - Adjusted the SubscriptionInfoBar to reflect the new subscription state handling. * feat(subscriptions): update subscription messages and enhance bot functionality - Renamed `msg-subscribe-active-until` to `msg-subscription-active-until` for consistency in localization. - Added `msg-subscription-active-days` to inform users about remaining subscription days. - Refactored subscription handling in the bot to utilize updated subscription checks and improve user messaging. - Enhanced conversation flow by integrating chat action for typing indication during subscription interactions. * feat(subscriptions): enhance subscription handling and localization updates - Added new message `msg-subscribe-disabled` to inform users when their subscription is disabled. - Updated `msg-subscription-active-days` to ensure proper localization formatting. - Refactored subscription command in the bot to check for subscription status and respond accordingly. - Enhanced ProfilePage to conditionally render the SubscriptionInfoBar based on subscription status. - Updated GraphQL types and queries to include `proEnabled` for better subscription management. * feat(bot): enhance conversation handling by removing redundant typing indication - Added a chat action for 'typing' indication at the start of the bot's conversation flow. - Removed the redundant 'typing' action from individual conversation handlers to streamline the code. * feat(localization): update Russian localization with support contact and message adjustments - Added a new support contact message for user inquiries. - Refactored existing messages to utilize the new support contact variable for consistency. - Cleaned up redundant messages and ensured proper localization formatting across various sections. * feat(localization): add Pro subscription information and update command list - Introduced new localization entry for Pro subscription information. - Updated command list to include 'pro' command for better user guidance. - Enhanced existing subscription messages for clarity and consistency. * feat(localization): update Pro access terminology and enhance subscription messages - Replaced instances of "подписка" with "доступ" to clarify Pro access terminology. - Updated subscription-related messages for improved user understanding and consistency. - Enhanced command list and bot responses to reflect changes in Pro access messaging. * feat(subscriptions): enhance subscription flow and localization updates - Updated default locale to Russian for improved user experience. - Refactored subscription messages to include expiration dates and active subscription status. - Enhanced keyboard display for subscription options with clear expiration information. - Improved handling of subscription-related queries and responses for better clarity. * update support contact * update bot description * .github\workflows\deploy.yml: add BOT_PROVIDER_TOKEN
1436 lines
39 KiB
JavaScript
1436 lines
39 KiB
JavaScript
import { getClientWithToken } from '../apollo/client';
|
||
import * as GQL from '../types';
|
||
import { CustomersService } from './customers';
|
||
import { ERRORS, OrdersService } from './orders';
|
||
import { ServicesService } from './services';
|
||
import { SlotsService } from './slots';
|
||
import { SubscriptionsService } from './subscriptions';
|
||
import dayjs from 'dayjs';
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||
|
||
vi.mock('../apollo/client');
|
||
vi.mock('./customers');
|
||
vi.mock('./services');
|
||
vi.mock('./slots');
|
||
vi.mock('./subscriptions');
|
||
vi.mock('../config/env', () => {
|
||
return {
|
||
env: {
|
||
LOGIN_GRAPHQL: 'test',
|
||
PASSWORD_GRAPHQL: 'test',
|
||
URL_GRAPHQL: 'test',
|
||
},
|
||
};
|
||
});
|
||
|
||
const mockGetClientWithToken = vi.mocked(getClientWithToken);
|
||
const mockCustomersService = vi.mocked(CustomersService);
|
||
const mockServicesService = vi.mocked(ServicesService);
|
||
const mockSlotsService = vi.mocked(SlotsService);
|
||
const mockSubscriptionsService = vi.mocked(SubscriptionsService);
|
||
|
||
describe('OrdersService', () => {
|
||
/**
|
||
* @type {OrdersService}
|
||
*/
|
||
let ordersService;
|
||
const mockUser = { telegramId: 123_456_789 };
|
||
|
||
const mockCustomer = {
|
||
active: true,
|
||
documentId: 'customer-123',
|
||
firstName: 'John',
|
||
lastName: 'Doe',
|
||
role: GQL.Enum_Customer_Role.Customer,
|
||
telegramId: 123_456_789,
|
||
};
|
||
|
||
const mockMaster = {
|
||
active: true,
|
||
documentId: 'master-123',
|
||
firstName: 'Jane',
|
||
lastName: 'Master',
|
||
role: GQL.Enum_Customer_Role.Master,
|
||
telegramId: 987_654_321,
|
||
};
|
||
|
||
const now = dayjs().minute(0).second(0).millisecond(0);
|
||
vi.setSystemTime(now.toDate());
|
||
|
||
const mockSlot = {
|
||
datetime_end: now.add(6, 'hour').toISOString(),
|
||
datetime_start: now.toISOString(),
|
||
documentId: 'slot-123',
|
||
master: mockMaster,
|
||
orders: [],
|
||
state: GQL.Enum_Slot_State.Open,
|
||
};
|
||
|
||
const mockService = {
|
||
active: true,
|
||
documentId: 'service-123',
|
||
duration: '01:00:00', // 1 час
|
||
master: mockMaster,
|
||
name: 'Test Service',
|
||
};
|
||
|
||
const mockOrder = {
|
||
client: mockCustomer,
|
||
datetime_end: now.add(1, 'hour').toISOString(),
|
||
datetime_start: now.toISOString(),
|
||
documentId: 'order-123',
|
||
services: [mockService],
|
||
slot: mockSlot,
|
||
state: GQL.Enum_Order_State.Created,
|
||
};
|
||
|
||
const mockGetCustomerResult = {
|
||
data: {
|
||
customers: [mockCustomer],
|
||
},
|
||
};
|
||
|
||
beforeEach(() => {
|
||
ordersService = new OrdersService(mockUser);
|
||
vi.clearAllMocks();
|
||
|
||
// Глобальный мок для _getUser
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
|
||
// Глобальный мок для checkIsBanned
|
||
vi.spyOn(ordersService, 'checkIsBanned').mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
|
||
// Глобальные моки для сервисов
|
||
mockServicesService.mockImplementation(() => ({
|
||
getService: vi.fn().mockResolvedValue({
|
||
service: mockService,
|
||
}),
|
||
}));
|
||
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: vi.fn().mockResolvedValue({
|
||
slot: mockSlot,
|
||
}),
|
||
}));
|
||
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: vi.fn().mockResolvedValue({
|
||
customer: mockCustomer,
|
||
}),
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [mockMaster],
|
||
}),
|
||
}));
|
||
|
||
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('createOrder', () => {
|
||
const mockVariables = {
|
||
input: {
|
||
client: 'customer-123',
|
||
datetime_start: now.toISOString(),
|
||
services: ['service-123'],
|
||
slot: 'slot-123',
|
||
},
|
||
};
|
||
|
||
const mockMutationResult = {
|
||
data: {
|
||
createOrder: mockOrder,
|
||
},
|
||
errors: undefined,
|
||
};
|
||
|
||
it('should successfully create order for customer', async () => {
|
||
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
if (query === GQL.GetOrdersDocument) {
|
||
return Promise.resolve({ data: { orders: [] } }); // нет пересекающихся заказов
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: mockMutate,
|
||
query: mockQuery,
|
||
});
|
||
|
||
const result = ordersService.createOrder(mockVariables);
|
||
|
||
await expect(result).resolves.toBe(mockMutationResult.data);
|
||
});
|
||
|
||
it('should successfully create approved order for master', async () => {
|
||
const masterCustomer = {
|
||
...mockCustomer,
|
||
role: GQL.Enum_Customer_Role.Master,
|
||
};
|
||
|
||
// Переопределяем мок для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: masterCustomer,
|
||
});
|
||
|
||
const masterSlot = {
|
||
...mockSlot,
|
||
master: masterCustomer,
|
||
};
|
||
|
||
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve({
|
||
data: { customers: [masterCustomer] },
|
||
});
|
||
}
|
||
|
||
if (query === GQL.GetOrdersDocument) {
|
||
return Promise.resolve({ data: { orders: [] } });
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: mockMutate,
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Переопределяем моки для мастера
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: vi.fn().mockResolvedValue({
|
||
slot: masterSlot,
|
||
}),
|
||
}));
|
||
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: vi.fn().mockResolvedValue({
|
||
customer: masterCustomer,
|
||
}),
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [masterCustomer],
|
||
}),
|
||
}));
|
||
|
||
const result = ordersService.createOrder({
|
||
...mockVariables,
|
||
input: {
|
||
...mockVariables.input,
|
||
client: masterCustomer.documentId,
|
||
},
|
||
});
|
||
|
||
await expect(result).resolves.toBe(mockMutationResult.data);
|
||
});
|
||
|
||
it('should throw error when slot is missing', async () => {
|
||
const variablesWithoutSlot = {
|
||
input: {
|
||
client: 'customer-123',
|
||
datetime_start: now.toISOString(),
|
||
services: ['service-123'],
|
||
},
|
||
};
|
||
|
||
const result = ordersService.createOrder(variablesWithoutSlot);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.MISSING_SLOT);
|
||
});
|
||
|
||
it('should throw error when services are missing', async () => {
|
||
const variablesWithoutServices = {
|
||
input: {
|
||
client: 'customer-123',
|
||
datetime_start: now.toISOString(),
|
||
slot: 'slot-123',
|
||
},
|
||
};
|
||
|
||
const result = ordersService.createOrder(variablesWithoutServices);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.MISSING_SERVICES);
|
||
});
|
||
|
||
it('should throw error when datetime_start is missing', async () => {
|
||
const variablesWithoutStart = {
|
||
input: {
|
||
client: 'customer-123',
|
||
services: ['service-123'],
|
||
slot: 'slot-123',
|
||
},
|
||
};
|
||
|
||
const result = ordersService.createOrder(variablesWithoutStart);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.MISSING_START_TIME);
|
||
});
|
||
|
||
it('should throw error when client is missing', async () => {
|
||
const variablesWithoutClient = {
|
||
input: {
|
||
datetime_start: now.toISOString(),
|
||
services: ['service-123'],
|
||
slot: 'slot-123',
|
||
},
|
||
};
|
||
|
||
const result = ordersService.createOrder(variablesWithoutClient);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.MISSING_CLIENT);
|
||
});
|
||
|
||
it('should throw error when order time is in the past', async () => {
|
||
const pastTime = now.subtract(1, 'hour');
|
||
const variablesWithPastTime = {
|
||
input: {
|
||
client: 'customer-123',
|
||
datetime_end: pastTime.add(1, 'hour').toISOString(),
|
||
datetime_start: pastTime.toISOString(),
|
||
services: ['service-123'],
|
||
slot: 'slot-123',
|
||
},
|
||
};
|
||
|
||
const result = ordersService.createOrder(variablesWithPastTime);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NO_ORDER_IN_PAST);
|
||
});
|
||
|
||
it('should throw error when slot is not found', async () => {
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
const mockGetSlot = vi.fn().mockResolvedValue({
|
||
slot: null, // слот не найден
|
||
});
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: mockGetSlot,
|
||
}));
|
||
|
||
const result = ordersService.createOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.INVALID_MASTER);
|
||
});
|
||
|
||
it('should throw error when order is out of slot time', async () => {
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
const mockGetSlot = vi.fn().mockResolvedValue({
|
||
slot: mockSlot,
|
||
});
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: mockGetSlot,
|
||
}));
|
||
|
||
const mockGetCustomer = vi.fn().mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: mockGetCustomer,
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [mockMaster],
|
||
}),
|
||
}));
|
||
|
||
const variablesWithOutOfSlotTime = {
|
||
input: {
|
||
client: 'customer-123',
|
||
datetime_end: now.add(8, 'hour').toISOString(),
|
||
datetime_start: now.add(7, 'hour').toISOString(), // после окончания слота
|
||
services: ['service-123'],
|
||
slot: 'slot-123',
|
||
},
|
||
};
|
||
|
||
const result = ordersService.createOrder(variablesWithOutOfSlotTime);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NO_ORDER_OUT_OF_SLOT);
|
||
});
|
||
|
||
it('should throw error when slot is closed', async () => {
|
||
const closedSlot = {
|
||
...mockSlot,
|
||
state: GQL.Enum_Slot_State.Closed,
|
||
};
|
||
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
const mockGetSlot = vi.fn().mockResolvedValue({
|
||
slot: closedSlot,
|
||
});
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: mockGetSlot,
|
||
}));
|
||
|
||
const mockGetCustomer = vi.fn().mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: mockGetCustomer,
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [mockMaster],
|
||
}),
|
||
}));
|
||
|
||
const result = ordersService.createOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.SLOT_CLOSED);
|
||
});
|
||
|
||
it('should throw error when client is not found', async () => {
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
const mockGetSlot = vi.fn().mockResolvedValue({
|
||
slot: mockSlot,
|
||
});
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: mockGetSlot,
|
||
}));
|
||
|
||
const mockGetCustomer = vi.fn().mockResolvedValue({
|
||
customer: null, // клиент не найден
|
||
});
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: mockGetCustomer,
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [mockMaster],
|
||
}),
|
||
}));
|
||
|
||
const result = ordersService.createOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NOT_FOUND_CLIENT);
|
||
});
|
||
|
||
it('should throw error when client is inactive', async () => {
|
||
const inactiveCustomer = {
|
||
...mockCustomer,
|
||
active: false,
|
||
};
|
||
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
const mockGetSlot = vi.fn().mockResolvedValue({
|
||
slot: mockSlot,
|
||
});
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: mockGetSlot,
|
||
}));
|
||
|
||
const mockGetCustomer = vi.fn().mockResolvedValue({
|
||
customer: inactiveCustomer,
|
||
});
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: mockGetCustomer,
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [mockMaster],
|
||
}),
|
||
}));
|
||
|
||
const result = ordersService.createOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.INACTIVE_CLIENT);
|
||
});
|
||
|
||
it('should throw error when master is inactive', async () => {
|
||
const inactiveMaster = {
|
||
...mockMaster,
|
||
active: false,
|
||
};
|
||
|
||
const slotWithInactiveMaster = {
|
||
...mockSlot,
|
||
master: inactiveMaster,
|
||
};
|
||
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
const mockGetSlot = vi.fn().mockResolvedValue({
|
||
slot: slotWithInactiveMaster,
|
||
});
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: mockGetSlot,
|
||
}));
|
||
|
||
const mockGetCustomer = vi.fn().mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: mockGetCustomer,
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [inactiveMaster],
|
||
}),
|
||
}));
|
||
|
||
const result = ordersService.createOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.INACTIVE_MASTER);
|
||
});
|
||
|
||
it('should throw error when customer tries to book themselves as master', async () => {
|
||
const activeCustomerAsMaster = {
|
||
...mockCustomer,
|
||
active: true,
|
||
role: GQL.Enum_Customer_Role.Master,
|
||
};
|
||
|
||
const slotWithCustomerAsMaster = {
|
||
...mockSlot,
|
||
master: activeCustomerAsMaster,
|
||
};
|
||
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
const mockGetSlot = vi.fn().mockResolvedValue({
|
||
slot: slotWithCustomerAsMaster,
|
||
});
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: mockGetSlot,
|
||
}));
|
||
|
||
const mockGetCustomer = vi.fn().mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: mockGetCustomer,
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [activeCustomerAsMaster],
|
||
}),
|
||
}));
|
||
|
||
const result = ordersService.createOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NO_MASTER_SELF_BOOK);
|
||
});
|
||
|
||
it('should throw error when customer is not linked to master', async () => {
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
const mockGetSlot = vi.fn().mockResolvedValue({
|
||
slot: mockSlot,
|
||
});
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: mockGetSlot,
|
||
}));
|
||
|
||
const mockGetCustomer = vi.fn().mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: mockGetCustomer,
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [], // клиент не связан с мастером
|
||
}),
|
||
}));
|
||
|
||
const result = ordersService.createOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.INVALID_MASTER);
|
||
});
|
||
|
||
it('should throw error when time overlaps with other orders', async () => {
|
||
const overlappingOrder = {
|
||
...mockOrder,
|
||
documentId: 'order-456',
|
||
};
|
||
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
if (query === GQL.GetOrdersDocument) {
|
||
return Promise.resolve({
|
||
data: { orders: [overlappingOrder] },
|
||
}); // есть пересекающиеся заказы
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
const mockGetSlot = vi.fn().mockResolvedValue({
|
||
slot: mockSlot,
|
||
});
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: mockGetSlot,
|
||
}));
|
||
|
||
const mockGetCustomer = vi.fn().mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: mockGetCustomer,
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [mockMaster],
|
||
}),
|
||
}));
|
||
|
||
const result = ordersService.createOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.OVERLAPPING_TIME);
|
||
});
|
||
|
||
it('should throw error when service duration is invalid', async () => {
|
||
const serviceWithoutDuration = {
|
||
...mockService,
|
||
duration: null,
|
||
};
|
||
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
const mockGetSlot = vi.fn().mockResolvedValue({
|
||
slot: mockSlot,
|
||
});
|
||
mockSlotsService.mockImplementation(() => ({
|
||
getSlot: mockGetSlot,
|
||
}));
|
||
|
||
const mockGetCustomer = vi.fn().mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
mockCustomersService.mockImplementation(() => ({
|
||
getCustomer: mockGetCustomer,
|
||
getInvitedBy: vi.fn().mockResolvedValue({
|
||
invitedBy: [mockMaster],
|
||
}),
|
||
}));
|
||
|
||
const mockGetService = vi.fn().mockResolvedValue({
|
||
service: serviceWithoutDuration,
|
||
});
|
||
mockServicesService.mockImplementation(() => ({
|
||
getService: mockGetService,
|
||
}));
|
||
|
||
const result = ordersService.createOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.INVALID_SERVICE_DURATION);
|
||
});
|
||
|
||
it('should calculate datetime_end based on service duration', async () => {
|
||
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetCustomerDocument) {
|
||
return Promise.resolve(mockGetCustomerResult);
|
||
}
|
||
|
||
if (query === GQL.GetOrdersDocument) {
|
||
return Promise.resolve({ data: { orders: [] } });
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: mockMutate,
|
||
query: mockQuery,
|
||
});
|
||
|
||
await ordersService.createOrder(mockVariables);
|
||
|
||
expect(mockMutate).toHaveBeenCalledWith({
|
||
mutation: GQL.CreateOrderDocument,
|
||
variables: {
|
||
...mockVariables,
|
||
input: {
|
||
...mockVariables.input,
|
||
datetime_end: now.add(1, 'hour').toISOString(), // 1 час от начала
|
||
state: GQL.Enum_Order_State.Created,
|
||
},
|
||
},
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('updateOrder', () => {
|
||
const mockVariables = {
|
||
data: {
|
||
datetime_end: now.add(2, 'hour').toISOString(),
|
||
datetime_start: now.add(1, 'hour').toISOString(),
|
||
state: GQL.Enum_Order_State.Approved,
|
||
},
|
||
documentId: 'order-123',
|
||
};
|
||
|
||
const mockMutationResult = {
|
||
data: {
|
||
updateOrder: {
|
||
...mockOrder,
|
||
...mockVariables.data,
|
||
},
|
||
},
|
||
errors: undefined,
|
||
};
|
||
|
||
it('should successfully update order by master', async () => {
|
||
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetOrderDocument) {
|
||
return Promise.resolve({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
if (query === GQL.GetOrdersDocument) {
|
||
return Promise.resolve({ data: { orders: [] } });
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: mockMutate,
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const result = await ordersService.updateOrder(mockVariables);
|
||
|
||
expect(result).toBe(mockMutationResult.data);
|
||
expect(mockMutate).toHaveBeenCalledWith({
|
||
mutation: GQL.UpdateOrderDocument,
|
||
variables: {
|
||
...mockVariables,
|
||
data: {
|
||
...mockVariables.data,
|
||
order_number: undefined,
|
||
},
|
||
},
|
||
});
|
||
});
|
||
|
||
it('should successfully update order by client (cancelling)', async () => {
|
||
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetOrderDocument) {
|
||
return Promise.resolve({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
client: mockCustomer,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: mockMutate,
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для клиента
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
|
||
const clientVariables = {
|
||
...mockVariables,
|
||
data: {
|
||
state: GQL.Enum_Order_State.Cancelling,
|
||
},
|
||
};
|
||
|
||
const result = await ordersService.updateOrder(clientVariables);
|
||
|
||
expect(result).toBe(mockMutationResult.data);
|
||
});
|
||
|
||
it('should throw error when order is not found', async () => {
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: { order: null },
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const result = ordersService.updateOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.MISSING_ORDER);
|
||
});
|
||
|
||
it('should throw error when user has no permission', async () => {
|
||
const unauthorizedUser = {
|
||
...mockCustomer,
|
||
documentId: 'unauthorized-user',
|
||
};
|
||
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
client: mockCustomer,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для неавторизованного пользователя
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: unauthorizedUser,
|
||
});
|
||
|
||
const result = ordersService.updateOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION);
|
||
});
|
||
|
||
it('should throw error when client tries to change more than one field', async () => {
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
client: mockCustomer,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для клиента
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
|
||
const clientVariables = {
|
||
...mockVariables,
|
||
data: {
|
||
datetime_start: now.add(1, 'hour').toISOString(),
|
||
state: GQL.Enum_Order_State.Cancelling,
|
||
},
|
||
};
|
||
|
||
const result = ordersService.updateOrder(clientVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION);
|
||
});
|
||
|
||
it('should throw error when client tries to change state to non-cancelling', async () => {
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
client: mockCustomer,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для клиента
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
|
||
const clientVariables = {
|
||
...mockVariables,
|
||
data: {
|
||
state: GQL.Enum_Order_State.Approved,
|
||
},
|
||
};
|
||
|
||
const result = ordersService.updateOrder(clientVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION);
|
||
});
|
||
|
||
it('should throw error when trying to change client', async () => {
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const variablesWithClient = {
|
||
...mockVariables,
|
||
data: {
|
||
...mockVariables.data,
|
||
client: 'new-client-id',
|
||
},
|
||
};
|
||
|
||
const result = ordersService.updateOrder(variablesWithClient);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION);
|
||
});
|
||
|
||
it('should throw error when trying to change services', async () => {
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const variablesWithServices = {
|
||
...mockVariables,
|
||
data: {
|
||
...mockVariables.data,
|
||
services: ['new-service-id'],
|
||
},
|
||
};
|
||
|
||
const result = ordersService.updateOrder(variablesWithServices);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION);
|
||
});
|
||
|
||
it('should throw error when trying to change slot', async () => {
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const variablesWithSlot = {
|
||
...mockVariables,
|
||
data: {
|
||
...mockVariables.data,
|
||
slot: 'new-slot-id',
|
||
},
|
||
};
|
||
|
||
const result = ordersService.updateOrder(variablesWithSlot);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NO_PERMISSION);
|
||
});
|
||
|
||
it('should throw error when order slot is not found', async () => {
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
client: mockCustomer,
|
||
slot: null,
|
||
},
|
||
},
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для клиента заказа
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockCustomer,
|
||
});
|
||
|
||
const variablesWithSingleField = {
|
||
...mockVariables,
|
||
data: {
|
||
state: GQL.Enum_Order_State.Cancelling,
|
||
},
|
||
};
|
||
|
||
const result = ordersService.updateOrder(variablesWithSingleField);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NOT_FOUND_ORDER_SLOT);
|
||
});
|
||
|
||
it('should throw error when trying to change completed order state', async () => {
|
||
const completedOrder = {
|
||
...mockOrder,
|
||
state: GQL.Enum_Order_State.Completed,
|
||
};
|
||
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: {
|
||
order: completedOrder,
|
||
},
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const result = ordersService.updateOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.NO_CHANGE_STATE_COMPLETED);
|
||
});
|
||
|
||
it('should throw error when time is invalid', async () => {
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetOrderDocument) {
|
||
return Promise.resolve({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const variablesWithInvalidTime = {
|
||
...mockVariables,
|
||
data: {
|
||
datetime_end: now.add(1, 'hour').toISOString(),
|
||
datetime_start: now.add(2, 'hour').toISOString(), // конец раньше начала
|
||
},
|
||
};
|
||
|
||
const result = ordersService.updateOrder(variablesWithInvalidTime);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.INVALID_TIME);
|
||
});
|
||
|
||
it('should throw error when time overlaps with other orders', async () => {
|
||
const overlappingOrder = {
|
||
...mockOrder,
|
||
documentId: 'order-456',
|
||
};
|
||
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetOrderDocument) {
|
||
return Promise.resolve({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
if (query === GQL.GetOrdersDocument) {
|
||
return Promise.resolve({
|
||
data: { orders: [overlappingOrder] },
|
||
});
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const result = ordersService.updateOrder(mockVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.OVERLAPPING_TIME);
|
||
});
|
||
|
||
it('should successfully complete order and set order number', async () => {
|
||
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetOrderDocument) {
|
||
return Promise.resolve({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
client: mockCustomer,
|
||
datetime_start: now.subtract(1, 'hour').toISOString(), // заказ в прошлом
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
if (query === GQL.GetOrdersDocument) {
|
||
return Promise.resolve({
|
||
data: {
|
||
orders: [
|
||
{
|
||
...mockOrder,
|
||
order_number: 5,
|
||
},
|
||
],
|
||
},
|
||
});
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: mockMutate,
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const completeVariables = {
|
||
...mockVariables,
|
||
data: {
|
||
state: GQL.Enum_Order_State.Completed,
|
||
},
|
||
};
|
||
|
||
const result = await ordersService.updateOrder(completeVariables);
|
||
|
||
expect(result).toBe(mockMutationResult.data);
|
||
expect(mockMutate).toHaveBeenCalledWith({
|
||
mutation: GQL.UpdateOrderDocument,
|
||
variables: {
|
||
...completeVariables,
|
||
data: {
|
||
...completeVariables.data,
|
||
order_number: 6, // 5 + 1
|
||
},
|
||
},
|
||
});
|
||
});
|
||
|
||
it('should successfully complete order and set order number to 1 when no previous completed orders', async () => {
|
||
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
|
||
const mockQuery = vi.fn().mockImplementation(({ query }) => {
|
||
if (query === GQL.GetOrderDocument) {
|
||
return Promise.resolve({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
client: mockCustomer,
|
||
datetime_start: now.subtract(1, 'hour').toISOString(), // заказ в прошлом
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
if (query === GQL.GetOrdersDocument) {
|
||
return Promise.resolve({
|
||
data: {
|
||
orders: [], // нет других завершенных заказов
|
||
},
|
||
});
|
||
}
|
||
|
||
return Promise.resolve({ data: {} });
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: mockMutate,
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const completeVariables = {
|
||
...mockVariables,
|
||
data: {
|
||
state: GQL.Enum_Order_State.Completed,
|
||
},
|
||
};
|
||
|
||
const result = await ordersService.updateOrder(completeVariables);
|
||
|
||
expect(result).toBe(mockMutationResult.data);
|
||
expect(mockMutate).toHaveBeenCalledWith({
|
||
mutation: GQL.UpdateOrderDocument,
|
||
variables: {
|
||
...completeVariables,
|
||
data: {
|
||
...completeVariables.data,
|
||
order_number: undefined,
|
||
},
|
||
},
|
||
});
|
||
});
|
||
|
||
it('should throw error when trying to complete order before start time', async () => {
|
||
const futureOrder = {
|
||
...mockOrder,
|
||
datetime_start: now.add(1, 'hour').toISOString(),
|
||
};
|
||
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: {
|
||
order: futureOrder,
|
||
},
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: vi.fn(),
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const completeVariables = {
|
||
...mockVariables,
|
||
data: {
|
||
state: GQL.Enum_Order_State.Completed,
|
||
},
|
||
};
|
||
|
||
const result = ordersService.updateOrder(completeVariables);
|
||
|
||
await expect(result).rejects.toThrow(ERRORS.CANNOT_COMPLETE_BEFORE_START);
|
||
});
|
||
|
||
it('should return early when no datetime changes are provided', async () => {
|
||
const mockMutate = vi.fn().mockResolvedValue(mockMutationResult);
|
||
const mockQuery = vi.fn().mockResolvedValue({
|
||
data: {
|
||
order: {
|
||
...mockOrder,
|
||
slot: { ...mockSlot, master: mockMaster },
|
||
},
|
||
},
|
||
});
|
||
|
||
mockGetClientWithToken.mockResolvedValue({
|
||
mutate: mockMutate,
|
||
query: mockQuery,
|
||
});
|
||
|
||
// Мокаем _getUser для мастера
|
||
vi.spyOn(ordersService, '_getUser').mockResolvedValue({
|
||
customer: mockMaster,
|
||
});
|
||
|
||
const variablesWithoutTime = {
|
||
...mockVariables,
|
||
data: {
|
||
state: GQL.Enum_Order_State.Approved,
|
||
},
|
||
};
|
||
|
||
const result = await ordersService.updateOrder(variablesWithoutTime);
|
||
|
||
expect(result).toBe(mockMutationResult.data);
|
||
});
|
||
});
|
||
});
|