/* eslint-disable dot-notation */ import dot from 'dot-object' import config from '../../../config' import * as PropertyController from '../../../controllers/property' import * as UserController from '../../../controllers/user' import { prismaClient as prisma } from '../../../prismaClient' import GeneralUtils from '../../general' import { condoManagerLogger } from '../../logger' import * as Twilio from '../../twilio' import getExistingResident from '../common/getExistingResident' export default async function createOrUpdateResident({ compte, thirdPartyService, condoManagerCompteUserNumber = 1, firstName, lastName, projectId, email, phoneNumber, homePhoneNumber, officePhoneNumber, officePhoneNumberExt, isTenant = compte.TypeCompte === '7', }: { compte: any thirdPartyService: any condoManagerCompteUserNumber: number firstName: string lastName: string projectId: string email: string phoneNumber: string homePhoneNumber: string officePhoneNumber: string officePhoneNumberExt: string isTenant: boolean }) { email = GeneralUtils.trimAndLowerCase(email) homePhoneNumber = GeneralUtils.formatPhoneNumberToHaveTheSameAsTwilio(homePhoneNumber) officePhoneNumber = GeneralUtils.formatPhoneNumberToHaveTheSameAsTwilio(officePhoneNumber) phoneNumber = GeneralUtils.formatPhoneNumberToHaveTheSameAsTwilio(phoneNumber) // Validate data because we don't want in our data some false values like "129i1321932130123912" email = GeneralUtils.isValidEmail(email) ? email : null homePhoneNumber = GeneralUtils.isValidPhoneNumber(homePhoneNumber) ? homePhoneNumber : null officePhoneNumber = GeneralUtils.isValidPhoneNumber(officePhoneNumber) ? officePhoneNumber : null // Mobile we can verify with Twilio phoneNumber = GeneralUtils.isValidPhoneNumber(phoneNumber) ? phoneNumber : null const result = await Twilio.getFormattedNumber(phoneNumber) if (result) { const { phoneNumber: validMobilePhoneNumber } = result phoneNumber = validMobilePhoneNumber } if (!email && !phoneNumber) { // Take home/office phone number if user doesn't have any mobile phone number // ONLY if the user doesn't have a email/mobile phone number phoneNumber = homePhoneNumber || officePhoneNumber } const compteUnitNumber = compte.NoCondo const compteIsInactive = compte.Inactif === '1' const condoManagerNoCompteReel = compte.NoCompteReel const condoManagerNomCompte = compte.NomCompte // Only use if we want to process a specific resident for debugging // if (email !== 'eric.trudel12@gmail.com') { // return // } if (config.thirdPartySync.logProcessAction) { condoManagerLogger.info(`♻️ Processing a user for compte ${condoManagerNomCompte}`) } if (config.thirdPartySync.logData) { condoManagerLogger.info('Logging resident data', { condoManagerCompteUserNumber, firstName, lastName, projectId, email, phoneNumber, isTenant, compteIsInactive, }) } // For now just process compte if the compte has a phone number/email if (email || phoneNumber || homePhoneNumber || officePhoneNumber) { const existingUser = await getExistingResident({ projectId, condoManagerCompteUserNumber, condoManagerNoCompteReel, condoManagerNomCompte, userEmail: email, userPhoneNumber: phoneNumber, }) if (config.thirdPartySync.logData) { if (existingUser) { condoManagerLogger.info(`Existing user:\n${JSON.stringify(existingUser, null, 2)}`) } else { condoManagerLogger.info("Can't find existing user") } } if (!existingUser && compteIsInactive) { return } if (existingUser?.role === 'MANAGER') { throw new Error(`Compte: ${condoManagerNomCompte} is linked to an existing manager account`) } const walterProperty = ( await prisma.property.findMany({ take: 1, where: { building: { project: { id: projectId, }, }, address: { apartmentNumber: compteUnitNumber, }, }, select: { id: true, users: { select: { id: true, }, }, owners: { select: { id: true, }, }, address: { select: { apartmentNumber: true, }, }, }, }) )[0] if (!walterProperty) { throw new Error('Property should have been created first') } if (compteIsInactive) { const userWithProperties = await prisma.user.findOne({ where: { id: existingUser.id }, select: { id: true, property: { select: { id: true, address: { select: { apartmentNumber: true, }, }, building: { select: { project: { select: { id: true, }, }, }, }, }, }, properties: { select: { id: true, address: { select: { apartmentNumber: true, }, }, building: { select: { project: { select: { id: true, }, }, }, }, }, }, }, }) const userPropertiesForProject = [ ...userWithProperties.properties, userWithProperties.property, ].filter((property) => property?.building?.project?.id === projectId) // Only remove user from project if the current CM compte is his last unit in this project if ( userPropertiesForProject.length === 1 && userPropertiesForProject[0]?.address.apartmentNumber === compteUnitNumber ) { if (config.thirdPartySync.logCRUDAction) { condoManagerLogger.info( `🗑 Removing user ${existingUser.id} from project ${projectId} because compte ${compte.NomCompte} is inactive` ) } return UserController.removeResidentFromProject(existingUser.id, projectId) } // Only remove property if user still have it const needToRemoveProperty = [existingUser.property, ...existingUser.properties] .filter(Boolean) .find((property) => property.id === walterProperty.id) if (needToRemoveProperty) { return PropertyController.removePropertyFromUser(existingUser.id, walterProperty.id) } return } const userLanguage = compte.Langue === '0' ? 'fr' : 'en' const userData = { ...(!existingUser?.hasInstalledApp && existingUser?.preferedLanguage !== userLanguage && { preferedLanguage: userLanguage, }), ...(!existingUser?.role && { role: 'RESIDENT', }), ...(!existingUser?.currentProject && { currentProject: { connect: { id: projectId, }, }, }), ...(!existingUser?.projectsActive?.some((p) => p.id === projectId) && { projectsActive: { connect: [ { id: projectId, }, ], }, }), ...(!existingUser?.projects.some((p) => p.id === projectId) && { projects: { connect: [ { id: projectId, }, ], }, }), ...(isTenant && { projectsTenants: { connect: [ { id: projectId, }, ], }, }), // Remove tenant if finaly he's not a tenant ...(!isTenant && existingUser?.projectsTenants?.some(({ id }) => id === projectId) && { projectsTenants: { disconnect: [{ id: projectId }], }, }), // We should only add the firstName and lastName when we create the user // But we put it here in case the existing user doesn't even have a first and last name ...(!existingUser?.firstName && { firstName, }), ...(!existingUser?.lastName && { lastName, }), ...(!existingUser?.condoManagerNomComptes?.some( (condoManagerCompte) => condoManagerCompte.project?.id === projectId && condoManagerCompte.noCompteReel === compte.NoCompteReel && condoManagerCompte.userNumber === condoManagerCompteUserNumber ) && { condoManagerNomComptes: { create: { project: { connect: { id: projectId, }, }, nomCompte: compte.NomCompte, noCompteReel: compte.NoCompteReel, userNumber: condoManagerCompteUserNumber, }, }, }), } const myMappings = [ { prismaPath: 'firstName', thirdPartyValue: firstName, prismaPathSync: 'firstNameSyncStatuses', }, { prismaPath: 'lastName', thirdPartyValue: lastName, prismaPathSync: 'lastNameSyncStatuses', }, { prismaPath: 'email', thirdPartyValue: email, prismaPathSync: 'emailSyncStatuses', }, { prismaPath: 'phone.number', thirdPartyValue: phoneNumber, prismaPathSync: 'phone.phoneSyncStatuses', }, { prismaPath: 'homePhone.number', thirdPartyValue: homePhoneNumber, prismaPathSync: 'homePhone.phoneSyncStatuses', }, { prismaPath: 'officePhone.number', thirdPartyValue: officePhoneNumber, prismaPathSync: 'officePhone.phoneSyncStatuses', }, { prismaPath: 'officePhone.extension', thirdPartyValue: officePhoneNumberExt, prismaPathSync: 'officePhone.extensionSyncStatuses', }, ] await thirdPartyService.mappings.reduce(async (promise, mapping) => { await promise const myMapping = myMappings.find((m) => `User.${m.prismaPath}` === mapping.prismaSource) if (!myMapping) { throw new Error('Something wrong here... we should have a mapping') } const foundSyncStatus = dot .pick(myMapping.prismaPathSync, existingUser) ?.find( (syncStatus) => syncStatus.mapping.mappedService.friendlyName === thirdPartyService.friendlyName ) let prismaValue = dot.pick(myMapping.prismaPath, existingUser) let prismaValueIsSameAsThirdParty = prismaValue === myMapping.thirdPartyValue if (config.thirdPartySync.logData) { condoManagerLogger.debug('thirdPartyValue', myMapping.thirdPartyValue) condoManagerLogger.debug('foundSyncStatus', foundSyncStatus) condoManagerLogger.debug('prismaValue', prismaValue) condoManagerLogger.debug('prismaValueIsSameAsThirdParty', prismaValueIsSameAsThirdParty) } // User and his sync status was created // Directly update it if (existingUser && foundSyncStatus) { // Update the data directly if the user haven't been invited. Because we might just // made a mistake on the import if (!existingUser.hasReceivedInvitation && !existingUser.hasInstalledApp) { if (config.thirdPartySync.logData) { condoManagerLogger.debug( 'Update sync status and user data because user hasnt been invitated/installed yet' ) } const updatePath = myMapping.prismaPath.replace(/\./g, '.update.') condoManagerLogger.info( `Field: ${myMapping.prismaPath}, Third party: ${myMapping.thirdPartyValue} vs Prisma: ${prismaValue}` ) // BUG HAPPEN BECAUSE SOMETIMES THE USER HAS 2 COMPTES. 1 with phone number // AND THE OTHER WITHOUT!! // Only for number for now. If the value of CM for any phone is null // Delete it if we already have a value since the user isn't launch yet. // if (myMapping.prismaPath.split('.')[1] === 'number') { // dot.str(myMapping.prismaPath, { delete: true }, userData) // } else { dot.str(updatePath, myMapping.thirdPartyValue, userData) // } prismaValue = myMapping.thirdPartyValue prismaValueIsSameAsThirdParty = true } await prisma.syncStatus.update({ where: { id: foundSyncStatus.id, }, data: { project: { connect: { id: projectId } }, status: prismaValueIsSameAsThirdParty ? 'SYNCED' : 'CONFLICTING_DATA', dataToConfirm: prismaValueIsSameAsThirdParty ? null : myMapping.thirdPartyValue, lastSeenPrismaValue: prismaValue, overwriteWalterConfirmed: false, overwriteThirdPartyServiceConfirmed: false, }, }) } // User not exist OR we can't find syncStatus for mapping in user so create it else { type SyncStatusData = { overwriteWalterConfirmed: boolean overwriteThirdPartyServiceConfirmed: boolean project: any mapping: any status?: string dataToConfirm?: any lastSeenPrismaValue?: any } // Shared data const syncStatusData: SyncStatusData = { overwriteWalterConfirmed: false, overwriteThirdPartyServiceConfirmed: false, project: { connect: { id: projectId } }, mapping: { connect: { id: mapping.id } }, } // Don't create phone object with empty number if (!prismaValue && !myMapping.thirdPartyValue) { return } if (existingUser) { // CREATE // The syncStatus wasn't created for whatever reason // (It was supposed to be created when we created the user) syncStatusData.status = prismaValueIsSameAsThirdParty ? 'SYNCED' : 'CONFLICTING_DATA' syncStatusData.dataToConfirm = myMapping.thirdPartyValue syncStatusData.lastSeenPrismaValue = prismaValue } // Else it's a new user so sync status is sync for sure! else { syncStatusData.status = 'SYNCED' syncStatusData.dataToConfirm = null syncStatusData.lastSeenPrismaValue = '' } // phone.phoneSyncStatuses -> phone.create.phoneSyncStatuses.create const creationSyncPath = `${myMapping.prismaPathSync.replace(/\./g, '.create.')}.create` dot.str(creationSyncPath, syncStatusData, userData) const creationPath = myMapping.prismaPath.replace(/\./g, '.create.') dot.str(creationPath, myMapping.thirdPartyValue, userData) } }, Promise.resolve) if (config.thirdPartySync.logData) { condoManagerLogger.info(`User data:\n${JSON.stringify(userData, null, 2)}`) } // const userCreatedOrUpdated = await TwoWaySync.syncThirdParty({ // thirdPartyService, // prismaMutation, // prismaMutationVariables, // createData: createUserData, // updateData: createOrUpdateUserData, // thirdPartyFriendlyName, // thirdPartyPrefix: 'Comptes.', // thirdPartyCurrentData: compte, // prismaPrefix: 'User.', // prismaCurrentRecord: existingUser, // thirdPartyAddionalData: { // compteUserNumber: condoManagerCompteUserNumber // } // }) if (config.thirdPartySync.writeData) { let userCreatedOrUpdated if (Object.keys(userData).length > 0) { try { if (existingUser) { if (config.thirdPartySync.logCRUDAction) { condoManagerLogger.info('Updating a user') } userCreatedOrUpdated = await UserController.updateUser({ where: { id: existingUser.id, }, data: userData, }) } else { if (config.thirdPartySync.logCRUDAction) { condoManagerLogger.info('Creating a user') } userCreatedOrUpdated = await UserController.createUser({ ...userData, }) } if (config.thirdPartySync.logData) { condoManagerLogger.info(`User updated/created: ${userCreatedOrUpdated?.id}`) } } catch (error) { condoManagerLogger.error(error) } } else { // Since we will not create/update the user userCreatedOrUpdated = existingUser } if (userCreatedOrUpdated) { // Add users to property const propertyUpdateData = { ...((compte.EstResidant === '1' || isTenant) && !walterProperty.users.some((u) => u.id === userCreatedOrUpdated.id) && { users: { connect: [ { id: userCreatedOrUpdated.id, }, ], }, }), ...(!walterProperty.owners.some((u) => u.id === userCreatedOrUpdated.id) && { owners: { connect: [ { id: userCreatedOrUpdated.id, }, ], }, }), } if (Object.keys(propertyUpdateData).length > 0) { if (config.thirdPartySync.logCRUDAction) { condoManagerLogger.info( `Connecting user ${userCreatedOrUpdated.id} to property ${walterProperty.id}` ) } await PropertyController.updateProperty({ where: { id: walterProperty.id, }, data: propertyUpdateData, }) } } } } if (config.thirdPartySync.logProcessAction) { condoManagerLogger.info(`✅ Finish processing a user for compte ${condoManagerNomCompte}`) } }