salix/modules/client/back/models/client.js

526 lines
18 KiB
JavaScript

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 >= 0 || 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;
}
});
});
};