import { Building, Property, Project, LineItem, Option } from 'nexus-plugin-prisma/client' import { prismaClient as prisma } from '../prismaClient' import { keys, filter, union, transform, isEqual, isObject } from 'lodash' import chunk from 'lodash/chunk' import * as dot from 'dot-object' import { logger } from './logger' const getDuplicates = (array, fields) => { const duplicated = [] array.forEach((item, index) => { array.forEach((item2, index1) => { if (index1 === index) { return false } if ( fields.every( (field) => !duplicated.some((duplicateItem) => { return fields.some( (field) => dot.pick(field, duplicateItem) && dot.pick(field, item) && dot.pick(field, duplicateItem) === dot.pick(field, item) ) }) && dot.pick(field, item2) && dot.pick(field, item) && dot.pick(field, item2) === dot.pick(field, item) ) ) { duplicated.push(item2) } }) }) return duplicated } const getValueForField = (path, obj = {}) => path.split('.').reduce((prev, curr) => (prev ? prev[curr] : null), obj) function cleanString(str) { return str .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[|&;$%@"<>()+,]/g, '') } function wait(ms = 0) { return new Promise((resolve) => setTimeout(resolve, ms)) } const splitTextWithLineBreak = (text) => text ? text.split('\n').map((item) => ({ text: item, isLineBreak: isEmpty(item), })) : [] function uniq(array, param?) { const result = [] const map = new Map() for (const item of array) { if (!map.has(param ? dot.pick(param, item) : item)) { map.set(param ? dot.pick(param, item) : item, true) // set any value to Map result.push(item) } } return result.filter(Boolean) } const requiredParam = (argName: string) => { throw new Error(`${argName} is required`) } const promiseSerial = (funcs) => funcs.reduce( (promise, func) => promise.then((result) => func().then(Array.prototype.concat.bind(result))), Promise.resolve([]) ) const getUserName = (user) => { if (!user) { return '' } else if (user.firstName && user.lastName) { return `${user.firstName} ${user.lastName}` } else if (user.firstName) { return user.firstName } else if (user.managingCompany) { return user.managingCompany.shortName || user.managingCompany.longName || '' } else { return user.lastName || user.fullname || '' } } const getUserNameAndProject = (user, project) => { let str = getUserName(user) if (project?.name) { str += `- ${project.name}` } return str } const getFirstNameAndLastNameFromFullName = (fullName) => { if (!fullName) { return [] } if (fullName.includes(',')) { const [lastName, firstName] = fullName.split(',') return [firstName?.trim() || '', lastName?.trim() || ''] } if (fullName.includes(' ')) { const [firstName, ...rest] = fullName.split(' ') return [firstName?.trim() || '', rest.map((str) => str?.trim() || '').join(' ')] } return [fullName?.trim() || '', ''] } const getShortUserName = (user) => { if (!user) { return '' } else if (user.firstName) { return user.firstName } else if (user.lastName) { return user.lastName } else if (user.managingCompany) { return user.managingCompany.shortName || user.managingCompany.longName || '' } else { return user.fullname || '' } } const isFunction = (functionToCheck) => functionToCheck && {}.toString.call(functionToCheck) === '[object Function]' const getThreeLettersInitial = (str) => { if (!str) { return '' } return str .replace('Syndicat', '') .replace('des', '') .replace('copropriétaires', '') .replace('S.D.C', '') .match(/\b(\w)/g) .join('') .toUpperCase() .substr(0, 3) } const removeRichTextareaLineBreak = (str) => { if (!str) { return '' } return str.replace(/

(
|<\/br>||
| )<\/p>/gm, '
') } const getUserNameAndUnits = async (user, project) => { if (!user) { return '' } let name = getUserName(user) if (!project) { logger.error("Missing project in 'getUserNameAndUnits'") return name } // Fetch data if we're missing some to show the user name with his correct properties // const [userWithData, projectProperties] = await Promise.all([ user.properties ? user : prisma.user.findOne({ where: { id: user.id }, select: { id: true, property: { select: { id: true, address: { select: { apartmentNumber: true, }, }, building: { select: { id: true, }, }, }, }, properties: { select: { id: true, address: { select: { apartmentNumber: true, }, }, building: { select: { id: true, }, }, }, }, }, }), project?.building?.properties?.some((property) => property.address) ? project.building.properties || [] : prisma.project.findOne({ where: { id: project.id }, select: { building: { select: { properties: { select: { address: { select: { apartmentNumber: true, }, }, }, }, }, }, }, }), ]) const userUniqProperties = uniq([userWithData.property, ...(userWithData.properties || [])], 'id') if (projectProperties && projectProperties.length) { const userPropertiesForProject = projectProperties.filter((property) => userUniqProperties.some( (userProperty) => userProperty.address && userProperty.address.apartmentNumber === property.address.apartmentNumber ) ) if (userPropertiesForProject.length) { name += ` - ${arrayDisplay({ array: userPropertiesForProject, valueToDisplay: 'address.apartmentNumber', separatorString: ', ', preItemString: '#', })}` } } return name } function arrayDisplay({ array, valueToDisplay, separatorString = ' - ', preItemString = '' }) { return array .filter(Boolean) .reduce( (final, item, index) => (final += `${preItemString}${dot.pick(valueToDisplay, item)}${ array.length - 1 !== index ? separatorString : '' }`), '' ) } const formatToCurrency = (amount) => { return isNaN(amount) || amount === null ? '' : `$${parseFloat(amount).toFixed(2)}` } const getFormattedAddress = (address) => { if (!address) { return '' } return `${address.address1 ? `${address.address1}, ` : ''}${ address.apartmentNumber ? `Apt. ${address.apartmentNumber}, ` : '' }${address.city ? `${address.city}, ` : ''}${address.state ? `${address.state}, ` : ''}${ address.country ? `${address.country}, ` : '' }${address.zip ? `${address.zip}` : ''}` } const groupBy = (array, f) => { const groups = {} array.forEach((item) => { const group = f ? JSON.stringify(f(item)) : item groups[group] = groups[group] || [] groups[group].push(item) }) return Object.keys(groups).map((group) => groups[group]) } const groupByProperties = (array, properties) => { const groups = [] for (let i = 0, len = array.length; i < len; i += 1) { const obj = array[i] if (groups.length === 0) { groups.push([obj]) } else { let equalGroup = false for (let a = 0, glen = groups.length; a < glen; a += 1) { const group = groups[a] let equal = true const firstElement = group[0] properties.forEach(function (property) { if (firstElement[property] !== obj[property]) { equal = false } }) if (equal) { equalGroup = group } } if (equalGroup) { // @ts-ignore equalGroup.push(obj) } else { groups.push([obj]) } } } return groups } const changedKeys = (o1, o2) => { const keysVar = union(keys(o1), keys(o2)) return filter(keysVar, (key) => o1[key] !== o2[key]) } // @ts-ignore const rtrim = (str, chars) => { chars = chars || WHITE_SPACES let end = str.length - 1 const charLen = chars.length let found = true let i let c while (found && end >= 0) { found = false i = -1 c = str.charAt(end) while (++i < charLen) { if (c === chars[i]) { found = true end-- break } } } return end >= 0 ? str.substring(0, end + 1) : '' } const ltrim = (str, chars) => { chars = chars || WHITE_SPACES let start = 0 const len = str.length const charLen = chars.length let found = true let i let c while (found && start < len) { found = false i = -1 c = str.charAt(start) while (++i < charLen) { if (c === chars[i]) { found = true start++ break } } } return start >= len ? '' : str.substr(start, len) } const renderWithLineBreak = (strings = []) => { return strings.reduce( (final, string, index) => (final += string ? `${index === 0 ? '' : '\n'}${string}` : ''), '' ) } const WHITE_SPACES = [ ' ', '\n', '\r', '\t', '\f', '\v', '\u00A0', '\u1680', '\u180E', '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', '\u2008', '\u2009', '\u200A', '\u2028', '\u2029', '\u202F', '\u205F', '\u3000', ] // @ts-ignore const trim = (str) => { const chars = WHITE_SPACES return ltrim(rtrim(str, chars), chars) } const truncate = ({ str, maxChars = 100, append = '...', onlyFullWords = true }) => { if (!str) { return '' } maxChars = onlyFullWords ? maxChars + 1 : maxChars str = trim(str) if (str.length <= maxChars) { return str } str = str.substr(0, maxChars - append.length) // crop at last space or remove trailing whitespace str = onlyFullWords ? str.substr(0, str.lastIndexOf(' ')) : trim(str) return str + append } const getStringBetween = ({ str, a, b }) => { const match = str.match(new RegExp(a + '(.*)' + b)) return match ? match[1] : null // str.substring(str.lastIndexOf(a) + 1, str.lastIndexOf(b)) } const isEmpty = (value) => value === undefined || value === null || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim().length === 0) const isValidPhoneNumber = (phoneNumber) => { if (!phoneNumber) { return false } const phoneVerificationRegex = /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,6}$/im return phoneNumber.match(phoneVerificationRegex) } const isValidEmail = (email) => { const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ return re.test(String(email).toLowerCase()) } const trimAll = (obj) => { return Object.keys(obj).reduce( (total, k) => ({ ...total, [k]: obj[k] && typeof obj[k] === 'string' ? obj[k].trim() : obj[k], }), {} ) } const firstLetterToUpperCase = (string) => string ? string.charAt(0).toUpperCase() + string.slice(1) : '' const firstLetterToLowerCase = (string) => string ? string.charAt(0).toLowerCase() + string.slice(1) : '' const getStringMessageBasedOnArray = (array) => { return array.reduce( (total, curr, index) => (total += curr ? `${curr}${array.length - 1 === index ? '' : ', '}` : ''), '' ) } function formatPhoneNumber(phoneNumberString) { if (!phoneNumberString) { return null } const cleaned = ('' + phoneNumberString).replace(/\D/g, '') const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/) if (match) { return '(' + match[1] + ') ' + match[2] + '-' + match[3] } return null } function formatToPlainPhoneNumber(phoneNumber) { if (!phoneNumber) { return '' } return phoneNumber.replace(/[^0-9]/g, '').trim() } function difference(object, base) { // If one or the other is empty... if (!base || !object) { return object || base } const changes = (object, base) => { let arrayIndexCounter = 0 return transform(object, (result, value, key) => { if (!isEqual(value, base[key])) { const resultKey = Array.isArray(base) ? arrayIndexCounter++ : key result[resultKey] = isObject(value) && isObject(base[key]) ? changes(value, base[key]) : value } }) } return changes(object, base) } function trimAndLowerCase(string) { if (!string) { return '' } return string.trim().toLowerCase() } function isSameEmail(email1, email2) { if (!email1 || !email2) { return false } return trimAndLowerCase(email1) === trimAndLowerCase(email2) } function isSamePhoneNumber(phoneNumber1, phoneNumber2) { if (!phoneNumber1 || !phoneNumber2) { return false } return ( formatPhoneNumberToHaveTheSameAsTwilio(phoneNumber1) === formatPhoneNumberToHaveTheSameAsTwilio(phoneNumber2) ) } function convertToArray(thing) { if (!thing) { return [] } return Array.isArray(thing) ? thing : [thing] } const formatPhoneNumberToHaveTheSameAsTwilio = (phoneNumber: string): string => { if (!phoneNumber) { return '' } const plainPhoneNumber = formatToPlainPhoneNumber(phoneNumber) if (!plainPhoneNumber) { return '' } if (plainPhoneNumber.length === 10) { return `+1${plainPhoneNumber}` } return `+${plainPhoneNumber}` } const formatEmail = (email) => (email ? email.trim().toLowerCase() : '') function ProvinceStateWithCode(input) { const states = [ ['Alberta', 'AB'], ['British Columbia', 'BC'], ['Manitoba', 'MB'], ['New Brunswick', 'NB'], ['Newfoundland', 'NF'], ['Northwest Territory', 'NT'], ['Nova Scotia', 'NS'], ['Nunavut', 'NU'], ['Ontario', 'ON'], ['Prince Edward Island', 'PE'], ['Québec', 'QC'], ['Saskatchewan', 'SK'], ['Yukon', 'YT'], ['Arizona', 'AZ'], ['Alabama', 'AL'], ['Alaska', 'AK'], ['Arizona', 'AZ'], ['Arkansas', 'AR'], ['California', 'CA'], ['Colorado', 'CO'], ['Connecticut', 'CT'], ['Delaware', 'DE'], ['Florida', 'FL'], ['Georgia', 'GA'], ['Hawaii', 'HI'], ['Idaho', 'ID'], ['Illinois', 'IL'], ['Indiana', 'IN'], ['Iowa', 'IA'], ['Kansas', 'KS'], ['Kentucky', 'KY'], ['Kentucky', 'KY'], ['Louisiana', 'LA'], ['Maine', 'ME'], ['Maryland', 'MD'], ['Massachusetts', 'MA'], ['Michigan', 'MI'], ['Minnesota', 'MN'], ['Mississippi', 'MS'], ['Missouri', 'MO'], ['Montana', 'MT'], ['Nebraska', 'NE'], ['Nevada', 'NV'], ['New Hampshire', 'NH'], ['New Jersey', 'NJ'], ['New Mexico', 'NM'], ['New York', 'NY'], ['North Carolina', 'NC'], ['North Dakota', 'ND'], ['Ohio', 'OH'], ['Oklahoma', 'OK'], ['Oregon', 'OR'], ['Pennsylvania', 'PA'], ['Rhode Island', 'RI'], ['South Carolina', 'SC'], ['South Dakota', 'SD'], ['Tennessee', 'TN'], ['Texas', 'TX'], ['Utah', 'UT'], ['Vermont', 'VT'], ['Virginia', 'VA'], ['Washington', 'WA'], ['West Virginia', 'WV'], ['Wisconsin', 'WI'], ['Wyoming', 'WY'], ] input = input.replace(/\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() }) for (let i = 0; i < states.length; i++) { if (states[i][0] === input) { return { code: states[i][1], full: states[i][0] } } } input = input.toUpperCase() for (let i = 0; i < states.length; i++) { if (states[i][1] === input) { return { code: states[i][1], full: states[i][0] } } } return null } function CountryWithCodes(input) { const countries = [ ['United States of America', 'US', 'USA'], ['United States', 'US', 'USA'], ['Canada', 'CA', 'CAN'], ] input = input.toUpperCase() for (let i = 0; i < countries.length; i++) { if (countries[i][0].toUpperCase() === input) { return { code2: countries[i][1], code3: countries[i][2], full: countries[i][0] } } } for (let i = 0; i < countries.length; i++) { if (countries[i][1] === input) { return { code2: countries[i][1], code3: countries[i][2], full: countries[i][0] } } } for (let i = 0; i < countries.length; i++) { if (countries[i][2] === input) { return { code2: countries[i][1], code3: countries[i][2], full: countries[i][0] } } } return null } function serializeObjForQueryString(obj, prefix?) { const str = [] let p for (p in obj) { if (Object.prototype.hasOwnProperty.call(obj, p)) { const k = prefix ? prefix + '[' + p + ']' : p const v = obj[p] str.push( v !== null && typeof v === 'object' ? serializeObjForQueryString(v, k) : encodeURIComponent(k) + '=' + encodeURIComponent(v) ) } } return str.join('&') } type LineItemWithOptions = (LineItem & { option: Option })[] const getOrderLineItemsInlineString = (lineItems: LineItemWithOptions) => { return ( lineItems.reduce( (total, lineItem, index) => (total += `${lineItem.option.title}${ lineItem.option.description ? ` - ${lineItem.option.description} ` : ' ' }(${lineItem.quantity}x)${index !== lineItems.length - 1 ? ', ' : ''}`), '' ) || '' ) } const loopChunksPromise = (array, chunkSize, itemProcessFunction, onFinishProcessChunk?) => { const chunks = chunk(array, chunkSize) // @ts-ignore return chunks.reduce(async (promise, chunk) => { await promise await Promise.all(chunk.map(itemProcessFunction)) if (onFinishProcessChunk && isFunction(onFinishProcessChunk)) { const res = onFinishProcessChunk() if (res?.then) { await res } } }, Promise.resolve) } const removeAccents = (str) => { if (!str) { return '' } return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '') } const throwIfObjHasNotProperties = (obj, properties) => { properties.forEach((property) => { if (!Object.prototype.hasOwnProperty.call(obj, property)) { throw new Error(`Missing property '${property}' in object`) } }) } export default { wait, formatEmail, isSameEmail, isSamePhoneNumber, getDuplicates, throwIfObjHasNotProperties, isValidEmail, formatPhoneNumberToHaveTheSameAsTwilio, truncate, removeAccents, getOrderLineItemsInlineString, serializeObjForQueryString, cleanString, uniq, trimAndLowerCase, requiredParam, getUserName, getFormattedAddress, changedKeys, isValidPhoneNumber, firstLetterToLowerCase, firstLetterToUpperCase, getStringMessageBasedOnArray, splitTextWithLineBreak, renderWithLineBreak, formatPhoneNumber, formatToPlainPhoneNumber, difference, getUserNameAndUnits, convertToArray, arrayDisplay, getUserNameAndProject, getFirstNameAndLastNameFromFullName, getThreeLettersInitial, removeRichTextareaLineBreak, loopChunksPromise, }