Merge branch 'dev' into 5914-warmfix-renameTable
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Alex Moreno 2023-11-14 09:36:47 +00:00
commit 2cd9c9c688
23 changed files with 834 additions and 245 deletions

View File

@ -10,5 +10,9 @@
"eslint.format.enable": true, "eslint.format.enable": true,
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint" "editor.defaultFormatter": "dbaeumer.vscode-eslint"
} },
"cSpell.words": [
"salix",
"fdescribe"
]
} }

View File

@ -0,0 +1,54 @@
module.exports = Self => {
Self.remoteMethod('getList', {
description: 'Get list of the available and active notification subscriptions',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
description: 'User to modify',
http: {source: 'path'}
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/getList`,
verb: 'GET'
}
});
Self.getList = async(id, options) => {
const activeNotificationsMap = new Map();
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const availableNotificationsMap = await Self.getAvailable(id, myOptions);
const activeNotifications = await Self.app.models.NotificationSubscription.find({
fields: ['id', 'notificationFk'],
include: {relation: 'notification'},
where: {userFk: id}
}, myOptions);
for (active of activeNotifications) {
activeNotificationsMap.set(active.notificationFk, {
id: active.id,
notificationFk: active.notificationFk,
name: active.notification().name,
description: active.notification().description,
active: true
});
availableNotificationsMap.delete(active.notificationFk);
}
return {
active: [...activeNotificationsMap.entries()],
available: [...availableNotificationsMap.entries()]
};
};
};

View File

@ -0,0 +1,13 @@
const models = require('vn-loopback/server/server').models;
describe('NotificationSubscription getList()', () => {
it('should return a list of available and active notifications of a user', async() => {
const userId = 9;
const {active, available} = await models.NotificationSubscription.getList(userId);
const notifications = await models.Notification.find({});
const totalAvailable = notifications.length - active.length;
expect(active.length).toEqual(2);
expect(available.length).toEqual(totalAvailable);
});
});

View File

@ -1,62 +1,74 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
require('../methods/notification/getList')(Self);
Self.observe('before save', async function(ctx) { Self.observe('before save', async function(ctx) {
await checkModifyPermission(ctx);
});
Self.observe('before delete', async function(ctx) {
await checkModifyPermission(ctx);
});
async function checkModifyPermission(ctx) {
const models = Self.app.models; const models = Self.app.models;
const instance = ctx.instance;
const userId = ctx.options.accessToken.userId; const userId = ctx.options.accessToken.userId;
const user = await ctx.instance.userFk;
const modifiedUser = await getUserToModify(null, user, models);
if (userId != modifiedUser.id && userId != modifiedUser.bossFk) let notificationFk;
throw new UserError('You dont have permission to modify this user'); let workerId;
});
Self.remoteMethod('deleteNotification', { if (instance) {
description: 'Deletes a notification subscription', notificationFk = instance.notificationFk;
accepts: [ workerId = instance.userFk;
{ } else {
arg: 'ctx', const notificationSubscription = await models.NotificationSubscription.findById(ctx.where.id);
type: 'object', notificationFk = notificationSubscription.notificationFk;
http: {source: 'context'} workerId = notificationSubscription.userFk;
},
{
arg: 'notificationId',
type: 'number',
required: true
},
],
returns: {
type: 'object',
root: true
},
http: {
verb: 'POST',
path: '/deleteNotification'
} }
});
Self.deleteNotification = async function(ctx, notificationId) { const worker = await models.Worker.findById(workerId, {fields: ['id', 'bossFk']});
const models = Self.app.models; const available = await Self.getAvailable(workerId);
const user = ctx.req.accessToken.userId; const hasAcl = available.has(notificationFk);
const modifiedUser = await getUserToModify(notificationId, null, models);
if (user != modifiedUser.id && user != modifiedUser.bossFk) if (!hasAcl || (userId != worker.id && userId != worker.bossFk))
throw new UserError('You dont have permission to modify this user'); throw new UserError('The notification subscription of this worker cant be modified');
await models.NotificationSubscription.destroyById(notificationId);
};
async function getUserToModify(notificationId, userFk, models) {
let userToModify = userFk;
if (notificationId) {
const subscription = await models.NotificationSubscription.findById(notificationId);
userToModify = subscription.userFk;
}
return await models.Worker.findOne({
fields: ['id', 'bossFk'],
where: {
id: userToModify
}
});
} }
Self.getAvailable = async function(userId, options) {
const availableNotificationsMap = new Map();
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const roles = await models.RoleMapping.find({
fields: ['roleId'],
where: {principalId: userId}
}, myOptions);
const availableNotifications = await models.NotificationAcl.find({
fields: ['notificationFk', 'roleFk'],
include: {relation: 'notification'},
where: {
roleFk: {
inq: roles.map(role => role.roleId),
},
}
}, myOptions);
for (available of availableNotifications) {
availableNotificationsMap.set(available.notificationFk, {
id: null,
notificationFk: available.notificationFk,
name: available.notification().name,
description: available.notification().description,
active: false
});
}
return availableNotificationsMap;
};
}; };

View File

@ -1,74 +1,126 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
describe('loopback model NotificationSubscription', () => { describe('loopback model NotificationSubscription', () => {
it('Should fail to delete a notification if the user is not editing itself or a subordinate', async() => { it('should fail to add a notification subscription if the worker doesnt have ACLs', async() => {
const tx = await models.NotificationSubscription.beginTransaction({}); const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx, accessToken: {userId: 9}};
const user = 9; await models.NotificationSubscription.create({notificationFk: 1, userFk: 62}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
});
it('should fail to add a notification subscription if the user isnt editing itself or subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 1}};
await models.NotificationSubscription.create({notificationFk: 1, userFk: 9}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
});
it('should fail to delete a notification subscription if the user isnt editing itself or subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
const notificationSubscriptionId = 2; const notificationSubscriptionId = 2;
const ctx = {req: {accessToken: {userId: user}}}; await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
let error;
try {
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
} catch (e) {
error = e;
}
expect(error.message).toContain('You dont have permission to modify this user');
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
throw e; error = e;
} }
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
}); });
it('Should delete a notification if the user is editing itself', async() => { it('should add a notification subscription if the user is editing itself', async() => {
const tx = await models.NotificationSubscription.beginTransaction({}); const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx, accessToken: {userId: 9}};
const user = 9; await models.NotificationSubscription.create({notificationFk: 2, userFk: 9}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should delete a notification subscription if the user is editing itself', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
const notificationSubscriptionId = 6;
await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should add a notification subscription if the user is editing a subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
await models.NotificationSubscription.create({notificationFk: 1, userFk: 5}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should delete a notification subscription if the user is editing a subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 19}};
const notificationSubscriptionId = 4; const notificationSubscriptionId = 4;
const ctx = {req: {accessToken: {userId: user}}}; await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId);
expect(deletedNotification).toBeNull();
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
throw e; error = e;
} }
});
it('Should delete a notification if the user is editing a subordinate', async() => { expect(error).toBeUndefined();
const tx = await models.NotificationSubscription.beginTransaction({});
try {
const options = {transaction: tx};
const user = 9;
const notificationSubscriptionId = 5;
const ctx = {req: {accessToken: {userId: user}}};
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId);
expect(deletedNotification).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('Application', 'executeProc', '*', 'ALLOW', 'ROLE', 'employee'),
('Application', 'executeFunc', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('NotificationSubscription', 'getList', 'READ', 'ALLOW', 'ROLE', 'employee');

View File

@ -2788,6 +2788,11 @@ INSERT INTO `util`.`notification` (`id`, `name`, `description`)
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`) INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
VALUES VALUES
(1, 9), (1, 9),
(1, 1),
(2, 1),
(3, 9),
(4, 1),
(5, 9),
(6, 9); (6, 9);
INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`) INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`)
@ -2800,6 +2805,8 @@ INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`)
VALUES VALUES
(1, 1109), (1, 1109),
(1, 1110), (1, 1110),
(2, 1110),
(4, 1110),
(2, 1109), (2, 1109),
(1, 9), (1, 9),
(1, 3), (1, 3),

View File

@ -2352,6 +2352,90 @@ BEGIN
END IF; END IF;
END ;; END ;;
DELIMITER ; DELIMITER ;
DELIMITER ;;
CREATE DEFINER=`root`@`localhost` FUNCTION `account`.`user_hasRoutinePriv`(vType ENUM('PROCEDURE', 'FUNCTION'),
vChain VARCHAR(100),
vUserFk INT
) RETURNS tinyint(1)
READS SQL DATA
BEGIN
/**
* Search if the user has privileges on routines.
*
* @param vType procedure or function
* @param vChain string passed with this syntax dbName.tableName
* @param vUserFk user to ckeck
* @return vHasPrivilege
*/
DECLARE vHasPrivilege BOOL DEFAULT FALSE;
DECLARE vDb VARCHAR(50);
DECLARE vObject VARCHAR(50);
DECLARE vChainExists BOOL;
DECLARE vExecutePriv INT DEFAULT 262144;
-- 262144 = CONV(1000000000000000000, 2, 10)
-- 1000000000000000000 execution permission expressed in binary base
SET vDb = SUBSTRING_INDEX(vChain, '.', 1);
SET vChain = SUBSTRING(vChain, LENGTH(vDb) + 2);
SET vObject = SUBSTRING_INDEX(vChain, '.', 1);
SELECT COUNT(*) INTO vChainExists
FROM mysql.proc
WHERE db = vDb
AND `name` = vObject
AND `type` = vType
LIMIT 1;
IF NOT vChainExists THEN
RETURN FALSE;
END IF;
DROP TEMPORARY TABLE IF EXISTS tRole;
CREATE TEMPORARY TABLE tRole
(INDEX (`name`))
ENGINE = MEMORY
SELECT r.`name`
FROM user u
JOIN roleRole rr ON rr.role = u.role
JOIN `role` r ON r.id = rr.inheritsFrom
WHERE u.id = vUserFk;
SELECT TRUE INTO vHasPrivilege
FROM mysql.global_priv gp
JOIN tRole tr ON tr.name = gp.`User`
OR CONCAT('$', tr.name) = gp.`User`
WHERE JSON_VALUE(gp.Priv, '$.access') >= vExecutePriv
AND gp.Host = ''
LIMIT 1;
IF NOT vHasPrivilege THEN
SELECT TRUE INTO vHasPrivilege
FROM mysql.db db
JOIN tRole tr ON tr.name = db.`User`
WHERE db.Db = vDb
AND db.Execute_priv = 'Y';
END IF;
IF NOT vHasPrivilege THEN
SELECT TRUE INTO vHasPrivilege
FROM mysql.procs_priv pp
JOIN tRole tr ON tr.name = pp.`User`
WHERE pp.Db = vDb
AND pp.Routine_name = vObject
AND pp.Routine_type = vType
AND pp.Proc_priv = 'Execute'
LIMIT 1;
END IF;
DROP TEMPORARY TABLE tRole;
RETURN vHasPrivilege;
END ;;
DELIMITER ;
/*!50003 SET sql_mode = @saved_sql_mode */ ; /*!50003 SET sql_mode = @saved_sql_mode */ ;
/*!50003 SET character_set_client = @saved_cs_client */ ; /*!50003 SET character_set_client = @saved_cs_client */ ;
/*!50003 SET character_set_results = @saved_cs_results */ ; /*!50003 SET character_set_results = @saved_cs_results */ ;

View File

@ -0,0 +1,27 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.execute = async(ctx, type, query, params, options) => {
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
params = params ?? [];
const myOptions = {userId: ctx.req.accessToken.userId};
if (typeof options == 'object')
Object.assign(myOptions, options);
const chain = query.split(' ')[1];
const [canExecute] = await models.ProcsPriv.rawSql(
'SELECT account.user_hasRoutinePriv(?,?,?)',
[type, chain, userId],
myOptions);
if (!Object.values(canExecute)[0]) throw new UserError(`You don't have enough privileges`, 'ACCESS_DENIED');
const argString = params.map(() => '?').join(',');
const [response] = await models.ProcsPriv.rawSql(query + `(${argString})`, params, myOptions);
return response;
};
};

View File

@ -0,0 +1,41 @@
module.exports = Self => {
Self.remoteMethodCtx('executeFunc', {
description: 'Return result of function',
accessType: '*',
accepts: [
{
arg: 'routine',
type: 'string',
description: 'The routine name',
required: true,
http: {source: 'path'}
},
{
arg: 'schema',
type: 'string',
description: 'The routine schema',
required: true,
},
{
arg: 'params',
type: ['any'],
description: 'The params array',
},
],
returns: {
type: 'any',
root: true
},
http: {
path: `/:routine/execute-func`,
verb: 'POST'
}
});
Self.executeFunc = async(ctx, routine, schema, params, options) => {
const query = `SELECT ${schema}.${routine}`;
const response = await Self.execute(ctx, 'FUNCTION', query, params, options);
return Object.values(response)[0];
};
};

View File

@ -0,0 +1,39 @@
module.exports = Self => {
Self.remoteMethodCtx('executeProc', {
description: 'Return result of procedure',
accessType: '*',
accepts: [
{
arg: 'routine',
type: 'string',
description: 'The routine name',
required: true,
http: {source: 'path'}
},
{
arg: 'schema',
type: 'string',
description: 'The routine schema',
required: true,
},
{
arg: 'params',
type: ['any'],
description: 'The params array',
},
],
returns: {
type: 'any',
root: true
},
http: {
path: `/:routine/execute-proc`,
verb: 'POST'
}
});
Self.executeProc = async(ctx, routine, schema, params, options) => {
const query = `CALL ${schema}.${routine}`;
return Self.execute(ctx, 'PROCEDURE', query, params, options);
};
};

View File

@ -0,0 +1,161 @@
const models = require('vn-loopback/server/server').models;
describe('Application execute()/executeProc()/executeFunc()', () => {
const userWithoutPrivileges = 1;
const userWithPrivileges = 9;
const userWithInheritedPrivileges = 120;
let tx;
function getCtx(userId) {
return {
req: {
accessToken: {userId},
headers: {origin: 'http://localhost'}
}
};
}
beforeEach(async() => {
tx = await models.Application.beginTransaction({});
const options = {transaction: tx};
await models.Application.rawSql(`
CREATE OR REPLACE PROCEDURE vn.myProcedure(vMyParam INT)
BEGIN
SELECT vMyParam myParam, t.*
FROM ticket t
LIMIT 2;
END
`, null, options);
await models.Application.rawSql(`
CREATE OR REPLACE FUNCTION bs.myFunction(vMyParam INT) RETURNS int(11)
BEGIN
RETURN vMyParam;
END
`, null, options);
await models.Application.rawSql(`
GRANT EXECUTE ON PROCEDURE vn.myProcedure TO developer;
GRANT EXECUTE ON FUNCTION bs.myFunction TO developer;
`, null, options);
});
it('should throw error when execute procedure and not have privileges', async() => {
const ctx = getCtx(userWithoutPrivileges);
let error;
try {
const options = {transaction: tx};
await models.Application.execute(
ctx,
'PROCEDURE',
'CALL vn.myProcedure',
[1],
options
);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual(`You don't have enough privileges`);
});
it('should execute procedure and get data', async() => {
const ctx = getCtx(userWithPrivileges);
try {
const options = {transaction: tx};
const response = await models.Application.execute(
ctx,
'PROCEDURE',
'CALL vn.myProcedure',
[1],
options
);
expect(response.length).toEqual(2);
expect(response[0].myParam).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
describe('Application executeProc()', () => {
it('should execute procedure and get data (executeProc)', async() => {
const ctx = getCtx(userWithPrivileges);
try {
const options = {transaction: tx};
const response = await models.Application.executeProc(
ctx,
'myProcedure',
'vn',
[1],
options
);
expect(response.length).toEqual(2);
expect(response[0].myParam).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
describe('Application executeFunc()', () => {
it('should execute function and get data', async() => {
const ctx = getCtx(userWithPrivileges);
try {
const options = {transaction: tx};
const response = await models.Application.executeFunc(
ctx,
'myFunction',
'bs',
[1],
options
);
expect(response).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should execute function and get data with user with inherited privileges', async() => {
const ctx = getCtx(userWithInheritedPrivileges);
try {
const options = {transaction: tx};
const response = await models.Application.executeFunc(
ctx,
'myFunction',
'bs',
[1],
options
);
expect(response).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
});

View File

@ -2,4 +2,7 @@
module.exports = function(Self) { module.exports = function(Self) {
require('../methods/application/status')(Self); require('../methods/application/status')(Self);
require('../methods/application/post')(Self); require('../methods/application/post')(Self);
require('../methods/application/execute')(Self);
require('../methods/application/executeProc')(Self);
require('../methods/application/executeFunc')(Self);
}; };

View File

@ -0,0 +1,44 @@
{
"name": "ProcsPriv",
"base": "VnModel",
"options": {
"mysql": {
"table": "mysql.procs_priv"
}
},
"properties": {
"name": {
"id": 1,
"type": "string",
"mysql": {
"columnName": "Routine_name"
}
},
"schema": {
"id": 3,
"type": "string",
"mysql": {
"columnName": "Db"
}
},
"role": {
"type": "string",
"mysql": {
"columnName": "user"
}
},
"type": {
"id": 2,
"type": "string",
"mysql": {
"columnName": "Routine_type"
}
},
"host": {
"type": "string",
"mysql": {
"columnName": "Host"
}
}
}
}

View File

@ -321,9 +321,9 @@
"Select a different client": "Seleccione un cliente distinto", "Select a different client": "Seleccione un cliente distinto",
"Fill all the fields": "Rellene todos los campos", "Fill all the fields": "Rellene todos los campos",
"The response is not a PDF": "La respuesta no es un PDF", "The response is not a PDF": "La respuesta no es un PDF",
"Ticket without Route": "Ticket sin ruta",
"Booking completed": "Reserva completada", "Booking completed": "Reserva completada",
"The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación", "The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación",
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina", "The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina",
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina" "quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina",
"The notification subscription of this worker cant be modified": "La subscripción a la notificación de este trabajador no puede ser modificada"
} }

View File

@ -49,5 +49,13 @@
}, },
"Container": { "Container": {
"dataSource": "vn" "dataSource": "vn"
},
"ProcsPriv": {
"dataSource": "vn",
"options": {
"mysql": {
"table": "mysql.procs_priv"
}
}
} }
} }

View File

@ -158,7 +158,7 @@
}, },
"user": { "user": {
"type": "belongsTo", "type": "belongsTo",
"model": "Account", "model": "VnUser",
"foreignKey": "id" "foreignKey": "id"
}, },
"payMethod": { "payMethod": {

View File

@ -5,177 +5,177 @@ const config = require('vn-print/core/config');
const storage = require('vn-print/core/storage'); const storage = require('vn-print/core/storage');
module.exports = async function(ctx, Self, tickets, reqArgs = {}) { module.exports = async function(ctx, Self, tickets, reqArgs = {}) {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
if (tickets.length == 0) return; if (tickets.length == 0) return;
const failedtickets = []; const failedtickets = [];
for (const ticket of tickets) { for (const ticket of tickets) {
try { try {
await Self.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id], {userId}); await Self.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id], {userId});
const [invoiceOut] = await Self.rawSql(` const [invoiceOut] = await Self.rawSql(`
SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued
FROM ticket t FROM ticket t
JOIN invoiceOut io ON io.ref = t.refFk JOIN invoiceOut io ON io.ref = t.refFk
JOIN company cny ON cny.id = io.companyFk JOIN company cny ON cny.id = io.companyFk
WHERE t.id = ? WHERE t.id = ?
`, [ticket.id]); `, [ticket.id]);
const mailOptions = { const mailOptions = {
overrideAttachments: true, overrideAttachments: true,
attachments: [] attachments: []
}; };
const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed; const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed;
if (invoiceOut) { if (invoiceOut) {
const args = { const args = {
reference: invoiceOut.ref, reference: invoiceOut.ref,
recipientId: ticket.clientFk, recipientId: ticket.clientFk,
recipient: ticket.recipient, recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail replyTo: ticket.salesPersonEmail
}; };
const invoiceReport = new Report('invoice', args); const invoiceReport = new Report('invoice', args);
const stream = await invoiceReport.toPdfStream(); const stream = await invoiceReport.toPdfStream();
const issued = invoiceOut.issued; const issued = invoiceOut.issued;
const year = issued.getFullYear().toString(); const year = issued.getFullYear().toString();
const month = (issued.getMonth() + 1).toString(); const month = (issued.getMonth() + 1).toString();
const day = issued.getDate().toString(); const day = issued.getDate().toString();
const fileName = `${year}${invoiceOut.ref}.pdf`; const fileName = `${year}${invoiceOut.ref}.pdf`;
// Store invoice // Store invoice
await storage.write(stream, { await storage.write(stream, {
type: 'invoice', type: 'invoice',
path: `${year}/${month}/${day}`, path: `${year}/${month}/${day}`,
fileName: fileName fileName: fileName
}); });
await Self.rawSql('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id], {userId}); await Self.rawSql('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id], {userId});
if (isToBeMailed) { if (isToBeMailed) {
const invoiceAttachment = { const invoiceAttachment = {
filename: fileName, filename: fileName,
content: stream content: stream
}; };
if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') { if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
const exportation = new Report('exportation', args); const exportation = new Report('exportation', args);
const stream = await exportation.toPdfStream(); const stream = await exportation.toPdfStream();
const fileName = `CITES-${invoiceOut.ref}.pdf`; const fileName = `CITES-${invoiceOut.ref}.pdf`;
mailOptions.attachments.push({ mailOptions.attachments.push({
filename: fileName, filename: fileName,
content: stream content: stream
}); });
} }
mailOptions.attachments.push(invoiceAttachment); mailOptions.attachments.push(invoiceAttachment);
const email = new Email('invoice', args); const email = new Email('invoice', args);
await email.send(mailOptions); await email.send(mailOptions);
} }
} else if (isToBeMailed) { } else if (isToBeMailed) {
const args = { const args = {
id: ticket.id, id: ticket.id,
recipientId: ticket.clientFk, recipientId: ticket.clientFk,
recipient: ticket.recipient, recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail replyTo: ticket.salesPersonEmail
}; };
const email = new Email('delivery-note-link', args); const email = new Email('delivery-note-link', args);
await email.send(); await email.send();
} }
// Incoterms authorization // Incoterms authorization
const [{firstOrder}] = await Self.rawSql(` const [{firstOrder}] = await Self.rawSql(`
SELECT COUNT(*) as firstOrder SELECT COUNT(*) as firstOrder
FROM ticket t FROM ticket t
JOIN client c ON c.id = t.clientFk JOIN client c ON c.id = t.clientFk
WHERE t.clientFk = ? WHERE t.clientFk = ?
AND NOT t.isDeleted AND NOT t.isDeleted
AND c.isVies AND c.isVies
`, [ticket.clientFk]); `, [ticket.clientFk]);
if (firstOrder == 1) { if (firstOrder == 1) {
const args = { const args = {
id: ticket.clientFk, id: ticket.clientFk,
companyId: ticket.companyFk, companyId: ticket.companyFk,
recipientId: ticket.clientFk, recipientId: ticket.clientFk,
recipient: ticket.recipient, recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail replyTo: ticket.salesPersonEmail
}; };
const email = new Email('incoterms-authorization', args); const email = new Email('incoterms-authorization', args);
await email.send(); await email.send();
const [sample] = await Self.rawSql( const [sample] = await Self.rawSql(
`SELECT id `SELECT id
FROM sample FROM sample
WHERE code = 'incoterms-authorization' WHERE code = 'incoterms-authorization'
`); `);
await Self.rawSql(` await Self.rawSql(`
INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?) INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?)
`, [ticket.clientFk, sample.id, ticket.companyFk], {userId}); `, [ticket.clientFk, sample.id, ticket.companyFk], {userId});
}; }
} catch (error) { } catch (error) {
// Domain not found // Domain not found
if (error.responseCode == 450) if (error.responseCode == 450)
return invalidEmail(ticket); return invalidEmail(ticket);
// Save tickets on a list of failed ids // Save tickets on a list of failed ids
failedtickets.push({ failedtickets.push({
id: ticket.id, id: ticket.id,
stacktrace: error stacktrace: error
}); });
} }
} }
// Send email with failed tickets // Send email with failed tickets
if (failedtickets.length > 0) { if (failedtickets.length > 0) {
let body = 'This following tickets have failed:<br/><br/>'; let body = 'This following tickets have failed:<br/><br/>';
for (const ticket of failedtickets) { for (const ticket of failedtickets) {
body += `Ticket: <strong>${ticket.id}</strong> body += `Ticket: <strong>${ticket.id}</strong>
<br/> <strong>${ticket.stacktrace}</strong><br/><br/>`; <br/> <strong>${ticket.stacktrace}</strong><br/><br/>`;
} }
smtp.send({ smtp.send({
to: config.app.reportEmail, to: config.app.reportEmail,
subject: '[API] Nightly ticket closure report', subject: '[API] Nightly ticket closure report',
html: body html: body
}); });
} }
async function invalidEmail(ticket) { async function invalidEmail(ticket) {
await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [ await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
ticket.clientFk ticket.clientFk
], {userId}); ], {userId});
const oldInstance = `{"email": "${ticket.recipient}"}`; const oldInstance = `{"email": "${ticket.recipient}"}`;
const newInstance = `{"email": ""}`; const newInstance = `{"email": ""}`;
await Self.rawSql(` await Self.rawSql(`
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance) INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [ VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
ticket.clientFk, ticket.clientFk,
oldInstance, oldInstance,
newInstance newInstance
], {userId}); ], {userId});
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong> const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong> al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta
o no está disponible.<br/><br/> o no está disponible.<br/><br/>
Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente. Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente.
Actualiza la dirección de email con una correcta.`; Actualiza la dirección de email con una correcta.`;
smtp.send({ smtp.send({
to: ticket.salesPersonEmail, to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán', subject: 'No se ha podido enviar el albarán',
html: body html: body
}); });
} }
}; };

View File

@ -20,4 +20,5 @@ import './dms/create';
import './dms/edit'; import './dms/edit';
import './note/index'; import './note/index';
import './note/create'; import './note/create';
import './notifications';

View File

@ -0,0 +1,2 @@
<vn-card>
</vn-card>

View File

@ -0,0 +1,21 @@
import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
constructor($element, $) {
super($element, $);
}
async $onInit() {
const url = await this.vnApp.getUrl(`worker/${this.$params.id}/notifications`);
window.open(url).focus();
}
}
ngModule.vnComponent('vnWorkerNotifications', {
template: require('./index.html'),
controller: Controller,
bindings: {
ticket: '<'
}
});

View File

@ -15,6 +15,7 @@
{"state": "worker.card.timeControl", "icon": "access_time"}, {"state": "worker.card.timeControl", "icon": "access_time"},
{"state": "worker.card.calendar", "icon": "icon-calendar"}, {"state": "worker.card.calendar", "icon": "icon-calendar"},
{"state": "worker.card.pda", "icon": "phone_android"}, {"state": "worker.card.pda", "icon": "phone_android"},
{"state": "worker.card.notifications", "icon": "notifications"},
{"state": "worker.card.pbx", "icon": "icon-pbx"}, {"state": "worker.card.pbx", "icon": "icon-pbx"},
{"state": "worker.card.dms.index", "icon": "cloud_upload"}, {"state": "worker.card.dms.index", "icon": "cloud_upload"},
{ {
@ -112,6 +113,14 @@
"params": { "params": {
"worker": "$ctrl.worker" "worker": "$ctrl.worker"
} }
}, {
"url": "/notifications",
"state": "worker.card.notifications",
"component": "vn-worker-notifications",
"description": "Notifications",
"params": {
"worker": "$ctrl.worker"
}
}, { }, {
"url": "/time-control?timestamp", "url": "/time-control?timestamp",
"state": "worker.card.timeControl", "state": "worker.card.timeControl",