"use strict";
/* TODO auto-generated eslint ignore, please fix! */
/* eslint @typescript-eslint/no-inferrable-types: "off", no-var: "off", prefer-const: "off" */
/**
 * Rating service
 *
 *  Copyright (C) 2010-2019 R-T Specialty, LLC.
 *
 *  This file is part of the Liza Data Collection Framework.
 *
 *  liza is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.RatingService = void 0;
/**
 * Handle rating requests
 *
 * XXX: This class was extracted from Server and needs additional
 * refactoring, testing, and cleanup.
 *
 * TODO: Logging should be implemented by observers
 */
var RatingService = /** @class */ (function () {
    /**
     * Initialize rating service
     *
     * @param _logger        - logging system
     * @param _dao           - database connection
     * @param _rater_manager - rating manager
     * @param _createDelta   - delta constructor
     * @param _ts_ctor       - a timestamp constructor
     */
    function RatingService(_logger, _dao, _rater_manager, _createDelta, _ts_ctor) {
        this._logger = _logger;
        this._dao = _dao;
        this._rater_manager = _rater_manager;
        this._createDelta = _createDelta;
        this._ts_ctor = _ts_ctor;
        /** The maximum amount of retries to attempt */
        this.RETRY_MAX_ATTEMPTS = 12;
        /** Seconds to wait between retries */
        this.RETRY_DELAY = 5;
    }
    /**
     * Sends rates to the client
     *
     * Note that the promise will be resolved after all data saving is
     * complete; the request will be sent back to the client before then.
     *
     * @param session - user session
     * @param quote   - quote to export
     * @param cmd     - applicable of command request
     * @param force   - (optional) force a rate, overriding quote valid checks
     *
     * @return result promise
     */
    RatingService.prototype.request = function (session, quote, cmd, force) {
        var _this = this;
        if (force === void 0) { force = false; }
        return new Promise(function (resolve) {
            // cmd represents a request for a single rater
            if (!cmd && !force && _this._isQuoteValid(quote)) {
                var actions = [];
                var data = quote.getRatingData();
                _this._processRetries(quote, data, actions);
                // send last rated data
                return resolve({
                    content: {
                        data: data,
                        initialRatedDate: quote.getRatedDate(),
                        lastRatedDate: quote.getLastPremiumDate(),
                    },
                    actions: actions,
                });
            }
            resolve(_this._performRating(session, quote, cmd));
        }).catch(function (err) {
            _this._logRatingError(quote, err);
            throw err;
        });
    };
    /**
     * Whether quote is still valid
     *
     * TODO: This class shouldn't be making this determination, and this
     * method is nondeterministic.
     *
     * @param quote - quote to check
     *
     * @return whether quote is still valid
     */
    RatingService.prototype._isQuoteValid = function (quote) {
        // quotes are valid for 30 days
        var re_date = this._ts_ctor() - 60 * 60 * 24 * 30;
        if (quote.getLastPremiumDate() > re_date) {
            this._logger.log(this._logger.PRIORITY_INFO, "Skipping '%s' rating for quote #%s; quote is still valid", quote.getProgramId(), quote.getId());
            return true;
        }
        return false;
    };
    /**
     * Perform rating and process result
     *
     * @param session - user session
     * @param quote   - quote to process
     * @param indv    - individual supplier to rate (or empty)
     *
     * @return promise for results of rating
     */
    RatingService.prototype._performRating = function (session, quote, indv) {
        var _this = this;
        return new Promise(function (resolve, reject) {
            var rater = _this._rater_manager.byId(quote.getProgramId());
            _this._logger.log(_this._logger.PRIORITY_INFO, "Performing '%s' rating for quote #%s", quote.getProgramId(), quote.getId());
            // Only update the rate request timestamp on the first request made
            if (quote.getRetryAttempts() === 0) {
                var ts = _this._ts_ctor();
                var meta = { liza_timestamp_rate_request: [ts] };
                quote.setMetadata(meta);
                _this._dao.saveQuoteMeta(quote, meta);
            }
            rater.rate(quote, session, indv, function (rate_data, actions, override) {
                if (override === void 0) { override = false; }
                actions = actions || [];
                _this.postProcessRaterData(rate_data, actions, quote);
                quote.setRatingData(rate_data);
                var class_dest = {};
                var cleaned = _this._cleanRateData(rate_data, class_dest);
                // save all data server-side (important: do after
                // post-processing); async
                _this._saveRatingData(quote, rate_data, class_dest, indv, override, function () {
                    var content = {
                        data: cleaned,
                        initialRatedDate: quote.getRatedDate(),
                        lastRatedDate: quote.getLastPremiumDate(),
                    };
                    resolve({ content: content, actions: actions });
                });
            }, function (message) {
                _this._logRatingError(quote, Error(message));
                reject(Error(message));
            });
        });
    };
    /**
     * Saves rating data
     *
     * Data will be merged with existing bucket data and saved. The idea behind
     * this is to allow us to reference the data (e.g. for reporting) even if
     * the client does not save it.
     *
     * @param quote    - quote to save data to
     * @param data     - rating data
     * @param indv     - individual supplier, or empty
     * @param override - rating was overridden and results are custom
     * @param c        - callback
     */
    RatingService.prototype._saveRatingData = function (quote, data, classes, indv, override, c) {
        // only update the last premium calc date on the initial request
        if (!indv) {
            var cur_date = this._ts_ctor();
            // If we overrode the rating process we do not want to mark it as a
            // valid rate
            quote.setLastPremiumDate(override ? 0 : cur_date);
            quote.setRatedDate(cur_date);
            var quote_data = quote.getRatingData();
            var save_data = { ratedata: data };
            var rdelta_data = {
                'rdelta.ratedata': {
                    data: this._createDelta(data, quote_data),
                    concluding_save: false,
                    timestamp: cur_date,
                },
            };
            // Save quote classes
            for (var key in classes) {
                save_data['classData.' + key] = classes[key];
            }
            // save the last prem status (we pass an empty object as the save
            // data argument to ensure that we do not save the actual bucket
            // data, which may cause a race condition with the below merge call)
            this._dao.saveQuote(quote, function () { }, function () { }, save_data, rdelta_data);
        }
        this._dao.mergeBucket(quote, data, c, c);
    };
    /**
     * Process rater data returned from a rater
     *
     * @param data     - rating data returned
     * @param actions  - actions to send to client
     * @param quote    - quote used for rating
     */
    RatingService.prototype.postProcessRaterData = function (data, actions, quote) {
        var meta = data._cmpdata || {};
        // the metadata will not be provided to the client
        delete data._cmpdata;
        // rating worksheets are returned as metadata
        this._processWorksheetData(quote.getId(), data);
        this._processRetries(quote, data, actions);
        var program = quote.getProgram();
        if (program.ineligibleLockCount > 0 &&
            +meta.count_ineligible >= program.ineligibleLockCount) {
            // lock the quote client-side (we don't send them the reason; they
            // don't need it) to the current step
            actions.push({ action: 'lock' });
            var lock_reason = 'Supplier ineligibility restriction';
            var lock_step = quote.getCurrentStepId();
            // the next step is the step that displays the rating results
            quote.setExplicitLock(lock_reason, lock_step + 1);
            // important: only save the lock state, not the step states, as we
            // have a race condition with async. rating (the /visit request may
            // be made while we're rating, and when we come back we would then
            // update the step id with a prior, incorrect step)
            this._dao.saveQuoteLockState(quote);
        }
        // if any have been deferred, instruct the client to request them
        // individually
        if (Array.isArray(meta.deferred) && meta.deferred.length > 0) {
            var torate = [];
            meta.deferred.forEach(function (alias) {
                actions.push({ action: 'indvRate', id: alias });
                torate.push(alias);
            });
            // we log that we're performing rating, so we should also log when
            // it is deferred (otherwise the logs will be rather confusing)
            this._logger.log(this._logger.PRIORITY_INFO, "'%s' rating deferred for quote #%s; will rate: %s", quote.getProgramId(), quote.getId(), torate.join(','));
        }
    };
    /**
     * Send rating error to user and log
     *
     * @param quote   - problem quote
     * @param err     - error
     */
    RatingService.prototype._logRatingError = function (quote, err) {
        // well that's no good
        this._logger.log(this._logger.PRIORITY_ERROR, 'Rating for quote %d (program %s) failed: %s', quote.getId(), quote.getProgramId(), err.message + '\n-!' + (err.stack || '').replace(/\n/g, '\n-!'));
    };
    /**
     * Clear all pending retry attempts
     *
     * @param data Rating results
     *
     * @return only retry attempt fields
     */
    RatingService.prototype._clearRetries = function (data) {
        var cleared = {};
        for (var field in data.__result_ids) {
            // Reset the field to zero
            data[data.__result_ids[field] + '___retry'] = [0];
            cleared[data.__result_ids[field] + '___retry'] = [0];
        }
        data['__rate_pending'] = [0];
        cleared['__rate_pending'] = [0];
        return cleared;
    };
    /**
     * Process and save worksheet data from rating results
     *
     * @param qid  - quote id
     * @param data - rating result
     */
    RatingService.prototype._processWorksheetData = function (qid, data) {
        var _this = this;
        // TODO: this should be done earlier on, so that this is not necessary
        var wre = /^(.+)___worksheet$/;
        var worksheets = {};
        // extract worksheets for each supplier
        for (var field in data) {
            var match;
            if ((match = field.match(wre))) {
                var name = match[1];
                worksheets[name] = data[field];
                delete data[field];
            }
        }
        this._dao.setWorksheets(qid, worksheets, function (err) {
            if (err) {
                _this._logger.log(_this._logger.PRIORITY_ERROR, 'Failed to save rating worksheets for quote %d', qid, err.message + '\n-!' + (err.stack || '').replace(/\n/g, '\n-!'));
            }
        });
    };
    /**
     * Get worksheet data
     *
     * @param quote    - quote from which to look up worksheet data
     * @param supplier - supplier name
     * @param index    - worksheet index
     */
    RatingService.prototype.getWorksheet = function (quote, supplier, index) {
        var qid = quote.getId();
        return this._dao.getWorksheet(qid, supplier, index);
    };
    /**
     * Prepares rate data to be sent back to the client
     *
     * There are certain data saved server-side that there is no use serving to
     * the client.
     *
     * @param data    - rate data
     * @param classes - classification data
     *
     * @return modified rate data
     */
    RatingService.prototype._cleanRateData = function (data, classes) {
        // forceful cast because the below loop will copy everything
        var result = {};
        // clear class data
        for (var key in data) {
            var mdata;
            // supplier___classes
            if ((mdata = key.match(/^(.*)___classes$/))) {
                classes[mdata[1]] = data[key];
                continue;
            }
            result[key] = data[key];
        }
        return result;
    };
    /**
     * Process retry logic
     *
     * @param quote    - quote used for rating
     * @param data     - rating data returned
     * @param actions  - actions to sent to the client
     */
    RatingService.prototype._processRetries = function (quote, data, actions) {
        var _a;
        // Gather determinant factors
        var retries = quote.getRetryCount(data);
        var pending_count = retries.true_count;
        var retry_total = retries.field_count;
        var supplier_total = ((_a = data.__result_ids) === null || _a === void 0 ? void 0 : _a.length) || retry_total;
        var retry_attempts = quote.getRetryAttempts();
        // Make the assumption here that if there are no retry fields at all then
        // we do not want deferred rating
        var missing_retries = retry_total === 0 ? 0 : supplier_total - retry_total;
        // Make determinations
        var max_attempts = retry_attempts >= this.RETRY_MAX_ATTEMPTS;
        var has_pending = missing_retries + pending_count > 0;
        // Clear retry attempts when we have no more pending rates
        quote.setRetryAttempts(has_pending ? retry_attempts + 1 : 0);
        data['__rate_pending'] = [pending_count];
        this._dao.mergeData(quote, {
            'ratedata.__rate_pending': [pending_count],
            retryAttempts: quote.getRetryAttempts(),
        });
        if (has_pending && !max_attempts) {
            // Set rate event value to -1 so that it will be in the background
            actions.push({
                action: 'delay',
                seconds: this.RETRY_DELAY,
                then: {
                    action: 'rate',
                    value: -1,
                },
            });
        }
        else if (has_pending && max_attempts) {
            var clear_data_1 = this._clearRetries(data);
            var save_data_1 = {};
            // Update quote ratedata
            quote.setRatingData(data);
            // do not overwrite data that we still want
            Object.keys(clear_data_1).forEach(function (key) {
                save_data_1['ratedata.' + key] = clear_data_1[key];
                save_data_1['data.' + key] = clear_data_1[key];
            });
            this._dao.saveQuote(quote, function () { }, function () { }, save_data_1);
        }
    };
    return RatingService;
}());
exports.RatingService = RatingService;
//# sourceMappingURL=data:application/json;base64, 