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.validateAsync('socialName', socialNameIsUnique, { message: 'The company name must be unique' }); async function socialNameIsUnique(err, done) { const filter = { where: { and: [ {socialName: this.socialName}, {isActive: true}, {id: {neq: this.id}} ] } }; const client = await Self.app.models.Client.findOne(filter); if (client) err(); done(); } Self.validateAsync('iban', ibanNeedsValidation, { message: 'The IBAN does not have the correct format' }); async function ibanNeedsValidation(err, 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; if (code != 'es') return done(); if (!validateIban(this.iban)) 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; if (!this.fi || !validateTin(this.fi, 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.UserAccount.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 userId = loopBackContext.active.accessToken.userId; const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', ctx.options); 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 && !isSalesAssistant; const cantChangeSageData = (sageTaxTypeChanged || sageTransactionTypeChanged) && !isSalesAssistant; 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); 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 headers = httpRequest.headers; const origin = headers.origin; 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 = `${origin}/#!/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 headers = httpRequest.headers; const origin = headers.origin; 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 = `${origin}/#!/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 isFinancialBoss = await models.Account.hasRole(userId, 'financialBoss', ctx.options); if (!isFinancialBoss) { const lastCredit = await models.ClientCredit.findOne({ where: { clientFk: finalState.id }, order: 'id DESC' }, ctx.options); const lastAmount = lastCredit && lastCredit.amount; const lastWorkerId = lastCredit && lastCredit.workerFk; const lastWorkerIsFinancialBoss = await models.Account.hasRole(lastWorkerId, 'financialBoss', ctx.options); if (lastAmount == 0 && lastWorkerIsFinancialBoss) throw new UserError(`You can't change the credit set to zero from a financialBoss`); const creditLimits = await models.ClientCreditLimit.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`); } await models.ClientCredit.create({ amount: changes.credit, clientFk: finalState.id, workerFk: userId }, ctx.options); }; const app = require('vn-loopback/server/server'); app.on('started', function() { const account = app.models.Account; account.observe('before save', async ctx => { if (ctx.isNewInstance) return; if (ctx.currentInstance) ctx.hookState.oldInstance = JSON.parse(JSON.stringify(ctx.currentInstance)); }); account.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; const isClient = await Self.app.models.Client.count({id: oldData.id}); if (isClient) { const loopBackContext = LoopBackContext.getCurrentContext(); const userId = loopBackContext.active.accessToken.userId; const logRecord = { originFk: oldData.id, userFk: userId, action: 'update', changedModel: 'Account', oldInstance: {name: oldData.name, active: oldData.active}, newInstance: {name: changes.name, active: changes.active} }; await Self.app.models.ClientLog.create(logRecord); } } }); }); };