diff --git a/back/methods/notification/clean.js b/back/methods/notification/clean.js new file mode 100644 index 000000000..e6da58af8 --- /dev/null +++ b/back/methods/notification/clean.js @@ -0,0 +1,46 @@ +module.exports = Self => { + Self.remoteMethod('clean', { + description: 'clean notifications from queue', + accessType: 'WRITE', + returns: { + type: 'object', + root: true + }, + http: { + path: `/clean`, + verb: 'POST' + } + }); + + Self.clean = async options => { + const models = Self.app.models; + const status = ['sent', 'error']; + + const myOptions = {}; + let tx; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + const config = await models.NotificationConfig.findOne({}, myOptions); + const cleanDate = new Date(); + cleanDate.setDate(cleanDate.getDate() - config.cleanDays); + + await models.NotificationQueue.destroyAll({ + where: {status: {inq: status}}, + created: {lt: cleanDate} + }, myOptions); + + if (tx) await tx.commit(); + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/back/methods/notification/send.js b/back/methods/notification/send.js new file mode 100644 index 000000000..80faf0305 --- /dev/null +++ b/back/methods/notification/send.js @@ -0,0 +1,81 @@ +const {Email} = require('vn-print'); +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethod('send', { + description: 'Send notifications from queue', + accessType: 'WRITE', + returns: { + type: 'object', + root: true + }, + http: { + path: `/send`, + verb: 'POST' + } + }); + + Self.send = async options => { + const models = Self.app.models; + const findStatus = 'pending'; + + const myOptions = {}; + if (typeof options == 'object') + Object.assign(myOptions, options); + + const notificationQueue = await models.NotificationQueue.find({ + where: {status: findStatus}, + include: [ + { + relation: 'notification', + scope: { + include: { + relation: 'subscription', + scope: { + include: { + relation: 'user', + scope: { + fields: ['name', 'email', 'lang'] + } + } + } + } + } + + } + ] + }, myOptions); + + const statusSent = 'sent'; + const statusError = 'error'; + + for (const queue of notificationQueue) { + const queueName = queue.notification().name; + const queueParams = JSON.parse(queue.params); + + for (const notificationUser of queue.notification().subscription()) { + try { + const sendParams = { + recipient: notificationUser.user().email, + lang: notificationUser.user().lang + }; + + if (notificationUser.userFk == queue.authorFk) { + await queue.updateAttribute('status', statusSent); + continue; + } + + const newParams = Object.assign({}, queueParams, sendParams); + const email = new Email(queueName, newParams); + + if (process.env.NODE_ENV != 'test') + await email.send(); + + await queue.updateAttribute('status', statusSent); + } catch (error) { + await queue.updateAttribute('status', statusError); + } + } + } + }; +}; diff --git a/back/methods/notification/specs/clean.spec.js b/back/methods/notification/specs/clean.spec.js new file mode 100644 index 000000000..4c9dc563d --- /dev/null +++ b/back/methods/notification/specs/clean.spec.js @@ -0,0 +1,42 @@ +const models = require('vn-loopback/server/server').models; + +describe('Notification Clean()', () => { + it('should delete old rows with error', async() => { + const userId = 9; + const status = 'error'; + const tx = await models.NotificationQueue.beginTransaction({}); + const options = {transaction: tx}; + + const notification = await models.Notification.findOne({}, options); + const notificationConfig = await models.NotificationConfig.findOne({}); + + const cleanDate = new Date(); + cleanDate.setDate(cleanDate.getDate() - (notificationConfig.cleanDays + 1)); + + let before; + let after; + + try { + const notificationDelete = await models.NotificationQueue.create({ + notificationFk: notification.name, + authorFk: userId, + status: status, + created: cleanDate + }, options); + + before = await models.NotificationQueue.findById(notificationDelete.id, null, options); + + await models.Notification.clean(options); + + after = await models.NotificationQueue.findById(notificationDelete.id, null, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + + expect(before.notificationFk).toEqual(notification.name); + expect(after).toBe(null); + }); +}); diff --git a/back/methods/notification/specs/send.spec.js b/back/methods/notification/specs/send.spec.js new file mode 100644 index 000000000..f0b186e06 --- /dev/null +++ b/back/methods/notification/specs/send.spec.js @@ -0,0 +1,33 @@ +const models = require('vn-loopback/server/server').models; + +describe('Notification Send()', () => { + it('should send notification', async() => { + const statusPending = 'pending'; + const tx = await models.NotificationQueue.beginTransaction({}); + const options = {transaction: tx}; + const filter = { + where: { + status: statusPending + } + }; + + let before; + let after; + + try { + before = await models.NotificationQueue.find(filter, options); + + await models.Notification.send(options); + + after = await models.NotificationQueue.find(filter, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + + expect(before.length).toEqual(3); + expect(after.length).toEqual(0); + }); +}); diff --git a/back/model-config.json b/back/model-config.json index 830a78fd4..29676e979 100644 --- a/back/model-config.json +++ b/back/model-config.json @@ -77,6 +77,21 @@ "Module": { "dataSource": "vn" }, + "Notification": { + "dataSource": "vn" + }, + "NotificationAcl": { + "dataSource": "vn" + }, + "NotificationConfig": { + "dataSource": "vn" + }, + "NotificationQueue": { + "dataSource": "vn" + }, + "NotificationSubscription": { + "dataSource": "vn" + }, "Province": { "dataSource": "vn" }, @@ -101,6 +116,9 @@ "Town": { "dataSource": "vn" }, + "Url": { + "dataSource": "vn" + }, "UserConfig": { "dataSource": "vn" }, diff --git a/back/models/notification.js b/back/models/notification.js new file mode 100644 index 000000000..65e82e3c7 --- /dev/null +++ b/back/models/notification.js @@ -0,0 +1,4 @@ +module.exports = Self => { + require('../methods/notification/send')(Self); + require('../methods/notification/clean')(Self); +}; diff --git a/back/models/notification.json b/back/models/notification.json new file mode 100644 index 000000000..56f66bf1d --- /dev/null +++ b/back/models/notification.json @@ -0,0 +1,30 @@ +{ + "name": "Notification", + "base": "VnModel", + "options": { + "mysql": { + "table": "util.notification" + } + }, + "properties": { + "id": { + "type": "number", + "id": true, + "description": "Identifier" + }, + "name": { + "type": "string", + "required": true + }, + "description": { + "type": "string" + } + }, + "relations": { + "subscription": { + "type": "hasMany", + "model": "NotificationSubscription", + "foreignKey": "notificationFk" + } + } +} \ No newline at end of file diff --git a/back/models/notificationAcl.json b/back/models/notificationAcl.json new file mode 100644 index 000000000..e3e97f52d --- /dev/null +++ b/back/models/notificationAcl.json @@ -0,0 +1,21 @@ +{ + "name": "NotificationAcl", + "base": "VnModel", + "options": { + "mysql": { + "table": "util.notificationAcl" + } + }, + "relations": { + "notification": { + "type": "belongsTo", + "model": "Notification", + "foreignKey": "notificationFk" + }, + "role": { + "type": "belongsTo", + "model": "Role", + "foreignKey": "roleFk" + } + } +} \ No newline at end of file diff --git a/back/models/notificationConfig.json b/back/models/notificationConfig.json new file mode 100644 index 000000000..b00ed3675 --- /dev/null +++ b/back/models/notificationConfig.json @@ -0,0 +1,19 @@ +{ + "name": "NotificationConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "util.notificationConfig" + } + }, + "properties": { + "id": { + "type": "number", + "id": true, + "description": "Identifier" + }, + "cleanDays": { + "type": "number" + } + } +} \ No newline at end of file diff --git a/back/models/notificationQueue.json b/back/models/notificationQueue.json new file mode 100644 index 000000000..9790ea595 --- /dev/null +++ b/back/models/notificationQueue.json @@ -0,0 +1,38 @@ +{ + "name": "NotificationQueue", + "base": "VnModel", + "options": { + "mysql": { + "table": "util.notificationQueue" + } + }, + "properties": { + "id": { + "type": "number", + "id": true, + "description": "Identifier" + }, + "params": { + "type": "string" + }, + "status": { + "type": "string" + }, + "created": { + "type": "date" + } + }, + "relations": { + "notification": { + "type": "belongsTo", + "model": "Notification", + "foreignKey": "notificationFk", + "primaryKey": "name" + }, + "author": { + "type": "belongsTo", + "model": "Account", + "foreignKey": "authorFk" + } + } +} \ No newline at end of file diff --git a/back/models/notificationSubscription.json b/back/models/notificationSubscription.json new file mode 100644 index 000000000..43fa6db27 --- /dev/null +++ b/back/models/notificationSubscription.json @@ -0,0 +1,33 @@ +{ + "name": "NotificationSubscription", + "base": "VnModel", + "options": { + "mysql": { + "table": "util.notificationSubscription" + } + }, + "properties": { + "notificationFk": { + "type": "number", + "id": true, + "description": "Identifier" + }, + "userFk": { + "type": "number", + "id": true, + "description": "Identifier" + } + }, + "relations": { + "notification": { + "type": "belongsTo", + "model": "Notification", + "foreignKey": "notificationFk" + }, + "user": { + "type": "belongsTo", + "model": "Account", + "foreignKey": "userFk" + } + } +} \ No newline at end of file diff --git a/back/models/url.json b/back/models/url.json new file mode 100644 index 000000000..8610ff28b --- /dev/null +++ b/back/models/url.json @@ -0,0 +1,25 @@ +{ + "name": "Url", + "base": "VnModel", + "options": { + "mysql": { + "table": "salix.url" + } + }, + "properties": { + "appName": { + "type": "string", + "required": true, + "id": 1 + }, + "environment": { + "type": "string", + "required": true, + "id": 2 + }, + "url": { + "type": "string", + "required": true + } + } +} diff --git a/db/changes/10490-august/00-packingSiteConfig.sql b/db/changes/10490-august/00-packingSiteConfig.sql new file mode 100644 index 000000000..945b5a54c --- /dev/null +++ b/db/changes/10490-august/00-packingSiteConfig.sql @@ -0,0 +1,12 @@ +CREATE TABLE `vn`.`packingSiteConfig` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `shinobiUrl` varchar(255) NOT NULL, + `shinobiToken` varchar(255) NOT NULL, + `shinobiGroupKey` varchar(255) NOT NULL, + `avgBoxingTime` INT(3) NULL, + PRIMARY KEY (`id`) + ); + +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('Boxing', '*', '*', 'ALLOW', 'ROLE', 'employee'); diff --git a/db/changes/10490-august/00-packingSiteUpdate.sql b/db/changes/10490-august/00-packingSiteUpdate.sql new file mode 100644 index 000000000..14313fd52 --- /dev/null +++ b/db/changes/10490-august/00-packingSiteUpdate.sql @@ -0,0 +1,56 @@ +ALTER TABLE `vn`.`packingSite` ADD monitorId varchar(255) NULL; + +UPDATE `vn`.`packingSite` + SET monitorId = 'VbiUcajdaT' + WHERE code = 'h1'; +UPDATE `vn`.`packingSite` + SET monitorId = 'qKMPn9aaVe' + WHERE code = 'h2'; +UPDATE `vn`.`packingSite` + SET monitorId = '3CtdIAGPAv' + WHERE code = 'h3'; +UPDATE `vn`.`packingSite` + SET monitorId = 'Xme2hiqz1f' + WHERE code = 'h4'; +UPDATE `vn`.`packingSite` + SET monitorId = 'aulxefgfJU' + WHERE code = 'h5'; +UPDATE `vn`.`packingSite` + SET monitorId = '6Ou0D1bhBw' + WHERE code = 'h6'; +UPDATE `vn`.`packingSite` + SET monitorId = 'eVUvnE6pNw' + WHERE code = 'h7'; +UPDATE `vn`.`packingSite` + SET monitorId = '0wsmSvqmrs' + WHERE code = 'h8'; +UPDATE `vn`.`packingSite` + SET monitorId = 'r2l2RyyF4I' + WHERE code = 'h9'; +UPDATE `vn`.`packingSite` + SET monitorId = 'EdjHLIiDVD' + WHERE code = 'h10'; +UPDATE `vn`.`packingSite` + SET monitorId = 'czC45kmwqI' + WHERE code = 'h11'; +UPDATE `vn`.`packingSite` + SET monitorId = 'PNsmxPaCwQ' + WHERE code = 'h12'; +UPDATE `vn`.`packingSite` + SET monitorId = 'agVssO0FDC' + WHERE code = 'h13'; +UPDATE `vn`.`packingSite` + SET monitorId = 'f2SPNENHPo' + WHERE code = 'h14'; +UPDATE `vn`.`packingSite` + SET monitorId = '6UR7gUZxks' + WHERE code = 'h15'; +UPDATE `vn`.`packingSite` + SET monitorId = 'bOB0f8WZ2V' + WHERE code = 'h16'; +UPDATE `vn`.`packingSite` + SET monitorId = 'MIR1nXaL0n' + WHERE code = 'h17'; +UPDATE `vn`.`packingSite` + SET monitorId = '0Oj9SgGTXR' + WHERE code = 'h18'; diff --git a/db/changes/10490-august/00-salix_url.sql b/db/changes/10490-august/00-salix_url.sql new file mode 100644 index 000000000..ea5c3b606 --- /dev/null +++ b/db/changes/10490-august/00-salix_url.sql @@ -0,0 +1,33 @@ +CREATE TABLE `salix`.`url` ( + `appName` varchar(100) NOT NULL, + `environment` varchar(100) NOT NULL, + `url` varchar(255) NOT NULL, + PRIMARY KEY (`appName`,`environment`) +); + +INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) + VALUES + ('salix', 'production', 'https://salix.verdnatura.es/#!/'); +INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) + VALUES + ('salix', 'test', 'https://test-salix.verdnatura.es/#!/'); +INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) + VALUES + ('salix', 'dev', 'http://localhost:5000/#!/'); +INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) + VALUES + ('lilium', 'production', 'https://lilium.verdnatura.es/#/'); +INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) + VALUES + ('lilium', 'test', 'https://test-lilium.verdnatura.es/#/'); +INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) + VALUES + ('lilium', 'dev', 'http://localhost:8080/#/'); + +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('Url', '*', 'READ', 'ALLOW', 'ROLE', 'employee'); + +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('Url', '*', 'WRITE', 'ALLOW', 'ROLE', 'it'); diff --git a/db/changes/10490-goldenSummer/00-aclNotification.sql b/db/changes/10490-goldenSummer/00-aclNotification.sql new file mode 100644 index 000000000..51d6b2471 --- /dev/null +++ b/db/changes/10490-goldenSummer/00-aclNotification.sql @@ -0,0 +1,3 @@ +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('Notification', '*', 'WRITE', 'ALLOW', 'ROLE', 'developer'); diff --git a/db/changes/10491-august/00-notificationProc.sql b/db/changes/10491-august/00-notificationProc.sql new file mode 100644 index 000000000..475b2e389 --- /dev/null +++ b/db/changes/10491-august/00-notificationProc.sql @@ -0,0 +1,28 @@ +DROP FUNCTION IF EXISTS `util`.`notification_send`; +DELIMITER $$ +CREATE DEFINER=`root`@`localhost` FUNCTION `util`.`notification_send`(vNotificationName VARCHAR(255), vParams TEXT, vAuthorFk INT) + RETURNS INT + MODIFIES SQL DATA +BEGIN +/** + * Sends a notification. + * + * @param vNotificationName The notification name + * @param vParams The notification parameters formatted as JSON + * @param vAuthorFk The notification author or %NULL if there is no author + * @return The notification id + */ + DECLARE vNotificationFk INT; + + SELECT id INTO vNotificationFk + FROM `notification` + WHERE `name` = vNotificationName; + + INSERT INTO notificationQueue + SET notificationFk = vNotificationFk, + params = vParams, + authorFk = vAuthorFk; + + RETURN LAST_INSERT_ID(); +END$$ +DELIMITER ; diff --git a/db/changes/10491-august/00-notificationTables.sql b/db/changes/10491-august/00-notificationTables.sql new file mode 100644 index 000000000..2db7d9874 --- /dev/null +++ b/db/changes/10491-august/00-notificationTables.sql @@ -0,0 +1,63 @@ +USE util; + +CREATE TABLE notification( + id INT PRIMARY KEY, + `name` VARCHAR(255) UNIQUE, + `description` VARCHAR(255) +); + +CREATE TABLE notificationAcl( + notificationFk INT, + roleFk INT(10) unsigned, + PRIMARY KEY(notificationFk, roleFk) +); + +ALTER TABLE `util`.`notificationAcl` ADD CONSTRAINT `notificationAcl_ibfk_1` FOREIGN KEY (`notificationFk`) REFERENCES `util`.`notification` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE; + +ALTER TABLE `util`.`notificationAcl` ADD CONSTRAINT `notificationAcl_ibfk_2` FOREIGN KEY (`roleFk`) REFERENCES `account`.`role`(`id`) + ON DELETE RESTRICT + ON UPDATE CASCADE; + +CREATE TABLE notificationSubscription( + notificationFk INT, + userFk INT(10) unsigned, + PRIMARY KEY(notificationFk, userFk) +); + +ALTER TABLE `util`.`notificationSubscription` ADD CONSTRAINT `notificationSubscription_ibfk_1` FOREIGN KEY (`notificationFk`) REFERENCES `util`.`notification` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE; + +ALTER TABLE `util`.`notificationSubscription` ADD CONSTRAINT `notificationSubscription_ibfk_2` FOREIGN KEY (`userFk`) REFERENCES `account`.`user`(`id`) + ON DELETE CASCADE + ON UPDATE CASCADE; + +CREATE TABLE notificationQueue( + id INT PRIMARY KEY AUTO_INCREMENT, + notificationFk VARCHAR(255), + params JSON, + authorFk INT(10) unsigned NULL, + `status` ENUM('pending', 'sent', 'error') NOT NULL DEFAULT 'pending', + created DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX(notificationFk), + INDEX(authorFk), + INDEX(status) +); + +ALTER TABLE `util`.`notificationQueue` ADD CONSTRAINT `nnotificationQueue_ibfk_1` FOREIGN KEY (`notificationFk`) REFERENCES `util`.`notification` (`name`) + ON DELETE CASCADE + ON UPDATE CASCADE; + +ALTER TABLE `util`.`notificationQueue` ADD CONSTRAINT `notificationQueue_ibfk_2` FOREIGN KEY (`authorFk`) REFERENCES `account`.`user`(`id`) + ON DELETE CASCADE + ON UPDATE CASCADE; + +CREATE TABLE notificationConfig( + id INT PRIMARY KEY AUTO_INCREMENT, + cleanDays MEDIUMINT +); + +INSERT INTO notificationConfig + SET cleanDays = 90; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 34e592b9a..8c10926a8 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -918,21 +918,21 @@ INSERT INTO `vn`.`expeditionStateType`(`id`, `description`, `code`) (3, 'Perdida', 'LOST'); -INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `isBox`, `created`, `itemFk`, `counter`, `workerFk`, `externalId`, `packagingFk`, `stateTypeFk`) +INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `isBox`, `created`, `itemFk`, `counter`, `workerFk`, `externalId`, `packagingFk`, `stateTypeFk`, `hostFk`) VALUES - (1, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 15, 1, 18, 'UR9000006041', 94, 1), - (2, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 16, 2, 18, 'UR9000006041', 94, 1), - (3, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), NULL, 3, 18, 'UR9000006041', 94, 2), - (4, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), NULL, 4, 18, 'UR9000006041', 94, 2), - (5, 1, 2, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), NULL, 1, 18, NULL, 94, 3), - (6, 7, 3, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), NULL, 1, 18, NULL, 94, 3), - (7, 2, 4, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), NULL, 1, 18, NULL, 94, NULL), - (8, 3, 5, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), NULL, 1, 18, NULL, 94, 1), - (9, 3, 6, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), NULL, 1, 18, NULL, 94, 2), - (10, 7, 7, 71, NOW(), NULL, 1, 18, NULL, 94, 3), - (11, 7, 8, 71, NOW(), NULL, 1, 18, NULL, 94, 3), - (12, 7, 9, 71, NOW(), NULL, 1, 18, NULL, 94, 3), - (13, 1, 10, 71, NOW(), NULL, 1, 18, NULL, 94, 3); + (1, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 15, 1, 18, 'UR9000006041', 94, 1, 'pc1'), + (2, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 16, 2, 18, 'UR9000006041', 94, 1, NULL), + (3, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), NULL, 3, 18, 'UR9000006041', 94, 2, NULL), + (4, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), NULL, 4, 18, 'UR9000006041', 94, 2, NULL), + (5, 1, 2, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), NULL, 1, 18, NULL, 94, 3, NULL), + (6, 7, 3, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), NULL, 1, 18, NULL, 94, 3, NULL), + (7, 2, 4, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), NULL, 1, 18, NULL, 94, NULL,NULL), + (8, 3, 5, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), NULL, 1, 18, NULL, 94, 1, NULL), + (9, 3, 6, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), NULL, 1, 18, NULL, 94, 2, NULL), + (10, 7, 7, 71, NOW(), NULL, 1, 18, NULL, 94, 3, NULL), + (11, 7, 8, 71, NOW(), NULL, 1, 18, NULL, 94, 3, NULL), + (12, 7, 9, 71, NOW(), NULL, 1, 18, NULL, 94, 3, NULL), + (13, 1, 10,71, NOW(), NULL, 1, 18, NULL, 94, 3, NULL); INSERT INTO `vn`.`expeditionState`(`id`, `created`, `expeditionFk`, `typeFk`, `userFk`) @@ -2658,6 +2658,39 @@ INSERT INTO `vn`.`workerTimeControlConfig` (`id`, `dayBreak`, `dayBreakDriver`, VALUES (1, 43200, 32400, 129600, 259200, 604800, '', '', 'Leidos.exito', 'Leidos.error', 'timeControl', 5.33, 0.33, 40, '22:00:00', '06:00:00', 57600, 1200, 18000, 57600, 6, 13); +INSERT INTO `vn`.`host` (`id`, `code`, `description`, `warehouseFk`, `bankFk`) + VALUES + (1, 'pc1', 'pc host', 1, 1); + +INSERT INTO `vn`.`packingSite` (`id`, `code`, `hostFk`, `monitorId`) + VALUES + (1, 'h1', 1, ''); + +INSERT INTO `vn`.`packingSiteConfig` (`shinobiUrl`, `shinobiToken`, `shinobiGroupKey`, `avgBoxingTime`) + VALUES + ('', 'SHINNOBI_TOKEN', 'GROUP_TOKEN', 6000); +INSERT INTO `util`.`notificationConfig` + SET `cleanDays` = 90; + +INSERT INTO `util`.`notification` (`id`, `name`, `description`) + VALUES + (1, 'print-email', 'notification fixture one'); + +INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`) + VALUES + (1, 9); + +INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`) + VALUES + (1, 'print-email', '{"id": "1"}', 9, 'pending', util.VN_CURDATE()), + (2, 'print-email', '{"id": "2"}', null, 'pending', util.VN_CURDATE()), + (3, 'print-email', null, null, 'pending', util.VN_CURDATE()); + +INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`) + VALUES + (1, 1109), + (1, 1110); + INSERT INTO `vn`.`routeConfig` (`id`, `defaultWorkCenterFk`) VALUES (1, 9); diff --git a/front/core/services/app.js b/front/core/services/app.js index 889b24d01..fb0a08777 100644 --- a/front/core/services/app.js +++ b/front/core/services/app.js @@ -54,6 +54,21 @@ export default class App { localStorage.setItem('salix-version', newVersion); } } + + getUrl(route, appName = 'lilium') { + const env = process.env.NODE_ENV; + const filter = { + where: {and: [ + {appName: appName}, + {environment: env} + ]} + }; + + return this.logger.$http.get('Urls/findOne', {filter}) + .then(res => { + return res.data.url + route; + }); + } } ngModule.service('vnApp', App); diff --git a/modules/ticket/back/methods/boxing/getVideo.js b/modules/ticket/back/methods/boxing/getVideo.js new file mode 100644 index 000000000..6f471e837 --- /dev/null +++ b/modules/ticket/back/methods/boxing/getVideo.js @@ -0,0 +1,88 @@ +const https = require('https'); + +module.exports = Self => { + Self.remoteMethod('getVideo', { + description: 'Get packing video', + accessType: 'READ', + accepts: [ + { + arg: 'id', + type: 'number', + required: true, + description: 'Ticket id' + }, + { + arg: 'filename', + type: 'string', + required: true, + description: 'Time to add' + }, + { + arg: 'req', + type: 'object', + http: {source: 'req'} + }, + { + arg: 'res', + type: 'object', + http: {source: 'res'} + } + ], + http: { + path: `/getVideo`, + verb: 'GET', + }, + }); + + Self.getVideo = async(id, filename, req, res, options) => { + const models = Self.app.models; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const packingSiteConfig = await models.PackingSiteConfig.findOne({}, myOptions); + + const query = ` + SELECT + e.id, + ps.monitorId, + e.created + FROM expedition e + JOIN host h ON Convert(h.code USING utf8mb3) COLLATE utf8mb3_unicode_ci = e.hostFk + JOIN packingSite ps ON ps.hostFk = h.id + WHERE e.id = ?;`; + const [expedition] = await models.Expedition.rawSql(query, [id]); + const monitorId = expedition.monitorId; + + const videoUrl = + `/${packingSiteConfig.shinobiToken}/videos/${packingSiteConfig.shinobiGroupKey}/${monitorId}/${filename}`; + + const headers = Object.assign({}, req.headers, { + host: 'shinobi.verdnatura.es' + }); + const httpOptions = { + host: 'shinobi.verdnatura.es', + path: videoUrl, + port: 443, + headers + }; + + return new Promise((resolve, reject) => { + const req = https.request(httpOptions, shinobiRes => { + shinobiRes.pause(); + res.writeHeader(shinobiRes.statusCode, shinobiRes.headers); + shinobiRes.pipe(res); + }); + + req.on('error', () => { + reject(); + }); + + req.on('end', () => { + resolve(); + }); + req.end(); + }); + }; +}; diff --git a/modules/ticket/back/methods/boxing/getVideoList.js b/modules/ticket/back/methods/boxing/getVideoList.js new file mode 100644 index 000000000..3d45a720d --- /dev/null +++ b/modules/ticket/back/methods/boxing/getVideoList.js @@ -0,0 +1,78 @@ +const axios = require('axios'); +const models = require('vn-loopback/server/server').models; + +module.exports = Self => { + Self.remoteMethod('getVideoList', { + description: 'Get video list of expedition id', + accessType: 'READ', + accepts: [ + { + arg: 'id', + type: 'number', + required: true, + description: 'Expedition id' + }, { + arg: 'from', + type: 'number', + required: false, + }, { + arg: 'to', + type: 'number', + required: false, + } + ], returns: { + type: ['object'], + root: true + }, + http: { + path: `/getVideoList`, + verb: 'GET', + }, + }); + + Self.getVideoList = async(id, from, to, options) => { + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const packingSiteConfig = await models.PackingSiteConfig.findOne({}, myOptions); + + const query = ` + SELECT + e.id, + ps.monitorId, + e.created + FROM expedition e + JOIN host h ON Convert(h.code USING utf8mb3) COLLATE utf8mb3_unicode_ci = e.hostFk + JOIN packingSite ps ON ps.hostFk = h.id + WHERE e.id = ?;`; + const [expedition] = await models.PackingSiteConfig.rawSql(query, [id]); + + if (!from && !expedition) return []; + let start = new Date(expedition.created); + let end = new Date(start.getTime() + (packingSiteConfig.avgBoxingTime * 1000)); + + if (from && to) { + start.setHours(from, 0, 0); + end.setHours(to, 0, 0); + } + const offset = start.getTimezoneOffset(); + start = new Date(start.getTime() - (offset * 60 * 1000)); + end = new Date(end.getTime() - (offset * 60 * 1000)); + + const videoUrl = + `/${packingSiteConfig.shinobiToken}/videos/${packingSiteConfig.shinobiGroupKey}/${expedition.monitorId}`; + const timeUrl = `?start=${start.toISOString().split('.')[0]}&end=${end.toISOString().split('.')[0]}`; + const url = `${packingSiteConfig.shinobiUrl}${videoUrl}${timeUrl}`; + + let response; + + try { + response = await axios.get(url); + } catch (e) { + return []; + } + return response.data.videos.map(video => video.filename); + }; +}; diff --git a/modules/ticket/back/methods/boxing/specs/getVideo.spec.js b/modules/ticket/back/methods/boxing/specs/getVideo.spec.js new file mode 100644 index 000000000..8e8cdc5b9 --- /dev/null +++ b/modules/ticket/back/methods/boxing/specs/getVideo.spec.js @@ -0,0 +1,40 @@ +const models = require('vn-loopback/server/server').models; +const https = require('https'); + +xdescribe('boxing getVideo()', () => { + it('should return data', async() => { + const tx = await models.PackingSiteConfig.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const id = 1; + const video = 'video.mp4'; + + const response = { + pipe: () => {}, + on: () => {}, + end: () => {}, + }; + + const req = { + headers: 'apiHeader', + data: { + pipe: () => {}, + on: () => {}, + } + }; + + spyOn(https, 'request').and.returnValue(response); + + const result = await models.Boxing.getVideo(id, video, req, null, options); + + expect(result[0]).toEqual(response.data.videos[0].filename); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/ticket/back/methods/boxing/specs/getVideoList.spec.js b/modules/ticket/back/methods/boxing/specs/getVideoList.spec.js new file mode 100644 index 000000000..c6d1a3e07 --- /dev/null +++ b/modules/ticket/back/methods/boxing/specs/getVideoList.spec.js @@ -0,0 +1,36 @@ +const models = require('vn-loopback/server/server').models; +const axios = require('axios'); + +describe('boxing getVideoList()', () => { + it('should return video list', async() => { + const tx = await models.PackingSiteConfig.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const id = 1; + const from = 1; + const to = 2; + + const response = { + data: { + videos: [{ + id: 1, + filename: 'video1.mp4' + }] + } + }; + + spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(response))); + + const result = await models.Boxing.getVideoList(id, from, to, options); + + expect(result[0]).toEqual(response.data.videos[0].filename); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/ticket/back/model-config.json b/modules/ticket/back/model-config.json index 8a6ac0c00..21e800b36 100644 --- a/modules/ticket/back/model-config.json +++ b/modules/ticket/back/model-config.json @@ -5,6 +5,9 @@ "AnnualAverageInvoiced": { "dataSource": "vn" }, + "Boxing": { + "dataSource": "vn" + }, "Component": { "dataSource": "vn" }, @@ -20,6 +23,9 @@ "Packaging": { "dataSource": "vn" }, + "PackingSiteConfig": { + "dataSource": "vn" + }, "PrintServerQueue": { "dataSource": "vn" }, diff --git a/modules/ticket/back/models/boxing.js b/modules/ticket/back/models/boxing.js new file mode 100644 index 000000000..c251ea0e4 --- /dev/null +++ b/modules/ticket/back/models/boxing.js @@ -0,0 +1,4 @@ +module.exports = Self => { + require('../methods/boxing/getVideo')(Self); + require('../methods/boxing/getVideoList')(Self); +}; diff --git a/modules/ticket/back/models/boxing.json b/modules/ticket/back/models/boxing.json new file mode 100644 index 000000000..5aecbcabe --- /dev/null +++ b/modules/ticket/back/models/boxing.json @@ -0,0 +1,12 @@ +{ + "name": "Boxing", + "base": "PersistedModel", + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + } + ] +} diff --git a/modules/ticket/back/models/packingSiteConfig.json b/modules/ticket/back/models/packingSiteConfig.json new file mode 100644 index 000000000..8f0032d41 --- /dev/null +++ b/modules/ticket/back/models/packingSiteConfig.json @@ -0,0 +1,28 @@ +{ + "name": "PackingSiteConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "packingSiteConfig" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "shinobiUrl": { + "type": "string" + }, + "shinobiGroupKey":{ + "type":"string" + }, + "shinobiToken":{ + "type":"string" + }, + "avgBoxingTime":{ + "type":"number" + } + } +} diff --git a/modules/ticket/front/boxing/index.html b/modules/ticket/front/boxing/index.html new file mode 100644 index 000000000..7fb3b870e --- /dev/null +++ b/modules/ticket/front/boxing/index.html @@ -0,0 +1,2 @@ + + diff --git a/modules/ticket/front/boxing/index.js b/modules/ticket/front/boxing/index.js new file mode 100644 index 000000000..4e6b398f2 --- /dev/null +++ b/modules/ticket/front/boxing/index.js @@ -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(`ticket/${this.$params.id}/boxing`); + window.open(url).focus(); + } +} + +ngModule.vnComponent('vnTicketBoxing', { + template: require('./index.html'), + controller: Controller, + bindings: { + ticket: '<' + } +}); diff --git a/modules/ticket/front/index.js b/modules/ticket/front/index.js index 85a03ffb6..0558d251d 100644 --- a/modules/ticket/front/index.js +++ b/modules/ticket/front/index.js @@ -33,3 +33,4 @@ import './dms/index'; import './dms/create'; import './dms/edit'; import './sms'; +import './boxing'; diff --git a/modules/ticket/front/locale/es.yml b/modules/ticket/front/locale/es.yml index 752dd7b36..748ba210f 100644 --- a/modules/ticket/front/locale/es.yml +++ b/modules/ticket/front/locale/es.yml @@ -4,6 +4,7 @@ Agency: Agencia Amount: Importe Base to commission: Base comisionable Boxes: Cajas +Boxing: Encajado by: por Checked: Comprobado Client: Cliente @@ -45,7 +46,7 @@ Price gap: Diferencia de precio Quantity: Cantidad Remove lines: Eliminar lineas Route: Ruta -SET OK: PONER OK +SET OK: PONER OK Shipment: Salida Shipped: F. envío Some fields are invalid: Algunos campos no son válidos @@ -81,4 +82,4 @@ Sale tracking: Líneas preparadas Pictures: Fotos Log: Historial Packager: Encajador -Palletizer: Palletizador \ No newline at end of file +Palletizer: Palletizador diff --git a/modules/ticket/front/routes.json b/modules/ticket/front/routes.json index ba7cfa887..4be8e2183 100644 --- a/modules/ticket/front/routes.json +++ b/modules/ticket/front/routes.json @@ -24,7 +24,8 @@ {"state": "ticket.card.saleChecked", "icon": "assignment"}, {"state": "ticket.card.components", "icon": "icon-components"}, {"state": "ticket.card.saleTracking", "icon": "assignment"}, - {"state": "ticket.card.dms.index", "icon": "cloud_download"} + {"state": "ticket.card.dms.index", "icon": "cloud_download"}, + {"state": "ticket.card.boxing", "icon": "science"} ] }, "keybindings": [ @@ -66,7 +67,7 @@ "abstract": true, "params": { "ticket": "$ctrl.ticket" - } + } }, { "url" : "/step-one", @@ -273,6 +274,15 @@ "params": { "ticket": "$ctrl.ticket" } + }, + { + "url": "/boxing", + "state": "ticket.card.boxing", + "component": "vn-ticket-boxing", + "description": "Boxing", + "params": { + "ticket": "$ctrl.ticket" + } } ] -} \ No newline at end of file +}