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,
}