const UserError = require('vn-loopback/util/user-error'); const getFinalState = require('vn-loopback/util/hook').getFinalState; const isMultiple = require('vn-loopback/util/hook').isMultiple; const validateTin = require('vn-loopback/util/validateTin'); const validateIban = require('vn-loopback/util/validateIban'); const LoopBackContext = require('loopback-context'); module.exports = Self => { // Methods require('./client-methods')(Self); // Validations Self.validatesPresenceOf('street', { message: 'Street cannot be empty' }); Self.validatesPresenceOf('city', { message: 'City cannot be empty' }); Self.validatesUniquenessOf('fi', { message: 'TIN must be unique' }); Self.validatesFormatOf('email', { message: 'Invalid email', allowNull: true, allowBlank: true, with: /^[\W]*([\w+\-.%]+@[\w\-.]+\.[A-Za-z]{1,61}[\W]*,{1}[\W]*)*([\w+\-.%]+@[\w\-.]+\.[A-Za-z]{1,61})[\W]*$/ }); Self.validatesLengthOf('postcode', { allowNull: true, allowBlank: true, min: 3, max: 10 }); Self.validatesFormatOf('street', { message: 'Street should be uppercase', allowNull: false, allowBlank: false, with: /^[^a-z]*$/ }); Self.validatesFormatOf('socialName', { message: 'Social name should be uppercase', allowNull: false, allowBlank: false, with: /^[^a-z]*$/ }); Self.validateAsync('socialName', socialNameIsUnique, { message: 'The company name must be unique' }); async function socialNameIsUnique(err, done) { if (!this.countryFk) return done(); const filter = { include: { relation: 'country', scope: { fields: { isSocialNameUnique: true, }, }, }, where: { and: [ {socialName: this.socialName}, {isActive: true}, {id: {neq: this.id}} ] } }; const client = await Self.app.models.Country.findById(this.countryFk, {fields: ['isSocialNameUnique']}); const existingClient = await Self.findOne(filter); if (existingClient && (existingClient.country().isSocialNameUnique || client.isSocialNameUnique)) err(); done(); } Self.validateAsync('iban', ibanNeedsValidation, { message: 'The IBAN does not have the correct format' }); async function ibanNeedsValidation(err, done) { if (!this.bankEntityFk) return done(); const bankEntity = await Self.app.models.BankEntity.findById(this.bankEntityFk); const filter = { fields: ['code'], where: {id: bankEntity.countryFk} }; const country = await Self.app.models.Country.findOne(filter); if (!validateIban(this.iban, country?.code)) err(); done(); } Self.validateAsync('fi', tinIsValid, { message: 'Invalid TIN' }); async function tinIsValid(err, done) { if (!this.isTaxDataChecked) return done(); const filter = { fields: ['code'], where: {id: this.countryFk} }; const country = await Self.app.models.Country.findOne(filter); const code = country ? country.code.toLowerCase() : null; const countryCode = this.fi?.toLowerCase().substring(0, 2); if (!this.fi || !validateTin(this.fi, code) || (this.isVies && countryCode == code)) err(); done(); } Self.validate('payMethod', hasSalesMan, { message: 'Cannot change the payment method if no salesperson' }); function hasSalesMan(err) { if (this.payMethod && !this.salesPersonUser) err(); } Self.validate('isEqualizated', cannotHaveET, { message: 'Cannot check Equalization Tax in this NIF/CIF' }); function cannotHaveET(err) { if (!this.fi) return; const tin = this.fi.toUpperCase(); const cannotHaveET = /^[A-B]/.test(tin); if (cannotHaveET && this.isEqualizated) err(); } Self.validateAsync('payMethodFk', hasIban, { message: 'That payment method requires an IBAN' }); function hasIban(err, done) { Self.app.models.PayMethod.findById(this.payMethodFk, (_, instance) => { const isMissingIban = instance && instance.isIbanRequiredForClients && !this.iban; if (isMissingIban) err(); done(); }); } Self.validateAsync('bankEntityFk', hasBic, { message: 'That payment method requires a BIC' }); function hasBic(err, done) { if (this.iban && !this.bankEntityFk) err(); done(); } Self.validateAsync('isToBeMailed', isToBeMailed, { message: 'There is no assigned email for this client' }); function isToBeMailed(err, done) { if (this.isToBeMailed == true && !this.email) err(); done(); } Self.validateAsync('defaultAddressFk', isActive, {message: 'Unable to default a disabled consignee'} ); async function isActive(err, done) { if (!this.defaultAddressFk) return done(); const address = await Self.app.models.Address.findById(this.defaultAddressFk); if (address && !address.isActive) err(); done(); } Self.validateAsync('postCode', hasValidPostcode, { message: `The postcode doesn't exist. Please enter a correct one` }); async function hasValidPostcode(err, done) { if (!this.postcode) return done(); const models = Self.app.models; const postcode = await models.Postcode.findById(this.postcode); if (!postcode) err(); done(); } function isAlpha(value) { const regexp = new RegExp(/^[ñça-zA-Z0-9\s]*$/i); return regexp.test(value); } Self.observe('before save', async ctx => { const changes = ctx.data || ctx.instance; const orgData = ctx.currentInstance; const businessTypeFk = changes && changes.businessTypeFk || orgData && orgData.businessTypeFk; const isTaxDataChecked = changes && changes.isTaxDataChecked || orgData && orgData.isTaxDataChecked; let invalidBusinessType = false; if (!ctx.isNewInstance) { const isWorker = await Self.app.models.Account.findById(orgData.id); const changedFields = Object.keys(changes); const hasChangedOtherFields = changedFields.some(key => key !== 'businessTypeFk'); if (!businessTypeFk && !isTaxDataChecked && !isWorker && !hasChangedOtherFields) invalidBusinessType = true; } if (ctx.isNewInstance) { if (!businessTypeFk && !isTaxDataChecked) invalidBusinessType = true; } if (invalidBusinessType) throw new UserError(`The type of business must be filled in basic data`); }); Self.observe('before save', async ctx => { const changes = ctx.data || ctx.instance; const orgData = ctx.currentInstance; const models = Self.app.models; const loopBackContext = LoopBackContext.getCurrentContext(); const accessToken = {req: loopBackContext.active.accessToken}; const editVerifiedDataWithoutTaxDataChecked = models.ACL.checkAccessAcl( accessToken, 'Client', 'editVerifiedDataWithoutTaxDataCheck', 'WRITE' ); const hasChanges = orgData && changes; const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked); const isTaxDataCheckedChanged = hasChanges && orgData.isTaxDataChecked != isTaxDataChecked; const sageTaxType = hasChanges && (changes.sageTaxTypeFk || orgData.sageTaxTypeFk); const sageTaxTypeChanged = hasChanges && orgData.sageTaxTypeFk != sageTaxType; const sageTransactionType = hasChanges && (changes.sageTransactionTypeFk || orgData.sageTransactionTypeFk); const sageTransactionTypeChanged = hasChanges && orgData.sageTransactionTypeFk != sageTransactionType; const cantEditVerifiedData = isTaxDataCheckedChanged && !editVerifiedDataWithoutTaxDataChecked; const cantChangeSageData = (sageTaxTypeChanged || sageTransactionTypeChanged ) && !editVerifiedDataWithoutTaxDataChecked; if (cantEditVerifiedData || cantChangeSageData) throw new UserError(`You don't have enough privileges`); }); Self.observe('before save', async function(ctx) { const changes = ctx.data || ctx.instance; const orgData = ctx.currentInstance; const finalState = getFinalState(ctx); const payMethodWithIban = 4; // Validate socialName format const hasChanges = orgData && changes; const socialName = changes && changes.socialName || orgData && orgData.socialName; const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked); const socialNameChanged = hasChanges && orgData.socialName != socialName; const isTaxDataCheckedChanged = hasChanges && orgData.isTaxDataChecked != isTaxDataChecked; if ((socialNameChanged || isTaxDataCheckedChanged) && !isAlpha(socialName)) throw new UserError(`The social name has an invalid format`); if (changes.salesPerson === null) { changes.credit = 0; changes.discount = 0; changes.payMethodFk = 5; // Credit card } const payMethodFk = changes.payMethodFk || (orgData && orgData.payMethodFk); const dueDay = changes.dueDay || (orgData && orgData.dueDay); if (payMethodFk == payMethodWithIban && dueDay == 0) changes.dueDay = 5; if (isMultiple(ctx)) return; if (!ctx.isNewInstance) { const isTaxDataCheckedChanged = !orgData.isTaxDataChecked && changes.isTaxDataChecked; if (isTaxDataCheckedChanged && !orgData.businessTypeFk) throw new UserError(`Can't verify data unless the client has a business type`); } // Credit changes if (changes.credit !== undefined) await Self.changeCredit(ctx, finalState, changes); // Credit management changes if ((changes?.rating != null && changes.rating >= 0) || (changes?.recommendedCredit != null && changes.recommendedCredit >= 0)) await Self.changeCreditManagement(ctx, finalState, changes); const oldInstance = {}; if (!ctx.isNewInstance) { const newProps = Object.keys(changes); Object.keys(orgData.__data).forEach(prop => { if (newProps.includes(prop)) oldInstance[prop] = orgData[prop]; }); } ctx.hookState.oldInstance = oldInstance; ctx.hookState.newInstance = changes; }); Self.observe('after save', async ctx => { if (ctx.isNewInstance) return; const hookState = ctx.hookState; const newInstance = hookState.newInstance; const oldInstance = hookState.oldInstance; const instance = ctx.instance; const models = Self.app.models; const payMethodChanged = oldInstance.payMethodFk != newInstance.payMethodFk; const ibanChanged = oldInstance.iban != newInstance.iban; const dueDayChanged = oldInstance.dueDay != newInstance.dueDay; if (payMethodChanged || ibanChanged || dueDayChanged) { const loopBackContext = LoopBackContext.getCurrentContext(); const httpCtx = {req: loopBackContext.active}; const httpRequest = httpCtx.req.http.req; const $t = httpRequest.__; const url = await Self.app.models.Url.getUrl(); const salesPersonId = instance.salesPersonFk; if (salesPersonId) { // Send email to client if (instance.email) { const {Email} = require('vn-print'); const worker = await models.EmailUser.findById(salesPersonId); const params = { id: instance.id, recipientId: instance.id, recipient: instance.email, replyTo: worker.email }; const email = new Email('payment-update', params); await email.send(); } const fullUrl = `${url}client/${instance.id}/billing-data`; const message = $t('Changed client paymethod', { clientId: instance.id, clientName: instance.name, url: fullUrl }); await models.Chat.sendCheckingPresence(httpCtx, salesPersonId, message); } } const workerIdBefore = oldInstance.salesPersonFk; const workerIdAfter = newInstance.salesPersonFk; const assignmentChanged = workerIdBefore != workerIdAfter; if (assignmentChanged) await Self.notifyAssignment(instance, workerIdBefore, workerIdAfter); }); // Send notification on client worker assignment Self.notifyAssignment = async function notifyAssignment(client, previousWorkerId, currentWorkerId) { const loopBackContext = LoopBackContext.getCurrentContext(); const httpCtx = {req: loopBackContext.active}; const httpRequest = httpCtx.req.http.req; const $t = httpRequest.__; const url = await Self.app.models.Url.getUrl(); const models = Self.app.models; let previousWorker = {name: $t('None')}; let currentWorker = {name: $t('None')}; if (previousWorkerId) { const worker = await models.Worker.findById(previousWorkerId, { include: {relation: 'user'} }); previousWorker.user = worker && worker.user().name; previousWorker.name = worker && worker.user().nickname; } if (currentWorkerId) { const worker = await models.Worker.findById(currentWorkerId, { include: {relation: 'user'} }); currentWorker.user = worker && worker.user().name; currentWorker.name = worker && worker.user().nickname; } const fullUrl = `${url}client/${client.id}/basic-data`; const message = $t('Client assignment has changed', { clientId: client.id, clientName: client.name, url: fullUrl, previousWorkerName: previousWorker.name, currentWorkerName: currentWorker.name }); if (previousWorkerId) await models.Chat.send(httpCtx, `@${previousWorker.user}`, message); if (currentWorkerId) await models.Chat.send(httpCtx, `@${currentWorker.user}`, message); }; // Credit change validations Self.changeCredit = async function changeCredit(ctx, finalState, changes) { const models = Self.app.models; const userId = ctx.options.accessToken.userId; const accessToken = {req: {accessToken: ctx.options.accessToken}}; const canEditCredit = await models.ACL.checkAccessAcl(accessToken, 'Client', 'editCredit', 'WRITE'); if (!canEditCredit) { const lastCredit = await models.ClientCredit.findOne({ field: ['workerFk', 'amount'], where: { clientFk: finalState.id }, order: 'id DESC' }, ctx.options); if (lastCredit && lastCredit.amount == 0) { const zeroCreditEditor = await models.ACL.checkAccessAcl(accessToken, 'Client', 'zeroCreditEditor', 'WRITE'); const lastCreditIsNotEditable = await models.ACL.checkAccessAcl( {req: {accessToken: {userId: lastCredit.workerFk}}}, 'Client', 'zeroCreditEditor', 'WRITE' ); if (lastCreditIsNotEditable && !zeroCreditEditor) throw new UserError(`You can't change the credit set to zero from a financialBoss`); } const creditLimits = await models.RoleCreditLimit.find({ fields: ['roleFk'], where: { maxAmount: {gte: changes.credit} } }, ctx.options); const requiredRoles = []; for (limit of creditLimits) requiredRoles.push(limit.roleFk); const userRequiredRoles = await models.RoleMapping.count({ roleId: {inq: requiredRoles}, principalType: 'USER', principalId: userId }, ctx.options); if (userRequiredRoles <= 0) throw new UserError(`You don't have enough privileges to set this credit amount`); } }; Self.changeCreditManagement = async function changeCreditManagement(ctx, finalState, changes) { const models = Self.app.models; const loopBackContext = LoopBackContext.getCurrentContext(); const userId = loopBackContext.active.accessToken.userId; await models.ClientInforma.create({ clientFk: finalState.id, rating: changes.rating, recommendedCredit: changes.recommendedCredit, workerFk: userId }, ctx.options); }; const app = require('vn-loopback/server/server'); app.on('started', function() { const VnUser = app.models.VnUser; VnUser.observe('before save', async ctx => { if (ctx.isNewInstance) return; if (ctx.currentInstance) ctx.hookState.oldInstance = JSON.parse(JSON.stringify(ctx.currentInstance)); }); VnUser.observe('after save', async ctx => { const changes = ctx.data || ctx.instance; if (!ctx.isNewInstance && changes) { const oldData = ctx.hookState.oldInstance; let hasChanges; if (oldData) hasChanges = oldData.name != changes.name || oldData.active != changes.active; if (!hasChanges) return; } }); }); };