Merge pull request '#6274 workerTimeControl' (!1858) from 6274-loginWorkerTimeControl into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #1858
Reviewed-by: Alex Moreno <alexm@verdnatura.es>
Reviewed-by: Javi Gallego <jgallego@verdnatura.es>
Reviewed-by: Juan Ferrer <juan@verdnatura.es>
This commit is contained in:
Jorge Penadés 2024-01-03 11:31:48 +00:00
commit 6986ec1f8a
15 changed files with 804 additions and 627 deletions

View File

@ -1,14 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
const {models} = require('vn-loopback/server/server'); const {models} = require('vn-loopback/server/server');
const handlePromiseLogout = (Self, {id}, courtesyTime) => {
new Promise(res => {
setTimeout(() => {
res(Self.logout(id));
}
, courtesyTime * 1000);
});
};
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('renewToken', { Self.remoteMethodCtx('renewToken', {
description: 'Checks if the token has more than renewPeriod seconds to live and if so, renews it', description: 'Checks if the token has more than renewPeriod seconds to live and if so, renews it',
@ -28,14 +19,26 @@ module.exports = Self => {
const {accessToken: token} = ctx.req; const {accessToken: token} = ctx.req;
// Check if current token is valid // Check if current token is valid
const isValid = await validateToken(token);
if (isValid) const {renewPeriod, courtesyTime} = await models.AccessTokenConfig.findOne({
fields: ['renewPeriod', 'courtesyTime']
});
const now = Date.now();
const differenceMilliseconds = now - token.created;
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
const isNotExceeded = differenceSeconds < renewPeriod - courtesyTime;
if (isNotExceeded)
return token; return token;
const {courtesyTime} = await models.AccessTokenConfig.findOne({fields: ['courtesyTime']});
// Schedule to remove current token // Schedule to remove current token
handlePromiseLogout(Self, token, courtesyTime); setTimeout(async() => {
try {
await Self.logout(token.id);
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
}, courtesyTime * 1000);
// Create new accessToken // Create new accessToken
const user = await Self.findById(token.userId); const user = await Self.findById(token.userId);
@ -43,14 +46,4 @@ module.exports = Self => {
return {id: accessToken.id, ttl: accessToken.ttl}; return {id: accessToken.id, ttl: accessToken.ttl};
}; };
async function validateToken(token) {
const accessTokenConfig = await models.AccessTokenConfig.findOne({fields: ['renewPeriod', 'courtesyTime']});
const now = Date.now();
const differenceMilliseconds = now - token.created;
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
const isValid = differenceSeconds < accessTokenConfig.renewPeriod - accessTokenConfig.courtesyTime;
return isValid;
}
}; };

View File

@ -95,27 +95,30 @@
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",
"permission": "ALLOW" "permission": "ALLOW"
}, }, {
{ "property": "recoverPassword",
"property": "recoverPassword",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"property": "validateAuth",
"accessType": "EXECUTE", "accessType": "EXECUTE",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",
"permission": "ALLOW" "permission": "ALLOW"
}, }, {
{ "property": "validateAuth",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}, {
"property": "privileges", "property": "privileges",
"accessType": "*", "accessType": "*",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$authenticated", "principalId": "$authenticated",
"permission": "ALLOW" "permission": "ALLOW"
}, {
"property": "renewToken",
"accessType": "WRITE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
} }
], ],
"scopes": { "scopes": {

View File

@ -0,0 +1,17 @@
DELETE FROM `salix`.`ACL`
WHERE model = 'VnUser'
AND property = 'renewToken';
INSERT INTO `account`.`role` (name, description)
VALUES ('timeControl','Tablet para fichar');
INSERT INTO `account`.`roleInherit` (role, inheritsFrom)
VALUES (127, 11);
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('WorkerTimeControl', 'login', 'READ', 'ALLOW', 'ROLE', 'timeControl'),
('WorkerTimeControl', 'getClockIn', 'READ', 'ALLOW', 'ROLE', 'timeControl'),
('WorkerTimeControl', 'clockIn', 'WRITE', 'ALLOW', 'ROLE', 'timeControl');
CALL `account`.`role_sync`();

View File

@ -200,5 +200,6 @@
"Try again": "Try again", "Try again": "Try again",
"keepPrice": "keepPrice", "keepPrice": "keepPrice",
"Cannot past travels with entries": "Cannot past travels with entries", "Cannot past travels with entries": "Cannot past travels with entries",
"It was not able to remove the next expeditions:": "It was not able to remove the next expeditions: {{expeditions}}" "It was not able to remove the next expeditions:": "It was not able to remove the next expeditions: {{expeditions}}",
"Incorrect pin": "Incorrect pin."
} }

View File

@ -330,8 +330,8 @@
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mínima", "quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mínima",
"Cannot past travels with entries": "No se pueden pasar envíos con entradas", "Cannot past travels with entries": "No se pueden pasar envíos con entradas",
"It was not able to remove the next expeditions:": "No se pudo eliminar las siguientes expediciones: {{expeditions}}", "It was not able to remove the next expeditions:": "No se pudo eliminar las siguientes expediciones: {{expeditions}}",
"This user does not have an assigned tablet": "Este usuario no tiene tablet asignada", "This user does not have an assigned tablet": "Este usuario no tiene tablet asignada",
"Incorrect pin": "Pin incorrecto.",
"You already have the mailAlias": "Ya tienes este alias de correo", "You already have the mailAlias": "Ya tienes este alias de correo",
"The alias cant be modified": "Este alias de correo no puede ser modificado" "The alias cant be modified": "Este alias de correo no puede ser modificado"
} }

View File

@ -43,16 +43,9 @@ module.exports = Self => {
const isTeamBoss = await models.ACL.checkAccessAcl(ctx, 'Worker', 'isTeamBoss', 'WRITE'); const isTeamBoss = await models.ACL.checkAccessAcl(ctx, 'Worker', 'isTeamBoss', 'WRITE');
const isHimself = userId == workerId; const isHimself = userId == workerId;
if (!isSubordinate || (isSubordinate && isHimself && !isTeamBoss)) if (!isSubordinate || (isHimself && !isTeamBoss))
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);
query = `CALL vn.workerTimeControl_clockIn(?,?,?)`; return Self.clockIn(workerId, args.timed, args.direction, myOptions);
const [response] = await Self.rawSql(query, [workerId, args.timed, args.direction], myOptions);
if (response[0] && response[0].error)
throw new UserError(response[0].error);
await models.WorkerTimeControl.resendWeeklyHourEmail(ctx, workerId, args.timed, myOptions);
return response;
}; };
}; };

View File

@ -0,0 +1,45 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('clockIn', {
description: 'Check if the employee can clock in',
accessType: 'WRITE',
accepts: [
{
arg: 'workerFk',
type: 'number',
required: true,
},
{
arg: 'timed',
type: 'date'
},
{
arg: 'direction',
type: 'string'
},
],
http: {
path: `/clockIn`,
verb: 'POST'
},
returns: {
type: 'Object',
root: true
}
});
Self.clockIn = async(workerFk, timed, direction, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const query = 'CALL vn.workerTimeControl_clockIn(?, ?, ?)';
const [[response]] = await Self.rawSql(query, [workerFk, timed, direction], myOptions);
if (response && response.error)
throw new UserError(response.error);
return response;
};
};

View File

@ -0,0 +1,32 @@
module.exports = Self => {
Self.remoteMethod('getClockIn', {
description: 'Shows the clockings for each day, in columns per day',
accessType: 'READ',
accepts: [
{
arg: 'workerFk',
type: 'int',
required: true,
},
],
http: {
path: `/getClockIn`,
verb: 'GET'
},
returns: {
type: ['Object'],
root: true
},
});
Self.getClockIn = async(workerFk, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const query = `CALL vn.workerTimeControl_getClockIn(?, ?)`;
const [result] = await Self.rawSql(query, [workerFk, Date.vnNew()], myOptions);
return result;
};
};

View File

@ -0,0 +1,35 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('login', {
description: 'Consult the user\'s information and the buttons that must be activated after logging in',
accessType: 'READ',
accepts: [
{
arg: 'pin',
type: 'string',
required: true
},
],
returns: {
type: 'Object',
root: true
},
http: {
path: `/login`,
verb: 'POST'
}
});
Self.login = async(ctx, pin, options) => {
const myOptions = {};
const $t = ctx.req.__;
if (typeof options == 'object')
Object.assign(myOptions, options);
const query = `CALL vn.workerTimeControl_login(?)`;
const [[user]] = await Self.rawSql(query, [pin], myOptions);
if (!user) throw new UserError($t('Incorrect pin'));
return user;
};
};

View File

@ -1,6 +1,6 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('resendWeeklyHourEmail', { Self.remoteMethodCtx('resendWeeklyHourEmail', {
description: 'Adds a new hour registry', description: 'Send the records for the week of the date provided',
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [{
arg: 'id', arg: 'id',

View File

@ -0,0 +1,581 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('workerTimeControl clockIn()', () => {
const workerId = 9;
const salesBossId = 19;
const hankPymId = 1107;
const jessicaJonesId = 1110;
const HHRRId = 37;
const teamBossId = 13;
const monday = 1;
const tuesday = 2;
const thursday = 4;
const friday = 5;
const sunday = 7;
const inTime = '2001-01-01T00:00:00.000Z';
const activeCtx = {
accessToken: {userId: 50},
};
const ctx = {req: activeCtx};
beforeAll(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should correctly clock in', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
await models.WorkerTimeControl.clockIn(workerId, inTime, 'in', options);
const isClockIn = await models.WorkerTimeControl.findOne({
where: {
userFk: workerId
}
}, options);
expect(isClockIn).toBeDefined();
expect(isClockIn.direction).toBe('in');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
describe('as Role errors', () => {
it('should add if the current user is team boss and the target user is himself', async() => {
activeCtx.accessToken.userId = teamBossId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const createdTimeEntry = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should delete the created time entry for the team boss as himself', async() => {
activeCtx.accessToken.userId = teamBossId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const createdTimeEntry = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
await models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id, options);
const deletedTimeEntry = await models.WorkerTimeControl.findById(createdTimeEntry.id, null, options);
expect(deletedTimeEntry).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should edit the created time entry for the team boss as HHRR', async() => {
activeCtx.accessToken.userId = HHRRId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const createdTimeEntry = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
ctx.args = {direction: 'out'};
const updatedTimeEntry = await models.WorkerTimeControl.updateTimeEntry(
ctx, createdTimeEntry.id, options
);
expect(updatedTimeEntry.direction).toEqual('out');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
describe('as saleBoss editor', () => {
let workerId;
beforeEach(() => {
activeCtx.accessToken.userId = salesBossId;
workerId = hankPymId;
});
it('should fail to add a time entry if the target user has an absence that day', async() => {
const date = Date.vnNew();
date.setHours(8, 0, 0);
date.setDate(date.getDate() - 16);
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
try {
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`No está permitido trabajar`);
});
it('should fail to add a time entry for a worker without an existing contract', async() => {
const date = Date.vnNew();
date.setFullYear(date.getFullYear() - 2);
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
try {
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`No hay un contrato en vigor`);
});
it('should fail to add a time entry for a worker without an existing contract and exceeding time', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(0, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(20, 0, 1);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Superado el tiempo máximo entre entrada y salida`);
});
describe('direction errors', () => {
let date = Date.vnNew();
date.setDate(date.getDate() - 1);
let error;
it('should throw an error when trying "in" direction twice', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "in" direction after insert "in" and "middle"', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('Should throw an error when trying "out" before closing a "middle" couple', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "middle" after "out"', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "out" direction twice', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
});
describe('12h rest', () => {
activeCtx.accessToken.userId = salesBossId;
const workerId = hankPymId;
it('should throw an error when the 12h rest is not fulfilled yet', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(4, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso diario`);
});
it('should not fail as the 12h rest is fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(4, 1, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
describe('for 3500kg drivers with enforced 9h rest', () => {
activeCtx.accessToken.userId = salesBossId;
const workerId = jessicaJonesId;
it('should throw an error when the 9h enforced rest is not fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(1, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso diario`);
});
it('should not fail when the 9h enforced rest is fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(1, 1, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
describe('for 72h weekly rest', () => {
it('should throw an error when work 11 consecutive days', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, friday);
date.setHours(10, 0, 1);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso semanal`);
});
it('should throw an error when the 72h weekly rest is not fulfilled', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, sunday);
date.setHours(17, 59, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso semanal`);
});
it('should throw an error when the 72h weekly rest is fulfilled', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, sunday);
date.setHours(18, 00, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
});
});
function weekDay(date, dayToSet) {
const currentDay = date.getDay();
const distance = dayToSet - currentDay;
date.setDate(date.getDate() + distance);
return date;
}
function nextWeek(date) {
const sunday = 7;
const currentDay = date.getDay();
let newDate = date;
if (currentDay != 0)
newDate = weekDay(date, sunday);
newDate.setDate(newDate.getDate() + 1);
return newDate;
}
async function populateWeek(date, dayStart, dayEnd, ctx, workerId, options) {
const dateStart = new Date(weekDay(date, dayStart));
const dateEnd = new Date(dateStart);
dateEnd.setDate(dateStart.getDate() + dayEnd);
for (let i = dayStart; i <= dayEnd; i++) {
dateStart.setHours(10, 0, 0);
ctx.args = {timed: dateStart, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
dateStart.setHours(18, 0, 0);
ctx.args = {timed: dateStart, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
dateStart.setDate(dateStart.getDate() + 1);
}
}

View File

@ -0,0 +1,16 @@
const models = require('vn-loopback/server/server').models;
describe('workerTimeControl getClockIn()', () => {
it('should correctly get the timetable of a worker', async() => {
const response = await models.WorkerTimeControl.getClockIn(1106, {});
expect(response.length).toEqual(4);
const [inHrs, middleOutHrs, middleInHrs, outHrs] = response;
expect(inHrs['0daysAgo']).toEqual('07:00');
expect(middleOutHrs['0daysAgo']).toEqual('10:00');
expect(middleInHrs['0daysAgo']).toEqual('10:20');
expect(outHrs['0daysAgo']).toEqual('14:50');
});
});

View File

@ -0,0 +1,34 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
const UserError = require('vn-loopback/util/user-error');
describe('workerTimeControl login()', () => {
let ctx;
beforeAll(async() => {
ctx = {
accessToken: {userId: 9},
req: {
headers: {origin: 'http://localhost'},
__: key => key
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: ctx
});
});
it('should correctly login', async() => {
const response = await models.WorkerTimeControl.login(ctx, 9);
expect(response.name).toBe('developer');
});
it('should throw UserError if pin is not provided', async() => {
try {
await models.WorkerTimeControl.login(ctx);
} catch (error) {
expect(error).toBeInstanceOf(UserError);
expect(error.message).toBe('Incorrect pin');
}
});
});

View File

@ -3,18 +3,10 @@ const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('workerTimeControl add/delete timeEntry()', () => { describe('workerTimeControl add/delete timeEntry()', () => {
const HHRRId = 37;
const teamBossId = 13;
const employeeId = 1; const employeeId = 1;
const salesPersonId = 1106;
const salesBossId = 19; const salesBossId = 19;
const hankPymId = 1107; const hankPymId = 1107;
const jessicaJonesId = 1110;
const monday = 1; const monday = 1;
const tuesday = 2;
const thursday = 4;
const friday = 5;
const sunday = 7;
const activeCtx = { const activeCtx = {
accessToken: {userId: 50}, accessToken: {userId: 50},
}; };
@ -61,560 +53,11 @@ describe('workerTimeControl add/delete timeEntry()', () => {
expect(error.statusCode).toBe(400); expect(error.statusCode).toBe(400);
expect(error.message).toBe(`You don't have enough privileges`); expect(error.message).toBe(`You don't have enough privileges`);
}); });
it('should add if the current user is team boss and the target user is himself', async() => {
activeCtx.accessToken.userId = teamBossId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const [createdTimeEntry] = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should try but fail to delete his own time entry', async() => {
activeCtx.accessToken.userId = salesBossId;
const workerId = salesBossId;
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const [createdTimeEntry] = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
activeCtx.accessToken.userId = salesPersonId;
await models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(400);
expect(error.message).toBe(`You don't have enough privileges`);
});
it('should delete the created time entry for the team boss as himself', async() => {
activeCtx.accessToken.userId = teamBossId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const [createdTimeEntry] = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
await models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id, options);
const deletedTimeEntry = await models.WorkerTimeControl.findById(createdTimeEntry.id, null, options);
expect(deletedTimeEntry).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should delete the created time entry for the team boss as HHRR', async() => {
activeCtx.accessToken.userId = HHRRId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const [createdTimeEntry] = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
await models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id, options);
const deletedTimeEntry = await models.WorkerTimeControl.findById(createdTimeEntry.id, null, options);
expect(deletedTimeEntry).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should edit the created time entry for the team boss as HHRR', async() => {
activeCtx.accessToken.userId = HHRRId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const [createdTimeEntry] = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
ctx.args = {direction: 'out'};
const updatedTimeEntry = await models.WorkerTimeControl.updateTimeEntry(ctx, createdTimeEntry.id, options);
expect(updatedTimeEntry.direction).toEqual('out');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
}); });
describe('WorkerTimeControl_clockIn calls', () => { describe('WorkerTimeControl_clockIn calls', () => {
let workerId; beforeEach(() => activeCtx.accessToken.userId = salesBossId);
beforeEach(() => {
activeCtx.accessToken.userId = salesBossId;
workerId = hankPymId;
});
it('should fail to add a time entry if the target user has an absence that day', async() => {
const date = Date.vnNew();
date.setHours(8, 0, 0);
date.setDate(date.getDate() - 16);
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
try {
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`No está permitido trabajar`);
});
it('should fail to add a time entry for a worker without an existing contract', async() => {
const date = Date.vnNew();
date.setFullYear(date.getFullYear() - 2);
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
try {
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`No hay un contrato en vigor`);
});
it('should fail to add a time entry for a worker without an existing contract', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(0, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(20,0, 1);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Superado el tiempo máximo entre entrada y salida`);
});
describe('direction errors', () => {
let date = Date.vnNew();
date.setDate(date.getDate() - 1);
let error;
it('should throw an error when trying "in" direction twice', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "in" direction after insert "in" and "middle"', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('Should throw an error when trying "out" before closing a "middle" couple', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "middle" after "out"', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "out" direction twice', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
});
describe('12h rest', () => {
activeCtx.accessToken.userId = salesBossId;
const workerId = hankPymId;
it('should throw an error when the 12h rest is not fulfilled yet', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(4, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso diario`);
});
it('should not fail as the 12h rest is fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(4, 1, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
describe('for 3500kg drivers with enforced 9h rest', () => {
activeCtx.accessToken.userId = salesBossId;
const workerId = jessicaJonesId;
it('should throw an error when the 9h enforced rest is not fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(1, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso diario`);
});
it('should not fail when the 9h enforced rest is fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(1, 1, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
describe('for 72h weekly rest', () => {
it('should throw an error when work 11 consecutive days', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, friday);
date.setHours(10, 0, 1);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso semanal`);
});
it('should throw an error when the 72h weekly rest is not fulfilled', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, sunday);
date.setHours(17, 59, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso semanal`);
});
it('should throw an error when the 72h weekly rest is fulfilled', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, sunday);
date.setHours(18, 00, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
describe('WorkerTimeControl_calculate calls', () => { describe('WorkerTimeControl_calculate calls', () => {
let dated = Date.vnNew(); let dated = Date.vnNew();
dated.setDate(dated.getDate() - 7); dated.setDate(dated.getDate() - 7);
@ -836,25 +279,6 @@ function weekDay(date, dayToSet) {
return date; return date;
} }
function nextWeek(date) {
const sunday = 7;
const currentDay = date.getDay();
let newDate = date;
if (currentDay != 0)
newDate = weekDay(date, sunday);
newDate.setDate(newDate.getDate() + 1);
return newDate;
}
function lastWeek(date) {
const monday = 1;
newDate = weekDay(date, monday);
newDate.setDate(newDate.getDate() - 1);
return newDate;
}
async function populateWeek(date, dayStart, dayEnd, ctx, workerId, options) { async function populateWeek(date, dayStart, dayEnd, ctx, workerId, options) {
const dateStart = new Date(weekDay(date, dayStart)); const dateStart = new Date(weekDay(date, dayStart));
const dateEnd = new Date(dateStart); const dateEnd = new Date(dateStart);

View File

@ -10,6 +10,9 @@ module.exports = Self => {
require('../methods/worker-time-control/weeklyHourRecordEmail')(Self); require('../methods/worker-time-control/weeklyHourRecordEmail')(Self);
require('../methods/worker-time-control/getMailStates')(Self); require('../methods/worker-time-control/getMailStates')(Self);
require('../methods/worker-time-control/resendWeeklyHourEmail')(Self); require('../methods/worker-time-control/resendWeeklyHourEmail')(Self);
require('../methods/worker-time-control/login')(Self);
require('../methods/worker-time-control/getClockIn')(Self);
require('../methods/worker-time-control/clockIn')(Self);
Self.rewriteDbError(function(err) { Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY') if (err.code === 'ER_DUP_ENTRY')