1
0
Fork 0
liza/src/server/service/RatingService.ts

501 lines
16 KiB
TypeScript

/**
* 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/>.
*/
import { ClassificationData, RateResult, WorksheetData } from "../rater/Rater";
import { ClientActions } from "../../client/action/ClientAction";
import { PositiveInteger } from "../../numeric";
import { PriorityLog } from "../log/PriorityLog";
import { ProcessManager } from "../rater/ProcessManager";
import { Program } from "../../program/Program";
import { QuoteId } from "../../quote/Quote";
import { Server } from "../Server";
import { ServerDao } from "../db/ServerDao";
import { ServerSideQuote } from "../quote/ServerSideQuote";
import { UserRequest } from "../request/UserRequest";
import { UserResponse } from "../request/UserResponse";
import { DeltaConstructor } from "../../bucket/delta";
type RequestCallback = () => void;
/** Result of rating */
export type RateRequestResult = {
data: RateResult,
initialRatedDate: UnixTimestamp,
lastRatedDate: UnixTimestamp,
};
/**
* Handle rating requests
*
* XXX: This class was extracted from Server and needs additional
* refactoring, testing, and cleanup.
*
* TODO: Logging should be implemented by observers
*/
export class RatingService
{
/**
* Initialize rating service
*
* @param _logger - logging system
* @param _dao - database connection
* @param _server - server actions
* @param _rater_manager - rating manager
* @param _createDelta - delta constructor
*/
constructor(
private readonly _logger: PriorityLog,
private readonly _dao: ServerDao,
private readonly _server: Server,
private readonly _rater_manager: ProcessManager,
private readonly _createDelta: DeltaConstructor<number>,
) {}
/**
* 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 request - user request to satisfy
* @param _response - pending response
* @param quote - quote to export
* @param cmd - applicable of command request
*
* @return result promise
*/
request(
request: UserRequest,
_response: UserResponse,
quote: ServerSideQuote,
cmd: string,
): Promise<RateRequestResult>
{
return new Promise<RateRequestResult>( resolve =>
{
// cmd represents a request for a single rater
if ( !cmd && this._isQuoteValid( quote ) )
{
// send last rated data
this._server.sendResponse( request, quote, {
data: quote.getRatingData(),
initialRatedDate: quote.getRatedDate(),
lastRatedDate: quote.getLastPremiumDate()
}, [] );
// XXX: When this class is no longer responsible for
// sending the response to the server, this below data needs
// to represent the _current_ values, since as it is written
// now, it'll overwrite what is currently in the bucket
return resolve( {
data: { _unavailable_all: '0' },
initialRatedDate: <UnixTimestamp>0,
lastRatedDate: <UnixTimestamp>0,
} );
}
resolve( this._performRating( request, quote, cmd ) );
} )
.catch( err =>
{
this._sendRatingError( request, 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
*/
private _isQuoteValid( quote: ServerSideQuote ): boolean
{
// quotes are valid for 30 days
var re_date = Math.round( ( ( new Date() ).getTime() / 1000 ) -
( 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 request - user request to satisfy
* @param quote - quote to process
* @param indv - individual supplier to rate (or empty)
*
* @return promise for results of rating
*/
private _performRating(
request: UserRequest,
quote: ServerSideQuote,
indv: string,
): Promise<RateRequestResult>
{
return new Promise<RateRequestResult>( ( resolve, reject ) =>
{
const rater = this._rater_manager.byId( quote.getProgramId() );
this._logger.log( this._logger.PRIORITY_INFO,
"Performing '%s' rating for quote #%s",
quote.getProgramId(),
quote.getId()
);
rater.rate( quote, request.getSession(), indv,
( rate_data: RateResult, actions: ClientActions ) =>
{
actions = actions || [];
this.postProcessRaterData(
request, rate_data, actions, quote.getProgram(), quote
);
const class_dest = {};
const cleaned = this._cleanRateData(
rate_data,
class_dest
);
// TODO: move me during refactoring
this._dao.saveQuoteClasses(
quote, class_dest, () => {}, () => {}
);
// save all data server-side (important: do after
// post-processing); async
this._saveRatingData( quote, rate_data, indv, () =>
{
const result = {
data: cleaned,
initialRatedDate: quote.getRatedDate(),
lastRatedDate: quote.getLastPremiumDate()
};
this._server.sendResponse(
request, quote, result, actions
);
resolve( result );
} );
},
( message: string ) =>
{
this._sendRatingError( request, 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 c - callback
*/
private _saveRatingData(
quote: ServerSideQuote,
data: RateResult,
indv: string,
c: RequestCallback
): void
{
// only update the last premium calc date on the initial request
if ( !indv )
{
var cur_date = <UnixTimestamp>Math.round(
( new Date() ).getTime() / 1000
);
quote.setLastPremiumDate( cur_date );
quote.setRatedDate( cur_date );
const quote_data = quote.getRatingData();
const save_data = { ratedata: data };
const rdelta_data = {
"rdelta.ratedata": {
data: this._createDelta( data, quote_data ),
concluding_save: false,
timestamp: cur_date,
},
};
// 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, c, c, save_data, rdelta_data );
}
else
{
c();
}
// we're not going to worry about whether or not this fails; if it does,
// an error will be automatically logged, but we still want to give the
// user a rate (if this save fails, it's likely we have bigger problems
// anyway); this can also be done concurrently with the above request
// since it only modifies a portion of the bucket
this._dao.mergeBucket( quote, data, () => {}, () => {} );
}
/**
* Process rater data returned from a rater
*
* @param _request - user request to satisfy
* @param data - rating data returned
* @param actions - actions to send to client
* @param program - program used to perform rating
* @param quote - quote used for rating
*/
protected postProcessRaterData(
_request: UserRequest,
data: RateResult,
actions: ClientActions,
program: Program,
quote: ServerSideQuote,
): void
{
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 );
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: string[] = [];
meta.deferred.forEach( ( alias: string ) =>
{
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 request - user request to satisfy
* @param quote - problem quote
* @param err - error
*/
private _sendRatingError(
request: UserRequest,
quote: ServerSideQuote,
err: Error,
): void
{
// 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-!' )
);
this._server.sendError( request,
'There was a problem during the rating process. Unable to ' +
'continue. Please contact our support team for assistance.' +
// show details for internal users
( ( request.getSession().isInternal() )
? '<br /><br />[Internal] ' + err.message + '<br /><br />' +
'<hr />' + ( err.stack || "" ).replace( /\n/g, '<br />' )
: ''
)
);
}
/**
* Process and save worksheet data from rating results
*
* @param qid - quote id
* @param data - rating result
*/
private _processWorksheetData( qid: QuoteId, data: RateResult ): void
{
// TODO: this should be done earlier on, so that this is not necessary
const wre = /^(.+)___worksheet$/;
const worksheets: Record<string, WorksheetData> = {};
// 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, ( err: NullableError ) =>
{
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-!' )
);
}
} );
}
/**
* Serve worksheet data to user
*
* @param request - user request to satisfy
* @param quote - quote from which to look up worksheet data
* @param supplier - supplier name
* @param index - worksheet index
*/
serveWorksheet(
request: UserRequest,
quote: ServerSideQuote,
supplier: string,
index: PositiveInteger,
): void
{
var qid = quote.getId();
this._dao.getWorksheet( qid, supplier, index, data =>
{
this._server.sendResponse( request, quote, {
data: data
} );
} );
}
/**
* 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
*/
private _cleanRateData(
data: RateResult,
classes: ClassificationData
): RateResult
{
// forceful cast because the below loop will copy everything
const result = <RateResult>{};
// 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;
}
}