diff --git a/apps/condo/domains/organization/components/EmployeeForm/CreateEmployeeForm.tsx b/apps/condo/domains/organization/components/EmployeeForm/CreateEmployeeForm.tsx index 0b11cf77767..7f24c578ed3 100644 --- a/apps/condo/domains/organization/components/EmployeeForm/CreateEmployeeForm.tsx +++ b/apps/condo/domains/organization/components/EmployeeForm/CreateEmployeeForm.tsx @@ -20,7 +20,6 @@ import { useLayoutContext } from '@condo/domains/common/components/LayoutContext import { PhoneInput } from '@condo/domains/common/components/PhoneInput' import { useValidations } from '@condo/domains/common/hooks/useValidations' import { EmployeeRoleSelect } from '@condo/domains/organization/components/EmployeeRoleSelect' -import { useEmployeeValidations } from '@condo/domains/organization/hooks/useEmployeeValidations' import { OrganizationEmployeeRole, useInviteNewOrganizationEmployee, @@ -70,11 +69,10 @@ export const CreateEmployeeForm: React.FC = () => { ) const { changeMessage, requiredValidator, emailValidator, phoneValidator, trimValidator, specCharValidator } = useValidations() - const { alreadyRegisteredEmailValidator, alreadyRegisteredPhoneValidator } = useEmployeeValidations(organizationId) const validations: { [key: string]: Rule[] } = { - phone: [requiredValidator, phoneValidator, alreadyRegisteredPhoneValidator], - email: [emailValidator, alreadyRegisteredEmailValidator], + phone: [requiredValidator, phoneValidator], + email: [emailValidator], name: [ changeMessage(trimValidator, FullNameRequiredMessage), changeMessage(specCharValidator, FullNameInvalidCharMessage), @@ -143,7 +141,7 @@ export const CreateEmployeeForm: React.FC = () => { return ( <> - + diff --git a/apps/condo/domains/organization/components/EmployeeForm/UpdateEmployeeForm.tsx b/apps/condo/domains/organization/components/EmployeeForm/UpdateEmployeeForm.tsx index 569d7485aa7..3f8d5c29a96 100644 --- a/apps/condo/domains/organization/components/EmployeeForm/UpdateEmployeeForm.tsx +++ b/apps/condo/domains/organization/components/EmployeeForm/UpdateEmployeeForm.tsx @@ -9,7 +9,6 @@ import React, { useCallback, useEffect, useMemo } from 'react' import { useApolloClient } from '@open-condo/next/apollo' import { useAuth } from '@open-condo/next/auth' import { useIntl } from '@open-condo/next/intl' -import { useOrganization } from '@open-condo/next/organization' import { ActionBar, Alert, Button } from '@open-condo/ui' import Input from '@condo/domains/common/components/antd/Input' @@ -21,7 +20,6 @@ import { Loader } from '@condo/domains/common/components/Loader' import Prompt from '@condo/domains/common/components/Prompt' import { useValidations } from '@condo/domains/common/hooks/useValidations' import { EmployeeRoleSelect } from '@condo/domains/organization/components/EmployeeRoleSelect' -import { useEmployeeValidations } from '@condo/domains/organization/hooks/useEmployeeValidations' import { OrganizationEmployeeSpecialization } from '@condo/domains/organization/utils/clientSchema' import { OrganizationEmployee, OrganizationEmployeeRole } from '@condo/domains/organization/utils/clientSchema' import { @@ -55,13 +53,10 @@ export const UpdateEmployeeForm: React.FC = () => { const PromptTitle = intl.formatMessage({ id: 'form.prompt.title' }) const PromptHelpMessage = intl.formatMessage({ id: 'form.prompt.message' }) - const { organization } = useOrganization() const { query, push } = useRouter() const classifiersLoader = new ClassifiersQueryRemote(useApolloClient()) const { breakpoints } = useLayoutContext() - const organizationId = get(organization, 'id') - const employeeId = String(get(query, 'id', '')) const { obj: employee, loading: employeeLoading, error: employeeError } = OrganizationEmployee.useObject({ where: { id: employeeId } }) const { objs: employeeRoles, loading: employeeRolesLoading, error: employeeRolesError } = OrganizationEmployeeRole.useObjects({ where: { organization: { id: get(employee, ['organization', 'id']) } } }) @@ -77,9 +72,8 @@ export const UpdateEmployeeForm: React.FC = () => { [organizationEmployeeSpecializations]) const { emailValidator } = useValidations() - const { alreadyRegisteredEmailValidator } = useEmployeeValidations(organizationId, employeeId) const validations: { [key: string]: Rule[] } = { - email: [emailValidator, alreadyRegisteredEmailValidator], + email: [emailValidator], } const { user } = useAuth() diff --git a/apps/condo/domains/organization/constants/errors.js b/apps/condo/domains/organization/constants/errors.js index eb68bf898cd..775ebdcf44d 100644 --- a/apps/condo/domains/organization/constants/errors.js +++ b/apps/condo/domains/organization/constants/errors.js @@ -1,5 +1,6 @@ const ALREADY_ACCEPTED_INVITATION = 'ALREADY_ACCEPTED_INVITATION' -const ALREADY_INVITED = 'ALREADY_INVITED' +const ALREADY_INVITED_EMAIL = 'ALREADY_INVITED_EMAIL' +const ALREADY_INVITED_PHONE = 'ALREADY_INVITED_PHONE' const EMPTY_NAME_ERROR = '[name.is.too.short' const TIN_TOO_SHORT_ERROR = '[tin.is.too.short' const TIN_VALUE_INVALID = '[tin.value.is.invalid' @@ -8,7 +9,8 @@ const PARENT_NOT_HOLDING = 'PARENT_NOT_HOLDING' module.exports = { ALREADY_ACCEPTED_INVITATION, - ALREADY_INVITED, + ALREADY_INVITED_EMAIL, + ALREADY_INVITED_PHONE, EMPTY_NAME_ERROR, TIN_TOO_SHORT_ERROR, TIN_VALUE_INVALID, diff --git a/apps/condo/domains/organization/hooks/useEmployeeValidations.ts b/apps/condo/domains/organization/hooks/useEmployeeValidations.ts deleted file mode 100644 index b3872266dee..00000000000 --- a/apps/condo/domains/organization/hooks/useEmployeeValidations.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { UserTypeType } from '@app/condo/schema' -import get from 'lodash/get' -import isArray from 'lodash/isArray' -import isEmpty from 'lodash/isEmpty' -import { Rule } from 'rc-field-form/lib/interface' -import { useMemo } from 'react' - -import { useIntl } from '@open-condo/next/intl' - -import { normalizeEmail } from '@condo/domains/common/utils/mail' -import { normalizePhone } from '@condo/domains/common/utils/phone' -import { OrganizationEmployee } from '@condo/domains/organization/utils/clientSchema' -import { User } from '@condo/domains/user/utils/clientSchema' - - -type IUseEmployeeValidations = (organizationId: string, currentEmployeeId?: string) => Record - -export const useEmployeeValidations: IUseEmployeeValidations = (organizationId, currentEmployeeId) => { - const intl = useIntl() - const PhoneIsNotValidMsg = intl.formatMessage({ id: 'global.input.error.wrongMobilePhone' }) - const EmailIsNotValidMsg = intl.formatMessage({ id: 'pages.auth.EmailIsNotValid' }) - const UserAlreadyInListMsg = intl.formatMessage({ id: 'pages.users.UserIsAlreadyInList' }) - - const { objs: employees } = OrganizationEmployee.useObjects( - { - where: { - organization: { id: organizationId }, - isRejected: false, - isBlocked: false, - }, - }, - { fetchPolicy: 'network-only' }, - ) - - const { refetch: searchUsers } = User.useObjects({}, { fetchPolicy: 'network-only', skip: true }) - - const alreadyRegisteredPhoneValidator: Rule = useMemo(() => ({ - validator: async (_, value) => { - const normalizedPhone = normalizePhone(value) - if (!normalizedPhone) return Promise.reject(PhoneIsNotValidMsg) - - const { data: { objs } } = await searchUsers({ - where: { - phone: String(normalizedPhone), - type: UserTypeType.Staff, - }, - }) - const userIds = isArray(objs) - ? objs.map(user => get(user, 'id')).filter(Boolean) - : [] - - const alreadyInList = employees.some(employee => { - if (currentEmployeeId && get(employee, 'id') === currentEmployeeId) return false - - return userIds.some(id => id === get(employee, 'user.id')) || get(employee, 'phone') === normalizedPhone - }) - - if (alreadyInList) return Promise.reject(UserAlreadyInListMsg) - - return Promise.resolve() - }, - }), [PhoneIsNotValidMsg, UserAlreadyInListMsg, currentEmployeeId, employees]) - - const alreadyRegisteredEmailValidator: Rule = useMemo(() => ({ - validator: async (_, value) => { - if (isEmpty(value)) return Promise.resolve() - - const normalizedEmail = normalizeEmail(value) - if (!normalizedEmail) return Promise.reject(EmailIsNotValidMsg) - - const { data: { objs } } = await searchUsers({ - where: { - email: String(value), - type: UserTypeType.Staff, - }, - }) - const userIds = isArray(objs) - ? objs.map(user => get(user, 'id')).filter(Boolean) - : [] - - const alreadyInList = employees.some(employee => { - if (currentEmployeeId && get(employee, 'id') === currentEmployeeId) return false - - return userIds.some(id => id === get(employee, 'user.id')) || get(employee, 'email') === normalizedEmail - }) - - if (alreadyInList) return Promise.reject(UserAlreadyInListMsg) - - return Promise.resolve() - }, - }), [EmailIsNotValidMsg, UserAlreadyInListMsg, currentEmployeeId, employees]) - - return useMemo(() => ({ - alreadyRegisteredPhoneValidator, - alreadyRegisteredEmailValidator, - }), [alreadyRegisteredEmailValidator, alreadyRegisteredPhoneValidator]) -} diff --git a/apps/condo/domains/organization/schema/InviteNewOrganizationEmployeeService.js b/apps/condo/domains/organization/schema/InviteNewOrganizationEmployeeService.js index d6036837800..86ebca4bcc1 100644 --- a/apps/condo/domains/organization/schema/InviteNewOrganizationEmployeeService.js +++ b/apps/condo/domains/organization/schema/InviteNewOrganizationEmployeeService.js @@ -3,7 +3,7 @@ const get = require('lodash/get') const conf = require('@open-condo/config') const { GQLError, GQLErrorCode: { BAD_USER_INPUT, INTERNAL_ERROR } } = require('@open-condo/keystone/errors') -const { getById } = require('@open-condo/keystone/schema') +const { getById, find } = require('@open-condo/keystone/schema') const { GQLCustomSchema } = require('@open-condo/keystone/schema') const { WRONG_FORMAT, NOT_FOUND, WRONG_PHONE_FORMAT, DV_VERSION_MISMATCH } = require('@condo/domains/common/constants/errors') @@ -13,21 +13,29 @@ const { DIRTY_INVITE_NEW_EMPLOYEE_SMS_MESSAGE_TYPE, DIRTY_INVITE_NEW_EMPLOYEE_EM const { sendMessage } = require('@condo/domains/notification/utils/serverSchema') const access = require('@condo/domains/organization/access/InviteNewOrganizationEmployeeService') const { HOLDING_TYPE } = require('@condo/domains/organization/constants/common') +const { ALREADY_ACCEPTED_INVITATION, ALREADY_INVITED_EMAIL, ALREADY_INVITED_PHONE, UNABLE_TO_REGISTER_USER } = require('@condo/domains/organization/constants/errors') const { Organization, OrganizationEmployee, OrganizationEmployeeSpecialization } = require('@condo/domains/organization/utils/serverSchema') -const { REGISTER_NEW_USER_MUTATION } = require('@condo/domains/user/gql') +const guards = require('@condo/domains/organization/utils/serverSchema/guards') const { createUserAndSendLoginData } = require('@condo/domains/user/utils/serverSchema') -const { ALREADY_ACCEPTED_INVITATION, ALREADY_INVITED, UNABLE_TO_REGISTER_USER } = require('../constants/errors') -const guards = require('../utils/serverSchema/guards') const ERRORS = { inviteNewOrganizationEmployee: { - ALREADY_INVITED: { + ALREADY_INVITED_EMAIL: { mutation: 'inviteNewOrganizationEmployee', code: BAD_USER_INPUT, - type: ALREADY_INVITED, - message: 'Already invited into the organization', - messageForUser: 'api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED', + type: ALREADY_INVITED_EMAIL, + message: 'Employee with same email already invited into the organization', + messageForUser: 'api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED_EMAIL', + variable: ['email'], + }, + ALREADY_INVITED_PHONE: { + mutation: 'inviteNewOrganizationEmployee', + code: BAD_USER_INPUT, + type: ALREADY_INVITED_PHONE, + message: 'Employee with same phone already invited into the organization', + messageForUser: 'api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED_PHONE', + variable: ['phone'], }, WRONG_PHONE_FORMAT: { mutation: 'inviteNewOrganizationEmployee', @@ -125,10 +133,38 @@ const InviteNewOrganizationEmployeeService = new GQLCustomSchema('InviteNewOrgan if (!phone) throw new GQLError(ERRORS.inviteNewOrganizationEmployee.WRONG_PHONE_FORMAT, context) const userOrganization = await Organization.getOne(context, { id: organization.id }) let user = await guards.checkStaffUserExistency(context, email, phone) - const existedEmployee = await guards.checkEmployeeExistency(context, userOrganization, email, phone, user) - if (existedEmployee) { - throw new GQLError(ERRORS.inviteNewOrganizationEmployee.ALREADY_INVITED, context) + const sameOrganizationEmployees = await find('OrganizationEmployee', { + deletedAt: null, + organization: { id: userOrganization.id }, + OR: [ + { phone }, + { user: { phone } }, + email && { email }, + email && { user: { email } }, + ].filter(Boolean), + }) + + if (sameOrganizationEmployees.length > 0) { + const sameEmployee = sameOrganizationEmployees[0] + + if (sameEmployee.phone === phone) { + throw new GQLError(ERRORS.inviteNewOrganizationEmployee.ALREADY_INVITED_PHONE, context) + } + if (sameEmployee.email === email) { + throw new GQLError(ERRORS.inviteNewOrganizationEmployee.ALREADY_INVITED_EMAIL, context) + } + + if (sameEmployee.user) { + const userWithSameData = await getById('User', sameEmployee.user) + + if (userWithSameData.phone === phone) { + throw new GQLError(ERRORS.inviteNewOrganizationEmployee.ALREADY_INVITED_PHONE, context) + } + if (userWithSameData.email === email) { + throw new GQLError(ERRORS.inviteNewOrganizationEmployee.ALREADY_INVITED_EMAIL, context) + } + } } if (!user) { diff --git a/apps/condo/domains/organization/schema/InviteNewOrganizationEmployeeService.test.js b/apps/condo/domains/organization/schema/InviteNewOrganizationEmployeeService.test.js index 28a9904b3da..b9a37c4a0a1 100644 --- a/apps/condo/domains/organization/schema/InviteNewOrganizationEmployeeService.test.js +++ b/apps/condo/domains/organization/schema/InviteNewOrganizationEmployeeService.test.js @@ -167,8 +167,8 @@ describe('InviteNewOrganizationEmployeeService', () => { }) }) - describe('for Employee with duplicated phone', () => { - it('throws error with type ALREADY_INVITED', async () => { + describe('for Employee with duplicated data', () => { + it('throws error with type ALREADY_INVITED_EMAIL if email duplicated', async () => { const client = await makeClientWithRegisteredOrganization() const userAttrs = { name: faker.name.firstName(), @@ -180,28 +180,26 @@ describe('InviteNewOrganizationEmployeeService', () => { await inviteNewOrganizationEmployee(client, client.organization, userAttrs, role) const secondUserAttrs = { ...userAttrs, - email: createTestEmail(), + phone: createTestPhone(), } await catchErrorFrom(async () => { await inviteNewOrganizationEmployee(client, client.organization, secondUserAttrs, role) }, ({ errors }) => { expect(errors).toMatchObject([{ - message: 'Already invited into the organization', + message: 'Employee with same email already invited into the organization', path: ['obj'], extensions: { mutation: 'inviteNewOrganizationEmployee', code: 'BAD_USER_INPUT', - type: 'ALREADY_INVITED', - message: 'Already invited into the organization', + type: 'ALREADY_INVITED_EMAIL', + message: 'Employee with same email already invited into the organization', }, }]) }) }) - }) - describe('for Employee with duplicated email', () => { - it('throws error with type ALREADY_INVITED', async () => { + it('throws error with type ALREADY_INVITED_PHONE if phone duplicated', async () => { const client = await makeClientWithRegisteredOrganization() const userAttrs = { name: faker.name.firstName(), @@ -213,52 +211,20 @@ describe('InviteNewOrganizationEmployeeService', () => { await inviteNewOrganizationEmployee(client, client.organization, userAttrs, role) const secondUserAttrs = { ...userAttrs, - phone: createTestPhone(), + email: createTestEmail(), } await catchErrorFrom(async () => { await inviteNewOrganizationEmployee(client, client.organization, secondUserAttrs, role) }, ({ errors }) => { expect(errors).toMatchObject([{ - message: 'Already invited into the organization', - path: ['obj'], - extensions: { - mutation: 'inviteNewOrganizationEmployee', - code: 'BAD_USER_INPUT', - type: 'ALREADY_INVITED', - message: 'Already invited into the organization', - }, - }]) - }) - }) - }) - - describe('for Employee with duplicated User', () => { - it('throws error with type ALREADY_INVITED', async () => { - const [categoryClassifier1] = await createTestTicketCategoryClassifier(admin) - const client = await makeClientWithRegisteredOrganization() - const inviteClient = await makeClientWithRegisteredOrganization() - const [role] = await createTestOrganizationEmployeeRole(client, client.organization) - - const userAttrs = { - name: client.userAttrs.name, - email: client.userAttrs.email, - phone: inviteClient.userAttrs.phone, - } - const extraAttrs = { - specializations: [{ id: categoryClassifier1.id }], - } - await catchErrorFrom(async () => { - await inviteNewOrganizationEmployee(inviteClient, inviteClient.organization, userAttrs, role, extraAttrs) - }, ({ errors }) => { - expect(errors).toMatchObject([{ - message: 'Already invited into the organization', + message: 'Employee with same phone already invited into the organization', path: ['obj'], extensions: { mutation: 'inviteNewOrganizationEmployee', code: 'BAD_USER_INPUT', - type: 'ALREADY_INVITED', - message: 'Already invited into the organization', + type: 'ALREADY_INVITED_PHONE', + message: 'Employee with same phone already invited into the organization', }, }]) }) diff --git a/apps/condo/domains/organization/schema/OrganizationEmployee.js b/apps/condo/domains/organization/schema/OrganizationEmployee.js index 1fefbf7da95..83481d004f4 100644 --- a/apps/condo/domains/organization/schema/OrganizationEmployee.js +++ b/apps/condo/domains/organization/schema/OrganizationEmployee.js @@ -9,7 +9,7 @@ const { v4: uuid } = require('uuid') const { userIsAdmin } = require('@open-condo/keystone/access') const { GQLError, GQLErrorCode: { BAD_USER_INPUT } } = require('@open-condo/keystone/errors') const { historical, versioned, tracked, softDeleted, uuided, dvAndSender } = require('@open-condo/keystone/plugins') -const { GQLListSchema, getByCondition } = require('@open-condo/keystone/schema') +const { GQLListSchema, getByCondition, find } = require('@open-condo/keystone/schema') const { NOT_FOUND } = require('@condo/domains/common/constants/errors') const { EMAIL_WRONG_FORMAT_ERROR } = require('@condo/domains/common/constants/errors') @@ -17,11 +17,28 @@ const { normalizeEmail } = require('@condo/domains/common/utils/mail') const { normalizePhone } = require('@condo/domains/common/utils/phone') const { hasDbFields, hasOneOfFields } = require('@condo/domains/common/utils/validation.utils') const access = require('@condo/domains/organization/access/OrganizationEmployee') +const { ALREADY_INVITED_EMAIL, ALREADY_INVITED_PHONE } = require('@condo/domains/organization/constants/errors') const { ORGANIZATION_OWNED_FIELD } = require('@condo/domains/organization/schema/fields') const { softDeletePropertyScopeOrganizationEmployee } = require('@condo/domains/scope/utils/serverSchema') const ERRORS = { + ALREADY_INVITED_EMAIL: { + mutation: 'updateOrganizationEmployee', + code: BAD_USER_INPUT, + type: ALREADY_INVITED_EMAIL, + message: 'Employee with same email already invited into the organization', + messageForUser: 'api.organizationEmployee.ALREADY_INVITED_EMAIL', + variable: ['email'], + }, + ALREADY_INVITED_PHONE: { + mutation: 'updateOrganizationEmployee', + code: BAD_USER_INPUT, + type: ALREADY_INVITED_PHONE, + message: 'Employee with same phone already invited into the organization', + messageForUser: 'api.organizationEmployee.ALREADY_INVITED_PHONE', + variable: ['phone'], + }, NOT_FOUND_ROLE: { code: BAD_USER_INPUT, type: NOT_FOUND, @@ -74,10 +91,29 @@ const OrganizationEmployee = new GQLListSchema('OrganizationEmployee', { resolveInput: async ({ resolvedData }) => { return normalizeEmail(resolvedData['email']) || resolvedData['email'] }, - validateInput: async ({ resolvedData, addFieldValidationError }) => { - if (resolvedData['email'] && normalizeEmail(resolvedData['email']) !== resolvedData['email']) { + validateInput: async ({ resolvedData, existingItem, operation, context, addFieldValidationError }) => { + const resolvedEmail = resolvedData['email'] + if (!resolvedEmail) return + + if (resolvedEmail && normalizeEmail(resolvedEmail) !== resolvedEmail) { addFieldValidationError(`${EMAIL_WRONG_FORMAT_ERROR}mail] invalid format`) } + + if (operation === 'update') { + const employeeWithSameEmail = await find('OrganizationEmployee', { + id_not: existingItem.id, + deletedAt: null, + organization: { id: existingItem.organization }, + OR: [ + { email: resolvedEmail }, + { user: { email: resolvedEmail } }, + ], + }) + + if (employeeWithSameEmail.length > 0) { + throw new GQLError(ERRORS.ALREADY_INVITED_EMAIL, context) + } + } }, }, }, @@ -90,6 +126,26 @@ const OrganizationEmployee = new GQLListSchema('OrganizationEmployee', { if (resolvedData['phone'] === null) return null return normalizePhone(resolvedData['phone']) }, + validateInput: async ({ resolvedData, existingItem, operation, context }) => { + const resolvedPhone = resolvedData['phone'] + if (!resolvedPhone) return + + if (operation === 'update') { + const employeeWithSamePhone = await find('OrganizationEmployee', { + id_not: existingItem.id, + deletedAt: null, + organization: { id: existingItem.organization }, + OR: [ + { phone: resolvedPhone }, + { user: { phone: resolvedPhone } }, + ], + }) + + if (employeeWithSamePhone.length > 0) { + throw new GQLError(ERRORS.ALREADY_INVITED_PHONE, context) + } + } + }, }, }, role: { diff --git a/apps/condo/domains/organization/schema/OrganizationEmployee.test.js b/apps/condo/domains/organization/schema/OrganizationEmployee.test.js index 4584ed9a868..794f8c7f304 100644 --- a/apps/condo/domains/organization/schema/OrganizationEmployee.test.js +++ b/apps/condo/domains/organization/schema/OrganizationEmployee.test.js @@ -8,7 +8,7 @@ const { expectToThrowAuthenticationErrorToObjects, expectToThrowAccessDeniedErrorToObj, expectToThrowAuthenticationErrorToObj, - expectToThrowUniqueConstraintViolationError, expectToThrowGQLError, + expectToThrowUniqueConstraintViolationError, expectToThrowGQLError, catchErrorFrom, } = require('@open-condo/keystone/test.utils') const { @@ -385,6 +385,60 @@ describe('OrganizationEmployee', () => { expect(employee.email).toEqual(email) }) + test('employee: can not update phone if employee with same phone exist in organization', async () => { + const admin = await makeLoggedInAdminClient() + const client1 = await makeClientWithRegisteredOrganization() + const client2 = await makeClientWithNewRegisteredAndLoggedInUser() + + const [role] = await createTestOrganizationEmployeeRole(client1, client1.organization) + const [invitedEmployee] = await inviteNewOrganizationEmployee(client1, client1.organization, client2.userAttrs, role) + await acceptOrRejectOrganizationInviteById(client2, invitedEmployee) + + await catchErrorFrom(async () => { + await updateTestOrganizationEmployee(admin, invitedEmployee.id, { + phone: client1.userAttrs.phone, + }) + }, ({ errors }) => { + expect(errors).toMatchObject([{ + message: 'Employee with same phone already invited into the organization', + path: ['obj'], + extensions: { + mutation: 'updateOrganizationEmployee', + code: 'BAD_USER_INPUT', + type: 'ALREADY_INVITED_PHONE', + message: 'Employee with same phone already invited into the organization', + }, + }]) + }) + }) + + test('employee: can not update email if employee with same email exist in organization', async () => { + const admin = await makeLoggedInAdminClient() + const client1 = await makeClientWithRegisteredOrganization() + const client2 = await makeClientWithNewRegisteredAndLoggedInUser() + + const [role] = await createTestOrganizationEmployeeRole(client1, client1.organization) + const [invitedEmployee] = await inviteNewOrganizationEmployee(client1, client1.organization, client2.userAttrs, role) + await acceptOrRejectOrganizationInviteById(client2, invitedEmployee) + + await catchErrorFrom(async () => { + await updateTestOrganizationEmployee(admin, invitedEmployee.id, { + email: client1.userAttrs.email, + }) + }, ({ errors }) => { + expect(errors).toMatchObject([{ + message: 'Employee with same email already invited into the organization', + path: ['obj'], + extensions: { + mutation: 'updateOrganizationEmployee', + code: 'BAD_USER_INPUT', + type: 'ALREADY_INVITED_EMAIL', + message: 'Employee with same email already invited into the organization', + }, + }]) + }) + }) + test('employee: did not update phone and email when user reject invite', async () => { const admin = await makeLoggedInAdminClient() const client1 = await makeClientWithRegisteredOrganization() diff --git a/apps/condo/lang/en/en.json b/apps/condo/lang/en/en.json index 6e116f42474..7e71b52c9b2 100644 --- a/apps/condo/lang/en/en.json +++ b/apps/condo/lang/en/en.json @@ -1154,7 +1154,6 @@ "pages.users.status.NotActive": "Not active", "pages.users.status.Rejected": "User has rejected the invitation.", "pages.users.status.WaitAcceptOrReject": "User registered, waiting for an accept/reject action", - "pages.users.UserIsAlreadyInList": "The user is already in user list", "pages.banking.removeModal.contractor.title": "Remove {count, plural, one {contractor} other {contractors}} и {count, plural, one {his} other {their}} transactions?", "pages.banking.removeModal.contractor.description": "All transactions {count, plural, one {this contractor} other {this contractors}} will also be deleted", "pages.banking.removeModal.transaction.title": "Remove {count, plural, one {transaction} other {transactions}}?", @@ -2161,13 +2160,16 @@ "api.user.completeConfirmPhoneAction.SMS_CODE_EXPIRED": "SMS code expired, try to verify phone number again", "api.user.completeConfirmPhoneAction.SMS_CODE_MAX_RETRIES_REACHED": "Max retries reached for SMS code confirmation. Try to initiate phone confirmation again", "api.user.completeConfirmPhoneAction.SMS_CODE_VERIFICATION_FAILED": "SMS code verification mismatch", - "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED": "User is already invited into organization", + "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED_EMAIL": "Employee with same email is already in the organization", + "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED_PHONE": "Employee with same phone is already in the organization", "api.organization.inviteNewOrganizationEmployee.UNABLE_TO_REGISTER_USER": "Unable to register user", "api.organization.reInviteOrganizationEmployee.ORGANIZATION_NOT_FOUND": "Could not find organization to which this employee belongs", "api.organization.reInviteOrganizationEmployee.USER_NOT_FOUND": "Could not find user using specified contacts", "api.organization.reInviteOrganizationEmployee.EMPLOYEE_NOT_FOUND": "Could not employee using specified contacts", "api.organization.reInviteOrganizationEmployee.ALREADY_ACCEPTED_INVITATION": "Specified employee has already accepted invitation", "api.organizationEmployee.NOT_FOUND_ROLE": "Role not found for your organization", + "api.organizationEmployee.ALREADY_INVITED_EMAIL": "Employee with same email is already in the organization", + "api.organizationEmployee.ALREADY_INVITED_PHONE": "Employee with same phone is already in the organization", "api.common.INVALID_PHONE_NUMBER_FORMAT": "Invalid phone number format", "api.common.WRONG_PERCENT_VALUE": "Invalid percent value", "api.banking.createBankAccountRequest.INCORRECT_PROPERTY_ID": "Incorrect propertyId was provided. Please check that this id related to passed organizationId", diff --git a/apps/condo/lang/ru/ru.json b/apps/condo/lang/ru/ru.json index 90504377600..9148fda767d 100644 --- a/apps/condo/lang/ru/ru.json +++ b/apps/condo/lang/ru/ru.json @@ -1154,7 +1154,6 @@ "pages.users.status.NotActive": "Не активен", "pages.users.status.Rejected": "Пользователь отклонил приглашение.", "pages.users.status.WaitAcceptOrReject": "Зарегистрировался, пока не принял приглашение", - "pages.users.UserIsAlreadyInList": "Пользователь уже добавлен в список", "pages.banking.removeModal.contractor.title": "Удалить {count, plural, one {контрагента} other {контрагентов}} и {count, plural, one {его} other {их}} транзакции?", "pages.banking.removeModal.contractor.description": "Все транзакции {count, plural, one {этого контрагента} other {этих контрагентов}} также будут удалены", "pages.banking.removeModal.transaction.title": "Удалить {count, plural, one {транзакцию} other {транзакции}}?", @@ -2161,13 +2160,16 @@ "api.user.completeConfirmPhoneAction.SMS_CODE_EXPIRED": "Просрочен код подтверждения телефона по СМС, попробуйте пройти подтверждение номера телефона заново", "api.user.completeConfirmPhoneAction.SMS_CODE_MAX_RETRIES_REACHED": "Превышено количество попыток подтверждения номера телефона по СМС. Попробуйте заново пройти подтверждение номера телефона", "api.user.completeConfirmPhoneAction.SMS_CODE_VERIFICATION_FAILED": "Неверный код подтверждения телефона", - "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED": "Пользователь уже приглашен в организацию", + "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED_EMAIL": "Сотрудник с таким Email уже есть в организации", + "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED_PHONE": "Сотрудник с таким номером телефона уже есть в организации", "api.organization.inviteNewOrganizationEmployee.UNABLE_TO_REGISTER_USER": "Ошибка во время регистрации пользователя", "api.organization.reInviteOrganizationEmployee.ORGANIZATION_NOT_FOUND": "Не найдена организация, к которой принадлежит данный сотрудник", "api.organization.reInviteOrganizationEmployee.USER_NOT_FOUND": "Не найден пользователь по указанным контактам", "api.organization.reInviteOrganizationEmployee.EMPLOYEE_NOT_FOUND": "Не найден сотрудник по указанным контактам", "api.organization.reInviteOrganizationEmployee.ALREADY_ACCEPTED_INVITATION": "Указанный сотрудник уже принял приглашение", "api.organizationEmployee.NOT_FOUND_ROLE": "Роль не найдена в вашей организаии", + "api.organizationEmployee.ALREADY_INVITED_EMAIL": "Сотрудник с таким Email уже есть в организации", + "api.organizationEmployee.ALREADY_INVITED_PHONE": "Сотрудник с таким номером телефона уже есть в организации", "api.common.INVALID_PHONE_NUMBER_FORMAT": "Некорректный формат номера телефона", "api.common.WRONG_PERCENT_VALUE": "Некорректное значение процента", "api.banking.createBankAccountRequest.INCORRECT_PROPERTY_ID": "Неверное значение propertyId. Данный дом не принадлежит выбранной организации", diff --git a/apps/condo/schema.graphql b/apps/condo/schema.graphql index 7668ccf53df..7e5fd6b55af 100644 --- a/apps/condo/schema.graphql +++ b/apps/condo/schema.graphql @@ -82765,9 +82765,23 @@ type Mutation { `{ "mutation": "inviteNewOrganizationEmployee", "code": "BAD_USER_INPUT", - "type": "ALREADY_INVITED", - "message": "Already invited into the organization", - "messageForUser": "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED" + "type": "ALREADY_INVITED_EMAIL", + "message": "Employee with same email already invited into the organization", + "messageForUser": "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED_EMAIL", + "variable": [ + "email" + ] + }` + + `{ + "mutation": "inviteNewOrganizationEmployee", + "code": "BAD_USER_INPUT", + "type": "ALREADY_INVITED_PHONE", + "message": "Employee with same phone already invited into the organization", + "messageForUser": "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED_PHONE", + "variable": [ + "phone" + ] }` `{ diff --git a/apps/condo/schema.ts b/apps/condo/schema.ts index 9ff6ae7cc81..109dcd6f387 100644 --- a/apps/condo/schema.ts +++ b/apps/condo/schema.ts @@ -38660,9 +38660,23 @@ export type Mutation = { * `{ * "mutation": "inviteNewOrganizationEmployee", * "code": "BAD_USER_INPUT", - * "type": "ALREADY_INVITED", - * "message": "Already invited into the organization", - * "messageForUser": "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED" + * "type": "ALREADY_INVITED_EMAIL", + * "message": "Employee with same email already invited into the organization", + * "messageForUser": "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED_EMAIL", + * "variable": [ + * "email" + * ] + * }` + * + * `{ + * "mutation": "inviteNewOrganizationEmployee", + * "code": "BAD_USER_INPUT", + * "type": "ALREADY_INVITED_PHONE", + * "message": "Employee with same phone already invited into the organization", + * "messageForUser": "api.organization.inviteNewOrganizationEmployee.ALREADY_INVITED_PHONE", + * "variable": [ + * "phone" + * ] * }` * * `{