schema/Right.js
'use strict';
const SpecialRightIndex = require('../api/specialrights/index');
exports = module.exports = function(params) {
const dispunits = params.app.utility.dispunits;
const mongoose = params.mongoose;
let rightSchema = new params.mongoose.Schema({
name: { type: String, unique: true, required: true },
description: String,
timeCreated: { type: Date, default: Date.now },
type: { type: mongoose.Schema.Types.ObjectId, ref: 'Type' },
require_approval: { type: Boolean, default:true },
sortkey: Number,
special: { type: String }, // quantity evaluation rule used instead of the default quantity
// name will be readonly
// special rights are stored in api/specialrights/*
// proposed special rights will be filtered by company.country
consumption: { // consumption type
type: String,
enum:[
'proportion', // proportion: user the attendance percentage defined in user right collection
'businessDays', // businessDays: next business days are consumed up to consumptionBusinessDaysLimit
'workingDays', // workingDays: full working days are consumed
'duration', // duration: consumption equal worked duration
'length' // length: number of days or between the two dates (not usefull for hours)
],
default: 'proportion'
},
consumptionBusinessDaysLimit: { type: Number, default: 5 }, // Used if consumption=businessDays
// automatic distribution on this right on request creation
autoDistribution: { type: Boolean, default:true },
quantity: Number, // initial quantity for each renewal
// this can be overwritten by special quantity from a custom rule (ex: RTT)
// This can be null for infinite quantity (sickness leave)
quantity_unit: { type: String, enum:['D', 'H'], required: true }, // Days our Hours
halfDays: { type: Boolean, default:true }, // if quantiy_unit === 'D'
defaultAbsenceLength: Number, // defaut duration of the absence period
// this is used for a meternity leave for example
/**
* Add "quantity" every first day of month
* on each modification of the right, an array of rightAdjustments will be created in renewal
* from the current date to once the "max" quantity is reached or to the renewal date
* The adjustments array should be updated if new renewal is added or removed or if the right
* is modified but only for futur adjustments
*/
addMonthly: {
quantity: { type: Number, min:0 },
max: { type: Number, min:0 }
},
/**
* Auto create adjustements
* Create one adjustement of "quantity" for every "step" of consumed quantity in the types "types"
* list of adjustements will be computed on each modification of consumed quantity for each beneficiary of this right.
*/
autoAdjustment: {
types: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Type' }],
quantity: Number,
step: Number
},
timeSaving: {
active: { type: Boolean, default: false }, // The right quantity can be saved to CET or not
max: { type: Number, min:1 } // max saveable quantity from renewal
},
timeSavingAccount: { // used only if special==='timesavingaccount'
max: { type: Number, min:1 }, // max saveable quantity into renewal
savingInterval: {
useDefault: { type: Boolean, default: true }, // default saving interval is the renewal period
min: Number, // years before renewal start date
max: Number // years before renewal end date
}
},
activeFor: {
account: { type: Boolean, default:true },
// manager substituting one of his subordinate
manager: { type: Boolean, default:true },
// admin substituting one of the user with vacation account
admin: { type: Boolean, default:true }
},
// Hide the right in user list,
// but can be visible in step 2 when creating a request if active for account
hide: Boolean,
// activeSpan.min minimal number of days between entry date and request start date
// this is the time given to the approvers
// activeSpan.max maximal number of days between entry date and request end date
// by default, the parameter is set globally
activeSpan: {
useDefault: { type: Boolean, default:true },
min: Number,
max: Number
},
rules: [params.embeddedSchemas.RightRule],
// Count lunch on absence event if linked to this right, ex on a traineeship
lunch: { type: Boolean, default: false }
});
rightSchema.index({ 'name': 1 }, { unique: true });
rightSchema.set('autoIndex', params.autoIndex);
/**
* Pre save hook
*/
rightSchema.pre('save', function(next) {
// set monthly adjustments from today
this.updateAdjustments(next);
});
/**
* Pre remove hook
*/
rightSchema.pre('remove', function(next) {
let right = this;
let promises = [];
promises.push(right.removeRenewals());
promises.push(right.removeBeneficiaries());
Promise.all(promises).then(() => {
next();
}).catch(next);
});
/**
* delete all renewals linked to this right
* remove on model have no query hook so it not used here because we may have to chain multiple pre remove middlewares
* @return {Promise}
*/
rightSchema.methods.removeRenewals = function() {
let right = this;
let RightRenewal = right.model('RightRenewal');
return RightRenewal.find()
.where('right', right._id)
.exec()
.then(renewals => {
let removePromises = [];
renewals.forEach(renewal => {
removePromises.push(renewal.remove());
});
return Promise.all(removePromises);
});
};
/**
* Delete all beneficiaries linked to this right
* @return {Promise}
*/
rightSchema.methods.removeBeneficiaries = function() {
let Beneficiary = this.model('Beneficiary');
return Beneficiary.find()
.where('right', this._id)
.exec()
.then(beneficiaries => {
let removePromises = [];
beneficiaries.forEach(b => {
removePromises.push(b.remove());
});
return Promise.all(removePromises);
});
};
/**
* Get all users linked to this right with the beneficiary period
* resolve to array with objects :
* -user User document
* -beneficiary
*
* @param {Date} moment Optional date for collection association to users
*
* @return {Promise}
*/
rightSchema.methods.getBeneficiaryUsers = function(moment) {
let Beneficiary = this.model('Beneficiary');
return Beneficiary.find()
.where('right', this._id)
.exec()
.then(beneficiaries => {
let promises = beneficiaries.map(b => {
return b.getUsers(moment);
});
return Promise.all(promises)
.then(all => {
let list = [];
function push(user) {
list.push({
user: user,
beneficiary: this
});
}
for (let i=0; i<all.length; i++) {
let beneficiary = beneficiaries[i];
let users = all[i];
users.forEach(push.bind(beneficiary));
}
return list;
});
});
};
/**
* Check if the right is a time saving account
* @returns {boolean}
*/
rightSchema.methods.isTimeSavingAccount = function() {
return ('timesavingaccount' === this.special);
};
/**
* Count uses in requests
* @return {Promise} Resolve to a number
*/
rightSchema.methods.countUses = function() {
let right = this;
let AbsenceElem = this.model('AbsenceElem');
let Request = this.model('Request');
let absences = AbsenceElem.countDocuments()
.where('right.id', right._id)
.exec();
let tsdOrWorkRecover = Request.countDocuments({ $and: [
{ 'status.deleted' : { $ne: 'accepted' } },
{ $or:
[
{ 'time_saving_deposit.from.right': right._id },
{ 'time_saving_deposit.to.right': right._id },
{ 'workperiod_recover.right.id': right._id }
]
}
]})
.exec();
return Promise.all([absences, tsdOrWorkRecover])
.then(all => {
return (all[0] + all[1]);
});
};
/**
* update monthly adjustments
*/
rightSchema.methods.updateAdjustments = function(next) {
let right = this;
let async = require('async');
this.getAllRenewals()
.then(function(arr) {
let rightRenewalSchema = params.db.models.RightRenewal;
async.each(arr,function(renewal, renewalDone) {
renewal.removeFutureRightAdjustments();
if (renewal.createRightAdjustments(right)) {
// do not call pre save hook on renewal
rightRenewalSchema
.findByIdAndUpdate(renewal._id, { $set: { adjustments: renewal.adjustments }})
.exec(renewalDone);
} else {
renewalDone();
}
}, next);
});
};
/**
* Get max value or infinity if not set
* @return {Number}
*/
rightSchema.methods.getMonthlyMaxQuantity = function() {
var max = Infinity;
if (undefined !== this.addMonthly.max && 0 !== this.addMonthly.max) {
max = this.addMonthly.max;
}
return max;
};
/**
* Create renewal
* @return {Promise}
*/
rightSchema.methods.createRenewal = function createRenewal(start, finish) {
var model = this.model('RightRenewal');
var rightRenewal = new model();
rightRenewal.right = this._id;
rightRenewal.start = start;
rightRenewal.finish = finish;
return rightRenewal.save();
};
/**
* create a renewal for overtime conversion
* @param {Object} params Renewal parameters
* @return {Promise}
*/
rightSchema.methods.createOvertimeConversionRenewal = function createOvertimeConversionRenewal(params) {
if (!params.start) {
params.start = new Date();
}
if (!params.finish) {
params.finish = new Date(params.start);
params.finish.setFullYear(params.finish.getFullYear() + 1);
}
// round to full days
params.start.setHours(0,0,0,0);
params.finish.setHours(23,59,59,999);
return this.createRenewal(params.start, params.finish);
};
/**
* create a renewal for a recover request
* start on the date of the recovery period end end one year later
*
* @param {Object} request Request document
* @return {Promise}
*/
rightSchema.methods.createRecoveryRenewal = function createRecoveryRenewal(request) {
if (0 === request.events.length) {
throw new Error('No events on the recovery request');
}
if (undefined === request.events[0].dtstart) {
throw new Error('events must be populated on request');
}
return this.createOvertimeConversionRenewal(request.workperiod_recover[0].right.renewal);
};
rightSchema.methods.getBeneficiaryRef = function() {
let Beneficiary = this.model('Beneficiary');
return Beneficiary.findOne()
.where('right', this._id)
.exec()
.then(b => {
if (null === b) {
return undefined;
}
return b.ref;
});
};
/**
* Check if the right can be linked to a collection
* the methods verify that no user is linked to the right
* @return {Promise}
*/
rightSchema.methods.canLinkToCollection = function() {
let model = this.model('Beneficiary');
return model.find()
.where('right', this._id)
.where('ref', 'User')
.exec()
.then(arr => {
if (arr.length > 0) {
throw new Error('The right is linked to one or more users, linking to a collection is only allowed if there is no users linked to the right');
}
return true;
});
};
/**
* Check if the right can be linked to a user
* the methods verify that no colletion is linked to the right
* @return {Promise}
*/
rightSchema.methods.canLinkToUser = function() {
let model = this.model('Beneficiary');
return model.find()
.where('right', this._id)
.where('ref', 'RightCollection')
.exec()
.then(arr => {
if (arr.length > 0) {
throw new Error('The right is linked to one or more collections, please link the user to a collection, direct link to a right is only allowed on orphan rights');
}
return true;
});
};
/**
* Link the right to a collection
* The right must not be linked to a user
*
* @param {RightCollection|ObjectId} collection
* @return {Promise}
*/
rightSchema.methods.addCollectionBeneficiary = function addCollectionBeneficiary(collection) {
let model = this.model('Beneficiary');
let id = collection;
if (undefined !== collection._id) {
id = collection._id;
}
return this.canLinkToCollection()
.then(() => {
let beneficiary = new model();
beneficiary.right = this._id;
beneficiary.document = id;
beneficiary.ref = 'RightCollection';
return beneficiary.save();
});
};
/**
* Link the right to one user, use a beneficary document with a user ref instead of a right collection
* The right must not be linked to a collection
*
* @param {User|ObjectId|string} user
* @return {Promise}
*/
rightSchema.methods.addUserBeneficiary = function addUserBeneficiary(user) {
let model = this.model('Beneficiary');
let id = user;
if (undefined !== user._id) {
id = user._id;
}
return this.canLinkToUser()
.then(() => {
let beneficiary = new model();
beneficiary.right = this._id;
beneficiary.document = id;
beneficiary.ref = 'User';
return beneficiary.save();
});
};
/**
* Get special right object or null
*
* @return {SpecialRight}
*/
rightSchema.methods.getSpecialRight = function() {
let index = new SpecialRightIndex(params.app);
let list = index.objects;
let right = this;
if (null === right.special || undefined === right.special) {
return null;
}
if (undefined === list[right.special]) {
throw new Error('Undefined special right object '+right.special);
}
let SpecialRight = list[this.special];
return new SpecialRight(params.app);
};
/**
* Find right renewals
* @returns {Query} A mongoose query on the right renewal schema
*/
rightSchema.methods.getRenewalsQuery = function() {
return this.model('RightRenewal')
.find()
.where('right').equals(this._id);
};
/**
* Get all renewals
* @return {Promise} mongoose
*/
rightSchema.methods.getAllRenewals = function() {
return this.getRenewalsQuery().exec();
};
/**
* Get renewal using a id, but verifiy that the renewal is linked to the right
* @param {String} id
* @returns {Promise}
*/
rightSchema.methods.getRenewal = function(id) {
return this.getRenewalsQuery()
.where('_id').equals(id)
.exec()
.then(arr => {
if (!arr || 0 === arr.length) {
return null;
}
return arr[0];
});
};
/**
* Get renewal by date interval or null if no renewal
* requests straddling two periods are not allowed
*
* @param {Date} dtstart
* @param {Date} dtend
* @returns {Promise}
*/
rightSchema.methods.getPeriodRenewal = function(dtstart, dtend) {
return this.getRenewalsQuery()
.where('start').lte(dtstart)
.where('finish').gte(dtend)
.exec()
.then(arr => {
if (!arr || 0 === arr.length) {
return null;
}
return arr[0];
});
};
/**
* Get renewal With same date interval
*
* @param {Date} dtstart
* @param {Date} dtend
* @returns {Promise}
*/
rightSchema.methods.getSameRenewal = function(dtstart, dtend) {
return this.getRenewalsQuery()
.where('start', dtstart)
.where('finish', dtend)
.exec()
.then(arr => {
if (!arr || 0 === arr.length) {
return null;
}
return arr[0];
});
};
/**
* Get the quantity added on initial quantity between two dates
* this include initial quantity of the right and monthly updates
*
* @param {User} user
* @param {Date} dtstart
* @param {Date} dtend
*
* @returns {Promise}
*/
rightSchema.methods.getInitialQuantityInPeriod = function(user, dtstart, dtend, field) {
let right = this;
if (null === right.quantity) {
return Promise.resolve(Infinity);
}
if (undefined === field) {
// Will take first the renewal overlapping the start date
field = 'start';
}
let criterion = {};
criterion[field] = { $gte: dtstart, $lt: dtend };
return this.getRenewalsQuery()
.where(criterion)
.exec()
.then(arr => {
let promises = [];
arr.forEach(renewal => {
// on each renewal, get the max initial quantity in period
promises.push(renewal.getUserQuantity(user, dtend));
});
return Promise.all(promises);
})
.then(all => {
if (0 === all.length) {
if ('finish' === field) {
return 0;
}
// no renewals found in period
// retry with renewal end date as a second option
return right.getInitialQuantityInPeriod(user, dtstart, dtend, 'finish');
}
return all.reduce((sum, initialQuantity) => {
return sum + initialQuantity.value;
}, 0);
});
};
/**
* Get current renewal or null if no renewal
* @returns {Promise}
*/
rightSchema.methods.getCurrentRenewal = function() {
return this.getPeriodRenewal(new Date(), new Date());
};
/**
* Get renewal on date or null if no renewal
* @param {Date} moment Optional date
* @returns {Promise}
*/
rightSchema.methods.getMomentRenewal = function(moment) {
if (!moment) {
moment = new Date();
}
return this.getPeriodRenewal(moment, moment);
};
/**
* Get last renewal
* @returns {Promise}
*/
rightSchema.methods.getLastRenewal = function() {
var deferred = {};
deferred.promise = new Promise(function(resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});
this.getRenewalsQuery()
.limit(1)
.sort('-start')
.exec(function(err, arr) {
if (err) {
deferred.reject(err);
return;
}
if (!arr || 0 === arr.length) {
deferred.resolve(null);
return;
}
deferred.resolve(arr[0]);
});
return deferred.promise;
};
rightSchema.methods.getDispUnit = function(quantity) {
return dispunits(this.quantity_unit, quantity);
};
/**
* Validate right rules
* return false if one of the rules is not appliquable (ex: for request date when the request does not exists)
*
* @param {RightRenewal} renewal Right renewal
* @param {User} user Request appliquant
* @param {Date} dtstart Request start date
* @param {Date} dtend Request end date
* @param {Date} [timeCreated] Request creation date
*
* @return {Promise} Resolve to true or RightRule
*/
rightSchema.methods.validateRules = function(renewal, user, dtstart, dtend, timeCreated) {
if (undefined === timeCreated) {
timeCreated = new Date();
}
let promises = this.rules.map(rule => {
return rule.validateRule(renewal, user, dtstart, dtend, timeCreated);
});
return Promise.all(promises)
.then(all => {
for(let i=0; i<all.length; i++) {
if (!all[i]) {
return this.rules[i];
}
}
return true;
});
};
/**
* Get the consumed quantity proportionally to attendance
* @param {Number} attendance Attendance percentage
* @param {Number} quantity Period duration quantity
* @returns {Number}
*/
rightSchema.methods.getConsumedQuantityByAttendance = function(attendance, quantity) {
if (100 === attendance || undefined === attendance) {
return quantity;
}
// 50% -> x2
// 75% -> x1.333
// 25% -> x4
// 100% -> x1
const m = 100*(1/attendance);
return (m*quantity);
};
/**
* Count the number of business days up to the back to work date
* @throws {Error} Right must be in days
*
* @param {RightCollection} collection collection associated to the request appliquant
* must be the collection in effect on the absence element period
* @param {AbsenceElem} elem Absence element
*
* @returns {Promise}
*/
rightSchema.methods.getConsumedQuantityByBusinessDays = function(collection, elem) {
let right = this;
if ('D' !== right.quantity_unit) {
throw new Error('Consuption by business days must be used on a right with days as quantity unit');
}
let businessDays = collection.getDays();
let count = 0;
return new Promise((resolve, reject) => {
elem.getBackDate().then(backDate => {
let loop = new Date(elem.events[0].dtstart);
loop.setHours(0,0,0,0);
while(loop < backDate) {
if (-1 !== businessDays.indexOf(loop.getDay())) {
count++;
}
loop.setDate(loop.getDate()+1);
}
if (count <= 0) {
return reject('getConsumedQuantityByBusinessDays failed to get the consumption');
}
resolve(count);
}).catch(reject);
});
};
/**
* Get the consumed quantity on right from the duration quantity in an absence element
*
* @param {RightCollection} collection collection associated to the request appliquant
* must be the collection in effect on the absence element period
* @param {AbsenceElem} elem Absence element
*
* @return {Promise}
*/
rightSchema.methods.getConsumedQuantity = function(collection, elem) {
let right = this;
if ('proportion' === right.consumption) {
// consume more than duration quantity if attendance percentage lower than 100
return Promise.resolve(right.getConsumedQuantityByAttendance(collection.attendance, elem.quantity));
}
if ('businessDays' === right.consumption) {
// consume number of business days up to back to work date
return right.getConsumedQuantityByBusinessDays(collection, elem);
}
if ('workingDays' === right.consumption) {
// consume exact number of working days (no half-days)
return elem.getWorkingDays();
}
if ('duration' === right.consumption) {
// consume duration
return Promise.resolve(elem.quantity);
}
if ('length' === right.consumption) {
return elem.getLength();
}
return Promise.reject('Unexpected consumption type '+right.consumption);
};
/**
* get type promise
*
*
* @return {Promise}
*/
rightSchema.methods.getType = function() {
let right = this;
return right.populate('type')
.execPopulate()
.then(populatedRight => {
return populatedRight.type;
});
};
/**
* Update user stat linked to the current renewal
* @return {Promise}
*/
rightSchema.methods.updateUsersStat = function() {
let right = this;
return right.getMomentRenewal()
.then(renewal => {
if (null === renewal) {
return;
}
// ignore error on renewal
// a simple catch does not work here
return new Promise(resolve => {
renewal.updateUsersStat()
.catch(resolve)
.then(resolve);
});
});
};
/**
* Get a task to initialize rights when the database is created
* @param {Company} company Company document not yet saved
* @returns {Function} async task function for parallels initialization of tables
*/
rightSchema.statics.getInitTask = function(company) {
let model = this;
/**
* get rights data
* warning, security risk, company.country must be clean
* If no country set on company, the default.js file is used
*
* @returns {object}
*/
function getData() {
if (undefined === company.country || null === company.country) {
try {
return require('./data/default')(params.app);
} catch(e) {
return null;
}
}
if (!company.country.match(/^[A-Z]{2}$/)) {
return null;
}
try {
return require('./data/'+company.country.toLowerCase())(params.app);
} catch(e) {
return null;
}
}
/**
* Link to specified collectionId or all existing collections if not set
* @param {Right} right
* @param {String} collectionId
* @return {Promise}
*/
function linkToCollection(right, collectionId) {
let CollectionModel = right.model('RightCollection');
if (undefined !== collectionId) {
return right.addCollectionBeneficiary(collectionId);
}
return CollectionModel.find({})
.exec()
.then(all => {
return Promise.all(all.map(collection => {
return right.addCollectionBeneficiary(collection);
}));
});
}
/**
* Save one right, add one renewal, add right in a collection
* @return {Promise}
*/
function saveRight(rightData) {
function getRenewal() {
if (undefined === rightData.renewal) {
return null;
}
let period = {};
// company.timeCreated can be set to another date for tests
// or screenshots generation
let now = new Date(company.timeCreated);
period.start = new Date(company.timeCreated);
period.start.setHours(0,0,0,0);
period.start.setMonth(rightData.renewal.start.month, rightData.renewal.start.day);
if (period.start > now) {
period.start.setFullYear(period.start.getFullYear()-1);
}
let nbYears = 1;
if (undefined !== rightData.renewal && undefined !== rightData.renewal.nbYears) {
nbYears = rightData.renewal.nbYears;
}
period.finish = new Date(period.start);
period.finish.setFullYear(period.finish.getFullYear() + nbYears);
period.finish.setDate(period.finish.getDate()-1);
period.finish.setHours(23,59,59,999);
return period;
}
const right = new model();
if (undefined !== rightData._id) {
right._id = rightData._id;
}
right.special = rightData.special;
right.name = rightData.name;
right.quantity_unit = rightData.quantity_unit || 'D';
right.quantity = rightData.quantity;
right.defaultAbsenceLength = rightData.defaultAbsenceLength;
right.type = rightData.type;
right.rules = rightData.rules;
return right.save()
.then(right => {
let promises = [];
if (rightData.defaultAbsenceLength === undefined) {
promises.push(linkToCollection(right, rightData.collection));
}
// create renewal
let period = getRenewal();
if (null !== period) {
let renewalModel = right.model('RightRenewal');
let renewal = new renewalModel();
renewal.right = right._id;
renewal.set(period);
promises.push(renewal.save());
}
return Promise.all(promises);
});
}
/**
* initialize default rights on database creation
* @return {Promise}
*/
function createDefaults() {
let data = getData();
if (null === data) {
return Promise.resolve([]);
}
return Promise.all(
data.rights.map(rightData => {
return saveRight(rightData);
})
);
}
return createDefaults;
};
params.db.model('Right', rightSchema);
};