//@ts-nocheck import { schema } from 'nexus' import moment from 'moment' import GeneralUtils from '../../utils/general' import * as UserController from '../../controllers/user' import { send as sendNotifications } from '../../utils/notification' import * as SlackUtils from '../../utils/slack' import { User } from 'nexus-plugin-prisma/client' import * as Auth from '../../utils/auth' import { logger } from '../../utils/logger' import { prismaClient as prisma } from '../../prismaClient' export const ProxyDataForUserAndPostQueryType = schema.objectType({ name: 'ProxyDataForUserAndPostQueryType', definition(t) { t.field('user', { type: 'User' }) t.field('post', { type: 'Post' }) }, }) export const UserDef = schema.objectType({ name: 'User', definition(t) { t.model.id() t.model.role() t.model.cantDeliverEmailReason() t.model.signedInPlatform() t.model.createdAt() t.model.updatedAt() t.model.email() t.model.title() t.model.username() t.model.birthday() t.model.secondaryEmail() t.model.fullname() t.model.firstName() t.model.lastName() t.model.cantDeliverSMS() t.model.cantDeliverEmail() t.model.hasAppNotificationEnable() t.model.hasEmailNotificationEnable() t.model.hasSMSNotificationEnable() t.model.currentAppVersion() t.model.password() t.model.hasResetPassword() t.model.resetPasswordToken() t.model.expoNotificationToken() t.model.notificationToken() t.model.signedInAt() t.model.condoManagerNoCompteReel() t.model.condoManagerCompteUserNumber() t.model.appNotificationsBadgesNumber() t.model.isSignedOut() t.model.appLoginCode() t.model.hasReceivedOnboarding() t.model.hasReceivedInvitation() t.model.hasInstalledApp() t.model.hasInstalledDesktop() t.model.hasUninstalledApp() t.model.hasDog() t.model.stripeCustomerId() // TODO: Unsure t.model.operatingSystem() t.model.emergencyContact() t.model.preferedLanguage() t.model.active() t.model.isResident() t.model.officePhoneId() t.model.phoneId() t.model.serviceProviderId() t.model.avatarId() t.model.managingCompanyRoleId() t.model.managingCompanyId() t.model.addressId() t.model.propertyId() t.model.copropertyDueAmount() t.model.hasPaymentError() t.model.oneSignalUserId() // TODO: Unsure t.model.isTenant() t.model.hasReceivedInvitationAt() t.model.hasReceivedInvitationByEmail() t.model.hasReceivedInvitationByEmailAt() t.model.hasOpenedEmailInvitation() t.model.hasOpenedEmailInvitationAt() t.model.hasClickedOnCTAInEmailInvitation() t.model.hasClickedOnCTAInEmailInvitationAt() t.model.hasReceivedInvitationBySMS() t.model.hasReceivedInvitationBySMSAt() t.model.hasRegisterAt() t.model.waitingForApproval() t.model.hasReceivedSecondInvitation() t.model.hasReceivedSecondInvitationAt() t.model.hasReceivedSecondInvitationByEmail() t.model.hasReceivedSecondInvitationByEmailAt() t.model.hasReceivedSecondInvitationBySMS() t.model.hasReceivedSecondInvitationBySMSAt() t.model.hasPrivateChat() t.model.condoManagerId() t.model.hasAlarmSystem() t.model.managingCompanyName() t.model.hasOpenedSecondEmailInvitation() t.model.hasOpenedSecondEmailInvitationAt() t.model.hasClickedOnCTAInSecondEmailInvitation() t.model.hasClickedOnCTAInSecondEmailInvitationAt() t.model.hasConfirmedEmail() t.model.hasConfirmedEmailAt() t.model.confirmEmailToken() t.model.hasConfirmedPhone() t.model.hasConfirmedPhoneAt() t.model.hasConfirmedBasicInformations() t.model.condoManagerNomCompte() t.model.currentProjectId() t.model.note() t.model.sendOnPressEnter() t.model.cantDeliverEmailFullReason() t.model.address() t.model.avatar() t.model.createdBy() t.model.currentProject() t.model.homePhone() t.model.managingCompany() t.model.managingCompanyRole() t.model.officePhone() t.model.phone() t.model.property() t.model.serviceProvider() t.model.ads() t.model.additionalServices() t.model.cars() t.model.createdComments() t.model.issues() t.model.managerStoreOrders() t.model.meetingParticipantsIsRepresentedByToUser() t.model.meetingParticipantUserToUser() t.model.noteComments() t.model.notifications() t.model.receivedOffenses() t.model.packages() t.model.paymentsCreatedByToUser() t.model.paymentsCustomerToUser() t.model.paymentAccounts() t.model.paymentMethods() t.model.signedProxies() t.model.reservations() t.model.serviceRequestsAssignedToToUser() t.model.serviceRequestsAssociatedToToUser() t.model.serviceRequests() t.model.assignedTasks() t.model.associatedTasks() t.model.createdTasks() t.model.taskActivities() t.model.others() t.model.chatNotificationTypes() t.model.expoNotificationTokens() t.model.managerNotificationTypes() t.model.orderNotificationTypes() t.model.accessKeys() t.model.projectsActive() t.model.amenities() t.model.posts_AudienceOfPost() t.model.projectsBoardMember() t.model.car() t.model.condoManagerNomComptes() t.model.condoManagerCompteUserNumberSyncStatuses() t.model.condoManagerNoCompteReelSyncStatuses() t.model.contacts() t.model.emailSyncStatuses() t.model.eventsSeen() t.model.filesSeen() t.model.firstNameSyncStatuses() t.model.isResidentSyncStatuses() t.model.lastNameSyncStatuses() t.model.lockers() t.model.meetingParticipantUsers() t.model.mentionedComments() t.model.notes() t.model.notificationPreferences() t.model.orders() t.model.service() t.model.properties() t.model.parkings() t.model.pollQuestionAnswers() t.model.polls() t.model.postsSeen() t.model.propertiesPreviousOwnersOfProperty() t.model.previousProjects() t.model.propertiesPreviousUsersOfProperty() t.model.projectsManaging() t.model.remotes() t.model.rules() t.model.segments() t.model.serviceRequestsServiceRequestsSeenByUser() t.model.tasksTaskCollaborators() t.model.propertiesTenantsOfProperty() t.model.projectsTenants() t.model.offenses() t.model.cantGoEvents() t.model.goingEvents() t.model.projects() t.model.interestedEvents() t.model.packagesSeen() }, }) export const UserQueries = schema.extendType({ type: 'Query', definition(t) { t.crud.user() t.crud.users({ filtering: true, ordering: true, pagination: true }) t.field('usersCount', { type: 'Int', args: { where: schema.arg({ type: 'UserWhereInput' }), }, resolve: (parent, { where }, ctx: NexusContext) => { return ctx.prisma.user.count({ where }) }, }) t.field('me', { type: 'User', resolve: async (parent, args, ctx: NexusContext) => { const currentUserId = Auth.getUserId(ctx) const currentUser = await ctx.prisma.user.findOne({ where: { id: currentUserId } }) if (!currentUser || !currentUser.active) { throw new Error('Not authorized') } return currentUser }, }) t.field('userWithEmailOrPhoneNumber', { type: 'User', args: { email: schema.stringArg(), phoneNumber: schema.stringArg(), }, resolve: async (parent, { email, phoneNumber }) => { return UserController.querySingleUser({ email, phoneNumber }) }, }) t.field('resident', { type: 'User', args: { email: schema.stringArg(), phoneNumber: schema.stringArg(), }, resolve: (parent, { email, phoneNumber }) => { return UserController.querySingleUser({ email, phoneNumber, moreWhereAnd: { role: 'RESIDENT' }, }) }, }) t.field('getAllResidentsForCurrentUser', { type: 'User', list: true, resolve: async (parent, args, ctx: NexusContext) => { const id = Auth.getUserId(ctx) const currentUser = await ctx.prisma.user.findOne({ where: { id }, include: { managingCompany: { include: { projects: true } } }, }) return ctx.prisma.user.findMany({ where: { // AND: [ // { // currentProject: { // id: { // in: currentUser.managingCompany.projects.map(project => project.id) // } // } // }, // { // active: true // } // ] }, }) // if (currentUser.managingCompany) { // } else { // // For Provider for now // return [] // } }, }) // TMP becaue right now in the resident app we fetch unactive users t.field('users', { type: 'User', list: true, args: { where: schema.arg({ type: 'UserWhereInput' }), skip: schema.arg({ type: 'Int' }), first: schema.arg({ type: 'Int' }), }, resolve: async (parent, { where, skip, first }, ctx: NexusContext) => { const userId = Auth.getUserId(ctx) const [currentUser, fetchedUsers] = await Promise.all([ ctx.prisma.user.findOne({ where: { id: userId } }), ctx.prisma.user.findMany({ where, take: first, skip: skip }), ]) if (currentUser && currentUser.role === 'RESIDENT') { return fetchedUsers.filter((u) => u.active) } return fetchedUsers }, }) t.field('getProxyDataForUserAndPost', { type: 'ProxyDataForUserAndPostQueryType', args: { userWhere: schema.arg({ type: 'UserWhereUniqueInput' }), postWhere: schema.arg({ type: 'PostWhereUniqueInput' }), }, resolve: async (_, { userWhere, postWhere }, ctx: NexusContext) => { const [user, post] = await Promise.all([ ctx.prisma.user.findOne({ where: userWhere }), ctx.prisma.post.findOne({ where: postWhere }), ]) if (!user || !post) { throw new Error('Missing data') } if (post.type !== 'PROXY') { throw new Error('Invalid post') } return { user, post, } }, }) }, }) export const UserMutation = schema.extendType({ type: 'Mutation', definition(t) { t.crud.deleteOneUser({ alias: 'deleteUser' }) t.crud.updateOneUser({ alias: 'updateUser' }) t.crud.createOneUser({ alias: 'createUser' }) /** * UpdateCurrentUser */ t.field('updateCurrentUser', { type: 'User', args: { data: schema.arg({ type: 'UserUpdateInput' }), where: schema.arg({ type: 'UserWhereUniqueInput' }), }, resolve: async (parent, { data }, ctx: NexusContext) => { const id = Auth.getUserId(ctx) if (data.email) { data.email = data.email.toLowerCase().trim() } // Don't want to let someone update himself and merge with another user // Let's say I change my email to tristan@usewalter.com, I would merge my account // with his... NOT GOOD! if (data.email || data?.phone?.create || data?.phone?.update) { const existingUser = await UserController.querySingleUser({ email: data.email, phoneNumber: data.phone?.create?.number || data.phone?.update?.number, }) if (existingUser) { throw new Error("There's already a user with this email or this phone number") } } // Need to add user email verification before moving this to UserController.updateUser // Because what if a resident change his email for an email that is not his // We need to merge only after the user confirmed his email/phone number const updatedCurrentUser = ctx.prisma.user.update({ where: { id, }, data, }) // For third party sync status // Will need to put that in a job queue maybe // The best would be that we have some middleware somewhere... // So that everytime an email for a user changes we can trigger this try { await UserController.verifyAndUpdateThirdPartySyncStatus(id, data) } catch (error) { logger.error(error) } return updatedCurrentUser }, }) /** * UpdateUser */ t.field('updateUser', { type: 'User', args: { data: schema.arg({ type: 'UserUpdateInput' }), where: schema.arg({ type: 'UserWhereUniqueInput' }), }, resolve: async (parent, { data, where }, ctx: NexusContext) => { return UserController.updateUser({ where, data }) }, }) /** * CreateUser */ t.field('createUser', { type: 'User', args: { data: schema.arg({ type: 'UserCreateInput' }), }, resolve: async (parent, { data }, ctx: NexusContext) => { let currentUserId let currentUser try { currentUserId = Auth.getUserId(ctx) currentUser = await ctx.prisma.user.findOne({ where: { id: currentUserId }, select: { id: true, role: true, }, }) data.createdBy = { connect: { id: currentUserId, }, } } catch (error) { logger.debug(error) } // MAKE SURE THAT RESIDENT THAT REGISTER HIMSELF ARE BLOCKED IF PROJECT ASKED SO if ( data.role === 'RESIDENT' && data.currentProject?.connect?.id && (!currentUser || currentUser === 'RESIDENT') ) { const project = await ctx.prisma.project.findOne({ where: { id: data.currentProject.connect.id, }, }) if (data.properties?.connect?.length && project.newOwnersNeedApproval) { data.waitingForApproval = true } else if ( (data.isTenant || data.isResident || data.projectsTenants) && project.newTenantsNeedApproval ) { data.waitingForApproval = true } else if (project.newFamilyMembersNeedApproval) { data.waitingForApproval = true } } const createdUser = await UserController.createUser(data) // Send notification about new resident to managers if (data.currentProject?.connect?.id) { prisma.user .findOne({ where: { id: createdUser.id, }, select: { id: true, firstName: true, lastName: true, role: true, isResident: true, createdBy: { select: { id: true, role: true, }, }, property: { select: { id: true, address: { select: { apartmentNumber: true, }, }, }, }, properties: { select: { id: true, address: { select: { apartmentNumber: true, }, }, }, }, }, }) .then((newUserResident) => { if (!newUserResident.createdBy || newUserResident.createdBy.role === 'RESIDENT') { return Promise.all([ prisma.project .findOne({ where: { id: data.currentProject.connect.id } }) .managingCompany() .users({ select: { id: true } }), prisma.project.findOne({ where: { id: data.currentProject.connect.id } }), ]).then(async ([managingCompanyUsers, project]) => { return sendNotifications({ noAppNotification: true, projectId: project.id, users: managingCompanyUsers, title: data.waitingForApproval ? 'New resident just registered on Walter and needs approval' : 'New resident just registered on Walter', message: await GeneralUtils.getUserNameAndUnits(newUserResident, project), type: 'NEW_RESIDENT', data: { id: newUserResident.id, }, }) }) } }) .catch(logger.error) } return createdUser }, }) /** * Delete user */ t.field('deleteUser', { type: 'User', args: { where: schema.arg({ type: 'UserWhereUniqueInput' }), }, resolve: async (parent, { where }, ctx: NexusContext) => { await Auth.getUserId(ctx) return UserController.deleteUser(where.id) }, }) /** * removeResidentFromProject */ t.field('removeResidentFromProject', { type: 'Boolean', args: { userId: schema.stringArg(), projectId: schema.stringArg(), }, resolve: async (parent, { userId, projectId }, ctx: NexusContext) => { await UserController.removeResidentFromProject(userId, projectId) return true }, }) /** * Send invitations to uninvited */ t.field('inviteUninvited', { type: 'User', list: true, args: { projectId: schema.stringArg({ required: true }), }, // @ts-ignore // TODO: To review the resolve leaves an error even when trying to cast as unknown as User resolve: async (_, { projectId }, ctx: NexusContext) => { const users = await prisma.user.findMany({ where: { AND: [ { OR: [{ hasReceivedInvitation: false }, { hasReceivedInvitation: null }] }, { OR: [{ hasInstalledApp: false }, { hasInstalledApp: null }] }, { projects: { some: { id: projectId } } }, ], }, include: { phone: true, }, }) const validUsers = users.filter( (u) => (u.phone && !u.cantDeliverSMS) || (u.email && !u.cantDeliverEmail) ) const usersThatReceivedInvitations = await UserController.inviteUsersToDownloadWalter({ users: validUsers, projectId, }) // Project is now launch // We say 5 because we think that if there's more than 5 invites // It means that it's a real launch // Otherwise they might have created 1-2 users and invite uninvited for testing if (users.length > 5) { try { const project = await prisma.project.findOne({ where: { id: projectId }, select: { id: true, launchDate: true, name: true }, }) if (!project.launchDate) { await prisma.project.update({ where: { id: projectId }, data: { isLaunched: true, launchDate: moment().toDate() }, }) // Tell us that someone launched a project await SlackUtils.send({ channel: 'newProjectCondoManager', subject: `Residents have been invited in ${project.name}`, }) } } catch (error) { logger.error(error) } } const validUsersThatReceivedInvitation = usersThatReceivedInvitations.filter(Boolean) return Promise.resolve(validUsersThatReceivedInvitation) }, }) /** * Send invitations to uninstalled */ t.field('inviteUninstalled', { type: 'User', list: true, args: { projectId: schema.stringArg({ required: true }), }, // @ts-ignore // TODO: To review the resolve leaves an error even when trying to cast as unknown as User resolve: async (parent, { projectId }, ctx: NexusContext) => { const users = await ctx.prisma.user.findMany({ where: { AND: [ { OR: [{ hasInstalledApp: false }, { hasInstalledApp: null }] }, { projects: { some: { id: projectId } } }, ], }, include: { phone: true, }, }) const validUsers = users.filter( (u) => (u.phone && !u.cantDeliverSMS) || (u.email && !u.cantDeliverEmail) ) await UserController.inviteUsersToDownloadWalter({ users: validUsers, projectId }) // Project is now launch // We say 10 because we think that if there's more than 10 invites // It means that it's a real launch // Otherwise they might have created 1-2 users and invite uninvited for testing if (users.length > 5) { ctx.prisma.project .update({ where: { id: projectId }, data: { isLaunched: true, launchDate: moment().toDate() }, }) .catch(logger.error) } return validUsers }, }) /** * Send invitation single */ t.field('sendInvite', { type: 'Boolean', args: { // Here it should be userId: schema.stringArg(), instead of where:... where: schema.arg({ type: 'UserWhereUniqueInput' }), projectId: schema.stringArg(), type: schema.stringArg(), }, resolve: async (parent, { where, projectId }, ctx: NexusContext) => { const users = await ctx.prisma.user.findMany({ where, }) if (users.length > 1) { throw new Error('Single invitation should only be sent to 1 user') } const user = users[0] if (!user) { throw new Error("Can't find user to invite") } let invitationIsSent = false try { await UserController.sendSMSAppInvitationToResident({ projectId, userId: user.id, }) invitationIsSent = true await prisma.user.update({ where: { id: user.id, }, data: { hasReceivedInvitation: true, hasReceivedInvitationAt: new Date(), hasReceivedInvitationBySMS: true, hasReceivedInvitationBySMSAt: new Date(), }, }) } catch (error) { logger.debug(error) } try { await UserController.sendEmailAppInvitationToResident({ projectId, userId: user.id, }) invitationIsSent = true await prisma.user.update({ where: { id: user.id, }, data: { hasReceivedInvitation: true, hasReceivedInvitationAt: new Date(), hasReceivedInvitationByEmail: true, hasReceivedInvitationByEmailAt: new Date(), }, }) } catch (err) { logger.error(err) } if (!invitationIsSent) { throw new Error( "Couldn't invite resident. Make sure he/she has a valid email or mobile phone number" ) } return true }, }) }, }) // export const ExtendedQuery = schema.objectType({ // type: 'Query', // definition: t => { // t.field('userById', { // type: 'User', // args: { id: intArg('id of the user') }, // resolve: (root, args, ctx: NexusContext) => { // console.log('query user 2') // return ctx.prisma.user.getById(args.id) // }, // }) // }, // })