gadael/gadael

View on GitHub
schema/Request.js

Summary

Maintainability
F
1 wk
Test Coverage
'use strict';


/**
 * array map function to get Id from the document or ID
 * @param   {object|ObjectId}   document [[Description]]
 * @returns {ObjectId} [[Description]]
 */
function mapId(document) {
    return undefined === document._id ? document : document._id;
}

function getRemovePromises(documents) {
    let promises = [];
    documents.forEach(doc => {
        promises.push(doc.remove());
    });

    return Promise.all(promises);
}



exports = module.exports = function(params) {

    const mongoose = params.mongoose;

    var requestSchema = new mongoose.Schema({
        user: { // owner
            id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true },
            name: { type: String, required: true },
            department: String
        },

        timeCreated: { type: Date, default: Date.now },
        lastUpdate: { type: Date, default: Date.now },

        createdBy: {
          id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
          name: { type: String, required: true }
        },

        events: [{                                                              // for absence or workperiod_recover
            type: mongoose.Schema.Types.ObjectId, ref: 'CalendarEvent'          // for absences, the events match working periods
        }],                                                                     // duplicated from absence.distribution.events
                                                                                // for workperiod_recover, the events are in non working periods

        absence: {
            dtstart: Date,                                                      // dtstart from first event
            dtend: Date,                                                        // dtend from last event
            rightCollection: { type: mongoose.Schema.Types.ObjectId, ref: 'RightCollection' },
            distribution: [{ type: mongoose.Schema.Types.ObjectId, ref: 'AbsenceElem' }],
            compulsoryLeave: { type: mongoose.Schema.Types.ObjectId, ref: 'CompulsoryLeave' }
        },

        time_saving_deposit: [params.embeddedSchemas.TimeSavingDeposit],

        workperiod_recover: [params.embeddedSchemas.WorperiodRecover],

        status: {                                                               // approval status for request creation or request deletion
            created: { type: String, enum: [ null, 'waiting', 'accepted', 'rejected' ], default: null },
            deleted: { type: String, enum: [ null, 'waiting', 'accepted', 'rejected' ], default: null }
        },

        approvalSteps: [params.embeddedSchemas.ApprovalStep],                    // on request creation, approval steps are copied and contain references to users
                                                                                // informations about approval are stored in requestLog sub-documents instead
                                                                                // first position in array is the last approval step (top level department in user deparments ancestors)

        requestLog: [params.embeddedSchemas.RequestLog],                        // linear representation of all actions
                                                                                // create, edit, delete, and effectives approval steps

        validInterval: [params.embeddedSchemas.ValidInterval],                  // list of dates interval where the request is confirmed
                                                                                // absence: the quantity is consumed
                                                                                // time saving deposit: the quantity is available is time saving account
                                                                                // workperiod recover: the quantity is available in recovery right

        messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }]    // List of emails sent for this request
                                                                                // each new mail will use the message Ids to stay in the same conversation
    });







    /**
     * Register pre remove hook
     */
    requestSchema.pre('remove', function preRemoveHook(next) {

        let request = this;

        Promise.all([
            request.removeAbsenceDistribution(),
            request.removeEvents()
        ])
        .then(() => {
            next();
        })
        .catch(next);
    });


    requestSchema.post('remove', function postSaveHook(request) {
        request.updateAutoAdjustments();
    });


    /**
     * refresh appliquant renewals stat cache
     * @return {Promise}
     */
    requestSchema.methods.updateRenewalsStat = function() {

        let statDate = this.timeCreated;
        if (undefined !== this.events[0]) {
            statDate = this.events[0].dtstart;
        }

        return this.getUser()
        .then(user => {
            return user.updateRenewalsStat(statDate);
        });
    };


    /**
     * Update auto adjustments for all rights associated to request recipient
     * @return {Promise}
     */
    requestSchema.methods.updateAutoAdjustments = function() {

        let request = this;

        if (!request.absence || !request.absence.dtstart) {
            return Promise.resolve(true);
        }

        return request.getUser()
        .then(user => {
            return user.updateAutoAdjustments(request.absence.dtstart);
        })
        .then(() => {
            let Model = request.constructor;
            if (undefined !== Model.autoAdjustmentUpdated) {
                // this is for tests
                return Model.autoAdjustmentUpdated();
            }
            return true;
        });
    };


    /**
     * Get request owner user
     * @return {Promise}
     */
    requestSchema.methods.getUser = function() {
        return this.populate('user.id')
        .execPopulate()
        .then(populatedRequest => {
            return populatedRequest.user.id;
        });
    };

    /**
     * Remove absence distribution
     * @return {Promise}
     */
    requestSchema.methods.removeAbsenceDistribution = function() {

        let request = this;

        if (undefined === request.absence ||
            undefined === request.absence.distribution ||
            request.absence.distribution.length === 0) {
            return Promise.resolve(false);
        }


        let distribution = request.absence.distribution;
        let elementIds = distribution.map(mapId);

        let AbsenceElem = request.model('AbsenceElem');

        return AbsenceElem.find({ _id: { $in: elementIds } }).exec()
        .then(getRemovePromises);
    };


    /**
     * Remove all linked calendar events
     * @return {Promise}
     */
    requestSchema.methods.removeEvents = function() {
        let request = this;

        if (undefined === request.events ||
            undefined === request.events.length === 0) {
            return Promise.resolve(false);
        }

        let eventIds = request.events.map(mapId);

        let CalendarEvent = request.model('CalendarEvent');

        return CalendarEvent.find({ _id: { $in: eventIds } }).exec()
        .then(getRemovePromises);
    };



    /**
     * For absence request get the total quantity according to dates only
     * @return {Number}
     */
    requestSchema.methods.getQuantity = function() {
        let quantity = 0;
        this.absence.distribution.forEach(elem => {
            quantity += elem.quantity;
        });

        return quantity;
    };


    /**
     * For absence request get the total consumed quantity
     * @return {Number}
     */
    requestSchema.methods.getConsumedQuantity = function() {
        let consumed = 0;
        this.absence.distribution.forEach(elem => {
            consumed += elem.consumedQuantity;
        });

        return consumed;
    };

    /**
     * Get the last request log at a Date
     * @return {RequestLog}
     */
    requestSchema.methods.getDateLog = function(moment) {

        if (!this.requestLog) {
            return null;
        }

        for (let i=this.requestLog.length-1; i>=0; i--) {
            if (moment < this.requestLog[i].timeCreated) {
                return this.requestLog[i];
            }
        }

        return null;
    };


    /**
     * Get the status of request on a Date
     * using the request log
     * For this to work, we make the assumption that we never come back on a delete
     *
     * @return object
     */
    requestSchema.methods.getDateStatus = function(moment) {
        let requestLog = this.getDateLog();

        if (null === requestLog) {
            return this.status;
        }

        switch(requestLog.action) {
            case 'create':
            case 'modify':
                return {
                    created: 'accepted',
                    deleted: null
                };

            case 'wf_sent':
            case 'wf_accept':
            case 'wf_reject':
                if ('accepted' === this.status.deleted) {
                    return {
                        created: null,
                        deleted: 'waiting'
                    };
                }

                return {
                    created: 'waiting',
                    deleted: null
                };

            case 'wf_end':
                if ('accepted' === this.status.deleted) {
                    return {
                        created: null,
                        deleted: 'accepted'
                    };
                }

                return {
                    created: 'accepted',
                    deleted: null
                };

            case 'delete':
                return {
                    created: null,
                    deleted: 'accepted'
                };
        }

    };


    /**
     * Get string used in public URL (type folder)
     * @return string
     */
    requestSchema.methods.getUrlPathType = function() {
        if (this.absence && this.absence.distribution.length > 0) {
            return 'absences';
        }

        if (this.time_saving_deposit && this.time_saving_deposit.length > 0) {
            return 'time-saving-deposits';
        }

        if (this.workperiod_recover && this.workperiod_recover.length > 0) {
            return 'workperiod-recovers';
        }

        return null;
    };


    /**
     * Get a displayable request type, internationalized
     * @return {String}
     */
    requestSchema.methods.getDispType = function getDispType() {

        const gt = params.app.utility.gettext;

        if (this.absence && this.absence.distribution.length > 0) {
            return gt.gettext('Leave');
        }

        if (this.time_saving_deposit && this.time_saving_deposit.length > 0) {
            return gt.gettext('Time saving deposit');
        }

        if (this.workperiod_recover && this.workperiod_recover.length > 0) {
            return gt.gettext('Overtime declaration');
        }

        return gt.gettext('Unknown');
    };


    /**
     * Get a displayable status, internationalized
     * @return {String}
     */
    requestSchema.methods.getDispStatus = function getDispStatus() {

        const gt = params.app.utility.gettext;

        if (null !== this.status.created) {
            switch(this.status.created) {
                case 'waiting':
                    return gt.gettext('Waiting approval');
                case 'accepted':
                    return gt.gettext('Accepted');
                case 'rejected':
                    return gt.gettext('Rejected');
            }
        }


        if (null !== this.status.deleted) {
            switch(this.status.deleted) {
                case 'waiting':
                    return gt.gettext('Waiting deletion approval');
                case 'accepted':
                    return gt.gettext('Deleted');
                case 'rejected':
                    return gt.gettext('Deletion rejected');
            }
        }

        return gt.gettext('Undefined');
    };

    /**
     * Get last request log inserted for the approval workflow
     * @return {RequestLog}
     */
    requestSchema.methods.getLastApprovalRequestLog = function getLastApprovalRequestLog() {
        for(var i=this.requestLog.length-1; i>=0; i--) {
            if (this.requestLog[i].approvalStep !== undefined) {
                return this.requestLog[i];
            }
        }

        return null;
    };

    /**
     * Get last request log inserted, approval workflow is ignored
     * @example get the modification which initiated the workflow
     * @return {RequestLog}
     */
    requestSchema.methods.getLastNonApprovalRequestLog = function getLastNonApprovalRequestLog() {
        for(var i=this.requestLog.length-1; i>=0; i--) {
            if (this.requestLog[i].approvalStep === undefined) {
                return this.requestLog[i];
            }
        }

        return null;
    };


    /**
     * Get the last approval step with a saved item in request log
     * @return {ApprovalStep}
     */
    requestSchema.methods.getLastApprovalStep = function getLastApprovalStep() {

        if (this.approvalSteps === undefined) {
            return null;
        }

        if (0 === this.approvalSteps.length) {
            return null;
        }

        var log = this.getLastApprovalRequestLog();

        if (null === log) {
            // nothing done about approval
            return null;
        }

        return this.approvalSteps.id(log.approvalStep);
    };


    /**
     * Get the waiting approval step or null
     * @return {ApprovalStep}
     */
    requestSchema.methods.getWaitingApprovalStep = function getWaitingApprovalStep() {
        if (this.approvalSteps === undefined) {
            return null;
        }

        let steps = this.approvalSteps.filter(step => {
            return (step.status === 'waiting');
        });

        if (0 === steps.length) {
            return null;
        }

        if (1 !== steps.length) {
            throw new Error('Unexpected number of waiting steps on request '+this._id);
        }

        return steps[0];
    };


    /**
     * Get next approval step
     * return false if the last approval step in log was the last step in request
     *
     * @return {ApprovalStep|false}
     */
    requestSchema.methods.getNextApprovalStep = function getNextApprovalStep() {



        if (0 === this.approvalSteps.length) {
            return null;
        }

        var last = this.getLastApprovalStep();



        if (null === last) {
            return this.approvalSteps[0];
        }


        for(var i=this.approvalSteps.length-1; i>=0; i--) {
            if (this.approvalSteps[i]._id === last._id) {
                i--;
                break;
            }
        }


        if (this.approvalSteps[i] === undefined) {
            return false;
        }

        return this.approvalSteps[i];
    };



    /**
     * If last approval step is confirmed, notify the appliquant
     * otherwise notify the next manager using approvalsteps
     * @param {ApprovalStep} nextStep
     */
    requestSchema.methods.forwardApproval = function forwardApproval(nextStep) {

        nextStep.status = 'waiting';

        // TODO send message to managers of the nextStep


    };





    /**
     * Get the list of approvers without reply on the approval step
     * @param {ApprovalStep} approvalStep
     * @return {Array}      Array of user ID (mongoose objects)
     */
    requestSchema.methods.getRemainingApprovers = function getRemainingApprovers(approvalStep) {

        var interveners = [];

        this.requestLog.forEach(function(log) {

            if (undefined === log.approvalStep || !log.approvalStep.equals(approvalStep._id)) {
                return;
            }

            if (undefined === log.userCreated) {
                throw new Error('Wrong format in request log');
            }

            if (log.userCreated.id instanceof mongoose.Types.ObjectId) {
                interveners.push(log.userCreated.id.toString());
            } else {
                interveners.push(log.userCreated.id.id);
            }
        });

        var approvers = approvalStep.approvers.filter(function(approver) {

            if (approver.id instanceof mongoose.Types.ObjectId) {
                approver = approver.id.toString();
            } else {
                approver = approver.toString();
            }

            return (-1 === interveners.indexOf(approver));
        });

        return approvers;
    };


    /**
     * @return {Array} array of string
     */
    requestSchema.methods.getRemainingApproversOnWaitingSteps = function() {
        var approvers = [];
        var request = this;

        function feApprover(approver) {
            approver = approver.toString();
            if (-1 === approvers.indexOf(approver)) {
                approvers.push(approver);
            }
        }

        this.approvalSteps.forEach(function(step) {
            if (step.status !== 'waiting') {
                return;
            }

            request.getRemainingApprovers(step).forEach(feApprover);
        });

        return approvers;
    };


    /**
     * Create recovery right from request
     * @return {Promise}
     */
    requestSchema.methods.createRecoveryRight = function createRecoveryRight() {


        if (undefined === this.workperiod_recover || 0 === this.workperiod_recover.length) {
            return Promise.resolve(null);
        }


        var recover = this.workperiod_recover[0];
        var request = this;

        /**
         * @param {apiService   service
         * @param {Object} wrParams
         * @return {Promise}
         */
        function createRight()
        {
            var rightModel = request.model('Right');

            var right = new rightModel();
            right.name = recover.right.name;
            right.type = '5740adf51cf1a569643cc50a';
            right.quantity = recover.quantity;
            right.quantity_unit = recover.right.quantity_unit;
            right.rules = [{
                title: 'Active for request dates in the renewal period',
                type: 'request_period'
            }];

            return right.save();
        }


        return createRight()
        .then(right => {

            if (null === right) {
                return request;
            }

            recover.right.id = right._id;

            return right.createRecoveryRenewal(request)
            .then(renewal => {

                if (undefined === renewal._id) {
                    throw new Error('The new renewal ID is required');
                }

                recover.right.renewal.id = renewal._id;
                return request.save();
            })
            .then(() => {
                return right;
            });
        });

    };

    /**
     * Create overtime
     * @param {User}        user            Request owner
     * @return {Promise}    resolve to the Overtime document or null if overtime has not been created
     */
    requestSchema.methods.createOvertime = function(user)
    {
        if (1 !== this.workperiod_recover.length || params.app.config.company.workperiod_recovery_by_approver) {
            return Promise.resolve(null);
        }

        const recover = this.workperiod_recover[0];
        const request = this;
        const Overtime = request.model('Overtime');

        var overtime = new Overtime();
        overtime.user = request.user;
        overtime.day = request.events[0].dtstart;
        overtime.events = request.events;
        overtime.quantity = recover.gainedQuantity;
        overtime.settled = false;
        overtime.settlements = [];

        return overtime.save()
        .then(overtime => {
            recover.overtime = overtime._id;
            return request.save()
            .then(() => {
                return overtime;
            });
        });
    };

    /**
     * Create right and beneficiary
     * resolve to null if the request is not a recovery request
     * @param {User}        user            Request owner
     * @return {Promise}    resolve to the Beneficiary document or null if right has not been created
     */
    requestSchema.methods.createRecoveryBeneficiary = function(user)
    {
        if (!params.app.config.company.workperiod_recovery_by_approver) {
            return Promise.resolve(null);
        }

        const request  = this;
        return request.createRecoveryRight()
        .then(right => {
            if (null === right || undefined === right) {
                return Promise.resolve(null);
            }

            // link right to user using a beneficiary
            return right.addUserBeneficiary(user);
        });
    };

    /**
     * Open a validity interval
     */
    requestSchema.methods.openValidInterval = function()
    {
        this.validInterval.push({
            start: new Date(),
            finish: null
        });
    };


    /**
     * Close the current validity interval
     */
    requestSchema.methods.closeValidInterval = function()
    {
        var last = this.validInterval[this.validInterval.length -1];

        if (null !== last.finish) {
            throw new Error('No open interval found');
        }

        last.finish = new Date();
    };


    /**
     * Update document when an approval step has been accepted
     * @param {ApprovalStep} approvalStep
     * @param {User} user
     * @param {String} comment
     *
     * @return {Int}   Return the remaining approver on the step
     */
    requestSchema.methods.accept = function accept(approvalStep, user, comment) {

        if (!approvalStep.isApprover(user)) {
            throw new Error('User not allowed to accept this approval step');
        }

        // update approval step

        this.addLog('wf_accept', user, comment, approvalStep);

        if ('AND' === approvalStep.operator) {
            var remain = this.getRemainingApprovers(approvalStep);
            if (0 !== remain.length) {
                return remain.length;
            }
        }

        approvalStep.status = 'accepted';


        var nextStep = this.getNextApprovalStep();

        if (null === nextStep) {
            throw new Error('Nothing to accept');
        }

        if (false === nextStep || approvalStep._id === nextStep._id) {
            this.addLog('wf_end', user);

            if ('waiting' === this.status.created) {
                this.status.created = 'accepted';
                this.openValidInterval();
            }

            if ('waiting' === this.status.deleted) {
                this.status.deleted = 'accepted';
                this.addLog('delete', user, null, approvalStep);
                this.closeValidInterval();
            }

            // the workflow sheme has ended, remove approval steps list
            this.approvalSteps = [];


            return 0;
        }

        // add log entry
        this.forwardApproval(nextStep);

        return 0;
    };


    /**
     * Update document when an approval step has been rejected
     * @param {ApprovalStep} approvalStep
     * @param {User} user
     * @param {String} comment
     */
    requestSchema.methods.reject = function reject(approvalStep, user, comment) {

        if (!approvalStep.isApprover(user)) {
            throw new Error('User not allowed to accept this approval step');
        }

        approvalStep.status = 'rejected';

         // add log entry
         this.addLog('wf_reject', user, comment, approvalStep);

        if ('waiting' === this.status.created) {
            this.status.created = 'rejected';
        }

        if ('waiting' === this.status.deleted) {
            this.status.deleted = 'rejected';
        }

        // the workflow sheme has ended, remove approval steps list
        this.approvalSteps = [];
    };


    /**
     * Set status for all events associated to the request
     * @param {String} status   TENTATIVE | CONFIRMED | CANCELLED
     * @return {Promise}
     */
    requestSchema.methods.setEventsStatus = function setEventsStatus(status) {



        if (undefined === this.populated('events')) {
            throw new Error('The events path shoud be populated on request');
        }

        var promises = [];

        for(var i=0; i< this.events.length; i++) {
            this.events[i].status = status;
            promises.push(this.events[i].save());
        }

        return Promise.all(promises);
    };


    /**
    * Add a log document to request
    * @param {String} action
    * @param {String} comment
    * @param {ApprovalStep} approvalStep
    *
    */
    requestSchema.methods.addLog = function addLog(action, user, comment, approvalStep) {

        var log = {};

        log.action = action;
        log.comment = comment;
        log.userCreated = {
            id: user._id,
            name: user.getName()
        };

        if (approvalStep !== undefined) {
            log.approvalStep = approvalStep._id;
        }

        if (this.requestLog === undefined) {
            this.requestLog = [];
        }

        this.requestLog.push(log);
    };


    /**
     * Delete invalid elements
     * @returns {Promise} the list of deleted elements
     */
    requestSchema.methods.deleteElements = function() {
        let list = [];
        this.absence.distribution.forEach(element => {
            if (undefined !== element._id) {
                list.push(element._id);
            } else {
                list.push(element);
            }
        });

        let AbsenceElem = this.model('AbsenceElem');

        return AbsenceElem.find({ _id: { $in: list }}).exec()
        .then(elements => {
            return Promise.all(
                elements.map(element => {
                    return element.remove();
                })
            );
        });
    };

    /**
     * Utility method to populate fileds in all elements of the request
     * elements must be already populated
     *
     * @return Promise
     */
    requestSchema.methods.populateAbsenceElements = function() {
        let request = this;

        return Promise.all(
            request.absence.distribution.map(element => {
                return element.populate('events').execPopulate();
            })
        );
    };


    requestSchema.set('autoIndex', params.autoIndex);

    params.db.model('Request', requestSchema);
};